# Discord
Source: https://docs.poly.ai/adk/community/discord
We have a Discord server where ADK users and contributors hang out. Whether you're just getting started or building something advanced, it's the fastest way to get help, share ideas, and stay in the loop on what's new.
[Join the ADK Discord](https://discord.gg/nzGcCt6SE)
## What you'll find there
* **Help & troubleshooting** — get unstuck with setup, CLI commands, or resource configuration.
* **Show & tell** — share agents you've built, interesting patterns, or cool integrations.
* **Feature discussion** — chat about what you'd like to see next in the ADK.
* **Announcements** — hear about new releases and breaking changes early.
Come say hello — we'd love to hear what you're working on.
# Common anti-patterns
Source: https://docs.poly.ai/adk/concepts/anti-patterns
This page collects common implementation mistakes that make agents harder to predict, harder to maintain, or more likely to behave incorrectly at runtime.
The general rule is simple: keep prompts focused on conversation, keep Python focused on deterministic logic, and make control flow explicit.
## Flow navigation
Flow functions must **always advance** the flow. See the [flows reference](/adk/reference/flows) for navigation methods including `flow.goto_step()`.
A flow function should never leave the agent sitting in the same logical place without a clear next step.
### Avoid
* returning from a flow function without changing step or flow
* leaving navigation implicit
* assuming the model will somehow recover the flow state on its own
### Prefer
* `flow.goto_step(...)`
* returning an explicit transition
* making the next state obvious in code
**A stuck flow is usually a control-flow bug**
If a flow function does not move the agent forward, the conversation can become stuck in an invalid or confusing state.
## Metrics and logging
Metrics and logs should capture important events, not generate noise. See the [functions reference](/adk/reference/functions) for `conv.log` and metrics APIs.
### Avoid
* writing the same metric repeatedly in a loop
* emitting metrics every turn without a clear reason
* swallowing external API failures silently
### Prefer
* `write_once=True` when an event should only be recorded once
* logging meaningful outcomes around API calls and validation failures
* using `conv.log.info(...)`, `conv.log.warning(...)`, and `conv.log.error(...)` to make important behavior visible
**Good logging explains the shape of the call**
Logs and metrics should help you understand what happened in the conversation, not bury you in repetitive trivia.
## Logic in prompts vs code
Do not put deterministic branching logic into prompts or YAML instructions.
Prompts are for conversational behavior. Python is for comparisons, routing, validation, and state-driven decisions.
### Wrong
Encoding branching logic in prompts, for example:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
If $x == 0 do A, else do B.
```
### Right
Implement the check in Python and transition to the correct step or flow explicitly.
### Why this matters
When branching logic is buried in prompts:
* behavior becomes harder to test and verify
* routing becomes harder to debug
* deterministic behavior becomes dependent on how the model interprets the instruction
Use prompts for collecting information, presenting information, and guiding conversational style.
Use Python for comparisons, routing, validation, retries, and state-based decisions.
## “Anything else?” and exiting flows
Do not create a dedicated **“Anything else?”** step just to wrap up a flow.
When the flow is finished, exit the flow and return the appropriate closing prompt there.
### Avoid
* adding a special cleanup step whose only purpose is to ask whether the user needs anything else
* calling `conv.exit_flow()` and then also navigating somewhere else
### Wrong
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.exit_flow()
return {"transition": {"goto_flow": "Another Flow"}}
```
or
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.exit_flow()
conv.goto_flow("Another Flow")
```
In both cases, the navigation overrides the exit.
### Right
Use **one** of these approaches:
* exit the flow and return the closing content
* navigate to another step or flow
Do not do both.
**Exit and navigation are mutually exclusive**
If you call `conv.exit_flow()` and then also transition elsewhere, the transition wins.
## `end_turn=False`
`end_turn=False` is easy to misuse.
It should only be used when the agent speaks and then immediately performs another action in the **same turn**, without waiting for user input.
### Wrong
Using `end_turn: False` after the agent asks a question and is waiting for a reply.
That produces awkward control flow, because the question should simply be part of the normal utterance.
### Right
Use `end_turn: False` only when the agent must continue immediately, for example:
* the agent says something
* then immediately calls a function in the same turn
Example pattern:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
“Your balance is X.”
→ immediately call `balance_informed`
```
If the user is expected to answer, put the full question in the utterance and let the turn end normally.
## Don't copy project directories between projects
Copying an existing ADK project directory and pointing it at a different Agent Studio project will cause push failures. The `.agent_studio_config` file contains resource IDs from the source project, and platform-provisioned resources (voice settings, chat settings, personality, role, ASR settings) cannot be created through the ADK.
### Wrong
Copying a project directory, updating `project.yaml` with new IDs, then running `poly push` against a different project.
### Right
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly init
poly pull
```
Start every new project with `poly init` and `poly pull`. Copy individual resource files if you need to reuse them — never copy `.agent_studio_config` or the whole directory.
## Quick reference
| Anti-pattern | Better approach |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------- |
| Flow function returns without navigation | Always call `flow.goto_step(...)` or return a transition |
| Metric written repeatedly in a loop | Use `write_once=True` where appropriate |
| Branching logic in prompts | Put routing logic in Python |
| Dedicated “Anything else?” step | Exit the flow and return the closing prompt directly |
| `conv.exit_flow()` plus navigation | Choose exit **or** transition, not both |
| `end_turn=False` while waiting for a user answer | Only use it when the agent continues immediately in the same turn |
| Copying a project directory to a new project | Use `poly init` + `poly pull` for the target project; copy individual resource files as needed |
## Design principle
* make control flow explicit
* keep prompts conversational
* keep code deterministic
* prefer simple, testable paths over clever prompt tricks
## Related pages
Navigation methods, step transitions, and flow functions.
Logging, metrics, conv APIs, and lifecycle hooks.
Personality, role, and rules — the global prompt layer.
How the ADK maps resources to the local filesystem.
# Multi-user workflows and guardrails
Source: https://docs.poly.ai/adk/concepts/multi-user-and-guardrails
The **PolyAI ADK** was designed with **multi-user collaboration** in mind.
It allows multiple developers to work on the same Agent Studio project while preserving the same platform guardrails that prevent incompatible or invalid changes from being pushed.
## Why this matters
Local workflows are only useful if teams can collaborate safely.
Without guardrails, local editing quickly becomes chaotic:
* one developer overwrites another’s work
* invalid resources are pushed upstream
* branch state becomes unclear
* review becomes difficult
The ADK is designed to reduce those risks by combining local editing with validation, branching, and synchronization back to Agent Studio.
Developers can create and switch branches for isolated work.
Local changes can be checked before they are sent back to Agent Studio.
Changes can be compared and shared for review before merge.
The CLI validates that pushed changes remain valid for the project.
## Branch workflow
The collaborative workflow follows the standard [CLI working pattern](/adk/reference/cli#working-pattern), with each developer working on their own branch. Create a branch with [`poly branch create`](/adk/reference/cli#poly-branch-create), edit and push, then merge with [`poly branch merge`](/adk/reference/branch_merge) or through the Agent Studio UI when ready.
## Validation as a guardrail
Run `poly validate` before pushing to catch issues locally, before they reach Agent Studio.
Examples of what validation protects against include:
* invalid resource structures
* missing required values
* incompatible references between resources
* malformed configuration files
**Validate before pushing**
In collaborative workflows, treat `poly validate` as a standard step in the editing cycle, not an optional one.
## Pulling and merge behavior
If work is done to your branch in Agent Studio and you want to bring those changes into your local copy, you can run [`poly pull`](/adk/reference/cli#poly-pull):
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly pull
```
If the pulled changes conflict with your own local edits, the ADK will merge them and surface merge markers where conflicts occur.
The local workflow is not isolated from Agent Studio UI work - both sides affect branch state. Keep that in mind when collaborating.
## Review workflow
When changes are ready for review, generate a review artifact with [`poly review create`](/adk/reference/cli#poly-review):
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly review create
```
Use this to compare:
* local changes against the remote project
* one branch against another
* a feature branch against `main` or `sandbox`
A GitHub environment token is required. The output lets reviewers inspect changes without access to your local filesystem.
## Guardrails inherited from Agent Studio
The ADK is intentionally aligned with the Agent Studio platform.
That means it is not just a free-form local editing tool. It is structured so that developers should not be able to push changes that are incompatible with the project as defined by the platform.
In practice, this means:
* project resources must still conform to Agent Studio expectations
* references between resources must remain valid
* branch merges happen either through the CLI ([`poly branch merge`](/adk/reference/branch_merge)) or in Agent Studio
* deployment still happens through Agent Studio
**Local flexibility, platform constraints**
The ADK expands where and how developers can work, but it does not remove the constraints that keep projects valid and deployable.
## Best practices for teams
When multiple developers are working on the same project, a few habits make the workflow much smoother:
* create a branch before making substantial changes
* pull the latest changes before starting work
* validate locally before pushing
* use `poly diff` and `poly status` frequently
* review branch output before merging
* keep resource names stable and descriptive
## Common failure modes
Common collaboration problems usually come from process, not tooling.
Watch out for:
* editing directly on the wrong branch
* forgetting to pull before starting work
* pushing without validation
* mixing large unrelated changes into one branch
* treating Agent Studio UI edits and local edits as if they cannot collide
## Related pages
Learn how the local project structure maps to Agent Studio.
Review the commands used for branching, validation, diffing, and review.
# Resource architecture
Source: https://docs.poly.ai/adk/concepts/resource-architecture
Each resource type in the ADK has a specific purpose. This page helps you decide where to put new content or logic before you write it.
Choosing the wrong resource type is one of the most common sources of hard-to-debug agent behavior. The right question is not "how do I add this?" but "what kind of thing is this?"
## The core split
The ADK separates two concerns:
Information the agent should retrieve and communicate. Lives in topics.
What the agent should do, when, and how. Lives in rules, flows, and functions.
Keep these separate. Mixing factual content and behavioral instructions in the same resource makes both harder to maintain and harder to reason about.
## Decision table
| You are adding... | Use |
| --------------------------------------------------------------------- | ----------------------------------------------------- |
| A new FAQ, policy, or factual answer | Topic (`topics/`) |
| A global behavioral rule (always do X, never do Y) | `agent_settings/rules.txt` |
| Structured data collection from the caller | Entity + flow |
| Deterministic branching or routing logic | Function (`functions/`) |
| Call initialization — routing, variant selection, reading SIP headers | `functions/start_function.py` |
| A multi-step guided conversation | Flow (`flows/`) |
| Reusable SMS message content | SMS template (`config/sms_templates.yaml`) |
| Per-site or per-location configuration | Variant attributes (`config/variant_attributes.yaml`) |
| Agent identity and tone | `agent_settings/personality.yaml` and `role.yaml` |
## Rules vs topics vs functions
These three resources overlap in ways that create confusion.
**`rules.txt`** is for durable, global behavioral instructions that apply on every turn — for example, "always confirm the booking reference before making changes" or "do not discuss competitor products." Rules are not retrieved via RAG; they are always present in the prompt.
**Topics** are for subject-specific knowledge that should only appear when relevant. The agent retrieves the right topic when the caller asks about that subject. Put factual content and the behavioral instructions for that specific subject area in the topic, not in rules.
**Functions** are for anything that requires a deterministic outcome — checking a value, calling an API, routing to a different flow, or making a decision that must not be left to the model.
**A useful test**
If the instruction is always true, it belongs in rules. If it is only relevant when someone asks about a specific subject, it belongs in a topic. If it requires a comparison, calculation, or API call, it belongs in a function.
## Common mistakes
### Putting behavioral logic in topic content
The `content` field of a topic is retrieved by RAG and made available as context. It should contain facts, not instructions.
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Wrong — behavioral logic in content
content: |-
If the caller asks to cancel, transfer them to the cancellations queue.
# Right — behavioral logic in actions
actions: |-
If the caller asks to cancel, use {{ho:cancellations}} to transfer them.
```
### Putting facts in rules
`rules.txt` is not a good place for factual content because it is always present in the prompt, consuming context space even when the information is not relevant to the current turn. Keep facts in topics where they are only retrieved when needed.
### Writing prose conditionals in rules or topics
Logic like "if `{{vrbl:caller_number}}` is available, do X; otherwise do Y" is unreliable when the variable is empty. The model cannot reliably detect an empty variable from prompt text alone. Write the branch in Python instead.
## Where `start_function` fits
`start_function.py` runs once at call start, before the first user input. It is the right place for:
* reading SIP headers and setting variant routing
* initializing state variables the rest of the conversation depends on
* making a fast API call to preload caller context
It is **not** the right place for logic that only applies mid-conversation, or for slow API calls that would delay the greeting.
## Related pages
Common mistakes to avoid when building flows, writing prompts, and handling control flow.
Full reference for topic structure, content, and actions.
Personality, role, and rules — the global prompt layer.
Python functions for deterministic logic and lifecycle hooks.
How topics are retrieved, ranked, and used by the platform — including RAG mechanics and topic types.
Lifecycle hook reference — when it runs, what it can read, and common initialization patterns.
Alternative to Managed Topics for large, unstructured content sets — help articles, PDFs, FAQs.
Per-site configuration using variant attributes — how routing and attribute lookup work.
# Working locally
Source: https://docs.poly.ai/adk/concepts/working-locally
With the ADK, you work on Agent Studio projects from your local machine instead of exclusively through the browser.
Your local filesystem becomes your primary editing surface. You can:
* edit agent resources directly
* review changes with Git-style workflows
* validate changes before pushing
* work in **VS Code** or **Cursor** with the [PolyAI ADK extension](/adk/reference/tooling#polyai-adk-extension-for-vs-code-and-cursor), or pair the ADK with [AI coding agents](/adk/reference/tooling#claude-code) such as **Claude Code**
* test and iterate before merging in Agent Studio
Agent configuration lives on disk in a structured project directory.
Pull, edit, validate, push, and review changes using the `poly` CLI.
Agent Studio remains the source of deployment and preview. Branches can be merged with [`poly branch merge`](/adk/reference/branch_merge) from the CLI or through the Agent Studio UI.
The local workflow works naturally with editors, terminals, and AI-assisted coding tools.
## What a local project contains
Each local ADK project represents an Agent Studio project.
A project can define a voice or webchat agent, and its runtime behavior is controlled by resources such as flows, functions, topics, settings, and configuration files.
A typical project structure looks like this:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
//
├── _gen/ # Generated stubs - do not edit
├── agent_settings/ # Agent identity and behavior
│ ├── languages.yaml # Optional
│ ├── personality.yaml
│ ├── role.yaml
│ ├── rules.txt
│ ├── safety_filters.yaml # Optional
│ └── experimental_config.json # Optional
├── config/ # Configuration
│ ├── entities.yaml # Optional
│ ├── handoffs.yaml # Optional
│ ├── sms_templates.yaml # Optional
│ ├── translations.yaml # Optional
│ └── variant_attributes.yaml # Optional
├── context/ # Optional - document context files
│ └── {document_name}.md
├── voice/ # Voice channel settings
│ ├── configuration.yaml
│ ├── safety_filters.yaml # Optional
│ ├── speech_recognition/
│ └── response_control/
├── chat/ # Chat channel settings
│ ├── configuration.yaml
│ └── safety_filters.yaml # Optional
├── flows/ # Optional - flow definitions
├── functions/ # Global functions
├── topics/ # Knowledge base topics
├── test_suite/ # Optional - simulated conversation tests
└── project.yaml # Project metadata
```
**Generated files**
Files under `_gen/` are generated stubs and should not be edited directly.
## How local work maps to Agent Studio
The ADK does not replace Agent Studio. It acts as the local development layer around it.
A typical flow looks like this:
1. initialize or pull a project locally
2. create or switch to a branch
3. edit resources on disk
4. validate and inspect changes
5. push changes back to Agent Studio
6. test and review the branch in Agent Studio
7. merge when ready
This means the local filesystem becomes your main editing surface, while Agent Studio remains the place where work is previewed, reviewed, and deployed.
## Standard CLI workflow
See the [CLI working pattern](/adk/reference/cli#working-pattern) for the full step-by-step. The short version: init → pull → branch → edit → validate → push → review → merge → chat.
**Run commands from the project folder**
ADK commands are expected to be run from within the local project directory. If needed, use the `--path` flag to point to a project explicitly.
## Resource reference syntax
Many ADK resources support references to other resources or values.
These placeholders are used in prompts, rules, topic actions, and related text fields:
| Syntax | Resolves to | Common use |
| ------------------------------ | ------------------------------------------------- | -------------------------------------------- |
| `{{fn:function_name}}` | [Global function](/adk/reference/functions) | Rules, topic actions, advanced step prompts |
| `{{ft:function_name}}` | [Flow transition function](/adk/reference/flows) | Advanced step prompts within the same flow |
| `{{entity:entity_name}}` | [Collected entity value](/adk/reference/entities) | Flow prompts |
| `{{attr:attribute_name}}` | [Variant attribute](/adk/reference/variants) | Rules, prompts, greetings, personality, role |
| `{{twilio_sms:template_name}}` | [SMS template](/adk/reference/sms) | Rules, topic actions |
| `{{ho:handoff_name}}` | [Handoff destination](/adk/reference/handoffs) | Rules |
| `{{vrbl:variable_name}}` | [State variable](/adk/reference/variables) | Prompts, topic actions, SMS templates |
These references let settings, prompts, and behaviors point to resources by name rather than repeating hard-coded values.
**A Git-like workflow for Agent Studio**
Think of the ADK as a synchronization layer between your local files and the Agent Studio platform.
## Related pages
Review the main ADK commands and their purpose.
Write simulated conversation test cases under `test_suite/`.
Learn how branching, validation, and review fit into collaborative work.
# Confirm caller ID before sending SMS
Source: https://docs.poly.ai/adk/examples/confirm-caller-id-before-sms
This pattern covers a very common voice + SMS flow: the agent has the caller's number from the inbound call, confirms the last four digits with the caller, and sends an SMS to that number. If the caller's number is not available (for example, in a chat session), the agent asks for it instead.
## Files involved
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
functions/start_function.py ← stash caller_number at call start
functions/caller_number_confirmed.py ← check presence, compare last four
config/sms_templates.yaml ← the SMS template (alongside other templates)
topics/Send Booking Link.yaml ← topic that triggers the flow
```
## start\_function.py
Stash the raw `caller_number` at call start so it is available throughout the conversation.
**`start_function.py` may already exist**
If your project was set up via Quick Agent Setup, `start_function.py` likely already contains initialization logic. Add the `conv.state.caller_number` line to the existing function rather than replacing the file.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from _gen import * #
def start_function(conv: Conversation):
conv.state.caller_number = conv.caller_number
return str()
```
## caller\_number\_confirmed.py
Called when the caller has confirmed (or declined) sending the SMS. Branches on whether `caller_number` is present, then validates the last-four match before sending.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from _gen import * #
@func_description(
"Called after the caller confirms or provides a number for us to send the SMS to. "
"confirmed: True if they confirmed the number on file, False if they want to use a different one. "
"provided_number: the number they provided if confirmed is False."
)
@func_parameter("confirmed", "Whether the caller confirmed the number on file")
@func_parameter("provided_number", "The number the caller provided if confirmed is False; pass an empty string if confirmed is True")
def caller_number_confirmed(conv: Conversation, confirmed: bool, provided_number: str):
if confirmed and conv.state.caller_number:
to_number = conv.state.caller_number
elif provided_number:
to_number = provided_number
else:
return "Ask the caller to provide the number where we should send the link."
conv.send_sms_template(to_number=to_number, template="booking_link")
return "Tell the caller the link has been sent and ask if there is anything else you can help with."
```
## Send Booking Link.yaml
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
enabled: true
example_queries:
- Can you send me the booking link?
- Can you text me a link?
- Send me the details by text
- I'd like a link to book online
content: |-
The booking link is available by SMS.
actions: |-
## If the caller's number is available
Tell the caller you can send the link to the number ending in the last four digits of $caller_number.
Ask them to confirm or provide a different number.
Use {{fn:caller_number_confirmed}} once they respond.
## If the caller's number is not available
Ask the caller for the number where we should send the link.
Use {{fn:caller_number_confirmed}} with confirmed=False and the number they provide.
```
**Prose conditionals and empty variables**
The topic actions above use a natural-language conditional on `$caller_number`. This works when the variable is populated, but can behave unreliably if the variable is always empty (for example, in chat). If you need strict branching, move the presence check into `caller_number_confirmed` and call it unconditionally from the topic action.
## SMS template (config/sms\_templates.yaml)
All SMS templates are defined in a single `config/sms_templates.yaml` file under the `sms_templates` key.
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
sms_templates:
- name: booking_link
text: "Here's your booking link: https://book.example.com"
env_phone_numbers:
sandbox: ""
pre_release: ""
live: ""
```
## Related pages
Reference for SMS template structure and variable substitution.
How `conv.state` variables are discovered and referenced.
Why prose conditionals on variable presence are unreliable.
Full reference for `conv.caller_number`, `conv.send_sms_template`, and all other `conv` attributes.
When start function runs, what it can access, and how to use it for initialization.
Configuring SMS channels, sender numbers, and template structure.
# Examples
Source: https://docs.poly.ai/adk/examples/index
Small, focused examples that can be copied and adapted when building with the **PolyAI ADK**.
Validate the caller's identity before sending an SMS confirmation.
Customize the agent's closing message based on variant attributes.
Send an SMS with a link and fall back to a live transfer if it fails.
# SMS link with transfer fallback
Source: https://docs.poly.ai/adk/examples/sms-or-transfer-fallback
A common pattern in voice agents: offer to send the caller a link by SMS, but transfer to a live agent if SMS isn't an option or the caller asks for it. The decision lives in a function so it is deterministic — not subject to LLM interpretation.
## Files involved
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
functions/start_function.py ← stash caller_number
functions/send_link_or_transfer.py ← SMS or handoff decision
config/sms_templates.yaml ← SMS template (alongside other templates)
config/handoffs.yaml ← transfer destination (alongside other handoffs)
topics/Get Booking Link.yaml ← triggers the pattern
```
## send\_link\_or\_transfer.py
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from _gen import * #
@func_description(
"Called when the caller has been offered the booking link by SMS or by speaking to an agent. "
"wants_sms: True if they want the link by text, False if they want to speak to someone."
)
@func_parameter("wants_sms", "True if the caller wants the SMS link, False if they want to speak to an agent")
def send_link_or_transfer(conv: Conversation, wants_sms: bool):
if wants_sms:
to_number = conv.state.caller_number
if not to_number:
return (
"Tell the caller we were unable to send the SMS because we do not have their number. "
"Offer to transfer them to an agent instead or ask for a number to send to."
)
conv.send_sms_template(to_number=to_number, template="booking_link")
return "Tell the caller the link has been sent and ask if there is anything else you can help with."
else:
conv.call_handoff(
destination="agent_queue",
reason="caller_requested_agent",
utterance="Let me connect you with a member of the team now.",
)
```
## Get Booking Link.yaml
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
enabled: true
example_queries:
- Can you send me a link to book?
- How do I book online?
- I want to make a reservation online
- Send me a booking link
- Can I speak to someone about booking?
content: |-
Bookings can be made online at book.example.com, or by speaking with a team member.
actions: |-
Ask the caller if they would like the link sent by text message, or if they would prefer to speak to someone.
Use {{fn:send_link_or_transfer}} once they respond.
```
**Testing via `poly chat`: `caller_number` will be empty**
`conv.state.caller_number` is populated from inbound caller ID, which is only available on a real voice call. When testing with `poly chat`, `caller_number` is always empty regardless of `--channel` — `wants_sms: True` will always hit the "unable to send" branch.
To exercise the SMS path locally, mock the value in `start_function.py`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.state.caller_number = "+15551234567" # remove before deploying
```
## config/handoffs.yaml
The `destination` value passed to `conv.call_handoff` must match the `name` of a handoff defined in `config/handoffs.yaml`. All handoffs are defined in a single file under the `handoffs` key.
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
handoffs:
- name: agent_queue
description: Transfer to live agent queue
is_default: true
sip_config:
method: refer
phone_number: "+441234567890"
```
See the [handoffs reference](/adk/reference/handoffs) for all SIP method options and field details.
## Per-environment sender number
If the sender number differs between environments, the simplest approach is to configure `env_phone_numbers` directly in `config/sms_templates.yaml` — no code required:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
sms_templates:
- name: booking_link
text: "Here's your booking link: https://book.example.com"
env_phone_numbers:
sandbox: "+441111111111"
pre_release: "+442222222222"
live: "+443333333333"
```
The platform selects the right number automatically when `conv.send_sms_template()` is called. See the [SMS setup reference](https://docs.poly.ai/sms/introduction) for full template configuration options.
If you need more control — for example, when using `conv.send_sms()` to send free-form content rather than a template — you can read `conv.env` directly:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
ENV_SENDER_NUMBERS = {
"sandbox": "+441111111111",
"pre-release": "+442222222222",
"live": "+443333333333",
}
def send_link_or_transfer(conv: Conversation, wants_sms: bool):
if wants_sms:
from_number = ENV_SENDER_NUMBERS.get(conv.env, ENV_SENDER_NUMBERS["sandbox"])
conv.send_sms(
to_number=conv.state.caller_number,
from_number=from_number,
content="Here's your booking link: https://book.example.com",
)
return "Tell the caller the link has been sent."
...
```
**Use secrets for sender numbers in production**
Avoid hardcoding phone numbers in function code. Store them as [secrets](https://docs.poly.ai/secrets/introduction) and retrieve with `conv.utils.get_secret("sms_sender_live")`.
## Related pages
Structure and variable substitution for SMS templates.
Configure transfer destinations used by `conv.call_handoff`.
Return values, conv API, and function structure.
Full reference for `conv.send_sms_template`, `conv.call_handoff`, `conv.env`, and all other `conv` attributes.
Store sensitive values like sender numbers and retrieve them at runtime with `conv.utils.get_secret`.
Configuring SMS channels, sender numbers, and template structure.
# Venue-specific goodbye with clean hangup
Source: https://docs.poly.ai/adk/examples/venue-specific-goodbye
When the agent needs to say a specific goodbye and then hang up, a common mistake is relying on the LLM to produce the closing utterance. The LLM may add its own filler ("Thank you for calling, goodbye!") before the function executes, resulting in two closing statements — or the wrong one playing.
The fix is to return the closing utterance directly from a function, combined with `hangup: True`. The function controls exactly what is said and when the call ends.
## The problem
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
# In a topic action — don't do this for goodbyes
actions: |-
Thank the caller and say goodbye.
Use {{fn:hang_up_call}} to end the call.
```
The LLM speaks its own goodbye first, then the function executes. You end up with two closing statements.
## The solution
Return the utterance and hangup from the function itself. The function speaks the closing message and ends the call atomically — no LLM turn in between.
## Files involved
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
functions/goodbye_and_hang_up.py ← closing utterance + hangup
config/variant_attributes.yaml ← per-site closing messages (optional)
topics/Goodbye.yaml ← triggers the function
```
## goodbye\_and\_hang\_up.py
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from _gen import * #
@func_description(
"Called when the caller is ready to end the call. "
"Speaks the closing message and hangs up."
)
def goodbye_and_hang_up(conv: Conversation):
# Use a variant attribute for site-specific closings, or a plain string for a single site.
# getattr guards against AttributeError if the attribute is missing for the current variant.
default_closing = "Thanks for calling. Goodbye!"
closing = getattr(conv.variant, "closing_message", default_closing) if conv.variant else default_closing
return {
"utterance": closing,
"hangup": True,
}
```
## Goodbye.yaml
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
enabled: true
example_queries:
- Goodbye
- That's all I need
- Thanks, bye
- I'm done
- No, nothing else
content: ""
actions: |-
Use {{fn:goodbye_and_hang_up}} to close the call.
Do not say anything before calling the function.
```
**The 'do not say anything' instruction**
Including "Do not say anything before calling the function" in the topic action suppresses the LLM's tendency to add its own closing line before the function fires. The utterance in the function return value is what the caller hears.
## Variant attribute for site-specific closing messages
If different locations need different goodbyes, add a `closing_message` attribute to `config/variant_attributes.yaml`. All variants and attributes are defined in this single file.
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
variants:
- name: london
- name: new_york
is_default: true
attributes:
- name: closing_message
values:
london: "Thanks for calling the London store. Goodbye!"
new_york: "Thanks for calling the New York store. Have a great day!"
```
Then access it in the function via `conv.variant.closing_message` as shown above. Every variant must have a value for every attribute — leave it as an empty string if a variant has no custom closing.
**`is_default` may be reassigned on push**
The platform controls which variant is the default. After a push and pull, the `is_default` assignment in your local file may differ from what you wrote — the server picks the canonical default. This does not affect runtime behavior, but expect the value to change on round-trip.
## Testing variants
Use the `--variant` flag with `poly chat` to verify that each variant produces the correct closing message:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly chat --variant london
poly chat --variant new_york
```
The agent should end each session with the closing message configured for that variant. If `conv.variant` is `None` (for example, if no variants are pushed yet), the function falls back to the default string.
**`--variant` resolves against the deployed environment**
`--variant` looks up the variant name in the environment you are chatting against (default: `main` in sandbox). If you pushed `variant_attributes.yaml` on a feature branch but have not merged it yet, the variant names will not exist in sandbox and the flag will have no effect. Merge the branch first — with [`poly branch merge`](/adk/reference/branch_merge) or through Agent Studio — then run `poly chat --variant `.
## Related pages
Return values, utterance, hangup, and control flow.
Per-site configuration using variant attributes.
All supported return shapes — `utterance`, `hangup`, combined dicts, and transition objects.
How variant attributes are defined, routed, and accessed via `conv.variant`.
# First commands
Source: https://docs.poly.ai/adk/get-started/first-commands
Once the ADK is installed and your API key is set, the very first thing to do is create a local project with `poly init`. After that, the fastest way to get oriented is to inspect the CLI directly from inside that project folder.
## Create your local project with `poly init`
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly init
```
Run `poly init` with no arguments. The CLI walks you through interactive dropdowns:
1. **Region** — auto-selected if your API key only has access to one.
2. **Account** — auto-selected if there's only one in the region; otherwise pick from a searchable list.
3. **Project** — pick from a searchable list of every project the API key can see.
`poly init` then creates a subdirectory at `{account_id}/{project_id}` in your current directory and pulls the project configuration down from Agent Studio. When it completes, `cd` into that folder — every other `poly` command runs from inside the project directory.
**Skip the prompts if you already know the IDs**
For scripting or repeat runs, pass any combination of flags to skip the matching prompts:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly init --account_id --project_id
poly init --region --account_id --project_id
```
The IDs appear in the Agent Studio URL when your project is open:
```
https://studio.poly.ai///...
```
`poly init --json` requires all three flags (no interactive prompts in JSON mode).
If the project has already been initialized locally at a previous point, use `poly pull` to refresh it in place instead of running `poly init` again.
## View top-level help
Run `poly --help` to see every available command:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly --help
```
Each command also accepts `--help` for its own flags and options:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly push --help
```
## Core commands
The ADK provides the following core commands:
Initialize a new Agent Studio project locally.
Pull the latest project configuration from Agent Studio.
Push local changes back to Agent Studio.
View changed, new, and deleted files in your project.
Show differences between the local project and the remote version.
Manage project branches.
Format project resources.
Validate project configuration locally.
Create a GitHub gist for reviewing changes.
Start an interactive chat session with your agent.
Revert local changes.
## Explore any command
To learn what a command does and what flags it accepts, run it with `--help`:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly init --help
poly pull --help
poly push --help
```
## Common first-run behavior
### `poly status` shows variables you didn't create
After `poly init` or `poly pull`, `poly status` may report `variables/` entries as new files — for example, `variables/caller_number` or `variables/verified_record`. These are **virtual**: the ADK scans function code for `conv.state.*` assignments and tracks each one as a variable resource. No corresponding files exist on disk. This is expected and does not mean you have changes to push.
### `poly status` shows platform-generated functions as modified
After a fresh `poly init` or `poly pull`, `poly status` may report functions such as `functions/get_api_keys.py` or `functions/check_otp.py` as modified, even though you have not touched them. The diff is typically a single stripped blank line introduced by the platform. These are harmless — the ADK and the platform have slightly different whitespace conventions for generated code. You can push through them or ignore them.
### `poly branch switch` reports uncommitted changes
If `poly status` shows phantom `variables/` entries or modified platform functions and you try to switch branches, the ADK may block the switch:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Cannot switch branches with uncommitted changes. Use --force to switch and discard changes.
```
Use `--force` to override:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly branch switch --force
```
This does not lose any real work — the `variables/` entries are virtual and will reappear after the next pull.
### `poly chat` returns a 404 on a feature branch
`poly chat` defaults to chatting against your current branch's last pushed state. On most projects this works fine; on some projects the branch deployment endpoint returns a 404:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Error: 404 ... /branches//sequence
```
If you hit this, push your changes with [`poly push`](/adk/reference/cli#poly-push), merge the branch with [`poly branch merge`](/adk/reference/branch_merge) (or in the Agent Studio UI), then chat against sandbox instead:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly chat --environment sandbox
```
### `poly branch delete` fails with a 404 or requires a TTY
`poly branch delete` triggers the same platform endpoint as branch chat. If the endpoint is unavailable, the delete will fail with a 404 after the confirmation prompt. Additionally, running `poly branch delete` in a non-interactive environment (for example, from a script) throws `[Errno 22] Invalid argument` because the command requires a terminal for its confirmation prompt.
If you are stuck with a branch you cannot delete from the CLI, delete it through the Agent Studio UI instead.
## Next step
Continue to the command reference for a complete listing, or go straight to the tutorial to see a real workflow.
See a more detailed overview of the available commands.
Follow the step-by-step workflow for using the ADK in practice.
# Getting started with PolyAI
Source: https://docs.poly.ai/adk/get-started/get-started
The fastest way to get up and running is entirely from the command line. Two steps — install the ADK, then sign in — take you from an empty machine to a local project you can edit, push, and deploy. Sign-in uses `poly start` (self-serve) or `poly login` (enterprise), described below.
***
## Step 1 — Install the ADK
You need **uv** to manage the Python environment. If you already have it, skip the first line.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -LsSf https://astral.sh/uv/install.sh | sh # or: brew install uv
```
Then create a virtual environment and install the ADK:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
uv venv --python=3.14 --seed
source .venv/bin/activate
pip install polyai-adk
```
Confirm it worked:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly --help
```
**Suppress SyntaxWarnings from platform-generated code**
Platform-generated code uses regex patterns (such as `\d`) that trigger `SyntaxWarning` in Python 3.14's stricter string handling. This produces 40+ warning lines on every `poly` command and obscures normal output.
To suppress them, set this before running any `poly` command:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export PYTHONWARNINGS=ignore
```
**Optional — install the VS Code / Cursor extension**
If you plan to work in **VS Code** or **Cursor**, you can also install the [PolyAI ADK extension](/adk/reference/tooling#polyai-adk-extension-for-vs-code-and-cursor) for resource-aware editing on top of the CLI. The extension is additive — the `poly` command remains the source of truth for every workflow.
## Step 2 — Sign in and set up your API key
The right setup path depends on the type of Agent Studio account you have:
* **Self-serve accounts** (signed up at [studio.poly.ai](https://studio.poly.ai)) — use [`poly start`](#self-serve-accounts-poly-start) for an end-to-end setup that creates an account, an API key, and an optional first project.
* **Enterprise accounts** (a workspace provisioned by PolyAI on a regional cluster such as `us-1`, `euw-1`, or `uk-1`) — use [`poly login --region `](#enterprise-accounts-poly-login-or-manual-api-key) to sign in through your browser, or create an API key in the Agent Studio UI and [export it manually](#manual-api-key-export). `poly start` is self-serve only and does not work against enterprise clusters.
If you're not sure which you have, your PolyAI contact can confirm.
### Self-serve accounts — `poly start`
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly start
```
`poly start` handles everything you need to authenticate:
1. **Sign up or sign in** — opens a browser window for authentication. This can be on any device, not just the machine running the CLI.
2. **API key** — generates a key and saves it to `~/.poly/credentials.json`. Future `poly` commands pick it up automatically — no environment variables to manage.
3. **Create a project** — optionally creates a new Agent Studio project and pulls it down locally so you can start editing immediately.
**Already have a self-serve account?**
If `poly start` detects an existing API key (from the credential file or an environment variable), it asks whether to keep using it — accept and you skip straight to the project creation step. Decline and it runs the full sign-in flow again.
### Enterprise accounts — `poly login` or manual API key
Enterprise workspaces have two options. `poly login` is the quickest path for most users; the manual export is the fallback if you can't authenticate through the browser (for example, on a CI runner).
#### Option 1 — `poly login` (recommended)
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly login --region us-1 # or euw-1, uk-1
```
`poly login`:
1. Opens a browser window so you can sign in to your enterprise workspace.
2. Fetches (or creates) an API key for your user.
3. Saves it to `~/.poly/credentials.json` under the region you specified, so future `poly` commands pick it up automatically.
If you omit `--region`, the CLI prompts you to pick one. To sign in to more than one region from the same machine, re-run `poly login` with each region — the credential file stores them side by side.
#### Option 2 — Manual API key export
If you'd rather create the key yourself in the Agent Studio UI:
1. Log in to Agent Studio for your region and open your workspace.
2. In the **API Keys** tab (next to the **Users** tab), click **+ API key**.
Then export the key:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export POLY_ADK_KEY=
```
To make it permanent, add the export line to your shell profile (`~/.zshrc` or `~/.bashrc`). If you work across more than one region, use the region-scoped variables in [per-region API keys](#per-region-api-keys) below.
**How the ADK resolves API keys**
The ADK checks for credentials in the following order:
1. **Credential file** — `~/.poly/credentials.json` (written by `poly start` or `poly login`)
2. **Region-specific env var** — e.g. `POLY_ADK_KEY_US`
3. **General env var** — `POLY_ADK_KEY`
The first match wins. If nothing is found, the CLI raises an error.
## Step 3 — Start building
If `poly start` created a project for you, `cd` into the project directory. Otherwise, connect to an existing project:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly init
```
[`poly init`](/adk/reference/cli#poly-init) walks you through interactive dropdowns to pick a region, account, and project, then pulls the configuration locally.
From inside your project directory, the core workflow is:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly status # see what's changed
poly diff # inspect changes in detail
poly branch create dev # work on a branch
poly push # push changes to Agent Studio
poly chat # talk to your agent
```
Edit flows, functions, topics, and other resources in your editor of choice — they're just YAML and Python files. Push when you're ready to test in Agent Studio.
Follow the full step-by-step tutorial for local development.
Explore the full set of CLI commands available to you.
***
## Seed an agent from your website
If you're starting from scratch and want a working baseline, you can generate an agent from your company website inside Agent Studio. This gives you topics and agent settings pre-populated from your site's public content — a useful starting point before building locally.
1. Open [Agent Studio](https://studio.poly.ai) for self-serve, or your region's URL for enterprise, and sign in with the same account you authenticated with above.
2. Click **+ Agent** → **Quick Agent Setup**.
3. Enter your website URL and click **Create agent**.
Agent Studio crawls your site and generates a configuration — usually within a few minutes. Once it's ready, pull it into your local project:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly pull
```
**What gets generated**
Agent Studio populates **topics** (knowledge base entries) and basic **agent settings** (personality, role, rules) from your website's public content. It does not generate flows, variants, entities, handoffs, or integrations — those are for you to build locally with the ADK.
***
## Already have an agent in Agent Studio?
If you have an existing project — built in the browser, by a PolyAI team, or by any other method — connect it to the ADK with `poly init` once your API key is set up:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Self-serve accounts:
poly start # sign in and save your API key (skip if already done)
poly init # interactive prompts to pick region, account, and project
# Enterprise accounts (poly login — recommended):
poly login --region us-1 # or euw-1, uk-1
poly init
# Enterprise accounts (manual key export, fallback):
export POLY_ADK_KEY= # see "Manual API key export" above
poly init
```
`poly init` creates a local directory and pulls the full project configuration. From there the standard `poly status` / `poly push` / `poly pull` workflow applies.
***
## Per-region API keys
If you work across multiple regions, you can set region-scoped environment variables. The ADK checks the credential file first, then region-scoped env vars, then `POLY_ADK_KEY`.
| Region | Environment variable |
| --------- | ---------------------- |
| `us-1` | `POLY_ADK_KEY_US` |
| `euw-1` | `POLY_ADK_KEY_EUW` |
| `uk-1` | `POLY_ADK_KEY_UK` |
| `studio` | `POLY_ADK_KEY_STUDIO` |
| `staging` | `POLY_ADK_KEY_STAGING` |
| `dev` | `POLY_ADK_KEY_DEV` |
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export POLY_ADK_KEY_US=
export POLY_ADK_KEY= # used for any other region
```
***
## Next step
Understand what the ADK does and how it fits into Agent Studio.
# Prerequisites
Source: https://docs.poly.ai/adk/get-started/prerequisites
Before using the **PolyAI ADK**, you need a couple of local tools. If you run into any issues, contact [developers@poly-ai.com](mailto:developers@poly-ai.com).
## Local requirements
| Tool | Version | Notes |
| ------- | ------- | ---------------------------------------------------------------------- |
| **uv** | latest | Manages Python and virtual environments |
| **Git** | any | Optional — recommended for version control of your local project files |
### Install uv
`uv` manages Python versions for you, including the version required by the ADK. Install it with:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -LsSf https://astral.sh/uv/install.sh | sh
```
Alternatively, with Homebrew on macOS:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
brew install uv
```
See the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/) for more options.
## API key
The ADK needs an API key to communicate with Agent Studio. How you obtain it depends on your account type:
* **Self-serve accounts** ([studio.poly.ai](https://studio.poly.ai)) — `poly start` signs you in and saves the key automatically.
* **Enterprise accounts** (regional clusters such as `us-1`, `euw-1`, `uk-1`) — `poly login --region ` signs you in through the browser and saves the key. Alternatively, create the key in the Agent Studio UI and export it manually. `poly start` is self-serve only.
See [Getting started](/adk/get-started/get-started#step-2-sign-in-and-set-up-your-api-key) for the full walkthrough.
**API keys are workspace-scoped**
An API key grants access to one specific Agent Studio workspace. When you run `poly init`, it lists all projects visible to that key. If you see projects that don't look like yours, you may be using a key scoped to the wrong workspace. Contact your PolyAI contact to confirm.
## Checklist
Before continuing, confirm:
* `uv` is installed
* You have an API key — saved by `poly start` (self-serve), saved by `poly login` (enterprise), or exported manually as `POLY_ADK_KEY`
## Next step
Initialize a project, pull configuration, and push your first change.
# Walkthrough Video
Source: https://docs.poly.ai/adk/get-started/walkthrough-video
This walkthrough shows how to build a production-ready voice agent with the **PolyAI ADK**.
It demonstrates the end-to-end developer workflow and shows how the ADK fits alongside the [PolyAI ADK extension for VS Code and Cursor](/adk/reference/tooling#polyai-adk-extension-for-vs-code-and-cursor).
## Watch the video
## What this walkthrough covers
Watch the ADK used alongside the PolyAI ADK extension for VS Code or Cursor to accelerate implementation.
Follow the flow from setup to a working agent in a short amount of time.
**Best viewed alongside the docs**
This walkthrough is a useful companion to the installation and getting started pages. It is often easiest to watch the workflow once, then follow the written steps yourself.
## Next steps
Install the ADK and get your first project running.
Follow the step-by-step tutorial for building with the ADK.
# What is the PolyAI ADK?
Source: https://docs.poly.ai/adk/get-started/what-is-the-adk
The **PolyAI ADK (Agent Development Kit)** is a **CLI tool and Python package** for managing **Agent Studio** projects on your local machine.
It gives you a Git-like workflow for synchronizing project configuration between your local filesystem and the Agent Studio platform.
**The ADK manages configuration files — it does not run your agent**
The ADK handles pulling, editing, validating, and pushing project configuration between your local machine and Agent Studio. Agent execution — processing calls, running conversations — happens entirely inside Agent Studio. There is no local runtime.
## What you can do with the ADK
* Build and edit Agent Studio projects locally using standard tooling
* Synchronize project configuration with Agent Studio using `poly push` and `poly pull`
* Branch, validate, and review changes before deployment
* Edit and navigate projects in **VS Code** or **Cursor** with the [PolyAI ADK extension](/adk/reference/tooling#polyai-adk-extension-for-vs-code-and-cursor), or pair the ADK with [AI coding agents](/adk/reference/tooling#claude-code) such as **Claude Code**
* Collaborate across multiple developers on the same project
## Why it exists
The ADK moves most build-and-edit work out of the browser and into your local environment. You can [merge branches](/adk/reference/branch_merge) and run reviews from the CLI, while Agent Studio remains the home for deployment and production monitoring — but you no longer have to edit resources there by hand.
Instead of editing everything directly inside Agent Studio, you pull a project locally, make changes using your normal tools, and push those changes back to the platform.
This makes it straightforward to:
* edit resources in your own editor, with the tooling you already use
* collaborate across a team without overwriting each other's work
* validate and review changes before pushing them live
* automate repetitive build work with coding tools
## Multi-developer workflows
The ADK supports team workflows out of the box. See [multi-user workflows and guardrails](/adk/concepts/multi-user-and-guardrails) for details on branching, validation, and review.
It preserves the same guardrails as Agent Studio, so developers cannot push changes that are incompatible with the project.
**Git-like, but for Agent Studio**
Think of the ADK as the local development layer for Agent Studio: pull, edit locally, validate, and push.
## Next steps
See a practical demonstration of the ADK in use.
Set up uv, Git, and your API key before running your first commands.
Initialize a project, pull configuration, and push your first change.
# PolyAI ADK Docs
Source: https://docs.poly.ai/adk/index
Build and edit Agent Studio projects locally with the **PolyAI ADK**, then push them back to Agent Studio to review and deploy.
The ADK gives you a local, Git-like workflow for Agent Studio projects: pull, edit with standard tooling, validate, and push.
Source code, issues, and releases for the `polyai-adk` CLI.
## From zero to a local project
A few commands take you from an empty machine to a working local copy of your agent:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -LsSf https://astral.sh/uv/install.sh | sh # install uv (skip if you have it)
uv venv --python=3.14 --seed
source .venv/bin/activate
pip install polyai-adk
poly start # self-serve sign-up, API key, and project in one go
```
`poly start` is for self-serve accounts on [studio.poly.ai](https://studio.poly.ai). If your workspace is on an enterprise cluster (`us-1`, `euw-1`, `uk-1`), run `poly login --region ` instead — or export your API key manually. See [Getting started](/adk/get-started/get-started#enterprise-accounts-poly-login-or-manual-api-key).
See [Getting started](/adk/get-started/get-started) for the full walkthrough, [Prerequisites](/adk/get-started/prerequisites) for local tool setup, and [First commands](/adk/get-started/first-commands) for a guide to `poly init` and the core CLI.
## Start here
Build a working voice agent from your website in minutes, then pull it into the ADK.
Understand what the ADK does and where it fits in the Agent Studio workflow.
Follow the end-to-end workflow from project setup to deployment.
See every `poly` command and its flags.
## What this site covers
This documentation follows the developer journey:
* understanding what the ADK is and how it fits into Agent Studio
* installing it and running the first commands
* building, reviewing, and deploying agents
* reference for all CLI commands, resource types, and tooling
## Recommended path
If you are new to the ADK, follow this order:
1. follow [**Getting started**](/adk/get-started/get-started) — install the ADK, set up your API key (`poly start` for self-serve, `poly login` or a manual export for enterprise), and create your first project
2. read [**What is the PolyAI ADK?**](/adk/get-started/what-is-the-adk)
3. use [**First commands**](/adk/get-started/first-commands) — explore the core CLI commands
4. continue to [**Build an agent with the ADK**](/adk/tutorials/build-an-agent)
# License acknowledgements
Source: https://docs.poly.ai/adk/legal/licensing
PolyAI ADK uses several third-party open source software packages. We gratefully acknowledge the contributions of the open source community. The packages and licensing can be found in [licenses.json](https://github.com/polyai/adk/blob/main/licenses.json).
## Full license texts
### MIT License
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
### Apache License 2.0
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License.
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work.
"Contribution" shall mean any work of authorship submitted to the
Licensor for inclusion in the Work.
"Contributor" shall mean Licensor and any Legal Entity on behalf of
whom a Contribution has been received by Licensor.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
patent license to make, have made, use, offer to sell, sell,
import, and otherwise transfer the Work.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work; and
(d) If the Work includes a "NOTICE" text file, You must include
a readable copy of the attribution notices contained
within such NOTICE file.
You may add Your own attribution notices within Derivative Works
that You distribute.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
8. Limitation of Liability. In no event shall any Contributor be
liable for any damages arising from the use of the Work.
9. Accepting Warranty or Additional Liability. You may choose to offer
warranty, support, indemnity, or other liability obligations.
END OF TERMS AND CONDITIONS
```
### BSD 3-Clause License
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
BSD 3-Clause License
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
### Mozilla Public License 2.0 (MPL 2.0)
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
```
# Agent settings
Source: https://docs.poly.ai/adk/reference/agent_settings
Agent settings define the agent's identity and behavioral rules.
They live in agent\_settings/ and are made up of personality, role, and rules resources.
**Personality and role are platform-provisioned — update only**
The personality and role resources are created automatically by the platform when a project is created. They always exist on any Agent Studio project and can be updated with `poly push`, but cannot be created from scratch via the ADK. If these files appear in a project directory without matching entries in `.agent_studio_config` — for example, after copying a directory from another project — the push will fail with a "Create operation not supported" error. Always start a new project with [`poly init`](/adk/reference/cli#poly-init) and [`poly pull`](/adk/reference/cli#poly-pull) rather than copying an existing directory.
These settings shape how the agent presents itself and how it should behave across the conversation.
## Location
Agent settings live under:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent_settings/
├── languages.yaml # Optional
├── personality.yaml
├── role.yaml
├── rules.txt
├── safety_filters.yaml # Optional
└── experimental_config.json # Optional
```
## What agent settings control
Controls the agent's tone and conversational style.
Defines what the agent is and what kind of job it performs.
Provides plain-text instructions the agent should follow on every turn.
Configures the default language and any additional languages the agent supports.
Project-level content safety filtering across four categories.
Optional advanced feature flags and tuning.
## Personality
The `personality.yaml` file controls the agent's conversational tone.
### Fields
| Field | Description |
| ------------ | ------------------------------------- |
| `adjectives` | Map of personality traits to booleans |
| `custom` | Free-text personality description |
### Adjectives
Allowed adjective values are:
* `Polite`
* `Calm`
* `Kind`
* `Funny`
* `Energetic`
* `Thoughtful`
* `Other`
If `Other` is set to `true`, no other adjective can be selected.
**Non-standard adjectives**
The platform may return adjectives not in the local allowed set (for example, deprecated or newly added adjectives). Validation only fails for adjectives that are **enabled** (`true`) and not in the allowed set. Disabled (`false`) non-standard adjectives pass validation and are silently excluded from the update payload when pushing.
### `custom`
The `custom` field is a free-text description of the personality.
It supports:
* `{{attr:...}}`
* `{{vrbl:...}}`
### Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
adjectives:
Polite: true
Calm: true
Kind: true
custom: ""
```
## Role
The `role.yaml` file defines what the agent is.
This is usually the agent's role, title, or function in the business context.
### Fields
| Field | Description |
| ----------------- | ------------------------------------------------------- |
| `value` | Role name, such as `Customer Service Representative` |
| `additional_info` | Extra context about the role |
| `custom` | Free-text role description used when `value` is `other` |
If `value` is set to `other`, the `custom` field is used instead.
The `custom` field supports:
* `{{attr:...}}`
* `{{vrbl:...}}`
### Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
value: Customer Service Representative
additional_info: Handles customer inquiries and bookings
custom: ""
```
## Rules
The `rules.txt` file contains plain-text behavioral instructions that the agent should follow on every turn.
This is one of the most important files for shaping agent behavior.
### Supported references
The rules file supports the following references:
| Syntax | Meaning |
| -------------------------------------------- | ---------------------------------------------- |
| `{{fn:function_name}}` | [Global function](/adk/reference/functions) |
| `{{twilio_sms:template_name}}` | [SMS template](/adk/reference/sms) |
| `{{ho:handoff_name}}` | [Handoff destination](/adk/reference/handoffs) |
| `{{attr:attribute_name}}` | [Variant attribute](/adk/reference/variants) |
| `{{vrbl:variable_name}}` or `$variable_name` | [State variable](/adk/reference/variables) |
### Example
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Be helpful and professional at all times.
Use {{fn:validate_email}} when the user provides an email address.
For complex issues, use {{ho:escalation_handoff}} to transfer to a specialist.
Send confirmation via {{twilio_sms:confirmation_template}} after booking.
```
## Writing effective rules
Rules are most useful when they are:
* concise
* explicit
* actionable
* stable across turns
Good rules tell the agent what standard it should follow, not how to perform step-by-step branching logic.
**Use rules for behavioral guidance**
Rules are a good place for durable operating principles such as escalation behavior, safety guidance, or how the agent should handle common classes of requests.
## What not to put in rules
Avoid putting deterministic branching logic into `rules.txt`.
### Avoid
* long conditional logic chains
* step-by-step routing logic
* hard-coded values that should come from references
For example, do not write logic such as:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
If $x == 0 do A, else do B.
```
That kind of logic belongs in flows and Python functions.
### Prefer
* references such as `{{fn:...}}`, `{{attr:...}}`, and `{{vrbl:...}}`
* concise instructions that apply broadly
* deterministic logic handled in code or flow transitions
## Languages
The optional `languages.yaml` file configures which languages the agent supports. When present, it defines the default language and any additional languages.
See the [Languages reference](/adk/reference/languages) for full field descriptions, validation rules, and examples.
## Safety filters
The `safety_filters.yaml` file configures project-level content safety filtering. It controls whether harmful content is filtered across all channels by default.
See the [Safety filters reference](/adk/reference/safety_filters) for field descriptions, schema, and examples.
## Best practices
* keep rules concise and actionable
* use references instead of hard-coded values
* use `custom` personality and role text only when you need more than the structured fields provide
* treat rules as a global behavioral layer, not a place for detailed flow logic
## Related pages
Learn how referenced global functions are defined and used.
Configure default and additional language settings for the agent.
Define localized text strings per language.
Configure content safety filtering at the project and channel level.
Configure optional advanced features and runtime overrides.
# Branch merging
Source: https://docs.poly.ai/adk/reference/branch_merge
Merge a feature branch back into main with poly branch merge, including interactive and pre-defined conflict resolution.
`poly branch merge` is the CLI-native counterpart to merging in the Agent Studio web UI. It brings everything you've changed on the current branch back onto `main`, surfaces any conflicts in a structured table, and lets you resolve them either interactively or from a JSON file.
For the broader branching workflow (creating, switching, listing, deleting branches), see the [`poly branch` section of the CLI reference](/adk/reference/cli#poly-branch). For the team-level guardrails around branching and merging, see [Multi-user workflows and guardrails](/adk/concepts/multi-user-and-guardrails).
## When to use it
You'll typically reach for `poly branch merge` at the end of a feature loop:
1. Create a branch with `poly branch create my-feature` (see [`poly branch create`](/adk/reference/cli#poly-branch-create)).
2. Iterate locally, pushing with `poly push` (see [Working locally](/adk/concepts/working-locally)).
3. Test with `poly chat` against the branch's pushed state.
4. Merge back to `main` with `poly branch merge ''` — described on this page.
5. Optionally deploy through Agent Studio.
You can also merge from the Agent Studio web UI by switching to the branch and clicking **Merge**. The CLI command and the UI hit the same platform endpoint, so the result is identical.
## Basic usage
`poly branch merge` requires a merge message and merges the **current branch** into `main`. Switch to the source branch first if you aren't on it.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly branch switch my-feature
poly branch merge 'Merge my-feature into main'
```
If the merge has no conflicts, the branch is merged immediately and the CLI automatically switches your local checkout to `main`. Run `poly pull` afterwards if you need to refresh local state.
| Argument | Required | Description |
| ------------------------ | -------- | ---------------------------------------------------------------------------------- |
| `message` | yes | Merge commit message. Quote it if it contains spaces. |
| `--interactive`, `-i` | no | Resolve conflicts in an interactive prompt. |
| `--resolutions ` | no | Pre-defined resolutions as a JSON file path, inline JSON string, or `-` for stdin. |
| `--path ` | no | Project base path. Defaults to the current working directory. |
| `--json` | no | Print a single JSON object on stdout (machine-readable). |
| `--verbose` | no | Show full error tracebacks for debugging. |
## Conflicts
If the merge has conflicts, the command prints a conflict table and exits with a non-zero status code. The table shows, for each conflicting field:
* **Path** — the resource and field that conflicts (for example `topics > Booking > content`)
* **Base / Ours / Theirs** — the original value and the two competing values
* **Auto-merged value** — what the ADK would produce by line-merging the two sides
* **Auto-mergeable** — whether the auto-merged value contains any unresolved markers
If every conflict is auto-mergeable and you want to accept the auto-merge, re-run the command with `--interactive` and accept the suggestions, or pre-populate `--resolutions` with the auto-merge values.
### `--interactive` / `-i`
Interactive mode walks you through each conflict and asks how to resolve it. For every conflict you can:
* accept the auto-merge (when available)
* pick `main` (`ours`)
* pick branch (`theirs`)
* pick `base` (revert to the original value)
* open the value in your `$EDITOR` or `$VISUAL` for free-form editing
After you've answered every conflict the merge is re-attempted automatically.
**Set `$EDITOR` or `$VISUAL` before starting an interactive merge**
Interactive mode shells out to your editor for multiline or long values. If neither variable is set it falls back to `vi`. Setting `EDITOR=code --wait` (or your editor of choice) in your shell profile makes the experience much smoother.
### `--resolutions `
Use `--resolutions` to supply pre-defined resolutions non-interactively. The source can be:
* a path to a JSON file
* a literal JSON string
* `-` to read JSON from stdin
If the resolutions cover every conflict the merge proceeds without prompting. Combine `--resolutions` with `--interactive` to seed an interactive session — pre-defined choices are applied automatically and you'll only be prompted for the conflicts they don't cover.
#### Resolution file format
`--resolutions` expects a JSON array of objects:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
[
{
"path": ["topics", "Booking", "content"],
"strategy": "theirs"
},
{
"path": ["agent_settings", "rules", "value"],
"strategy": "theirs",
"value": "Custom resolved content here"
},
{
"path": ["flows", "main_flow", "steps", "greet", "prompt"],
"strategy": "ours"
}
]
```
| Field | Description |
| ---------- | -------------------------------------------------------------------------------------------------- |
| `path` | List of strings identifying the conflicted field. Match the `Path` column from the conflict table. |
| `strategy` | One of `"ours"` (keep `main`), `"theirs"` (keep branch), or `"base"` (revert to the original). |
| `value` | Optional custom value. Only honored with the `"theirs"` strategy. |
You can capture the structure of a resolution file by running `poly branch merge` once to surface the conflicts, then writing a JSON file that addresses each `path` row.
## After a successful merge
* The CLI switches your local checkout to `main`.
* Run [`poly pull`](/adk/reference/cli#poly-pull) if you need to refresh local state to match the post-merge `main`.
* Run [`poly chat`](/adk/reference/cli#poly-chat) against `main` (which falls back to the sandbox environment) to smoke-test the merged result.
* If you're ready to ship, follow up with [`poly deployments`](/adk/reference/cli#poly-deployments) to promote the merged state to a live environment.
## Merging through the Agent Studio web UI
You can also merge through the Agent Studio interface:
1. Open the project in Agent Studio.
2. Switch to the branch.
3. Click **Merge**.
The web UI surfaces the same conflicts as the CLI and lets you resolve them in the browser. Use whichever path fits your workflow — they hit the same platform endpoint, so there is no functional difference between them.
## Troubleshooting
| Symptom | Likely cause | Fix |
| ------------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `Merge message is required` | You ran `poly branch merge` with no message argument. | Pass a quoted message: `poly branch merge 'Describe the merge'`. |
| Conflict table appears and the command exits non-zero | One or more fields conflict between branch and `main`. | Re-run with `--interactive` or supply `--resolutions`. |
| `[Errno 22] Invalid argument` during interactive prompt | The shell isn't a TTY (CI, scripts, non-interactive containers). | Run interactively, or use `--resolutions` with a pre-built JSON file. |
| Editor doesn't open in interactive mode | `$EDITOR` and `$VISUAL` are unset. | Export one of them before running the merge. |
| Local changes block the merge | You have unpushed work on the source branch. | Run [`poly push`](/adk/reference/cli#poly-push) first, or [`poly revert`](/adk/reference/cli#poly-revert) to discard. |
## Related references
Validate agent behavior with conversation tests before merging.
IDE extensions and AI coding tools that integrate with the ADK workflow.
How branches, merges, and validation interact across a team.
# Chat settings
Source: https://docs.poly.ai/adk/reference/chat_settings
Chat settings configure how the agent behaves on the web chat channel.
They are defined in chat/configuration.yaml.
**Platform-provisioned — update only**
Chat settings are created automatically when a project is created. They can be updated with `poly push` but not created from scratch. See the [equivalent note on agent settings](/adk/reference/agent_settings) for details.
## Location
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
chat/
├── configuration.yaml
└── safety_filters.yaml # Optional
```
## What chat settings control
The first message the agent sends when a chat session starts.
Channel-specific instructions that shape how the agent writes in chat.
Optional chat-channel content safety filter overrides.
## Greeting
The greeting is the first message the agent sends when a chat session starts.
### Fields
| Field | Required | Description |
| ----------------- | -------- | ---------------------------------------------------------------------------- |
| `welcome_message` | Yes | Text of the greeting. Supports `{{attr:...}}` and `{{vrbl:...}}` references. |
| `language_code` | Yes | BCP-47 language code, for example `en-GB` or `en-US`. |
### Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
greeting:
welcome_message: Hi there! How can I help you today?
language_code: en-GB
```
## Style prompt
The style prompt contains channel-specific instructions that shape how the agent writes.
Use this for chat-specific guidance such as:
* keeping responses concise
* using bullet points for lists
* adjusting formatting for readability
This is separate from the agent's broader personality. Use it to control how the agent communicates specifically in web chat.
### Fields
| Field | Required | Description |
| -------- | -------- | ------------------------------------------------------------------ |
| `prompt` | No | Free-text style instructions. Resource references are not allowed. |
### Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
style_prompt:
prompt: You are a helpful and professional web chat assistant. Keep responses concise and use formatting where appropriate.
```
**Keep chat guidance channel-specific**
Use the style prompt for instructions that only apply to chat, such as formatting, brevity, or written tone. Use agent settings for broader identity and behavioral guidance.
## Safety filters
`chat/safety_filters.yaml` is an optional file that overrides the project-level safety filter settings for the chat channel. When present, it takes precedence over `agent_settings/safety_filters.yaml` for chat interactions.
See the [Safety filters reference](/adk/reference/safety_filters) for the full schema, field descriptions, and examples.
## Full example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
greeting:
welcome_message: Hi! How can I help you today?
language_code: en-GB
style_prompt:
prompt: You are a helpful and professional web chat assistant. Keep responses concise.
```
## Related pages
Configure content safety filtering at the project and channel level.
Define the agent's overall identity, role, and rules.
Configure the equivalent behavior for the voice channel.
# CLI reference
Source: https://docs.poly.ai/adk/reference/cli
The PolyAI ADK is accessed through the poly command.
Use the CLI help output as the first source of truth.
## Start with help
To see all available commands and options:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly --help
```
Each command also supports its own help output. For example:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly push --help
```
**Use help output as the source of truth**
The installed CLI is the fastest way to confirm the commands and flags available in your local environment.
## Core commands
### `poly start`
End-to-end onboarding for **self-serve** accounts on [studio.poly.ai](https://studio.poly.ai). `poly start` is hardcoded to the `studio` region — for any other region, use [`poly login`](#poly-login).
`poly start`:
1. Opens a browser window so you can sign up or sign in to a self-serve workspace.
2. Generates an API key (or reuses your existing one) and writes it to `~/.poly/credentials.json` under the `studio` region.
3. Optionally creates a new Agent Studio project and pulls it down locally.
If the ADK detects an existing API key in the credential file or environment, `poly start` asks whether to use it. Accept and the command skips ahead to the project-creation prompt; decline and it runs the full sign-in flow.
Examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly start
poly start --base-path /path/to/projects
```
| Flag | Description |
| ------------- | ---------------------------------------------------------------------------------- |
| `--base-path` | Base path to initialize the project in. Defaults to the current working directory. |
### `poly login`
Sign in to an existing Agent Studio account and save API key credentials for the CLI. Works against any region — including `studio`, which makes `poly login --region studio` a viable alternative to `poly start` for self-serve users on a new machine who already have an account and don't need to create a project.
`poly login`:
1. Prompts for a region if `--region` is not supplied.
2. Opens a browser window for sign-in via the Auth0 device authorization flow.
3. Fetches or creates an API key for your user and saves it to `~/.poly/credentials.json` under the chosen region.
Run `poly login` once per region you need access to — credentials for multiple regions are stored side by side in the credential file.
Examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly login
poly login --region us-1
poly login --region euw-1
poly login --region uk-1
poly login --region studio
```
| Flag | Description |
| ---------- | ------------------------------------------------------------------------------------------------------ |
| `--region` | Region to log in to. If omitted, you are prompted to pick one. Choices match the standard region list. |
### `poly project`
Manage Agent Studio projects.
#### `poly project create`
Create a new Agent Studio project under an account, then initialize it locally.
Run with no arguments and `poly project create` walks you through interactive prompts:
1. **Region** — auto-selected if your API key only has access to one.
2. **Account** — auto-selected if there's only one in the region; otherwise pick from a searchable list.
3. **Project name** — free-text name for the new project.
4. **Project ID** — optional slug. Defaults to a slugified version of the name (lowercased, spaces replaced with hyphens, special characters removed). Leave empty to let the platform generate one.
After the project is created in Agent Studio, `poly project create` automatically calls `poly init` to initialize the local project directory.
Examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly project create
poly project create --region us-1 --account_id my-account --name my-project
poly project create --region us-1 --account_id my-account --name "My Project" --id my-project
poly project create --region us-1 --account_id my-account --name my-project --greeting "Hi, how can I help?"
poly project create --base-path /path/to/projects
```
| Flag | Description |
| ---------------------- | ----------------------------------------------------------------------------------------------------------- |
| `--region` | Region for the new project. Choices match the standard region list. |
| `--account_id` | Account ID to create the project under. |
| `--name` | Display name for the new project. |
| `--id`, `--project_id` | Optional slug/ID for the project. Defaults to a slugified version of the name. |
| `--greeting` | Initial greeting message for the agent. Defaults to `"Hello, how can I help you?"`. |
| `--voice-id` | Voice ID for the agent. Defaults to a region-specific voice if not supplied. |
| `--base-path` | Base path to initialize the project in. Defaults to the current working directory. |
| `--json` | Print a single JSON object on stdout (machine-readable). Requires `--region`, `--account_id`, and `--name`. |
**`--json` requires explicit flags for `poly project create`**
When using `poly project create --json`, you must supply `--region`, `--account_id`, and `--name` explicitly. Interactive prompts are not supported in JSON mode.
#### Error handling
| Situation | Behaviour |
| ------------------------------------------------------------- | ------------------------------------------------- |
| `--json` used without `--region`, `--account_id`, or `--name` | Exits with `{ "success": false, "error": "..." }` |
| No accessible regions found | Exits with an error |
| No accounts found in the selected region | Exits with an error |
| API error during project creation | Exits with an error; local init is not attempted |
| No project ID returned by the API | Exits with an error; local init is not attempted |
### `poly init`
Initialize a new Agent Studio project locally.
Run with no arguments and `poly init` walks you through interactive dropdowns:
1. **Region** — auto-selected if your API key only has access to one.
2. **Account** — auto-selected if there's only one in the region; otherwise pick from a searchable list. Each entry is shown as `"name (id)"` to disambiguate accounts that share the same display name.
3. **Project** — pick from a searchable list of every project the API key can see. Each entry is shown as `"name (id)"` for the same reason.
If no projects are found in the selected account, `poly init` offers to create one. Accepting the prompt starts the [`poly project create`](#poly-project-create) flow with the region and account already pre-selected.
After selection, `poly init` creates the project directory at `{base_path}/{account_id}/{project_id}` and immediately pulls the current configuration from Agent Studio. Change into the project directory before running any other commands.
The human-readable project name is stored in `project.yaml` alongside the `project_id`, `account_id`, and `region`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
project_id: my-project
account_id: my-workspace
region: us-1
project_name: My Project
```
Pass any combination of `--region`, `--account_id`, and `--project_id` to skip the matching prompt. This is the form to use in scripts and CI.
Examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly init
poly init --account_id 123 --project_id my_project
poly init --region us-1 --account_id 123 --project_id my_project
poly init --base-path /path/to/projects
poly init --format
```
#### Error handling
If the account or project ID is invalid or inaccessible, `poly init` returns a descriptive error and cleans up any partially created directories so no empty folders are left behind.
| Situation | Error message |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `POLY_ADK_KEY` not set | `POLY_ADK_KEY environment variable is not set. Export your API key with: export POLY_ADK_KEY=` |
| No accounts found in the region | `No accounts found in the selected region.` |
| No projects found in the account | Prompts to create a new project (interactive) or exits with error (JSON mode). |
| Project not found | `Project '' not found in account ''.` |
| Permission denied | `Forbidden: you do not have permission to access project '' in account ''.` |
When using `--json`, the response includes `{ "success": false, "error": "..." }` with the same message.
### `poly pull`
Pull the latest project configuration from Agent Studio.
Examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly pull
poly pull --force
poly pull --format
```
If the branch you are currently on no longer exists in Agent Studio, `poly pull` automatically switches to the `main` branch and displays a warning message with the new branch name.
When using JSON output (`--json`), the response includes `new_branch_name` and `new_branch_id` fields if a branch switch occurred.
### `poly push`
Push local changes to Agent Studio.
Examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly push
poly push --dry-run
poly push --skip-validation
poly push --force
poly push --format
```
When pushing creates a new branch (for example, when pushing to Agent Studio for the first time on a branch), the CLI displays a message with the new branch name.
| Flag | Description |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--dry-run` | Run all validation and diff steps without sending changes to Agent Studio. |
| `--skip-validation` | Bypass local validation. Use sparingly — for example, when a platform-generated resource fails a strict ADK check but is known to be valid on the platform. |
| `--force`, `-f` | Force the push even when the local project diverges from the remote in unexpected ways. |
| `--format` | Run [`poly format`](#poly-format) over the project before pushing. |
**Call Link URL in chat output may be malformed**
Each chat session prints a Call Link URL for viewing the conversation in Agent Studio. On some deployments this URL has a doubled hostname (for example, `https://studio.studio.poly.ai/…`), which produces a 404. The conversation is still recorded — open Agent Studio directly and navigate to the conversation from there.
**`poly push` reports an error message when there is nothing to push**
If there are no local changes, `poly push` prints `Error: Failed to push` and `No changes detected`. The exit code is 0, so CI scripts that check return codes are not affected. The message is misleading but the command has not actually failed.
When using JSON output (`--json`), the response includes `new_branch_name` and `new_branch_id` fields if a new branch was created.
### `poly status`
View changed, new, and deleted files in your project.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly status
```
### `poly diff`
Show differences between the local project and the remote version.
Examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly diff
poly diff --files file1.yaml
poly diff --before main --after my-feature
```
### `poly revert`
Revert local changes.
Examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly revert
poly revert file1.yaml file2.yaml
```
`poly revert` with no arguments reverts every change in the working tree; pass file paths to revert only those files.
### `poly branch`
Manage project branches.
Examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly branch list
poly branch current
poly branch create my-feature
poly branch create my-hotfix --env live
poly branch create my-hotfix --env live --force
poly branch switch my-feature
poly branch switch my-feature --force
poly branch merge 'Merge feature branch'
poly branch merge 'Merge feature branch' --interactive
poly branch delete
poly branch delete my-feature
```
#### `poly branch merge`
Merge the current branch into `main` via the CLI. A merge message is required.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly branch merge 'Merge message'
poly branch merge 'Merge message' --interactive
poly branch merge 'Merge message' --resolutions resolutions.json
```
For the full merge workflow — conflict tables, `--interactive` flow, the `--resolutions` JSON format, post-merge behavior, and troubleshooting — see the dedicated [Branch merging reference](/adk/reference/branch_merge).
#### `poly branch delete`
Interactively select and delete one or more branches. The `main` branch cannot be deleted.
* Run without arguments to open an interactive checkbox prompt for selecting branches to delete.
* Pass a branch name directly to skip the interactive prompt and delete that branch after confirmation.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly branch delete
poly branch delete my-feature
```
**`poly branch delete` requires a TTY and may fail with a 404**
`poly branch delete` opens an interactive confirmation prompt and must be run in a terminal. In non-interactive environments (scripts, CI), it throws `[Errno 22] Invalid argument`.
On some projects, the delete command hits the same platform endpoint as branch chat and returns a 404 after the confirmation. If this happens, delete the branch through the Agent Studio UI instead.
#### `poly branch create`
Creates a new branch. By default the branch is sourced from the project's `main` branch (the sandbox environment).
| Flag | Description |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| `--env`, `--environment` | Source the new branch from a deployed environment snapshot instead of `main`. Choices: `sandbox`, `pre-release`, `live`. |
| `--force`, `-f` | Force branch creation even if there are uncommitted local changes on main. |
When `--env live` or `--env pre-release` is specified:
* the version of the deployed environment is pulled into your local workspace
* a branch is created from that snapshot
* the version is immediately pushed to the new branch, leaving a clean slate for hotfix changes
* the command can only be run from `main`
* if there are local changes, the command will fail unless `--force` is also passed
**Use `--env live` with caution**
Branching from a live deployment snapshot will overwrite your local project with the live state. Merging this branch back to main may roll back changes that were introduced after the snapshot was taken.
**Only one active branch is allowed at a time**
Agent Studio supports one non-main branch per project. Attempting to create a second branch while one already exists returns an error. Merge or delete the existing branch in Agent Studio before creating a new one.
### `poly format`
Format project resources.
Examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly format
poly format --check
poly format --files src/functions/booking.py
```
### `poly validate`
Validate project configuration locally.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly validate
```
### `poly review`
Create a GitHub Gist of Agent Studio project changes to share with others.
`poly review` requires a subcommand: `create`, `list`, or `delete`. Use `poly review create` to compare your local changes against the remote project, or pass `--before` and `--after` to compare two remote branches or versions. Add `--verbose` for full error tracebacks while troubleshooting.
Examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly review create
poly review create --before main --after feature-branch
poly review create --verbose
```
#### `poly review list`
Interactively select a review gist and open it in the browser.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly review list
poly review list --json
```
#### `poly review delete`
Interactively select and delete review gists. Use `--id` to delete a specific gist directly without an interactive prompt.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly review delete
poly review delete --id GIST_ID
poly review delete --json
```
### `poly chat`
Start an interactive chat session with your agent, or run scripted/automated conversations.
Examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly chat
poly chat --environment live
poly chat --channel webchat
poly chat --metadata
poly chat --lang fr-FR
poly chat --input-lang en-US --output-lang fr-FR
```
#### Non-interactive (scripted) mode
Supply messages directly on the command line or from a file to run `poly chat` without a human at the terminal. This is useful for automated testing pipelines and CI scripts.
**Inline messages** — use `-m`/`--message` (repeatable):
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly chat -m 'Hello' -m 'What can you help with?'
```
**File-based input** — use `--input-file`:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly chat --input-file ./script.txt
echo -e 'Hello\nGoodbye' | poly chat --input-file -
```
Each line of the file is sent as a separate message. Use `-` to read from stdin.
If the file path does not exist, `poly chat` exits with an error.
#### Resuming an existing conversation
Use `--conversation-id` (or `--conv-id`) to resume an existing conversation by its ID instead of creating a new session:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly chat --conv-id
poly chat --conv-id -m 'Follow-up message'
```
#### Pushing before chatting
Use `--push` to push the local project to Agent Studio before starting the chat session. This ensures local changes are live before testing without requiring a separate `poly push` step:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly chat --push
poly chat --push -m 'Hello'
```
If the push fails, the command exits without starting the chat session.
#### Language flags
Use language flags to specify the expected input and output language when chatting against multilingual agents. If not specified, the project default is used.
| Flag | Description |
| --------------- | ------------------------------------------------------------------- |
| `--lang` | Sets both input and output language (e.g. `en-US`, `fr-FR`). |
| `--input-lang` | Sets the input language (ASR) only. Overrides `--lang` for input. |
| `--output-lang` | Sets the output language (TTS) only. Overrides `--lang` for output. |
`--input-lang` and `--output-lang` take precedence over `--lang` when both are supplied.
#### `poly chat` flags summary
| Flag | Description |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--push` | Push the project before starting the chat session. |
| `-m`, `--message MSG` | Send a message non-interactively (repeatable). |
| `--input-file FILE` | Read messages line-by-line from a file (`-` for stdin). |
| `--conversation-id`, `--conv-id` | Resume an existing conversation by ID. |
| `--json` | Emit a single JSON object when the session ends (see below). |
| `--environment` | Target environment. Choices: `branch`, `sandbox`, `pre-release`, `live`. Defaults to `branch`. `branch` chats against the last **pushed** state of your current branch (not local uncommitted changes); on main it falls back to `sandbox`. Use `--push` to push local changes before chatting. |
| `--channel` | Channel to use (e.g. `webchat`, `voice`). |
| `--lang` | Set both input and output language. |
| `--input-lang` | Set input language only. |
| `--output-lang` | Set output language only. |
| `--variant` | Name of the variant to use for the chat session. |
| `--functions` | Show function events in output. |
| `--flows` | Show flow metadata in output. |
| `--state` | Show state changes in output. |
| `--metadata` | Show all metadata (equivalent to `--functions --flows --state`). |
### `poly conversations`
List and inspect conversations for the project using the public Conversations API.
`poly conversations` requires a subcommand: `list`, `get`, or `get-audio`.
Examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly conversations list
poly conversations get
poly conversations get-audio -o recording.wav
```
#### `poly conversations list`
List conversations for the project.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly conversations list
poly conversations list --limit 20 --offset 10
poly conversations list --json
```
| Flag | Description |
| ---------- | -------------------------------------------------------------------- |
| `--limit` | Max number of conversations to return. Defaults to `50`. |
| `--offset` | Number of conversations to skip. Defaults to `0`. |
| `--path` | Base path to the project. Defaults to the current working directory. |
| `--json` | Print a single JSON object on stdout (machine-readable). |
The default table view shows conversation ID (rendered as a clickable Agent Studio link), start time, duration, caller number, channel, variant (when present), handoff status, and a short summary heading.
#### `poly conversations get`
Get detailed information for a specific conversation, including all turns.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly conversations get
poly conversations get --json
```
| Argument / Flag | Description |
| ----------------- | -------------------------------------------------------------------- |
| `conversation_id` | The conversation ID to look up. Required. |
| `--path` | Base path to the project. Defaults to the current working directory. |
| `--json` | Print a single JSON object on stdout (machine-readable). |
The default output shows conversation metadata (channel, language, duration, timestamps, handoff, tags, PolyScore, summary, note) followed by a turn-by-turn transcript.
#### `poly conversations get-audio`
Download the audio recording for a conversation as a WAV file.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly conversations get-audio
poly conversations get-audio --direction user
poly conversations get-audio --redacted -o redacted.wav
poly conversations get-audio --json
```
| Argument / Flag | Description |
| ----------------- | ----------------------------------------------------------------------------------------------- |
| `conversation_id` | The conversation ID. Required. |
| `--direction` | Audio track to download. Choices: `combined`, `user`, `agent`. Defaults to `combined`. |
| `--redacted` | Download the redacted version of the audio. |
| `-o`, `--output` | Output file path. Defaults to `.wav`. |
| `--path` | Base path to the project. Defaults to the current working directory. |
| `--json` | Print a JSON summary on stdout instead of the success message (audio is still written to disk). |
### `poly docs`
Output resource documentation.
Examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly docs flows functions topics
poly docs context
poly docs --all
poly docs --all --output rules.md
```
Use `--output` to write the documentation to a local file. This is useful when working with AI coding tools — pass the output file as context to give the agent accurate knowledge of ADK resource types and conventions.
Available resource names include:
| Name | Description |
| --------------------- | -------------------------------------------------------- |
| `agent_settings` | Personality, role, rules |
| `api_integrations` | External HTTP API definitions |
| `chat_settings` | Chat greeting, style prompt |
| `context` | Context files for agent knowledge |
| `entities` | Structured data collection |
| `experimental_config` | Feature flags |
| `flows` | Multistep processes with steps, functions, conditions |
| `handoffs` | SIP call transfers |
| `functions` | Global and flow functions, decorators, state, metrics |
| `languages` | Default and additional language configuration |
| `tests` | Simulated conversation test cases |
| `safety_filters` | Content moderation settings |
| `sms` | Text message templates |
| `speech_recognition` | ASR settings, keyphrase boosting, transcript corrections |
| `response_control` | Pronunciations, phrase filters |
| `topics` | Knowledge base for RAG |
| `translations` | Localized text strings per language |
| `variants` | Per-variant configuration |
| `voice_settings` | Voice greeting, disclaimer, style prompt |
| `variables` | State variables referenced in code |
### `poly deployments`
Manage deployments for the project.
#### `poly deployments list`
List deployments for the project.
Examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly deployments list
poly deployments list --env live
poly deployments list --details
poly deployments show abc123def
poly deployments show abc123def --env live
```
#### `poly deployments list`
List deployments for the project.
| Flag | Description |
| ----------- | ------------------------------------------------------------------------------------------------------ |
| `--env` | Environment to list deployments for. Choices: `sandbox`, `pre-release`, `live`. Defaults to `sandbox`. |
| `--details` | Show additional deployment details. |
| `--verbose` | Show full error tracebacks for debugging. |
**Use `--details` for readable output**
The default tabular view may wrap long URLs across multiple rows, making it unreadable in narrow terminals. `--details` produces a vertical layout that is easier to read.
#### `poly deployments promote`
Promote a deployment to the next environment (`pre-release` or `live`), removing the need to use the Agent Studio UI.
Examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly deployments promote --from --to pre-release
poly deployments promote --from sandbox --to live --message "Release notes here"
poly deployments promote --from --to pre-release --dry-run
poly deployments promote --from --to live --force
```
| Flag | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--from` | ID or environment name of the deployment to promote. Required. |
| `--to` | Target environment. Choices: `pre-release`, `live`. Required. |
| `--message`, `-m` | Optional message to include with the promotion (e.g. release notes or changelog). If not specified, the existing deployment message is used. |
| `--force` | Skip the confirmation prompt. When used without `--message`, the existing deployment message is kept. This is the default in non-interactive mode (e.g. when `--json` is used). |
| `--dry-run` | Show what would be promoted without actually promoting. Displays the deployment hash, target environment, and changes included. |
| `--verbose` | Show full error tracebacks for debugging. |
When promoting to `live`, the command searches for the deployment in `pre-release` and uses sandbox as the linear history source for computing included changes. When promoting to `pre-release`, the command searches sandbox.
The output includes:
* the deployment hash being promoted
* whether it is a first-time promotion to that environment
* a list of **included deployments** (changes being promoted) or **reverting deployments** (when promoting to an older version)
Without `--force`, the command prompts for confirmation before proceeding and optionally allows you to enter or override the deployment message interactively.
#### `poly deployments rollback`
Roll back sandbox to a previous deployment version.
Examples:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly deployments rollback --to
poly deployments rollback --to --message "Rolling back due to regression"
poly deployments rollback --to --dry-run
poly deployments rollback --to --force
```
| Flag | Description |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `--to` | ID or environment name of the deployment to roll back to. Required. |
| `--message`, `-m` | Optional message to include with the rollback. If not specified, the existing deployment message is used. |
| `--force` | Skip the confirmation prompt. This is the default in non-interactive mode (e.g. when `--json` is used). |
| `--dry-run` | Show what would be rolled back without actually rolling back. Displays the target deployment and the deployments that would be reverted. |
| `--verbose` | Show full error tracebacks for debugging. |
The output includes a list of **reverting deployments** — the versions that will be undone when the rollback completes.
Without `--force`, the command prompts for confirmation before proceeding.
## Machine-readable JSON output
All core subcommands accept a `--json` flag that switches stdout to a single JSON object. This is designed for scripting, CI pipelines, and any integration that needs stable, parseable output rather than human-readable console text.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly status --json
poly push --json
poly pull --json
poly validate --json
poly diff --json
poly revert --json
poly branch list --json
poly branch create my-feature --json
poly branch switch my-feature --json
poly branch current --json
poly branch delete --json
poly branch delete my-feature --json
poly branch merge 'Merge message' --json
poly format --json
poly init --region us-1 --account_id 123 --project_id my_project --json
poly project create --region us-1 --account_id my-account --name my-project --json
poly chat --json -m 'Hello'
poly chat --json --input-file ./script.txt
poly deployments show abc123def --json
poly deployments list --json
poly deployments promote --from --to pre-release --force --json
poly deployments rollback --to --force --json
poly conversations list --json
poly conversations get --json
poly conversations get-audio --json
```
When `--json` is used:
* stdout contains exactly one JSON object
* the process exits with code `0` on success and non-zero on failure
* human-readable console messages are suppressed
**`--interactive` and `--json` cannot be used together**
`poly branch merge --interactive` requires a terminal for its conflict-resolution prompts and is incompatible with `--json`.
**`--json` implies `--force` for deployments commands**
When `--json` is used with `poly deployments promote` or `poly deployments rollback`, the confirmation prompt is automatically skipped (equivalent to passing `--force`).
### JSON output shapes
The exact fields vary by command. Common fields include:
| Command | Key fields |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `poly status --json` | `files_with_conflicts`, `modified_files`, `new_files`, `deleted_files` |
| `poly push --json` | `success`, `message`, `dry_run` |
| `poly pull --json` | `success`, `files_with_conflicts` |
| `poly validate --json` | `valid`, `errors` |
| `poly diff --json` | `diffs` |
| `poly revert --json` | `success`, `files_reverted` |
| `poly branch list --json` | `current_branch`, `branches` |
| `poly branch create --json` | `success`, `new_branch_id`, `branch_name` |
| `poly branch switch --json` | `success`, `switched_to`, `dry_run` |
| `poly branch current --json` | `current_branch` |
| `poly branch delete --json` | `success`, `deleted` |
| `poly branch merge --json` | `success`; on conflict: `conflicts`, `errors` |
| `poly format --json` | `success`, `check_only`, `format_errors`, `affected`, `ty_ran`, `ty_returncode`, `ty_timed_out` |
| `poly init --json` | `success`, `root_path` |
| `poly project create --json` | `success`, `root_path` (via init); on error: `success`, `error` |
| `poly chat --json` | `conversations` (array); optional `push` (when `--push` is used) |
| `poly deployments show --json` | `success`, `deployment`, `active_deployment_hashes`, `included_deployments`, `is_rollback` |
| `poly deployments promote --json` | `success`, `from_hash`, `to_env`, `message`, `included_deployments`; `dry_run` when `--dry-run` is used |
| `poly deployments rollback --json` | `success`, `target_hash`, `message`, `reverted_deployments`; `dry_run` when `--dry-run` is used |
| `poly conversations list --json` | `conversations`, `count`, `limit`, `offset` |
| `poly conversations get --json` | full conversation detail object |
| `poly conversations get-audio --json` | `success`, `conversation_id`, `direction`, `redacted`, `output_path`, `size_bytes` |
For `poly branch delete --json`, when a branch that was the current branch is deleted, the response also includes `"switched_to": "main"`.
For `poly branch merge --json`, a successful merge returns `{ "success": true }`. When conflicts or errors are present, the response includes `"conflicts"` and `"errors"` arrays containing the raw conflict and error objects from the platform.
For `poly deployments show --json`, the response includes:
* `deployment` — the full deployment record for the requested version hash.
* `active_deployment_hashes` — a map of environment names to the currently active version hash in each environment.
* `included_deployments` — the list of sandbox deployments included since the predecessor version in the queried environment.
* `is_rollback` — `true` if the deployment is a rollback to an older version.
Error responses always include `{ "success": false, "error": "...", "traceback": "..." }`.
**`init` with `--json` requires explicit flags**
When using `poly init --json`, you must supply `--region`, `--account_id`, and `--project_id` explicitly. Interactive prompts are not supported in JSON mode.
**`poly project create` with `--json` requires explicit flags**
When using `poly project create --json`, you must supply `--region`, `--account_id`, and `--name` explicitly. Interactive prompts are not supported in JSON mode.
#### `poly chat --json` output shape
When `--json` is used with `poly chat`, the command emits a single JSON object when the session ends:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"conversations": [
{
"conversation_id": "conv-123",
"url": "https://...",
"turns": [
{ "input": null, "response": "Hello! How can I help?", "conversation_ended": false },
{ "input": "What are your hours?", "response": "We are open 9am–5pm.", "conversation_ended": false }
]
}
]
}
```
* `conversations` is an array because `/restart` in scripted input produces multiple entries.
* `turns[0]` is always the agent greeting, with `"input": null`.
* If `--push` is also supplied, the output includes a `push` key: `{ "push": { "success": true, "message": "..." } }`.
* If `--functions`, `--flows`, or `--state` are also set, the relevant metadata fields are included in each turn.
#### `poly conversations get-audio --json` output shape
When `--json` is used with `poly conversations get-audio`, the audio is still written to disk and the command emits a JSON summary:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"success": true,
"conversation_id": "KA-123",
"direction": "combined",
"redacted": false,
"output_path": "KA-123.wav",
"size_bytes": 2000000
}
```
#### `poly deployments promote --json` output shape
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"success": true,
"from_hash": "abc123456xyz",
"to_env": "pre-release",
"message": "Release notes here",
"included_deployments": [...]
}
```
On dry run, `"dry_run": true` is added and `"success"` reflects the pre-flight state without any changes being made. On error, `"success": false` and `"error": "..."` are returned.
#### `poly deployments rollback --json` output shape
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"success": true,
"target_hash": "def789012xyz",
"message": "Rolling back due to regression",
"reverted_deployments": [...]
}
```
On dry run, `"dry_run": true` is added. On error, `"success": false` and `"error": "..."` are returned.
### `poly push --output-json-commands`
Adds a `commands` array to the JSON output of `poly push`, containing the serialized Agent Studio commands that were staged. Useful for dry-run review and integration testing.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly push --json --dry-run --output-json-commands
```
The output will include a `commands` key with each command serialized from its protobuf representation.
### Driving pull/push from a captured projection
The `--from-projection` flag on `pull`, `push`, `init`, and `branch switch` lets you supply a projection JSON directly (as a string or via stdin with `-`) instead of fetching it from the API. This is useful for offline workflows and integration testing.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly pull --from-projection - < projection.json
poly push --from-projection '{"topics": [...], ...}'
cat projection.json | poly pull --from-projection -
```
The `--output-json-projection` flag on `pull`, `init`, and `branch switch` includes the projection in the JSON output when `--json` is also set. This lets you capture a projection from one command and feed it into another.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly pull --json --output-json-projection | jq .projection > proj.json
poly push --from-projection - < proj.json
```
## Working pattern
A typical CLI workflow looks like this:
1. create a new project with `poly project create` or initialize an existing one with `poly init`
2. pull with `poly pull` if needed to refresh local state
3. create or switch to a branch
4. edit files
5. inspect changes with `poly status` and `poly diff`
6. validate with `poly validate`
7. push with `poly push`
8. optionally review with `poly review`
9. test or chat with the agent using `poly chat`
10. browse and debug conversations with `poly conversations list` and `poly conversations get`
11. merge the branch with `poly branch merge ''`
12. promote to pre-release or live with `poly deployments promote`
**Run commands from the project folder**
ADK commands are expected to be run from within your local project directory. If needed, use the --path flag to point to a project explicitly.
## Related pages
See how the CLI fits into a real workflow.
Conflict resolution, `--interactive` flow, and `--resolutions` JSON for `poly branch merge`.
Write and manage simulated conversation tests in `test_suite/`.
How the CLI fits into the daily edit/push/test loop.
# Entities
Source: https://docs.poly.ai/adk/reference/entities
Entities define structured data that the agent can collect from the user, such as a date of birth, phone number, or choice from a list.
Entities are used in flow steps to control what the agent should collect and what must be present before a condition can trigger. They can also be read in executed Python code.
## Location
Entities are defined in:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
config/entities.yaml
```
Entities are listed under the `entities` key.
## What an entity contains
Each entity has four main parts:
| Field | Description |
| ------------- | ------------------------------------------------------------------------------------------------- |
| `name` | Identifier for the entity, typically in snake\_case. Used in prompts as `{{entity:entity_name}}`. |
| `description` | Explains what the entity represents. This is shown to the model to guide extraction. |
| `entity_type` | The type of entity being collected. |
| `config` | Type-specific settings for that entity. |
## Entity types
| Type | Config fields | Description |
| -------------- | -------------------------------------------------- | --------------------------------------------- |
| `numeric` | `has_decimal`, `has_range`, `min`, `max` | Numbers such as account numbers or quantities |
| `alphanumeric` | `enabled`, `validation_type`, `regular_expression` | Mixed text such as booking references |
| `enum` | `options` | A fixed set of choices |
| `date` | `relative_date` | Calendar dates |
| `phone_number` | `enabled`, `country_codes` | Phone numbers with country validation |
| `time` | `enabled`, `start_time`, `end_time` | Times or time ranges |
| `address` | `{}` | Physical addresses |
| `free_text` | `{}` | Unstructured text input |
| `name_config` | `{}` | Person names |
## How entities are used
Use `{{entity:entity_name}}` to reference a collected value.
Read values using `conv.entities.entity_name.value`.
Use `required_entities` to gate a condition until the listed entities have been collected.
Use `extracted_entities` to tell the agent which entities to collect in that step.
## In prompts
You can reference a collected entity value in prompts using:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
{{entity:entity_name}}
```
This allows a later step to reuse information that has already been collected.
## In code
In function steps or related Python code, entity values can be read like this:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.entities.entity_name.value
```
Before reading a value, check that the entity exists:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if conv.entities.entity_name:
...
```
## In flow conditions
Entities are important in default flow steps:
* `extracted_entities` tells the agent what to collect in the current step
* `required_entities` tells a condition what must already be available before it can trigger
This allows flows to wait until the necessary information has been gathered before progressing.
**Automatic ASR biasing**
When entities are requested in a default step, ASR biasing is automatically configured based on the entity types being collected.
## Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
entities:
- name: date_of_birth
description: The customer's date of birth
entity_type: date
config:
relative_date: false
- name: party_size
description: Number of guests for the reservation
entity_type: numeric
config:
has_decimal: false
min: 1
max: 20
- name: meal_preference
description: The customer's preferred meal type
entity_type: enum
config:
options:
- vegetarian
- vegan
- standard
- halal
```
## Best practices
* use clear, descriptive snake\_case names
* keep descriptions specific enough to guide extraction well
* choose the most precise entity type available
* use `required_entities` to control when a step condition is allowed to fire
* use `extracted_entities` to make collection explicit in default steps
## Related pages
Learn how entities fit into default steps, conditions, and step transitions.
Compare collected entities with state variables used elsewhere in the project.
Full reference for `conv.entities` — accessing collected values, checking presence, and entity object shape.
# Experimental config
Source: https://docs.poly.ai/adk/reference/experimental_config
The experimental config file is an optional JSON file used to enable experimental features and advanced runtime settings for an agent.
Use it for:
* feature flags
* ASR tuning
* conversation control
* debug-oriented options
## Location
The file lives at:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent_settings/experimental_config.json
```
## What it contains
The file is a JSON object.
It may be:
* flat
* nested
* grouped by feature category
Top-level keys represent feature areas, and values contain the settings for those features.
## Example
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"asr": {
"disable_itn": true,
"eager_final": true
},
"conversation_control": {
"enhanced_tts_preprocessing_enabled": false,
"max_silence_count": 1000,
"min_chunk_size": 1
}
}
```
## Schema and validation
Available features and their types are defined in a bundled schema file:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
src/poly/resources/experimental_config_schema.yaml
```
The ADK validates `experimental_config.json` against this schema when you run:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly validate
```
Invalid configuration fails `poly validate` locally. Experimental config that fails validation is not read by the runtime in deployed agents.
### Custom schema path
If the bundled schema does not match the schema expected by your Agent Studio environment, you can point validation at a custom schema file by setting the `ADK_EXPERIMENTAL_CONFIG_SCHEMA_PATH` environment variable:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export ADK_EXPERIMENTAL_CONFIG_SCHEMA_PATH=/path/to/your/experimental_config_schema.yaml
poly validate
```
When `ADK_EXPERIMENTAL_CONFIG_SCHEMA_PATH` is set, the ADK uses that file instead of the bundled schema. When the variable is unset or empty, validation falls back to the bundled schema.
**Validate before pushing**
Experimental config can affect runtime behavior in subtle ways. Always run `poly validate` locally before pushing changes.
## When to use it
Use experimental config when you need behavior that goes beyond the standard Agent Studio settings.
Common use cases include:
Adjust speech recognition or speech output behavior beyond the standard channel settings.
Enable features before they are generally available.
Tune parameters such as silence handling or chunk size behavior.
## Feature reference
The following sections describe notable feature areas available in the schema.
### Audio enhancement
Configure audio enhancement processing applied to the incoming audio stream before speech recognition. Three providers are available: `ai-coustics`, `dolby`, and `krisp`.
#### `ai-coustics` VAD
The `ai-coustics` enhancer supports a `vad` (voice activity detection) sub-object for tuning how speech is detected in the audio stream.
| Field | Type | Description | Default | Range |
| ------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | ---------- |
| `sensitivity` | number | Energy threshold for speech detection. Energy threshold = 10^(-sensitivity). Higher values detect quieter speech. | `6.0` | 1.0 – 15.0 |
| `speech_hold_duration` | number | How long the VAD continues to report speech after the audio signal no longer contains speech (in seconds). Useful for bridging short pauses. | `0.03` | ≥ 0.0 |
| `minimum_speech_duration` | number | How long speech must be present before the VAD considers it speech (in seconds). Helps filter out short non-speech sounds like clicks or coughs. | `0.0` | 0.0 – 1.0 |
Example:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"audio_enhancement": {
"ai-coustics": {
"vad": {
"sensitivity": 6.0,
"speech_hold_duration": 0.03,
"minimum_speech_duration": 0.0
}
}
}
}
```
#### `krisp`
Krisp provides noise cancellation and voice isolation. Settings include:
| Field | Type | Description | Default |
| ------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------ |
| `model` | string | Krisp model variant: `"noise-cancellation"`, `"voice-isolation"`, `"telephony"`, `"telephony-lite"`, `"transcription"` | `"telephony-lite"` |
| `noise_suppression_level` | integer | Noise suppression intensity. `0` = off, `100` = max. | `100` |
| `frame_duration_ms` | integer | Audio frame duration in milliseconds. Allowed values: `10`, `15`, `20`, `30`, `32`. | `20` |
| `timeout_ms` | integer | Max milliseconds to wait for enhancement per chunk before falling back to original audio. `0` = no timeout. | `100` |
Example:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"audio_enhancement": {
"krisp": {
"model": "telephony-lite",
"noise_suppression_level": 100,
"frame_duration_ms": 20,
"timeout_ms": 100
}
}
}
```
### Barge-in
The barge-in section supports additional fields to control how interrupted speech is handled and displayed.
#### Interruption granularity
`interruption_granularity` controls where the split happens in agent speech when the user barges in.
| Value | Behavior |
| ----------------- | ---------------------------------------- |
| `"word"` | Audio-timing split at the word boundary. |
| `"sentence"` | Drop the interrupted sentence. |
| `"sentence_keep"` | Keep the interrupted sentence. |
| `"chunk"` | Drop the entire TTS chunk. |
#### Interruption display
`interruption_display` controls how interrupted text appears in Agent Studio `msg.Text` (and in LLM context if `interruption_display_llm` is not set).
| Value | Behavior |
| ------------ | ---------------------------------------------------- |
| `"ellipsis"` | Append `"..."` to the said portion. |
| `"tags"` | Wrap the unsaid portion in `` XML tags. |
| `"strip"` | Drop unsaid text silently. |
| `"none"` | Keep the full text unchanged. |
| `"barge"` | Append a `"[BARGE IN]"` marker. |
#### `interruption_display_llm`
An optional LLM-specific override for interrupted text display. Accepts the same values as `interruption_display`. When absent, inherits from `interruption_display`.
#### `truncate_interrupted_utterances`
| Field | Type | Default | Description |
| --------------------------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `truncate_interrupted_utterances` | boolean | `false` | When `true`, function-output utterances on interrupted turns are truncated to only the said (heard) portion, dropping unsaid text. Useful when TTS utterances are attached to function outputs and should reflect what the caller actually heard. |
#### `annotate_interrupted_function_calls`
| Field | Type | Description |
| ------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `annotate_interrupted_function_calls` | boolean | When `true`, function call results on interrupted turns are annotated with said/unsaid context so the LLM can judge whether the initiating question was fully communicated. Defaults to `false`. |
Example:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"barge_in": {
"interruption_granularity": "sentence",
"interruption_display": "ellipsis",
"interruption_display_llm": "tags",
"truncate_interrupted_utterances": true,
"annotate_interrupted_function_calls": false
}
}
```
### DTMF
Configure DTMF behavior, including disabling speech recognition for DTMF-only steps.
The `dtmf` object supports a `flow_overrides` map where each key is a flow name. Per-flow settings include:
| Field | Type | Description |
| ---------------- | ------- | ------------------------------------------------------------------------- |
| `disable_speech` | boolean | Whether to disable speech recognition when DTMF is enabled for this flow. |
| `steps` | object | Step-specific overrides. Each key is a step name. |
Per-step settings (nested under `steps`) include:
| Field | Type | Description |
| --------------------- | ------- | -------------------------------------------------------------------------------------------------- |
| `disable_speech` | boolean | Whether to disable speech recognition for this step. Takes precedence over the flow-level setting. |
| `first_digit_timeout` | integer | Timeout in seconds for the first DTMF digit input for this step. Minimum: `1`. |
Example:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"dtmf": {
"flow_overrides": {
"Payment Flow": {
"disable_speech": true,
"steps": {
"Enter Card Number": {
"disable_speech": true,
"first_digit_timeout": 5
}
}
}
}
}
}
```
### Language switching
Configure automatic language switching behavior.
| Field | Type | Default | Description |
| --------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `explicit_only` | boolean | `false` | When `true`, the agent only switches language when the user explicitly asks. When `false` (default), the agent may also switch spontaneously based on detected language in the transcription. |
Example:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"language_switching": {
"explicit_only": true
}
}
```
### Memory
Configure agent memory features, including repeat-caller identification.
#### `identifier_source`
By default, memory lookups use the caller or callee phone number as the identifier. The `identifier_source` field lets you supply a custom source instead.
| Field | Type | Description |
| ------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------- |
| `identifier_source` | string | Custom source for the memory lookup identifier. Must match the pattern `(sip_headers\|integration_attributes\|state):.+`. |
Example:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"memory": {
"identifier_source": "sip_headers:X-Customer-Id"
}
}
```
### OpenAI Realtime
Configure behavior for the OpenAI Realtime integration, including transcription settings.
#### `set_transcriber_language`
| Field | Type | Description | Default |
| -------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `set_transcriber_language` | boolean | When `true`, the conversation language code is passed to the transcriber in the session configuration, making the model adhere more strictly to the specified language. Do not use this in multilingual projects with a language detection component. | `false` |
Example:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"openai_realtime": {
"transcription": {
"set_transcriber_language": true
}
}
}
```
### Prompts
The `prompts` section supports channel-specific and language-related decorator overrides.
| Field | Type | Description |
| --------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| `webchat_decorator` | string | Optional webchat-specific decorator for the `webchat.polyai` channel. |
| `sms_decorator` | string | Optional SMS-specific decorator for the `sms.polyai` channel. |
| `voice_decorator` | string | Optional voice-specific decorator for `chat.polyai` or `sip.polyai` channels. |
| `language_switching_instructions` | string | Optional instructions for language switching behaviour. Must contain a `{available_languages}` placeholder. |
Example:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"prompts": {
"sms_decorator": "Keep responses brief and suitable for SMS.",
"language_switching_instructions": "You may switch to any of the following languages if the user requests it: {available_languages}."
}
}
```
### Webhooks
Configure webhook behavior for deployment events, including custom payload templates.
#### `payload_template`
The `payload_template` field controls the JSON body sent to a webhook URL. If omitted, the default deployment payload is sent as-is.
| Field | Type | Description |
| ------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------ |
| `payload_template` | object | Custom payload template. String values may contain `{{field}}` placeholders that are substituted with deployment event fields. |
**Available placeholder fields:**
* `deployment_id`
* `account_id`
* `project_id`
* `client_env`
* `artifact_version`
* `deployment_type`
* `timestamp`
* `user`
**Special placeholder:**
Use `{{payload}}` to inject the entire deployment payload object at a specific position in the template — for example, when a webhook receiver (such as GitHub's `repository_dispatch`) requires nesting under a specific key like `client_payload`.
When `{{payload}}` is not present in the template, the deployment payload fields are merged at the top level of the rendered result.
Example — GitHub `repository_dispatch`:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"webhooks": {
"payload_template": {
"event_type": "deployment-{{client_env}}",
"client_payload": "{{payload}}"
}
}
}
```
Example — flat template with individual fields:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"webhooks": {
"payload_template": {
"env": "{{client_env}}",
"version": "{{artifact_version}}",
"deployed_by": "{{user}}"
}
}
}
```
### `include_kb_functions_in_flows`
Controls whether knowledge base (KB) functions from retrieved RAG topics are shown to the model inside flows.
| Value | Behavior |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `true` | KB functions from retrieved RAG topics are shown to the model inside flows, even on steps that have their own `functions_referenced`. |
| `false` (default) | KB functions are hidden inside flows. |
This setting only affects behavior inside flows. Outside flows, KB functions are always shown. It can be overridden per-flow or per-step.
## Best practices
* only set values you actually intend to override
* omit defaults rather than copying them unnecessarily
* validate locally with `poly validate` before pushing
* remove flags that are no longer needed
* treat the file as an advanced override layer, not a dumping ground for ordinary config
## Related pages
See where experimental config sits within the broader agent settings area.
Compare experimental ASR controls with standard voice speech-recognition settings.
# Flows
Source: https://docs.poly.ai/adk/reference/flows
Flows choreograph multi-step processes. At any given moment, the model only sees the current step's prompt and tools.
A good flow keeps each step focused on a single task. Use Python for branching, validation, and routing logic, and use prompts for conversational behavior.
## What flows are for
Flows are best used when the agent needs to move through a structured process such as:
* collecting information in a defined order
* confirming details before taking action
* calling APIs or deterministic logic at specific points
* handling success, failure, and retry paths explicitly
LLM-driven steps for collecting information and transitioning based on conditions.
Steps with more control over ASR, DTMF, and callable transition functions.
Deterministic Python steps for routing, validation, and API calls.
## Entering a flow
A flow can be entered in several ways.
### From code
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.goto_flow("Flow Name")
```
This enters the flow at its configured start step.
### Via a returned transition
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {"transition": {"goto_flow": "Flow Name", "goto_step": "Step Name"}}
```
### Within a flow
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
flow.goto_step("Step Name")
```
This is only available inside flow functions.
## File structure
Flows live under the `flows/` directory.
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
flows/
└── {flow_name}/
├── flow_config.yaml
├── steps/
│ └── {step_name}.yaml
├── function_steps/
│ └── {function_step}.py
└── functions/
└── {function_name}.py
```
The flow directory name is derived from the flow's `name` field, converted to lowercase snake\_case. A flow named `Booking Flow` must live in `flows/booking_flow/`. If the directory name does not match, the ADK will not recognize the flow.
## Flow configuration
Each flow includes a `flow_config.yaml` file that defines the flow itself.
### Fields
| Field | Required | Description |
| ------------- | -------- | -------------------------------------- |
| `name` | No | Human-readable flow name |
| `description` | Yes | What the flow does |
| `start_step` | Yes | The step to enter when the flow starts |
### Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
name: Example Flow
description: Handles the booking process
start_step: Collect Details
```
## Step types
A step represents the agent's current position in the flow.
There are three step types:
1. default steps
2. advanced steps
3. function steps
## Default steps
Default steps live in `steps/*.yaml`.
These steps use LLM logic to process information and transition based on configured conditions. They cannot call transition functions from their prompt.
ASR biasing is automatically configured based on the entities requested in the step.
### Fields
| Field | Description |
| -------------------- | ----------------------------------- |
| `step_type` | Must be `default_step` |
| `name` | Human-readable step name |
| `conditions` | Conditions that control transitions |
| `extracted_entities` | Entities to collect in the step |
| `prompt` | Prompt shown to the model |
### Prompt behavior
Default step prompts may use entity references such as:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
{{entity:entity_name}}
```
They should not contain function calls.
### Conditions
Conditions define how the agent transitions out of a default step.
A condition can:
* go to another step
* exit the flow
### Condition fields
| Field | Description |
| ------------------- | ---------------------------------------------------------------- |
| `condition_type` | `step_condition` or `exit_flow_condition` |
| `description` | When this condition applies |
| `child_step` | Next step, only for `step_condition` |
| `required_entities` | Entities that must be collected before the condition can trigger |
### `child_step` rules
Use the correct step identifier depending on target type:
* **Default step** or **advanced step**: use the step's `name`
* **Function step**: use the Python filename without `.py`
## Advanced steps
Advanced steps also live in `steps/*.yaml`.
These steps support additional controls such as:
* custom ASR tuning
* DTMF collection rules
* transition function calls from the prompt
### Fields
| Field | Description |
| ------------- | ------------------------- |
| `step_type` | Must be `advanced_step` |
| `name` | Human-readable step name |
| `asr_biasing` | ASR tuning for the turn |
| `dtmf_config` | DTMF collection settings |
| `prompt` | Prompt shown to the model |
### ASR biasing
Advanced steps can tune ASR toward specific kinds of user input.
Supported ASR biasing fields include:
* `alphanumeric`
* `name_spelling`
* `numeric`
* `party_size`
* `precise_date`
* `relative_date`
* `single_number`
* `time`
* `yes_no`
* `address`
* `custom_keywords`
### DTMF configuration
Advanced steps can also define DTMF behavior, including:
* `inter_digit_timeout`
* `max_digits`
* `end_key`
* `collect_while_agent_speaking`
* `is_pii`
## Step prompt design
Prompts should be used for:
* collecting input
* presenting information
* shaping the conversational turn
Python should be used for:
* comparisons
* conditionals
* routing
* state-driven decisions
**Do not put deterministic branching logic into prompts**
Do not encode logic like “If \$x == 0 do A, else do B” in prompts. Put that logic in Python and transition to the correct step explicitly.
### Prompt tips
* use markdown headers to structure instructions
* keep one clear purpose per step
* include validation and edge cases where needed
* use voice-friendly phrasing for spoken interactions
* make transitions explicit
## Function steps
Function steps live in `function_steps/*.py`.
These are deterministic Python steps. They execute directly, with no model interpretation. Use them for:
* API calls
* validation
* state updates
* explicit routing
### Signature
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def function_name(conv: Conversation, flow: Flow):
```
### Important rules
Function steps:
* cannot define extra parameters
* cannot use `@func_description`
* must control flow explicitly
A function step must call either:
* `flow.goto_step(...)`
* `conv.exit_flow()`
and may also return a context string for the model.
### Common uses
Check whether collected input is valid before the flow continues.
Move to the correct step based on deterministic logic.
Call APIs and store the results in state.
Send the flow to an error step with a useful context string.
## Transition functions
Transition functions live in `functions/*.py` inside a flow.
They can be called from advanced-step prompts and are referenced using:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
{{ft:flow_function}}
```
Unlike function steps, transition functions:
* may define custom parameters
* may have a description shown to the model
* can be called by the model within the same flow
Logic reused across flows is usually better placed in global functions.
## Best practices
* keep one clear purpose per step
* start with a simple linear path, then add branching
* use confirmation steps before function steps that change state
* add explicit error and failure paths
* use meaningful step names
* test the full path from entry to exit
**Prefer simple flows first**
A clean A → B → C path is easier to reason about and test than a highly branched flow built too early.
## Common mistakes
* leaving a flow function without advancing the flow
* encoding branching logic in prompts
* using internal IDs instead of resource names
* putting too much deterministic logic into LLM-driven steps
* mixing `conv.exit_flow()` with additional navigation
* using `end_turn=False` when the user is actually expected to reply
## Design principles
1. start with a single path
2. add branching only where needed
3. use function steps for deterministic logic
4. use prompts for conversational behavior
5. make every transition explicit
## Related pages
Learn how global functions, transition functions, and function steps differ.
See how topics trigger flow entry via `conv.goto_flow` in their actions.
See how structured data collection fits into flow steps and conditions.
All supported function return shapes used in flow transitions — utterance, hangup, goto\_flow, and combined dicts.
Full reference for `conv.goto_flow`, `conv.exit_flow`, `flow.goto_step`, and all other flow navigation methods.
# Functions
Source: https://docs.poly.ai/adk/reference/functions
Functions are Python files that add deterministic logic to your agent.
They can be called by the model, used as flow steps, or run automatically at call start and end.
Functions are how the ADK handles behavior that should not be left to prompt interpretation alone.
## Where functions live
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
functions/
├── start_function.py
├── end_function.py
└── {function_name}.py
flows/{flow_name}/
├── functions/
│ └── {function_name}.py
└── function_steps/
└── {function_step}.py
```
## Function types
| Type | Location | Signature | Referenced as |
| ------------- | ------------------------------ | ----------------------------------------------- | -------------------------- |
| Global | `functions/` | `def name(conv: Conversation, ...)` | `{{fn:name}}` |
| Transition | `flows/{flow}/functions/` | `def name(conv: Conversation, flow: Flow, ...)` | `{{ft:name}}` |
| Function step | `flows/{flow}/function_steps/` | `def name(conv: Conversation, flow: Flow)` | Entered by flow conditions |
| Start | `functions/start_function.py` | `def start_function(conv: Conversation)` | Runs automatically |
| End | `functions/end_function.py` | `def end_function(conv: Conversation)` | Runs automatically |
## What functions are for
Functions are useful when you need deterministic behavior such as:
* validating input
* routing based on state
* calling APIs
* writing metrics
* setting variables
* transferring calls
* starting or ending flows explicitly
Reusable functions that can be called by the model.
Flow-local functions used for step transitions and routing.
Deterministic flow steps with no LLM decision-making.
Hooks that run automatically at the start or end of a call.
## File structure rules
Every `.py` file must define a function with the same name as the file, excluding `.py`.
That function is the entry point when the file is called by the model or runtime.
Every function file must include this import line:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from _gen import * #
```
Do not modify this line. The ADK matches it exactly when reading function files.
## Decorators
Global and transition functions use decorators to describe themselves to the model.
### Supported decorators
| Decorator | Purpose |
| -------------------------------- | -------------------------------------------------- |
| `@func_description("...")` | Describes when the function should be called |
| `@func_parameter("name", "...")` | Describes a parameter |
| `@func_latency_control(...)` | Configures delay messaging while the function runs |
Function steps do not support `@func_description` or `@func_parameter`.
**All parameters must have a type annotation and no default value**
Every parameter decorated with `@func_parameter` must have a Python type annotation (for example, `booking_ref: str`). Parameters without an annotation, or with an unsupported annotation such as `Optional[str]`, will raise a `ValueError` when the function is processed. Only the types listed in the table below are supported.
Default values are also not permitted. The ADK validates the function by constructing the expected signature string — `def name(conv: Conversation, param: type)` — and checking it appears literally in the code. A default value such as `param: str = ""` breaks this check and causes push to fail with `Function definition '...' not found in code`. If a parameter is logically optional, pass an explicit empty string or zero from the LLM call site instead.
## Parameter types
Supported parameter types map to schema types as follows:
| Python type | Schema type |
| ----------- | ----------- |
| `str` | `string` |
| `int` | `integer` |
| `float` | `number` |
| `bool` | `boolean` |
## Example
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from _gen import * #
@func_description("Look up a booking by reference number.")
@func_parameter("booking_ref", "The booking reference provided by the customer")
@func_parameter("include_history", "Whether to include booking history")
def lookup_booking(conv: Conversation, booking_ref: str, include_history: bool):
result = external_api.get_booking(booking_ref, include_history)
if not result:
return "No booking found. Ask the customer to verify the reference number."
conv.state.booking = str(result)
return f"Booking found: {result['status']}. Confirm the details with the customer."
```
## Naming guidance
Prefer naming functions after the **event that should trigger them**, rather than the internal action they perform.
### Prefer
* `first_name_provided`
* `booking_confirmed`
### Avoid
* `store_first_name`
* `send_confirmation`
This tells the model when to call the function.
## Returns and control flow
Functions can influence the conversation in several ways.
| Return or action | Effect |
| ------------------------------------------------ | ------------------------------------ |
| `return "string"` | Injects the string as system context |
| `conv.say("exact phrase")` | Sends or speaks exact text |
| `conv.goto_flow("name")` | Navigates to a flow |
| `flow.goto_step("Step Name", "reason")` | Navigates to a step |
| `conv.exit_flow()` | Exits the current flow |
| `conv.call_handoff(...)` | Transfers the call |
| `return {"hangup": True}` | Ends the call |
| `return {"transition": {...}}` | Navigates via returned transition |
| `return {"utterance": "...", "end_turn": False}` | Speaks and immediately continues |
**Use `end_turn=False` carefully**
Only use `end_turn=False` when the agent must continue immediately in the same turn. Do not use it when the user is expected to answer.
## Calling other functions
You can call functions from within functions.
### Global function call
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.functions.my_global_function(...)
```
### Flow function call
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
flow.functions.my_flow_function(...)
```
## Start function
`start_function.py` runs once at call start, before the first user input.
### Signature
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start_function(conv: Conversation):
```
### Typical uses
* initialize state
* read SIP headers
* set language
* write initial metrics
* send the agent into the first flow
## End function
`end_function.py` runs once at call end, after the conversation completes.
### Signature
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def end_function(conv: Conversation):
```
### Typical uses
* aggregate metrics
* write final outcome metrics
* trigger post-call behavior in live environments
## Utility modules
If a function file is not intended to be called by the model, it still needs a main function matching the filename.
Decorate that main function and have it return a utility-module message. Helper functions inside the file should not be decorated.
## State
Functions read and write conversation state via `conv.state`. See the [Variables reference](/adk/reference/variables) for the full details on setting, reading, and referencing state in prompts.
## Metrics and logging
Functions are a natural place to write metrics and logs.
### Metrics
Examples:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.write_metric("EVENT_NAME")
conv.write_metric("NAME", value)
conv.write_metric("NAME", write_once=True)
```
### Logging
Examples:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.log.info(...)
conv.log.warning(...)
conv.log.error(...)
```
### Good practices
* use `SCREAMING_SNAKE_CASE` for metric names
* use grouped naming patterns where helpful
* use `write_once=True` for one-time events
* log important outcomes around external calls and failures
## Related pages
See how function steps and transition functions fit into flow design.
Learn how state variables are discovered and referenced.
See how functions are called from topic actions using `{{fn:...}}`.
# Handoffs
Source: https://docs.poly.ai/adk/reference/handoffs
Handoffs configure SIP call transfers for voice agents. They define how and where a call should be transferred, or whether it should be ended.
Handoffs are used when an agent needs to escalate, transfer, or terminate a voice interaction in a controlled way.
**Handoffs are ADK-only**
The Agent Studio UI does not currently expose an editor for `config/handoffs.yaml`. Define handoffs through the ADK and push them with `poly push`. Template references of the form `{{ho:handoff_name}}` only resolve inside ADK-managed files (`rules.txt`, topic actions, flow prompts) — pasting them into a UI-editable field does not work at runtime.
## Location
Handoffs are defined in:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
config/handoffs.yaml
```
They are listed under the `handoffs` key.
## What a handoff contains
Each handoff includes the following fields:
| Field | Description |
| ------------- | ------------------------------------------------------------------------- |
| `name` | Identifier for the handoff. Referenced in rules as `{{ho:handoff_name}}`. |
| `description` | Explains what the handoff does. |
| `is_default` | Whether this is the default handoff. |
| `sip_config` | Transfer method configuration. |
| `sip_headers` | Optional custom SIP headers as key/value pairs. |
## SIP config types
A handoff uses one of three SIP methods:
| Method | Use | Fields |
| -------- | -------------------------- | ---------------------------------------------------------- |
| `invite` | Start an outbound new call | `phone_number`, `outbound_endpoint`, `outbound_encryption` |
| `refer` | Transfer an existing call | `phone_number` |
| `bye` | End the call | No extra fields |
### Notes
* `phone_number` should use **E.164 format**
* `outbound_encryption` can be `TLS/SRTP` or `UDP/RTP`
## Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
handoffs:
- name: escalation_handoff
description: Transfer to a live agent for complex issues
is_default: false
sip_config:
method: refer
phone_number: "+15551234567"
sip_headers:
- key: X-Reason
value: escalation
- name: end_call
description: End the call gracefully
is_default: false
sip_config:
method: bye
```
## How handoffs are used
Call a handoff directly with `conv.call_handoff(...)`.
Refer to a handoff using `{{ho:handoff_name}}`.
Instruct the model to call a function that performs the handoff.
## In code
You can trigger a handoff directly in code:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.call_handoff(destination="handoff_name", reason="transfer_reason")
```
## In rules
A handoff can be referenced in `rules.txt` using:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
{{ho:handoff_name}}
```
This is useful when rules need to explain when escalation or transfer should happen.
## In topics and flows
Topics and flows should generally not perform raw transfer logic directly in prompt text. Instead, they should guide the model toward calling a function that performs the handoff.
For example:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Use {{fn:transfer_call}} when the user needs to be transferred to a specialist.
```
## Round-trip behavior
After a push and pull, `sip_headers: []` may be added to handoff entries that did not originally define it. This is injected by the platform and does not affect runtime behavior — the empty list is equivalent to no SIP headers. Expect this field to appear on round-trip if you did not include it yourself.
## Best practices
* use clear, descriptive handoff names
* use E.164 format for phone numbers
* create one handoff definition per transfer purpose
* keep `sip_headers` minimal
* only add custom SIP headers when the receiving system actually requires them
**One purpose per handoff**
Avoid reusing a single handoff for multiple destinations or business cases. Clear handoff names make rules and code easier to understand.
## Related pages
See how handoffs are typically triggered from deterministic logic.
Learn how handoffs are referenced in rules.
Full reference for `conv.call_handoff` — destination, reason, utterance, and SIP header overrides.
# Languages
Source: https://docs.poly.ai/adk/reference/languages
Languages configure which languages your agent supports. A project has one default language and zero or more additional languages.
Language configuration drives translation validation — every configured language must have an entry in each translation key.
## Location
Languages are defined in:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent_settings/languages.yaml
```
The file is optional. When absent, the project runs as a single-language agent in the platform default.
## What languages contains
| Field | Description |
| ---------------------- | ---------------------------------------------------------- |
| `default_language` | The primary language code in BCP 47 format (e.g. `en-GB`). |
| `additional_languages` | List of additional language codes the agent supports. |
## Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
default_language: en-GB
additional_languages:
- fr-FR
- de-DE
```
## Validation
* `default_language` is required and must be a valid BCP 47 language tag.
* Each entry in `additional_languages` must be a valid BCP 47 language tag.
* A language code cannot appear as both the default and an additional language.
* Duplicate additional language codes are not allowed.
**Use region subtags**
Use standard BCP 47 codes with region subtags (e.g. `en-GB`, `fr-FR`, `de-DE`) so downstream voice and TTS configuration can resolve unambiguously.
## Best practices
* set the default language to the primary language of your user base
* add additional languages only when translations are ready for every translation key
* keep BCP 47 codes consistent across `languages.yaml`, `translations.yaml`, and voice settings
## Related pages
Define the localized text strings used for each configured language.
See how `languages.yaml` fits alongside personality, role, and rules.
Configure per-language voice and TTS behaviour for multilingual agents.
# Response control
Source: https://docs.poly.ai/adk/reference/response_control
Response control resources process the agent's output before it is spoken.
They are used to adjust spoken output by:
* fixing pronunciation
* intercepting or blocking phrases before speech synthesis
These resources are voice-channel specific and live under `voice/response_control/`.
## Location
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
voice/response_control/
├── pronunciations.yaml
└── phrase_filtering.yaml
```
Both files are optional.
## What response control does
Fix how words, abbreviations, or phrases are spoken by TTS.
Block or intercept phrases before they are spoken.
## Pronunciations
Pronunciation rules live in:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
voice/response_control/pronunciations.yaml
```
These rules are applied before speech synthesis and are useful when the agent says something incorrectly.
### What a pronunciation rule contains
Each item in the `pronunciations` list can include:
| Field | Required | Description |
| ---------------- | -------- | ----------------------------------------- |
| `regex` | Yes | Regex pattern to match in the output text |
| `replacement` | Yes | Replacement text for speech synthesis |
| `case_sensitive` | No | Whether matching is case-sensitive |
| `language_code` | No | Restrict the rule to a specific language |
| `description` | No | Notes about the rule |
Rules are ordered, so list position matters.
### Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
pronunciations:
- regex: "\\bDr\\."
replacement: Doctor
case_sensitive: true
- regex: "\\bMr\\."
replacement: Mister
case_sensitive: true
```
## Phrase filtering
Phrase-filtering rules live in:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
voice/response_control/phrase_filtering.yaml
```
These rules can block or intercept phrases before they are spoken. A matched phrase can also trigger a function.
### What a phrase filter contains
Each item in the `phrase_filtering` list can include:
| Field | Required | Description |
| --------------------- | -------- | ------------------------------------------- |
| `name` | Yes | Identifier for the filter |
| `description` | No | Explains what the filter does |
| `regular_expressions` | Yes | Regex patterns to match |
| `say_phrase` | No | Whether to still speak the matched phrase |
| `language_code` | No | Restrict the filter to a specific language |
| `function` | No | Global function to call when a match occurs |
### Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
phrase_filtering:
- name: Block Profanity
description: Blocks profane words from being spoken
regular_expressions:
- "\\bbadword\\b"
say_phrase: false
- name: Competitor Mention Handler
description: Intercept competitor names and redirect
regular_expressions:
- "\\bcompetitor_name\\b"
say_phrase: true
function: handle_competitor_mention
```
## When to use response control
Use response control when standard prompting is not enough and you need a more deterministic layer before output is spoken.
Typical cases include:
* fixing abbreviations or domain-specific terms in TTS
* preventing profanity from being spoken
* reducing the risk of unsafe or brand-damaging output
* intercepting special phrases and triggering code
## Best practices
### For pronunciations
* keep regex patterns targeted and readable
* use language-specific rules when pronunciation should vary by locale
* rely on rule ordering deliberately where multiple patterns could overlap
### For phrase filters
* use phrase filters for safety and brand protection
* keep regex patterns specific to avoid false positives
* only attach a `function` when you need a real side effect
* ensure the `function` value refers to a valid **global function**, not a flow function
**Phrase filters are powerful**
An over-broad regex can suppress or intercept normal output unexpectedly. Keep filters as specific as possible.
## Related pages
See where response control fits within the broader voice-channel configuration.
Learn how global functions can be triggered from phrase filters.
# Safety filters
Source: https://docs.poly.ai/adk/reference/safety_filters
Safety filters block harmful content from entering or leaving the conversation in real time.
They run on user input and on agent output, scoring each turn against four content categories and blocking it before it affects the conversation.
Filters can be configured at the project level and overridden per channel (voice and chat). Each category is enabled independently and tuned to a sensitivity level.
## Location
Safety filters are defined in up to three optional files:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent_settings/
└── safety_filters.yaml # Project-level (general) defaults
voice/
└── safety_filters.yaml # Voice channel override
chat/
└── safety_filters.yaml # Chat channel override
```
A channel-level file will override the project-level defaults for that channel. If no channel file exists, the channel inherits the project-level configuration.
## What safety filters control
Defaults applied to every channel when no channel-specific override is set.
Per-channel tuning for voice or chat, including a global enable toggle.
Violence, hate, sexual, and self-harm content.
Choose how aggressively each category filters: `lenient`, `medium`, or `strict`.
## Categories
All four categories must be configured. Each category has its own `enabled` flag and `level`.
| Category | Description |
| ----------- | ----------------------------------------- |
| `violence` | Filters violent or graphic content |
| `hate` | Filters hateful or discriminatory content |
| `sexual` | Filters sexually explicit content |
| `self_harm` | Filters self-harm related content |
## Fields
| Field | Where | Description |
| --------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled` | Channel files only | `true` or `false` — global toggle for the channel. Omit at project level; the project filter is active whenever any category is enabled. |
| `categories` | All files | Map of the four categories. Required. |
| `categories..enabled` | All files | `true` or `false` — whether this category is active. |
| `categories..level` | All files | Sensitivity level. One of `lenient`, `medium`, `strict`. |
### Sensitivity levels
| Level | Behavior |
| --------- | ------------------------------------ |
| `lenient` | Blocks only the most severe content. |
| `medium` | Balanced filtering. |
| `strict` | Blocks borderline content. |
## Project-level example
Project-level filters omit the global `enabled` key — the filter is active whenever at least one category is enabled.
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
categories:
violence:
enabled: true
level: medium
hate:
enabled: true
level: medium
sexual:
enabled: true
level: medium
self_harm:
enabled: true
level: medium
```
## Channel-level example
Channel files include a top-level `enabled` flag that turns filtering on or off for the entire channel.
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
enabled: true
categories:
violence:
enabled: true
level: strict
hate:
enabled: true
level: medium
sexual:
enabled: true
level: medium
self_harm:
enabled: true
level: strict
```
## Validation rules
`poly push` rejects safety filter files that don't satisfy these rules:
* All four categories (`violence`, `hate`, `sexual`, `self_harm`) must be present.
* Each category must set both `enabled` (boolean) and `level`.
* `level` must be one of `lenient`, `medium`, or `strict`.
* Channel files must include the top-level `enabled` flag as a boolean.
* Unrecognized category keys cause a validation error rather than being silently ignored.
## Best practices
* Keep settings consistent across channels unless a channel has a distinct risk profile (for example, a voice line that handles vulnerable callers may warrant `strict` on `self_harm`).
* Start at `medium` and adjust based on observed false positives and missed content.
* Review filter outcomes periodically — the right level depends on caller demographics and use case, not just the deployment.
* Treat the channel files as overrides, not duplicates: only commit a channel file when it actually differs from the project default.
## On the Agent Studio platform
The same settings can be configured in the Agent Studio UI. The platform docs cover the UI workflow and category descriptions in more depth:
Defaults applied across every channel.
Per-channel overrides for voice.
Per-channel overrides for chat.
## Related references
Configure personality, role, and rules alongside project-level safety filters.
Configure voice-channel greetings, disclaimers, and safety filter overrides.
Configure chat-channel greetings, style, and safety filter overrides.
# SMS templates
Source: https://docs.poly.ai/adk/reference/sms
SMS templates define reusable text messages that the agent can send during a conversation, such as confirmations, links, or verification codes.
Templates support dynamic content through variables stored in conversation state.
**SMS templates are ADK-only**
The Agent Studio UI does not currently expose an editor for `config/sms_templates.yaml`. Define templates through the ADK and push them with `poly push`. Template references of the form `{{twilio_sms:template_name}}` only resolve inside ADK-managed files (`rules.txt`, topic actions, flow prompts) — pasting them into a UI-editable field does not work at runtime.
## Location
SMS templates are defined in:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
config/sms_templates.yaml
```
Templates are listed under the `sms_templates` key.
## What an SMS template contains
Each template can include the following fields:
| Field | Description |
| ------------------- | ------------------------------------------------------------------------------------- |
| `name` | Identifier for the template. Referenced in prompts as `{{twilio_sms:template_name}}`. |
| `text` | Message body. Supports `{{vrbl:variable_name}}` placeholders from `conv.state`. |
| `env_phone_numbers` | Optional sender phone numbers for different environments. |
## Environment-specific sender numbers
If needed, you can define different sender numbers for different environments:
| Environment | Description |
| ------------- | --------------------------------- |
| `sandbox` | Sender number for sandbox use |
| `pre_release` | Sender number for pre-release use |
| `live` | Sender number for production use |
## Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
sms_templates:
- name: booking_confirmation
text: "Hi {{vrbl:customer_name}}, your booking for {{vrbl:booking_date}} is confirmed. Reference: {{vrbl:booking_ref}}"
env_phone_numbers:
sandbox: "+15551234567"
live: "+15559876543"
- name: verification_code
text: "Your verification code is {{vrbl:verification_code}}. It expires in 10 minutes."
```
## How SMS templates are used
Use `{{twilio_sms:template_name}}` to tell the agent which SMS should be sent.
Call a function that triggers the SMS through `conv` or the platform API.
Use `{{vrbl:...}}` placeholders to insert values from conversation state.
## In prompts and instructions
SMS templates can be referenced in rules, topics, and related instructions using:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
{{twilio_sms:template_name}}
```
This lets you reference the correct template by name without embedding the full message body in prompt text.
## Using variables
Template text can include placeholders drawn from `conv.state`, for example:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
{{vrbl:customer_name}}
```
Before the SMS is sent, the corresponding state variables should already be set in code.
For example:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.state.customer_name = "Alice"
conv.state.booking_date = "12 March"
conv.state.booking_ref = "ABC123"
```
## Best practices
* set required state variables before the SMS is triggered
* use separate templates for different purposes such as confirmation, verification, and follow-up
* keep templates short and clear
* configure `env_phone_numbers` when sender numbers differ between environments
* prefer template references over hard-coded SMS text in prompts
**Treat templates as reusable resources**
SMS templates are easier to manage when each template has one clear purpose and a stable name.
## Related pages
Learn how values are stored in `conv.state` and referenced with `{{vrbl:...}}`.
See how deterministic code can set variables and trigger SMS sending.
Configuring SMS channels, sender numbers, opt-out handling, and full template options.
Full reference for `conv.send_sms_template`, `conv.send_sms`, and all other `conv` methods.
# Speech recognition
Source: https://docs.poly.ai/adk/reference/speech_recognition
Speech recognition resources control how the agent processes user speech input on the voice channel.
These resources live under `voice/speech_recognition/` and are used to tune how the agent listens, recognizes, and post-processes spoken input.
**ASR settings are platform-provisioned — update only**
ASR settings are created automatically when a project is created. They can be updated with `poly push` but not created from scratch. See the [equivalent note on agent settings](/adk/reference/agent_settings) for details.
## Location
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
voice/speech_recognition/
├── asr_settings.yaml
├── keyphrase_boosting.yaml
└── transcript_corrections.yaml
```
All three files are voice-specific. Only `asr_settings.yaml` is the core settings file; the others are optional.
## What speech recognition controls
Configure global speech-recognition behavior such as barge-in and latency/accuracy style.
Bias recognition toward specific words or phrases.
Apply regex-based corrections after speech recognition.
## ASR settings
ASR settings are defined in:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
voice/speech_recognition/asr_settings.yaml
```
These settings control global speech-recognition behavior for the voice channel.
### Fields
| Field | Type | Description |
| ------------------- | -------- | -------------------------------------------------------------------------------- |
| `barge_in` | `bool` | Whether the user can interrupt the agent while it is speaking. Default: `false`. |
| `interaction_style` | `string` | Controls the latency/accuracy trade-off. Default: `balanced`. |
### Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
barge_in: false
interaction_style: balanced
```
### Interaction styles
| Style | Behavior |
| ----------------- | ----------------------------------------- |
| `precise` | Higher accuracy, higher latency |
| `balanced` | Default balance of speed and accuracy |
| `swift` | Faster responses, slightly lower accuracy |
| `sonic` / `turbo` | Lowest latency |
## Keyphrase boosting
Keyphrase boosting is defined in:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
voice/speech_recognition/keyphrase_boosting.yaml
```
It biases the recognizer toward specific words or phrases, which is useful for:
* brand names
* product names
* specialist terminology
* domain-specific jargon
### Structure
A `keyphrases` list where each entry includes:
| Field | Required | Description |
| ----------- | -------- | -------------------------------------------------- |
| `keyphrase` | Yes | The word or phrase to boost |
| `level` | No | Boost strength: `default`, `boosted`, or `maximum` |
### Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
keyphrases:
- keyphrase: PolyAI
level: maximum
- keyphrase: reservation
level: boosted
- keyphrase: check-in
level: default
```
## Transcript corrections
Transcript corrections are defined in:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
voice/speech_recognition/transcript_corrections.yaml
```
These rules post-process ASR output to fix common misrecognitions.
They are especially useful for:
* email domains
* repeated digits
* domain-specific phrases
* spoken forms that should be normalized into machine-friendly text
### Structure
A `corrections` list where each entry includes:
| Field | Required | Description |
| --------------------- | -------- | ----------------------------------- |
| `name` | Yes | Identifier for the correction group |
| `description` | No | Explains what the correction fixes |
| `regular_expressions` | Yes | Regex rules used for correction |
Each regex rule can include:
| Field | Required | Description |
| -------------------- | -------- | --------------------------------- |
| `regular_expression` | Yes | Pattern to match |
| `replacement` | Yes | Replacement text |
| `replacement_type` | No | `full` or `partial` / `substring` |
### Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
corrections:
- name: Email domain fix
description: Correct common email domain misrecognitions
regular_expressions:
- regular_expression: at gmail dot com
replacement: "@gmail.com"
replacement_type: full
- regular_expression: at hotmail dot com
replacement: "@hotmail.com"
replacement_type: full
- name: Number normalization
description: Normalize spoken numbers to digits
regular_expressions:
- regular_expression: \bdouble (\d)\b
replacement: \1\1
replacement_type: partial
```
## Best practices
* use `keyphrase_boosting` for terms the recognizer is likely to miss
* keep boosted keyphrases focused and specific
* use transcript corrections for common, repeated recognition errors
* avoid overly broad regex rules that may alter normal input unexpectedly
* choose the ASR interaction style deliberately based on latency and accuracy needs
**Use the lightest possible intervention**
Start with the default settings, then add boosting or transcript corrections only where recognition problems are actually recurring.
## Related pages
See how speech recognition fits into the wider voice-channel configuration.
Configure what happens to output before it is spoken.
# Tests
Source: https://docs.poly.ai/adk/reference/tests
Agent Studio test cases are simulated conversations that run your agent end-to-end in the sandbox environment. They are managed locally as YAML files under test\_suite/ and pushed to Agent Studio with poly push.
Each test case describes a scenario for a simulated user and a set of assertions to evaluate against the resulting conversation. Then, the tests run inside Agent Studio against the pushed branch.
## Where tests fit in the workflow
Tests sit between validation and merge in the standard [CLI working pattern](/adk/reference/cli#working-pattern). Edit locally, validate with `poly validate`, push, then trigger and inspect the test suite with `poly test`.
Use `poly validate` to check project configuration before pushing.
Define test cases under `test_suite/` and run them with `poly test run`.
Use `poly chat`, `poly test show`, and Agent Studio to spot-check behavior on the pushed branch.
## Location
Test cases are defined as one YAML file per test under:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
test_suite/
├── greeting_flow_test.yaml
└── webchat_smoke_test.yaml
```
The directory is optional. Create it only when you have test cases to define.
**Filename must match the test name**
The filename (without `.yaml`) must match the normalized form of the `name` field: lowercased, with punctuation replaced by underscores. `Greeting flow test` becomes `greeting_flow_test.yaml`. `poly push` rejects mismatched names.
## What a test case contains
| Field | Required | Description |
| -------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `name` | Yes | Human-readable test name. Must match the filename when normalized. |
| `scenario` | Yes | Natural-language description of what the simulated user does. Drives the simulator turn-by-turn. |
| `channel` | Yes | `voice` or `webchat`. |
| `language` | Yes | BCP 47 language tag (e.g. `en-GB`). Must be a configured language in the project. |
| `variant` | No | Name of a variant from `config/variant_attributes.yaml`. Defaults to the project default variant. |
| `tags` | No | List of strings used to group, filter, or schedule tests. Usable with `poly test run --tag`. |
| `prompt_assertions` | No | List of natural-language statements that must hold about the agent's behavior. Each is evaluated by an LLM judge. |
| `function_call_assertions` | No | List of expected function calls and their argument values. |
At least one of `prompt_assertions` or `function_call_assertions` should be set — a test with no assertions runs but cannot pass or fail.
## Prompt assertions
Each prompt assertion is a free-text statement evaluated against the full conversation by an LLM judge. Write them as observable behaviors, not internal reasoning.
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
prompt_assertions:
- The agent confirms the caller's booking reference before continuing
- The agent does not ask for the caller's date of birth
```
## Function call assertions
Each function call assertion checks that a global function was called and, optionally, with specific argument values.
| Field | Description |
| ----------- | ------------------------------------------------------------------------------------- |
| `name` | Global function name. Must match a function in `functions/`. |
| `arguments` | List of argument assertions. May be empty to check only that the function was called. |
Argument assertion fields:
| Field | Description |
| ---------------- | ------------------------------------------------ |
| `parameter_name` | Parameter as defined on the function. |
| `expected_value` | Expected value, expressed as a string. |
| `value_type` | One of `string`, `integer`, `number`, `boolean`. |
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
function_call_assertions:
- name: lookup_booking
arguments:
- parameter_name: booking_reference
expected_value: "ABC123"
value_type: string
- parameter_name: party_size
expected_value: "4"
value_type: integer
```
Only function name and argument values are asserted. The function does not have to be the only call in the conversation, and the order of calls is not checked.
## Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
name: Greeting flow test
scenario: Ask for help with booking.
channel: voice
language: en-GB
tags:
- booking
- smoke
prompt_assertions:
- The agent offers to help with booking
function_call_assertions:
- name: lookup_booking
arguments:
- parameter_name: booking_reference
expected_value: "ABC123"
value_type: string
```
A minimal webchat smoke test with only a prompt assertion:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
name: Webchat smoke test
scenario: Say hello on webchat.
channel: webchat
language: en-GB
tags:
- smoke
prompt_assertions:
- The agent greets the user
```
## Validation
`poly validate` checks each test case:
* `channel` must be `voice` or `webchat`
* `scenario` is required and non-empty
* `language` is required and must be one of the project's configured languages (`default_language` or `additional_languages`)
* `variant`, if set, must reference a variant declared in `config/variant_attributes.yaml`
* each `function_call_assertions[*].name` must match a global function under `functions/`
* each argument's `value_type` must be one of `string`, `integer`, `number`, `boolean`
* the filename must match the normalized `name`
Validation runs automatically as part of `poly push`.
## Push and run
Test cases follow the standard ADK lifecycle:
1. edit YAML files under `test_suite/` locally
2. validate with `poly validate`
3. push with `poly push` to sync to Agent Studio
4. trigger and monitor the suite with `poly test run`
`poly push` creates, updates, or deletes test cases on Agent Studio to match local state, including `prompt_assertions` and `tags`. Use `poly test run` to trigger execution and `poly test show` / `poly test list` to inspect results — all without leaving the terminal.
**Tests are branch-scoped**
Tests are pushed to the current branch and run against that branch's agent. Use a branch per scenario when iterating on flows or topics so test results map cleanly to the change under review.
## Running tests from the CLI
The `poly test` command group covers the full testing lifecycle.
### `poly test run`
Trigger a test run against the current branch. Runs all tests by default.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly test run
poly test run --tag smoke
poly test run --files test_suite/greeting_flow_test.yaml
poly test run --dry-run
poly test run --dont-poll
poly test run --push
```
After triggering, the CLI polls for results every 5 seconds and displays a live-updating table. For projects with 20 or fewer tests the full table is shown; for larger suites a compact rolling view is used instead. Both views update in place until the run completes.
| Flag | Description |
| ------------- | ---------------------------------------------------------------------------------------------- |
| `--files` | One or more specific test YAML files to run. |
| `--tag` | Run only tests that carry the specified tag(s). Multiple tags are OR-matched. |
| `--dry-run` | Preview which tests would run without triggering them. |
| `--dont-poll` | Trigger the run and exit immediately. Use `poly test show ` to check results later. |
| `--push` | Push the project before running tests. Equivalent to running `poly push` then `poly test run`. |
| `--path` | Base path to the project. Defaults to the current working directory. |
| `--json` | Machine-readable JSON output. |
When `--dont-poll` is used, the CLI prints the run ID and a `poly test show` command to retrieve results:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Use poly test show to check the status of the test run.
```
### `poly test list`
List past test runs for the current project and branch.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly test list
poly test list --limit 20
poly test list --offset 10
```
The table shows run ID, start time, status, total/passed/failed/error counts, and who triggered the run.
| Flag | Description |
| ---------- | -------------------------------------------------------------------- |
| `--limit` | Number of runs to return. Defaults to `10`. |
| `--offset` | Number of runs to skip. Defaults to `0`. |
| `--path` | Base path to the project. Defaults to the current working directory. |
| `--json` | Machine-readable JSON output. |
### `poly test show`
Inspect a completed test run or drill into a single test case.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly test show
poly test show
```
`poly test show ` prints a summary of the run (status, counts, timestamps) followed by a table of all individual test results.
`poly test show ` drills into a single test — showing assertion results, any function call failures, and the full conversation transcript turn-by-turn.
| Argument | Description |
| -------------- | -------------------------------------------------------------------------- |
| `run_id` | The test run ID. Required. |
| `test_case_id` | Optional. If supplied, shows detailed results for that specific test case. |
| Flag | Description |
| -------- | -------------------------------------------------------------------- |
| `--path` | Base path to the project. Defaults to the current working directory. |
| `--json` | Machine-readable JSON output. |
## Test run statuses
The CLI handles the full set of Agent Studio test run and test case statuses:
| Status | Meaning |
| ------------- | ------------------------------------ |
| `pending` | Queued, not yet started |
| `in_progress` | Currently running |
| `passed` | All assertions passed |
| `failed` | One or more assertions failed |
| `errored` | The test encountered an error |
| `timed_out` | The test run exceeded the time limit |
After a run completes, `poly test run` prints a summary of failures (assertion reasons, function call failures, and conversation IDs) and exits with a non-zero status code when any test failed or errored.
## JSON output
All `poly test` subcommands support `--json` for machine-readable output.
| Command | Key fields |
| ----------------------------------------------- | -------------------------------------------------------------------------------------- |
| `poly test run --json` | `success`, `test_run` (triggered run details); with `--dry-run`: `test_count`, `tests` |
| `poly test list --json` | `success`, `test_runs` |
| `poly test show --json` | `success`, `test_run` |
| `poly test show --json` | `success`, `test` |
## What to cover
Good coverage of a project usually includes:
* the happy path of every flow and major topic
* key error paths — missing booking, invalid input, unavailable slot
* function call shape — confirm the agent calls the right function with the right arguments for each branch of logic
* state transitions across turns — confirm later turns reference earlier user input
* behavior on the channels your project actually ships on (voice, webchat, or both)
## Best practices
* write `scenario` as a short, concrete user goal — "Ask to cancel a booking with reference ABC123" — not a script
* prefer prompt assertions for behavior, function call assertions for integration correctness
* keep each test case focused on one outcome; split combined scenarios into multiple files
* use `tags` consistently (`smoke`, `regression`, ``) so suites can be filtered with `--tag`
* cover error paths, not only success cases
* add a webchat and a voice variant of any critical path that runs on both channels
* validate as part of the normal edit loop, not just before merge
* combine the suite with `poly chat` and interactive review in Agent Studio when behavior depends on the full conversation flow
## Related pages
`poly validate`, `poly push`, `poly chat`, and `poly test` — the commands used in the test workflow.
Reference for the global functions named in function call assertions.
Define the variants referenced by the `variant` field.
Configure the languages a test case can target.
How tests fit into the daily edit / validate / push loop.
# Tooling
Source: https://docs.poly.ai/adk/reference/tooling
The PolyAI ADK fits naturally into a local developer workflow and can be used alongside standard editors, terminals, and AI-assisted coding tools.
The ADK is especially useful when paired with tools that help developers inspect, edit, generate, and review local project files efficiently.
## Recommended tooling
There are two well-supported paths for working with the ADK. They are not mutually exclusive — many developers use both.
### PolyAI ADK extension for VS Code and Cursor
The **PolyAI ADK extension** brings ADK-aware editing into **VS Code** and **Cursor**. It is the recommended path if you prefer an IDE-first workflow.
The extension helps with:
* navigating and editing flows, functions, topics, entities, and agent settings with resource-aware tooling
* catching common mistakes while you edit, before you push
* driving `poly` commands without leaving the editor
* pairing with your IDE's built-in AI features (Cursor's agent, VS Code Copilot, etc.) to generate and update project files
#### Install the extension
The extension is published on **Open VSX**, so it works in both VS Code and Cursor.
1. Open the Extensions view in VS Code or Cursor.
2. Search for `PolyAI ADK` and install it, or install it directly from the [Open VSX listing](https://open-vsx.org/extension/PolyAI/adk-extension).
3. Open a project that has been pulled down with `poly pull`.
Once installed, the extension auto-detects local ADK projects and exposes resource-aware navigation, validation, and commands.
### Claude Code
**Claude Code** is a good alternative when you want an agentic CLI workflow — useful for generating a project from a brief, applying patterns across many files, or running longer build tasks end-to-end.
The repository includes a `.claude/` directory with project-specific instructions and examples.
Claude Code is particularly useful for:
* generating project resources from structured requirements
* updating flows and functions at scale
* applying patterns reused across previous projects
* speeding up repetitive implementation work
#### Loading ADK rules into Claude Code
Before starting a session with Claude Code or another external coding tool, generate a documentation file and pass it as context:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly docs --all --output rules.md
```
Reference `rules.md` in your session prompt. This gives the coding tool accurate knowledge of ADK resource types, constraints, and conventions.
**Use both where useful**
The IDE extension and Claude Code cover different modes of work. You can edit in VS Code or Cursor day-to-day and still reach for Claude Code when you want an agent to generate or refactor a large slice of the project on your behalf.
## Other local tools
The ADK also fits well with standard local development tooling such as:
* a terminal
* Git
* Python
* `uv`
* code editors such as VS Code or IntelliJ-based IDEs
Useful for generating and updating ADK project files from structured inputs.
Helpful for navigating project structure, editing resources, and reviewing changes.
The `poly` CLI is the core interface for local project work.
## How tooling fits into the workflow
Tooling slots into the standard [CLI workflow](/adk/reference/cli#working-pattern): pull or init, edit with your tool of choice, validate, push, and review in Agent Studio.
**Tooling should reduce friction, not reduce scrutiny**
Faster editing and generation are valuable, but project review, validation, and testing still matter.
## Next steps
Configure personality, role, and rules that define agent behavior.
Build conversation flows with prompts, transitions, and entities.
Write Python functions the agent calls at runtime.
# Topics
Source: https://docs.poly.ai/adk/reference/topics
Topics are the agent's knowledge base. They are queried through retrieval-augmented generation (RAG), and when a user's input matches a topic, the agent retrieves its content and follows its actions.
Topics are how you teach the agent facts and guidance about specific subject areas without putting everything into flows or rules.
## Location
Topics live in the `topics/` directory.
Each topic is stored as its own YAML file:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
topics/{Topic Name}.yaml
```
Topic filenames may contain spaces — `topics/Make a Reservation.yaml` is valid. This differs from flow directories, which must be lowercase snake\_case. The topic file name does not affect the topic's behavior or how it is referenced.
## What a topic contains
Each topic has four main fields:
| Field | Description |
| ----------------- | -------------------------------------------------------------------------- |
| `enabled` | Whether the topic is active. Default: `true`. |
| `example_queries` | Example user inputs that should retrieve the topic. |
| `content` | Factual information retrieved by RAG. |
| `actions` | Behavioral instructions the agent should follow when the topic is matched. |
## Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
enabled: true
example_queries:
- What are your opening hours?
- When are you open?
- Are you open on weekends?
- What time do you close?
content: |-
The office is open Monday to Friday from 9am to 5pm.
Weekend hours are Saturday 10am to 2pm. Closed on Sundays.
actions: |-
Tell the user the opening hours from the content above.
## If the user asks about a specific location
Check the location using {{attr:office_location}} and provide the hours for that location.
## If the user wants to speak to someone
Use {{fn:transfer_to_agent}} to connect them with a representative.
```
## How topics work
A topic combines two kinds of information:
The factual material that the agent retrieves through RAG.
The behavioral instructions that tell the agent what to do with that content.
This split is important: content is for facts, actions are for behavior.
## Example queries
`example_queries` help the system understand when a topic should be retrieved.
### Good example queries should:
* cover different ways a user might ask about the same subject
* reflect realistic user phrasing
* stay focused on one subject area
### Limits and guidance
* use no more than **20** example queries
* cover meaningful variation, not every minor wording change
## Content
The `content` field contains factual information only.
This is the material that gets retrieved via RAG and made available to the agent when the topic is matched.
### Content rules
* keep it factual
* do not put function calls in content
* do not use `$variable` or resource references in content
* use multi-line YAML (`|-`) for longer content
### Do not use these in content
* `{{fn:...}}`
* `{{ft:...}}`
* `$variable`
* `{{attr:...}}`
**Keep content factual**
Topic content is for retrieved facts, not behavioral logic. Mixing the two makes topics harder to reason about and maintain.
## Actions
The `actions` field tells the agent how to behave when the topic is matched.
This is the only place inside a topic where you should use references and behavior-oriented instructions.
### Supported references in actions
| Syntax | Meaning |
| ------------------------------ | ------------------------------------------------------ |
| `{{fn:function_name}}` | Call a [global function](/adk/reference/functions) |
| `{{attr:attribute_name}}` | Read a [variant attribute](/adk/reference/variants) |
| `{{twilio_sms:template_name}}` | Reference an [SMS template](/adk/reference/sms) |
| `{{ho:handoff_name}}` | Reference a [handoff](/adk/reference/handoffs) |
| `$variable` | Reference a [state variable](/adk/reference/variables) |
### Writing good actions
Actions should be:
* clear
* scannable
* structured
* behavior-oriented
Use markdown headers like `##` and `###` to break up branches or conditions.
### Prefer
* structured conditional sections
* plain instructions like “Tell the user that...”
* clear points where a function should be called
### Avoid
* dense paragraphs mixing facts and behavior
* `"Say: '...'"` phrasing
* putting factual content into actions
## Best practices
* keep content and actions separate
* use one topic per subject area
* split large topics when they become too broad
* prefer structured `##` branches in actions
* disable topics with `enabled: false` during development instead of deleting them
**Tell, don't script**
Prefer instructions like “Tell the user that ...” over hard-coded dialog such as `Say: '...'`. This lets the agent vary phrasing naturally, especially across languages.
## Related pages
Learn how global functions referenced in topic actions are defined.
See how topics hand off to structured processes using `conv.goto_flow`.
See how variant attributes can be referenced from topic actions.
How topics are retrieved, ranked, and injected — RAG mechanics, topic types, and retrieval tuning.
# Translations
Source: https://docs.poly.ai/adk/reference/translations
Translations define localized text strings for your agent. Each translation has a key and a set of language-specific values, so the agent can respond in the user's configured language.
Translations are paired with [languages](/adk/reference/languages): every translation key must include an entry for the default language and for each additional language the project supports.
## Location
Translations are defined in:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
config/translations.yaml
```
Translations are listed under the `translations` key. The file is optional.
## What a translation contains
| Field | Description |
| -------------- | ------------------------------------------------------------------------------------ |
| `name` | Translation key identifier. Referenced in rules, topics, and flows as `{{tr:name}}`. |
| `translations` | Map of BCP 47 language codes to localized text strings. |
## Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
translations:
- name: greeting
translations:
en-GB: Hello, how can I help you?
fr-FR: Bonjour, comment puis-je vous aider?
- name: farewell
translations:
en-GB: Goodbye, have a nice day!
fr-FR: Au revoir, bonne journée!
```
## Validation
* `name` is required and cannot be empty.
* Each translation must have at least one language entry.
* If `agent_settings/languages.yaml` is present, every configured language (default + additional) must have an entry in each translation. Missing languages cause a validation error.
* Duplicate translation names are not allowed.
## Referencing translations
Translations are referenced by name in rules, topics, and flow prompts:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
{{tr:greeting}}
```
At runtime, the platform resolves the reference to the localized string for the current conversation language.
## Best practices
* use descriptive translation keys that indicate purpose (e.g. `greeting`, `error_not_found`, `confirmation_prompt`)
* ensure every configured language has an entry for every translation key before pushing
* keep translation values consistent in tone and meaning across languages
* prefer translation references over hard-coded strings in prompts when an agent supports more than one language
**Translation keys are shared across channels**
A translation key resolves to the same value on voice and chat. Use [SMS templates](/adk/reference/sms) when you need channel-specific copy.
## Related pages
Configure the default and additional languages that translations must cover.
See how translations fit alongside other agent configuration.
Reusable SMS bodies, complementary to translations for channel-specific copy.
# Variables
Source: https://docs.poly.ai/adk/reference/variables
Variables represent values stored in conv.state and are discovered automatically by scanning function code.
## How variables work
When you assign a value to `conv.state.customer_name` in code, `customer_name` becomes a tracked variable.
For example:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.state.customer_name = "Alice"
```
The ADK discovers variables by scanning:
* global functions
* flow functions
* function steps
This means variables are not manually declared in a separate configuration file. They emerge from the code that uses them.
## Why variables matter
Use variables to carry state across turns and reuse values in prompts, topics, and templates.
Store values that should survive across turns.
Reference saved values in prompts and instructions.
Reuse state in SMS templates and other generated text.
Let functions and flows branch based on previously stored values.
## Setting state in code
Set variables by writing to `conv.state`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.state.customer_name = "Alice"
conv.state.account_balance = 150.00
conv.state.is_verified = True
```
## Reading state in code
Read variables from `conv.state`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
name = conv.state.customer_name # returns None if not set
if conv.state.is_verified:
...
```
If a variable has not been set, reading it returns `None`.
## Using variables in prompts and templates
Variables can be referenced in prompts, topic actions, SMS templates, and related text fields using either of these forms:
* `{{vrbl:variable_name}}`
* `$variable_name`
Both forms are supported, but `{{vrbl:variable_name}}` is preferred because it is validated by the ADK.
### Example in prompt text
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
The customer's name is $customer_name and their balance is $account_balance.
```
### Example in a template
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
text: "Hi {{vrbl:customer_name}}, your booking is confirmed for {{vrbl:booking_date}}."
```
## Important rules
### In prompts
Use:
* `$variable`
* `{{vrbl:variable}}`
Do not use:
* `conv.state.variable`
### For structured values
Do not use:
* `$var.attribute`
If you need to expose a structured value in prompts, convert it to a string in Python first and store that string in state.
**Prompt syntax is not Python syntax**
In prompts and templates, use `$variable` or `{{vrbl:variable}}`, not `conv.state.variable`.
## Best practices
* variables are discovered automatically, so no manual registration is needed
* use descriptive snake\_case names
* initialize variables early, such as in `start_function` or near the beginning of a flow
* keep variable names consistent across functions and prompts
* prefer `{{vrbl:...}}` in user-facing text fields for better validation
## Related pages
Learn where variables are typically created, updated, and read.
See how state variables are used in reusable text messages.
# Variants
Source: https://docs.poly.ai/adk/reference/variants
Variant attributes provide per-variant configuration so prompts and behavior can change by location, environment, or tenant without separate code or deployments.
At runtime, the platform selects a variant, and the agent reads the attributes associated with that variant.
## Location
Variant attributes are defined in:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
config/variant_attributes.yaml
```
## What the file contains
The file has two top-level keys:
* `variants`
* `attributes`
## Variants
The `variants` section defines the available variants.
Each variant includes:
| Field | Required | Description |
| ------------ | -------- | ---------------------------------------------------------------------- |
| `name` | Yes | Unique identifier for the variant |
| `is_default` | No | Marks the fallback variant used when no variant is resolved at runtime |
Exactly one variant should have `is_default: true`.
## Attributes
The `attributes` section defines the values that vary by variant.
Each attribute includes:
| Field | Description |
| -------- | -------------------------------------------- |
| `name` | Attribute identifier, ideally in snake\_case |
| `values` | Map from variant name to string value |
Every attribute must provide a value for every defined variant, even if that value is an empty string.
## Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
variants:
- name: new_york
is_default: true
- name: london
- name: tokyo
attributes:
- name: office_phone
values:
new_york: "+12125551234"
london: "+442071234567"
tokyo: "+81312345678"
- name: office_hours
values:
new_york: "9am - 5pm EST"
london: "9am - 5pm GMT"
tokyo: "9am - 5pm JST"
- name: greeting_name
values:
new_york: "New York Office"
london: "London Office"
tokyo: "Tokyo Office"
- name: custom_disclaimer
values:
new_york: |-
This call is recorded for quality assurance.
You may request a copy of this recording.
london: |-
This call may be recorded in accordance with UK regulations.
tokyo: ""
```
## Why variants are useful
Variants let one agent behave differently in different contexts without duplicating the whole project.
Change names, labels, or brand-specific wording.
Swap phone numbers, addresses, and office hours.
Store values such as region codes, timezones, or flags.
Reuse the same logic with tenant-specific values.
## Using variant attributes in prompts and resource files
Use `{{attr:attribute_name}}` in supported text fields such as:
* flow step prompts
* topic actions
* rules
* greeting messages
* disclaimer messages
* personality `custom`
* role `custom`
### Example
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Our office number is {{attr:office_phone}}. We're open {{attr:office_hours}}.
```
## Using variant attributes in Python
In code, variant values are read from `conv.variant`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
phone = conv.variant.office_phone
hours = conv.variant.office_hours
```
Use the same attribute names that are defined in `variant_attributes.yaml`.
## Typical attribute types
Common uses include:
| Category | Examples |
| ------------- | ----------------------------------------------- |
| Branding | greeting name, company name |
| Contact | phone numbers, addresses, office hours |
| IDs | location ID, region code |
| Feature flags | `"True"` / `"False"` strings, checked in Python |
| URLs | portal links, payment links |
| Environment | timezone, `is_live` |
## Important formatting notes
* variant names with special characters should be quoted
* multi-line values should use `|-`
* every variant must have a value for every attribute
**Missing values will fail validation**
If a variant is missing from an attribute's `values` map, validation will fail.
## Best practices
* keep variant names stable over time
* set exactly one default variant
* provide a value or `""` for every variant in every attribute
* prefer `{{attr:...}}` over hard-coded strings when values vary by location or environment
* use multi-line YAML for disclaimers, instructions, or longer text values
## Related pages
See how variant attributes are used in topic actions.
Use variant attributes in greetings and disclaimers.
How variants are routed at runtime — SIP header routing, default selection, and `conv.variant` access.
# Voice settings
Source: https://docs.poly.ai/adk/reference/voice_settings
Voice settings configure how the agent behaves on the voice channel.
They are defined in voice/configuration.yaml.
**Platform-provisioned — update only**
Voice settings are created automatically when a project is created. They can be updated with `poly push` but not created from scratch. See the [equivalent note on agent settings](/adk/reference/agent_settings) for details.
Voice settings control what the agent says at the start of a call and how it should sound throughout the conversation.
## Location
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
voice/
├── configuration.yaml
└── safety_filters.yaml # Optional
```
## What voice settings control
The first message the agent speaks when a call starts.
Channel-specific instructions that shape how the agent speaks.
An optional message played before the greeting, such as a recording notice.
Optional voice-channel content safety filter overrides.
## Greeting
The greeting is the first message the agent speaks when a call starts.
### Fields
| Field | Required | Description |
| ----------------- | -------- | ---------------------------------------------------------------------------- |
| `welcome_message` | Yes | Text of the greeting. Supports `{{attr:...}}` and `{{vrbl:...}}` references. |
| `language_code` | Yes | BCP-47 language code, for example `en-GB` or `en-US`. |
### Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
greeting:
welcome_message: Hello! Welcome to our service. How can I assist you today?
language_code: en-GB
```
## Style prompt
The style prompt contains channel-specific instructions that shape how the agent speaks.
Use this for voice-specific guidance such as:
* phrasing
* verbosity
* spoken tone
* conversational pacing
This is separate from the agent's broader personality. Use it to shape how the agent should sound specifically on phone calls.
### Fields
| Field | Required | Description |
| -------- | -------- | ------------------------------------------------------------------ |
| `prompt` | No | Free-text style instructions. Resource references are not allowed. |
### Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
style_prompt:
prompt: You are a helpful and professional customer service assistant. Use natural, conversational phrasing.
```
**Keep voice guidance channel-specific**
Use the style prompt for voice-specific speaking guidance. Use agent settings for the broader identity, role, and behavioral rules that apply across the agent.
## Disclaimer message
A disclaimer message is an optional notice played at the start of a call before the greeting.
Typical examples include:
* recording notices
* compliance messages
* service disclaimers
### Fields
| Field | Required | Description |
| --------------- | -------- | ----------------------------------------------------------------------- |
| `message` | No | Disclaimer text. Supports `{{attr:...}}` and `{{vrbl:...}}` references. |
| `enabled` | No | Whether the disclaimer is played. |
| `language_code` | No | BCP-47 language code for the disclaimer. |
### Example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
disclaimer_messages:
message: This conversation may be recorded for quality assurance.
enabled: true
language_code: en-GB
```
## Safety filters
`voice/safety_filters.yaml` is an optional file that overrides the project-level safety filter settings for the voice channel. When present, it takes precedence over `agent_settings/safety_filters.yaml` for voice interactions.
See the [Safety filters reference](/adk/reference/safety_filters) for the full schema, field descriptions, and examples.
## Full example
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
greeting:
welcome_message: Hello! Welcome to our service. Your account shows {{attr:member_status}}. How can I assist you today?
language_code: en-GB
style_prompt:
prompt: You are a helpful and professional customer service assistant.
disclaimer_messages:
message: This conversation may be recorded for quality assurance.
enabled: true
language_code: en-GB
```
## Related voice resources
Configure content safety filtering at the project and channel level.
Configure ASR settings, keyphrase boosting, and transcript corrections.
Configure pronunciations and phrase filtering before output is spoken.
# Build an agent with the ADK
Source: https://docs.poly.ai/adk/tutorials/build-an-agent
This guide walks through how to go from a blank slate to a production-ready voice agent using **Agent Studio**, the **PolyAI ADK**, and — optionally — the **PolyAI ADK extension** for **VS Code** or **Cursor**, or a coding agent such as **Claude Code**.
There are two common ways to build with the ADK:
| Workflow | Description |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **CLI workflow** | The hands-on developer path. You run the commands yourself, edit files locally, and push changes back to Agent Studio. |
| **AI-agent workflow** | You provide a brief; a coding tool uses the ADK to generate and push the project files on your behalf. |
Gather the requirements, business rules, API information, and reference material.
Using the ADK, the project files are created, edited, validated, and prepared locally.
The generated work is pushed back into Agent Studio, where it can be reviewed, merged, and deployed.
## Architecture at a glance
| Role | Responsibility |
| ---------------- | ---------------------------------------------------------------------------------- |
| **You** | Provide requirements, project context, and business rules |
| **PolyAI ADK** | Connect the local project to Agent Studio and manage sync, validation, and tooling |
| **Coding agent** | Optionally generate and update files using the ADK |
| **Agent Studio** | Host, preview, review, merge, and deploy the agent |
## Local project structure
See [Working locally — What a local project contains](/adk/concepts/working-locally#what-a-local-project-contains) for the full directory tree. In short, the project mirrors what Agent Studio understands: `agent_settings/`, `flows/`, `functions/`, `topics/`, `voice/`, `chat/`, and `config/`.
## Workflow 1 - CLI workflow
The CLI workflow is the manual developer path. You use the ADK directly, edit the project locally, and push changes back to Agent Studio.
You can run this workflow in whichever editing surface you prefer: a plain terminal, or **VS Code** / **Cursor** with the [PolyAI ADK extension](/adk/reference/tooling#polyai-adk-extension-for-vs-code-and-cursor) for resource-aware navigation and validation. Both count as the CLI workflow — the difference is only the editing surface.
### Step 1 - Initialize your project
Link a local folder to an existing Agent Studio project. The agent must already exist in Agent Studio.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly init
```
`poly init` walks you through interactive dropdowns for region, account, and project. It creates the project directory and pulls the current configuration. Change into the project directory before running any further commands. See [First commands](/adk/get-started/first-commands) for flag options and details.
### Step 2 - Set up the environment
Configure any API keys or environment variables needed for the project. `poly init` pulls the current configuration automatically, but you can run `poly pull` at any time to refresh it:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly pull
poly pull -f
```
**Run commands from the project folder**
All CLI commands should be run from within the local project folder, unless you explicitly use the relevant path flag.
### Step 3 - Chat with the agent
Start an interactive chat session to confirm the connection works and inspect runtime behavior.
**`poly chat` runs against Agent Studio, not your local files**
`poly chat` connects to the last pushed state of your current branch (or sandbox on `main`). Push first, or use `poly chat --push`.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly chat
poly chat --environment sandbox --channel voice
poly chat --functions --flows --state
```
### Step 4 - Review the docs and understand the SDK
Use the CLI docs command to inspect the available resources and learn how they fit together.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly docs --all
poly docs flows functions topics
```
Resource-specific documentation is available in the reference section:
[agent settings](/adk/reference/agent_settings),
[voice settings](/adk/reference/voice_settings),
[chat settings](/adk/reference/chat_settings),
[flows](/adk/reference/flows),
[functions](/adk/reference/functions),
[topics](/adk/reference/topics),
[entities](/adk/reference/entities),
[handoffs](/adk/reference/handoffs),
[variants](/adk/reference/variants),
[SMS templates](/adk/reference/sms),
[variables](/adk/reference/variables),
[speech recognition](/adk/reference/speech_recognition),
[response control](/adk/reference/response_control),
[safety filters](/adk/reference/safety_filters),
[languages](/adk/reference/languages),
[translations](/adk/reference/translations), and
[experimental config](/adk/reference/experimental_config).
### Step 5 - Customize the agent
This is the core build phase. Create a branch, edit resources locally, track changes, and push them back.
**Read the anti-patterns page first**
Before editing, review the [common anti-patterns](/adk/concepts/anti-patterns) to avoid flow control bugs, logging noise, and prompt logic mistakes that are easy to introduce but hard to debug.
#### Branching
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly branch create my-feature
poly branch switch my-feature
poly branch current
poly branch list
```
#### Functions
Create or modify backend functions the agent calls at runtime. See the [functions reference](/adk/reference/functions) for the full API.
Typical locations include:
* global functions under the functions directory
* lifecycle hooks such as start and end functions
* flow-scoped functions
* function steps inside flows
#### Topics
Add or edit [knowledge-base topics](/adk/reference/topics) used for retrieval.
#### Agent settings
Update the [personality, role, and rules](/adk/reference/agent_settings) that define the agent's global behavior.
#### Flows
Build [conversation flows](/adk/reference/flows), including prompts, step transitions, [entities](/adk/reference/entities), and function steps.
#### Channel-specific settings
Adjust greeting messages, disclaimers, and style prompts for [voice](/adk/reference/voice_settings) and [chat](/adk/reference/chat_settings).
#### Safety filters
Configure [content safety filtering](/adk/reference/safety_filters) at the project level and per channel.
#### Handoffs, SMS, and variants
Define [escalation paths](/adk/reference/handoffs), [SMS templates](/adk/reference/sms), and [per-variant configuration](/adk/reference/variants).
#### Languages and translations
Configure [supported languages](/adk/reference/languages) and [localized text strings](/adk/reference/translations) for multilingual agents.
#### ASR and response control
Tune [speech recognition](/adk/reference/speech_recognition) and control [TTS behavior](/adk/reference/response_control).
#### Experimental config
Enable or tune [experimental features](/adk/reference/experimental_config) where needed.
### Step 6 - Track and validate changes
Inspect the local changes before pushing.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly status
poly diff
poly diff --files
poly validate
poly format
poly revert
poly revert
```
**`poly format` may crash on projects with YAML sub-resources**
Running `poly format` on a project that contains YAML-defined sub-resources (such as `config/handoffs.yaml` or `voice/configuration.yaml`) can produce errors like:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
[Errno 20] Not a directory: '.../config/handoffs.yaml/handoffs/Default_handoff'
```
This is a known bug in how the formatter resolves paths inside YAML files. Use `--files` to format specific Python files instead:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly format --files functions/my_function.py
```
**`poly validate` may fail on platform-generated functions**
Projects built via Quick Agent Setup often include server-generated functions such as `handoff.py` or `hangup.py` whose signatures do not declare a `conv: Conversation` parameter. The ADK's local validator will reject these, blocking `poly push`.
The cleanest fix is to add `conv: Conversation` to the function signature yourself:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def handoff(conv: Conversation):
...
```
Alternatively, skip local validation and let the platform validate the push instead:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly push --skip-validation
```
### Step 7 - Push changes
Push the local changes back to Agent Studio.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly push
poly push --dry-run
poly push -f
poly push --skip-validation
```
### Step 8 - Test against sandbox
Once your branch is merged in Agent Studio, test the agent by chatting with it against the sandbox environment.
**Pushing before chatting**
Push your latest changes before chatting — `poly chat` connects to the last pushed state. Target a specific environment with `--environment sandbox`, `--environment pre-release`, or `--environment live`.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly chat --environment sandbox
poly chat --environment sandbox --functions --flows
```
### Step 9 - Iterate on quality
Review, refine, and test again. You can also use the review command to share diffs with teammates.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly review create
poly review create --before main --after my-feature
```
Make test calls, inspect transcripts, refine prompts, flows, and functions, and then re-push.
### Step 10 - Deploy to production
Once the changes are pushed and validated, merge the branch in Agent Studio and deploy the project.
**Merging from the CLI or the Agent Studio web UI**
Merge from the CLI with `poly branch merge ''`, which merges the current branch into `main`. You can also merge through the Agent Studio web UI by switching to the branch and clicking **Merge**. After merging, run `poly chat --environment sandbox` to test. See the [Branch merging reference](/adk/reference/branch_merge) for the full conflict-resolution flow.
### Step 11 - Monitor performance
Use Agent Studio analytics to monitor containment, CSAT, handle time, and flagged transcripts. Pull changes back locally as needed and continue iterating.
## Workflow 2 - AI-agent workflow
The AI-agent workflow uses a coding agent — such as **Claude Code**, or an in-editor agent in **VS Code** or **Cursor** paired with the [PolyAI ADK extension](/adk/reference/tooling#polyai-adk-extension-for-vs-code-and-cursor) — to run the same development loop on your behalf.
Requirements, business rules, integrations, and API documentation.
It uses the ADK to read documentation, generate files, and push the result.
Agent Studio remains the place where the work is checked, merged, and deployed.
### Step 1 - Gather requirements
Collect the project context before you begin.
Include anything the coding tool will need to produce a working agent:
* API endpoint URLs
* business rules
* use-case descriptions
* internal notes or emails
* reference material
* links to API documentation
The more complete and structured your input is, the less correction the output requires.
**Front-load the context**
Gather everything up front. Providing context piecemeal produces piecemeal output.
### Step 2 - Create a new project in Agent Studio
Open **Agent Studio** and create a brand-new project.
The project starts empty:
* no knowledge base
* no flows
* no configuration
That blank starting point is intentional. The coding tool populates the project in later steps.
**Think of Agent Studio as the deployment target**
Agent Studio is where the project lives, but the coding tool generates the actual content.
### Step 3 - Start the coding tool via the CLI
Open your terminal and start the coding tool.
At this stage:
* the ADK must already be installed
* the Agent Studio project must already exist
* the coding tool should initialize and link the project using the ADK
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly init --region --account_id --project_id
```
`poly init` pulls the current configuration automatically — there is no need to run `poly pull` separately. The ADK acts as the bridge between your local environment and Agent Studio, letting the coding tool read from and write back to the project.
**Run `poly docs --all` before generating any files**
Immediately after initializing, run `poly docs --all` to produce a complete resource reference. Without it, a coding agent has no schema context for resource structure and field names, and will hallucinate them.
Note that `poly docs --all` documents the ADK's resource layer (topics, flows, entities, variants, and so on) but does not cover every runtime `Conversation` method. In particular, `conv.send_sms_template`, `conv.send_sms`, and `conv.caller_number` are not present in the output. For the full runtime API, direct the coding agent to the [conv object reference](https://docs.poly.ai/tools/classes/conv-object) on the platform docs.
### Step 4 - Give the coding tool its context
Provide the coding tool with the information you gathered earlier.
Include:
* project-specific requirements
* the URL to the business's public API documentation
* relevant internal context
* useful patterns or best practices from previous projects
Use the docs command to generate a reference file the coding tool can read:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly docs --all
```
### Step 5 - Generate the project files
Once the context is in place, the coding tool generates the project files.
This produces the assets the agent needs, including:
Dialog logic and routing for the agent.
Backend functions used during calls.
Information the agent can reference when answering questions.
Both real API connections and mock endpoints for testing.
The generated files follow ADK structure and are ready to push to Agent Studio.
### Step 6 - Push to Agent Studio
Once the files are generated, use the ADK to push them to Agent Studio.
A new branch is created in the project so the generated work can be reviewed safely before anything goes live.
When you switch to that branch in Agent Studio, you should see the generated changes, such as:
* updated greeting messages
* new knowledge base entries
* a built tracking flow
* real and mock API integrations
**Use the branch review step**
The branch-based workflow makes it possible to inspect what was generated before merging it into the main project.
### Step 7 - Review, merge, and deploy
Review the generated work inside Agent Studio.
Check that the key parts of the agent look correct:
* flows
* functions
* knowledge base entries
* API integrations
Once everything looks right:
1. merge the branch into `main` — either with [`poly branch merge`](/adk/reference/branch_merge) from the CLI or through the Agent Studio web UI
2. deploy the project
At that point, the agent is live.
## CLI command overview
| Command | Description |
| ----------------- | ------------------------------------------- |
| **poly init** | Initialize a new project locally |
| **poly pull** | Pull remote config into the local project |
| **poly push** | Push local changes to Agent Studio |
| **poly status** | List changed files |
| **poly diff** | Show diffs |
| **poly revert** | Revert local changes |
| **poly branch** | Branch management |
| **poly format** | Format resource files |
| **poly validate** | Validate project configuration locally |
| **poly review** | Create a diff review page |
| **poly chat** | Start an interactive session with the agent |
| **poly docs** | Output resource documentation |
## The overall loop
1. create or connect a project
2. build locally using the ADK
3. push to Agent Studio
4. review, merge, and deploy
## Next steps
Apply the workflow to a real-world example with flows, functions, and variants.
Understand resource architecture, local development patterns, and team workflows.
Explore the available ADK commands and options.
# Tutorials
Source: https://docs.poly.ai/adk/tutorials/index
Step-by-step guides for real workflows.
Follow the end-to-end workflow from project initialization to production deployment.
Build a complete voice agent that takes reservations, confirms details, and sends SMS confirmations — covering flows, entities, functions, topics, and testing.
# Build a restaurant booking agent
Source: https://docs.poly.ai/adk/tutorials/restaurant-booking-agent
This tutorial builds a complete voice agent for a restaurant. By the end you will have a working agent that greets callers, collects a reservation, and confirms the details.
You will use the ADK to define the agent, iterate on it, and push it for testing. Merging and deployment still happen in Agent Studio.
## What you will build
The agent handles calls to **Maison**, a fictional restaurant. When a caller asks to make a reservation, the agent:
1. Enters a booking flow and collects the caller's name, party size, and preferred date and time
2. Confirms the details back to the caller
3. Stores the booking on the conversation state
## What you will learn
This tutorial covers:
* creating an empty project in Agent Studio
* initializing a local project and pulling its configuration
* working on a branch
* defining entities for structured data collection
* building a multi-step flow with default steps and a function step
* writing a start function and a booking function
* triggering a flow from a topic
* tuning speech recognition with keyphrase boosting and transcript corrections
* adjusting spoken output with pronunciation rules
* previewing changes with `poly status` and `poly diff`
* pushing and testing with `poly chat`
## Prerequisites
Before you start:
* you have Python 3.14 or later installed
* you have `uv` installed
* you have installed the ADK: `pip install polyai-adk`
* you have a PolyAI API key exported as `POLY_ADK_KEY`
* **you have created an empty project in Agent Studio** — the ADK cannot create projects, it can only sync them. Open Agent Studio, create a new project, and make a note of its **account ID** and **project ID**. You will need both to initialize the local project.
**Finding your account ID and project ID**
Both IDs are visible in the Agent Studio project URL (e.g. `https://studio.poly.ai///...`) and on the project's settings page.
Verify the CLI is available:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly --version
```
## Part 1 — Set up the project
### Initialize
Create a directory and run `poly init` inside it:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
mkdir maison && cd maison
poly init
```
`poly init` walks you through interactive dropdowns for region, account, and project — pick the empty project you created earlier. Single options (one region, one account) are auto-selected. If you'd rather pass the IDs from the Agent Studio URL directly, use `poly init --region --account_id --project_id `.
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Initializing project /...
✓ Project initialized at /Users/yourname/maison//
```
If you prefer to pick values interactively, run `poly init` with no flags — you will be prompted for each one in turn. You can navigate with the arrow keys or start typing to filter the list.
**`poly init` creates a subdirectory**
The project is created at `{cwd}/{account_id}/{project_id}`, not directly in your current directory. After init completes, change into the project directory before running any other commands:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
cd /
```
`poly init` also pulls the current configuration from Agent Studio automatically. There is no need to run `poly pull` separately.
Your project directory now contains the initial configuration as YAML and Python files:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
//
├── project.yaml
├── _gen/
├── agent_settings/
│ ├── personality.yaml
│ ├── role.yaml
│ └── rules.txt
├── config/
│ └── handoffs.yaml
├── variables/ # Virtual — no files on disk
└── voice/
├── configuration.yaml
└── speech_recognition/
└── asr_settings.yaml
```
The `_gen/` directory contains auto-generated platform code. Do not edit it — it is overwritten on every pull.
Files like `config/entities.yaml`, `flows/`, and `topics/` are only created when you add those resources to the project. You will create them in this tutorial.
### Create a working branch
It is good practice to make changes on a branch. Create one now:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly branch create booking-flow
```
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Branch 'booking-flow' created (ID: BRANCH-XXXXXXXX)
```
You are now on the `booking-flow` branch. Any changes you push will go to that branch in Agent Studio, leaving `main` (and Sandbox) untouched.
**Check which branch you are on**
Run `poly status` at any time to see your current branch, region, and when the project was last pulled.
## Part 2 — Define the agent
### Personality
Open `agent_settings/personality.yaml`. Adjust the adjectives to suit the Maison brand:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
adjectives:
Polite: true
Calm: true
Kind: true
custom: ""
```
The file has two fields:
* **`adjectives`** — a map of preset tonal traits. Each is set to `true` or `false`; every selected trait is combined into the agent's personality.
* **`custom`** — a free-text description that can extend or replace the adjectives. It accepts `{{attr:...}}` and `{{vrbl:...}}` references, so the personality can vary per [variant](/adk/reference/variants) or per call.
**Allowed adjective values**
`adjectives` keys must come from a fixed set: `Polite`, `Calm`, `Kind`, `Funny`, `Energetic`, `Thoughtful`, and `Other`. Any other key causes `poly push` to fail with a validation error.
**How `Other` works**
`Other` is the "none of the above" switch. When you set `Other: true`, every other adjective must be `false` (or omitted) — combining `Other: true` with any other adjective set to `true` fails validation with:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Other adjective can only be set if no other adjectives are selected.
```
Use `Other: true` together with the `custom` field when the six presets do not capture the tone you want and you would rather describe the personality entirely in free form. You do **not** need `Other: true` just to use `custom` — `custom` can always be added on top of preset adjectives to refine them further.
### Role
Open `agent_settings/role.yaml` and describe what the agent is:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
value: Restaurant Reservations Agent
additional_info: Takes table reservations for Maison restaurant
custom: ""
```
### Rules
Open `agent_settings/rules.txt`. Rules give the agent standing instructions that apply across every turn. Add the following:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
You are the reservations agent for Maison, an upscale French restaurant.
Always be warm and welcoming.
When a caller wants to make a reservation, use {{fn:start_booking_flow}} to begin the booking process.
Never tell the caller their reservation is confirmed until the booking flow has completed.
```
`{{fn:start_booking_flow}}` references a global function you will define later. The model uses these references to understand when to call them.
**`{{fn:...}}` only works for global functions**
Only functions in the top-level `functions/` directory can be referenced with `{{fn:...}}` in rules and topics. Flow function steps (files inside `flows/{name}/function_steps/`) are called automatically by the flow — they cannot be referenced this way.
## Part 3 — Define the entities
Entities are the structured data values the agent can collect from a caller. Create `config/entities.yaml`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
entities:
- name: customer_name
description: The caller's full name for the reservation
entity_type: name_config
config: {}
- name: party_size
description: Number of guests for the reservation, between 1 and 20
entity_type: numeric
config:
has_decimal: false
has_range: true
min: 1
max: 20
- name: reservation_date
description: The date the caller wants to dine, such as "Friday" or "the 15th"
entity_type: date
config:
relative_date: true
- name: reservation_time
description: The time the caller wants to dine, such as "7pm" or "half past seven"
entity_type: time
config:
enabled: true
start_time: "12:00"
end_time: "22:00"
```
These four entities will be collected across the steps of the booking flow.
**`relative_date` and round-trips**
The `relative_date: true` field is valid and is sent to the platform on push. However, date entity config may not be returned by the platform on pull, so after a `poly pull` you may see `config: {}` for the `reservation_date` entity. This is a known platform behavior and does not affect how the entity works at runtime.
**Automatic ASR biasing**
When a default step lists these entities in `extracted_entities`, the platform automatically configures speech recognition to be more accurate for that kind of input — dates, times, numbers, and names.
## Part 4 — Build the booking flow
Flows live under `flows/`. Each flow gets its own directory. The directory name must be the snake\_case version of the flow's `name` field — so a flow named `Booking Flow` must live in `flows/booking_flow/`.
Create the directory structure:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
mkdir -p flows/booking_flow/steps
mkdir -p flows/booking_flow/function_steps
```
### Flow configuration
Create `flows/booking_flow/flow_config.yaml`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
name: Booking Flow
description: Collects reservation details and confirms a table booking
start_step: Collect Name
```
### Step 1 — Collect name
Create `flows/booking_flow/steps/collect_name.yaml`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
step_type: default_step
name: Collect Name
prompt: |
## Collect the caller's name
Ask the caller for the name the reservation should be under. Be warm and conversational.
Do not repeat the question if the caller has already given a name.
Collected so far: {{entity:customer_name}}
conditions:
- name: has_name
condition_type: step_condition
description: Caller has provided their name
required_entities:
- customer_name
child_step: Collect Party Size
extracted_entities:
- customer_name
```
### Step 2 — Collect party size
Create `flows/booking_flow/steps/collect_party_size.yaml`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
step_type: default_step
name: Collect Party Size
prompt: |
## Collect party size
Ask how many guests will be dining. The restaurant accepts between 1 and 20 guests.
If the caller gives a number outside that range, explain politely and ask again.
Name on reservation: {{entity:customer_name}}
Party size so far: {{entity:party_size}}
conditions:
- name: has_party_size
condition_type: step_condition
description: Caller has given a valid party size
required_entities:
- customer_name
- party_size
child_step: Collect Date
extracted_entities:
- party_size
```
### Step 3 — Collect date and time
Create `flows/booking_flow/steps/collect_date.yaml`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
step_type: default_step
name: Collect Date
prompt: |
## Collect reservation date and time
Ask the caller when they would like to dine. Collect both the date and the time.
Maison is open for lunch from noon and for dinner until 10pm.
Party: {{entity:customer_name}}, {{entity:party_size}} guests
Date collected: {{entity:reservation_date}}
Time collected: {{entity:reservation_time}}
conditions:
- name: has_date_and_time
condition_type: step_condition
description: Caller has given both date and time
required_entities:
- customer_name
- party_size
- reservation_date
- reservation_time
child_step: confirm_booking
extracted_entities:
- reservation_date
- reservation_time
```
`child_step: confirm_booking` uses the Python filename (without `.py`) rather than a step name — that is the convention for pointing to a function step.
### Step 4 — Confirm booking (function step)
Function steps are deterministic Python. They run without model interpretation. Create `flows/booking_flow/function_steps/confirm_booking.py`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from _gen import * #
def confirm_booking(conv: Conversation, flow: Flow):
"""Confirm the reservation and store the details."""
name = conv.entities.customer_name.value if conv.entities.customer_name else "Guest"
size = conv.entities.party_size.value if conv.entities.party_size else "?"
date = conv.entities.reservation_date.value if conv.entities.reservation_date else "?"
time = conv.entities.reservation_time.value if conv.entities.reservation_time else "?"
# Store the confirmed details in conversation state
conv.state.booking_confirmed = True
# Exit the flow and return a context string for the model
conv.exit_flow()
return f"Booking confirmed: table for {size} under {name} on {date} at {time}."
```
After this function runs, the model receives the returned context string and uses it to continue the conversation.
## Part 5 — Add a global function to enter the flow
Global functions live in the top-level `functions/` directory. Create `functions/start_booking_flow.py`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from _gen import * #
@func_description("Start the table reservation process for Maison restaurant")
def start_booking_flow(conv: Conversation):
"""Enter the booking flow."""
conv.goto_flow("Booking Flow")
```
This is what `{{fn:start_booking_flow}}` in your rules resolves to. When the model decides to call it, the agent enters the booking flow at its start step.
## Part 6 — Add a start function
The start function runs once at the beginning of every call, before the first user input. Use it to initialize state.
**`start_function.py` may already exist**
Projects built via Quick Agent Setup in Agent Studio often ship with a pre-populated `start_function.py` containing significant initialization logic. If the file already exists, add your initialization code to it rather than replacing it.
Create or update `functions/start_function.py`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from _gen import * #
def start_function(conv: Conversation):
"""Initialize conversation state at call start."""
conv.state.booking_confirmed = False
```
## Part 7 — Add a topic
Topics tell the agent what kinds of caller utterances map to which actions. Create `topics/Make a Reservation.yaml`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
enabled: true
content: The caller wants to make a table reservation at Maison restaurant.
example_queries:
- I'd like to book a table
- Can I make a reservation?
- Do you have any tables available on Saturday?
- I want to reserve a table for four
actions: Use {{fn:start_booking_flow}} to begin collecting the reservation details.
```
The model uses `content` and `example_queries` to understand when this topic applies, and `actions` to know what to do.
## Part 8 — Tune speech recognition
Voice agents listen on a noisy channel, and domain-specific vocabulary is usually the first thing ASR gets wrong. The ADK exposes two speech-recognition resources that do not require any code: **keyphrase boosting** and **transcript corrections**. Both live under `voice/speech_recognition/`.
### Keyphrase boosting
Create `voice/speech_recognition/keyphrase_boosting.yaml`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
keyphrases:
- keyphrase: Maison
level: maximum
- keyphrase: reservation
level: boosted
- keyphrase: party of
level: boosted
```
The recognizer biases toward these phrases when it is uncertain. Boost the brand name at `maximum` so the agent never mis-hears it, and apply lighter boosts to the phrases that disambiguate booking intent.
### Transcript corrections
Spoken times and numbers often come through in forms that are hard to parse. Create `voice/speech_recognition/transcript_corrections.yaml`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
corrections:
- name: Time normalization
description: Collapse common spoken time forms
regular_expressions:
- regular_expression: half past (\d{1,2})
replacement: \1:30
replacement_type: partial
- regular_expression: quarter past (\d{1,2})
replacement: \1:15
replacement_type: partial
- regular_expression: quarter to (\d{1,2})
replacement: \1:45
replacement_type: partial
```
These rules are applied after the recognizer returns text but before the model sees it. They only touch the transcript — the caller's audio is unaffected.
See the [speech recognition reference](/adk/reference/speech_recognition) for the full field list, interaction styles, and barge-in settings.
## Part 9 — Adjust how the agent speaks
Response control sits on the other side of the conversation: it shapes what the agent says before TTS. For a French-brand restaurant, the most common issue is pronunciation. Create `voice/response_control/pronunciations.yaml`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
pronunciations:
- regex: "\\bMaison\\b"
replacement: May-zon
case_sensitive: false
description: Ensure the restaurant name is spoken in the intended French style
```
You can add entries for any word or phrase the TTS mispronounces. See the [response control reference](/adk/reference/response_control) for the full field list, including phrase filtering.
## Part 10 — (Optional) Send an SMS confirmation
SMS templates are fully supported by the ADK, but are not editable in the Agent Studio UI and template references of the form `{{twilio_sms:...}}` do not resolve inside UI-editable fields. To keep SMS working reliably, trigger it from code instead of from a prompt reference — that keeps every moving part inside files the ADK owns.
**Skip this part if you are only testing via chat**
`conv.send_sms_template` needs `conv.caller_number`, which is only populated on the voice channel. In `poly chat`, the template won't actually be dispatched — but the code path is wired the same way and will fire the moment a real call comes in.
### Define the template
Create `config/sms_templates.yaml`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
sms_templates:
- name: booking_confirmation
text: >-
Hi {{vrbl:booking_name}}, your table for {{vrbl:booking_size}} at Maison
is confirmed for {{vrbl:booking_date}} at {{vrbl:booking_time}}.
We look forward to seeing you.
env_phone_numbers:
sandbox: ""
pre_release: ""
live: "+15551234567"
```
The `{{vrbl:...}}` placeholders pull from `conv.state` values — so the function step has to set them before sending.
### Send it from the function step
Replace the body of `flows/booking_flow/function_steps/confirm_booking.py` with:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from _gen import * #
def confirm_booking(conv: Conversation, flow: Flow):
"""Confirm the reservation, store details on state, and send an SMS."""
name = conv.entities.customer_name.value if conv.entities.customer_name else "Guest"
size = conv.entities.party_size.value if conv.entities.party_size else "?"
date = conv.entities.reservation_date.value if conv.entities.reservation_date else "?"
time = conv.entities.reservation_time.value if conv.entities.reservation_time else "?"
# Populate the variables the SMS template consumes
conv.state.booking_name = name
conv.state.booking_size = str(size)
conv.state.booking_date = str(date)
conv.state.booking_time = str(time)
conv.state.booking_confirmed = True
# Send the SMS if we have a caller number (voice channel only)
if conv.caller_number:
conv.send_sms_template(
to_number=conv.caller_number,
template="booking_confirmation",
)
conv.exit_flow()
return f"Booking confirmed: table for {size} under {name} on {date} at {time}."
```
Because the decision to send happens in Python, the model doesn't need to resolve `{{twilio_sms:...}}` and the UI gap for SMS templates stops mattering for this tutorial.
See the [SMS templates reference](/adk/reference/sms) for the full field list, environment-specific sender numbers, and the `conv.send_sms` helper for free-form messages.
## Part 11 — Review your changes
Before pushing, check what has changed:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly status
```
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
╭─────────────── Project Status ────────────────╮
│ Region │
│ Account ID │
│ Project ID │
│ Last Pulled 2026-04-21T09:15:00 │
│ Current Branch booking-flow │
╰───────────────────────────────────────────────╯
```
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
New files:
/Users/yourname/maison///config/entities.yaml
/Users/yourname/maison///flows/booking_flow/flow_config.yaml
/Users/yourname/maison///flows/booking_flow/steps/collect_name.yaml
/Users/yourname/maison///flows/booking_flow/steps/collect_party_size.yaml
/Users/yourname/maison///flows/booking_flow/steps/collect_date.yaml
/Users/yourname/maison///flows/booking_flow/function_steps/confirm_booking.py
/Users/yourname/maison///functions/start_booking_flow.py
/Users/yourname/maison///functions/start_function.py
/Users/yourname/maison///topics/Make a Reservation.yaml
/Users/yourname/maison///voice/speech_recognition/keyphrase_boosting.yaml
/Users/yourname/maison///voice/speech_recognition/transcript_corrections.yaml
/Users/yourname/maison///voice/response_control/pronunciations.yaml
/Users/yourname/maison///variables/booking_confirmed
```
**Variables in `poly status`**
The `variables/` entries appear because the ADK scans your function code for `conv.state.*` assignments and tracks each one as a variable. These entries are virtual — they do not correspond to files on disk and are not something you need to create or manage. This is expected output.
To see the exact content difference for any file, run `poly diff`:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly diff
```
This shows a unified diff of all local changes against the remote state, useful for reviewing before you push and for producing a diff for a code review.
## Part 12 — Push to Agent Studio
Push the changes to your branch in Agent Studio:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly push
```
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Pushing local changes for /...
Pushed / to Agent Studio.
```
The agent is now deployed to the `booking-flow` branch. Sandbox remains on `main` and is unaffected.
## Part 13 — Merge and test
By default, `poly chat` connects to your current branch's last pushed state. On `main` it falls back to the sandbox environment. To test `booking-flow` against sandbox alongside the rest of the project, merge it into `main` first.
**Merging from the CLI or Agent Studio**
Merge from the CLI with `poly branch merge ''` (run from the `booking-flow` branch), or open the project in Agent Studio, switch to the `booking-flow` branch, and merge through the web UI. See the [Branch merging reference](/adk/reference/branch_merge) for the full conflict-resolution flow, including `--interactive` and `--resolutions`.
After merging, run `poly chat` against the sandbox environment:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly chat --environment sandbox
```
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Starting chat for / (sandbox)...
Type your message. Press Ctrl+C to exit.
Agent: Welcome to Maison. How can I help you today?
You: I'd like to book a table for this Saturday
Agent: Of course! I'd be happy to help with that. Could I take the name for the reservation?
You: It's under Sarah Chen
Agent: Thank you, Sarah. And how many guests will be dining?
You: Just two of us
Agent: Perfect. And what time were you thinking for Saturday?
You: Around 7:30 in the evening
Agent: Wonderful. Let me confirm that — a table for 2 under Sarah Chen this Saturday at 7:30pm. Does that sound right?
You: Yes, that's perfect
Agent: Your reservation is confirmed. We look forward to welcoming you to Maison on Saturday. Is there anything else I can help you with?
```
**Useful chat flags**
* `poly chat --state` — prints `conv.state` after each turn so you can verify values are being set correctly
* `poly chat --functions` — shows which functions the model called each turn
* `poly chat --push` — pushes your latest changes before starting the session, useful during rapid iteration
**`poly chat --push` can create conflict markers**
If the remote state has diverged from your local copy, `poly chat --push` may write merge-conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) directly into your YAML files. If this happens, open the affected file, resolve the conflict by hand, and push again before continuing.
## Part 14 — After the merge
After merging in Agent Studio, switch back to `main` locally:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
poly branch switch main
poly pull
```
Pulling after a merge keeps your local copy in sync with the normalized remote state.
**YAML key order changes after a round-trip**
After a push and pull, the platform returns YAML with keys in alphabetical order. Fields you wrote in logical order (such as `name:` before `description:`) will be reordered. This is cosmetic and does not affect behavior, but `poly diff` will show changes after the first round-trip even when the content is the same.
## What to explore next
This tutorial covered a single flow with four steps. From here you can extend the agent in several directions.
**Multi-location support with variants**: If Maison has multiple locations, use `config/variant_attributes.yaml` to define per-location phone numbers, opening hours, and capacity limits. The agent reads the right values for each location at runtime using `{{attr:...}}` in prompts and `conv.variant.attribute_name` in code.
**Handling cancellations**: Add a second topic and flow for callers who want to cancel or modify a reservation. The flow structure is similar — collect a name or reference, confirm the record, then update state.
**External API calls**: Replace the stub booking logic in `confirm_booking.py` with a real HTTP call to your reservation system. Function steps are the right place for this — they run deterministically and can store results in `conv.state` for the model to reference.
**Richer error paths**: Add an explicit error step to the flow for when the booking cannot be completed. Route to it from `confirm_booking.py` using `flow.goto_step("Error")` and return a context string explaining what happened.
**Call handoffs**: Define SIP transfer destinations in `config/handoffs.yaml` and trigger them from code with `conv.call_handoff(...)`. Handoffs are ADK-only — they do not have a matching editor in the Agent Studio UI — so, like SMS, the most reliable pattern is to call them from a function or function step rather than relying on a `{{ho:...}}` placeholder. See the [handoffs reference](/adk/reference/handoffs).
## Related pages
Full reference for flow configuration, step types, and conditions.
All entity types and their configuration fields.
How global functions, transition functions, and function steps differ.
How topics connect caller intent to agent actions.
Keyphrase boosting, transcript corrections, and ASR settings.
Pronunciations and phrase filtering for spoken output.
Per-location configuration without duplicating your project.
# Annotations
Source: https://docs.poly.ai/analytics/conversations/annotations
Flag good and bad agent responses, mark factual errors, and capture transcript and Knowledge gaps during review.
Use annotations to give per-turn feedback during conversation review. Annotations create a running log of what's working and what isn't, so you can track issues, share them with your PolyAI representative, and measure improvement over time.
Annotations are available on both **voice** and **webchat** conversations from the [Conversation Review](/analytics/conversations/review) side panel.
## Agent turn annotations
Hover over any agent turn in the transcript to reveal a four-icon feedback toolbar:
| Icon | Annotation | What it captures |
| ------------------------------------ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| | **Good response** | The agent handled the turn well. Use this for spot-checking and to mark positive examples. |
| | **Bad response** | The agent's response was poor. Opens a reason picker so you can categorize the failure. |
| | **Wrong information** | The agent gave a factually incorrect response. Use this alongside or instead of Bad response when the issue is content accuracy rather than response shape. |
| | **Copy turn text** | Copies the turn's text to your clipboard — handy when sharing examples in Slack or tickets. |
### Bad response reasons
When you click a searchable reason picker opens. Pick the category that best matches the failure:
* **Response too general** — the answer was vague when it should have been specific.
* **Response too specific** — the answer over-committed to detail the agent shouldn't have had.
* **Incorrect handoff text** — the agent transferred or routed with the wrong wording or to the wrong destination.
* **Long response** — the answer was too long, especially relevant for voice where verbosity hurts.
* **Hallucination** — the agent stated something it had no basis for.
Use the search field to filter the list when you know the category name.
### Wrong information
Click to mark a turn as containing factually wrong content. **Wrong information** is a single toggle — there are no sub-reasons.
You can apply **Wrong information** in combination with **Bad response** — they describe different aspects (content correctness vs. response quality).
## Caller turn annotations (voice only)
For voice conversations, caller turns can also be annotated to capture ASR and Knowledge issues:
* **Wrong transcription** — the ASR (automatic speech recognition) produced an incorrect transcript. Use this when the agent misheard the caller. Feeds back into ASR tuning and [keyphrase boosting](/voice-channel/advanced/call-settings#keyphrases).
* **Missing topic** — the caller asked something the agent could not answer because no matching topic exists in the [Knowledge](/knowledge/faqs/introduction) area. Feeds the prioritization of new topics.
These annotations are available on the caller side of voice transcripts. Webchat sessions don't have ASR or audio, so they don't surface these types.
## What happens after you annotate
* Annotations are saved to the conversation and visible to anyone with access to Conversation Review.
* Share annotations with your PolyAI representative to drive ASR accuracy improvements, Knowledge updates, or agent-behavior changes.
* **Bad response** and **Wrong information** annotations build a queue of agent quality issues to address.
* **Missing topic** annotations directly prioritize your [Knowledge](/knowledge/faqs/introduction) backlog.
* Over time, annotations create a log of recurring issues that helps you track improvement.
## Best practices
* **Annotate consistently** — Review a sample of conversations regularly and annotate issues as you find them.
* **Pick the most specific reason** — "Hallucination" is more actionable than a generic Bad response without a reason.
* **Combine when useful** — A turn can be both **Bad response: Response too general** and **Wrong information** if both apply.
* **Act on annotations** — Use them as input when updating your Knowledge area, [ASR keyphrase boosting](/voice-channel/advanced/call-settings#keyphrases), [transcript corrections](/voice-channel/advanced/call-settings#keyphrases), or agent behavior configuration.
## Related pages
Browse and inspect conversations to find issues for annotation.
Debug agent behavior at each turn after reviewing transcripts.
# Diagnosis
Source: https://docs.poly.ai/analytics/conversations/diagnosis
Toggle diagnostic layers to inspect tool calls, flows, latency, and topic citations on the Transcription tab.
Use **Conversation Diagnosis** when you need to understand *why* the agent behaved a certain way on a specific turn – which tools ran, which topics were cited, and where latency occurred.
Diagnosis is a **toggle group** on the Transcription tab of the [Conversation review](/analytics/conversations/review) side panel. Switch any layer on or off to overlay extra information on each turn. Layers persist between conversations, so the views you care about stay enabled as you move through the table.
## Available diagnosis layers
### Conversation variables
Displays live variable values captured during the call (for example, booking IDs, customer names, flags), with diff markers when a variable changes turn-to-turn.
### Flows and steps
Tracks the agent's navigation through [flows](/flows/introduction) and steps, showing the execution path and the decisions made at each branch.
### Tool calls
Shows the [tools](/tools/introduction) the agent triggered during the call, including call parameters and outcomes.
### Topic citations
Highlights the [Knowledge](/knowledge/faqs/introduction) topics the agent matched for each response.
### Sources
Shows which [Sources](/knowledge/sources/introduction) source files the agent retrieved for each turn. Click a source name to open an inline preview panel showing the retrieved content. Use **Open in Knowledge** in the panel to navigate directly to the source.
### Transcript corrections
Displays where the automatic transcript was edited for clarity or accuracy. Use it to distinguish ASR errors from agent logic problems.
### Turn latency
Measures how long the agent took to respond at each turn.
Latency visualization includes component-level timing breakdowns.
#### Latency breakdown
When viewing turn latency, you can inspect timing for:
| Component | Description |
| ----------------------- | ----------------------------------------------------------------------- |
| **LLM requests** | Time spent waiting for the language model to generate a response |
| **Function calls** | Time spent executing functions, including API calls and data processing |
| **Total response time** | Combined time from user speech end to agent response start |
Use these breakdowns to:
* Identify slow function calls that need optimization
* Understand LLM response times for different query types
* Find bottlenecks causing user-perceived delays
* Compare latency across different conversation types
High LLM latency may indicate complex prompts that could be simplified. High function call latency often points to slow external API dependencies.
### Interruptions
Shows when the caller interrupted the agent, or when barge-in was detected. Use this to understand if caller behavior affected the conversation flow.
### Variants
Identifies which [variant](/knowledge/variants/introduction) handled each part of the call.
### Logs
Displays structured runtime logs emitted from your functions during the call. The Logs layer surfaces:
* Entries from [`conv.log.info()`, `conv.log.warning()`, and `conv.log.error()`](/tools/classes/conv-log).
* API response logs from [`conv.log_api_response()`](/tools/classes/conv-object#log_api_response).
Use the Logs layer to debug function behavior, track external API responses, and correlate runtime events with what the caller experienced. See [Conversation log](/tools/classes/conv-log) for the full `conv.log` API.
### Entities
Lists **extracted entities** captured from the user, like booking numbers, account IDs, or city names. Especially useful in transactional scenarios where the agent needs to capture structured data from free text.
## Using diagnosis for optimization
Combine multiple layers to understand agent behavior:
1. Enable **Turn latency** to identify slow responses.
2. Check **Tool calls** for those turns to see if external calls are causing delays.
3. Use **Sources** to verify the content the agent retrieved from Sources.
4. Use **Flows and steps** to confirm the agent followed the expected path.
5. Use **Logs** to read structured `conv.log` output and API response payloads emitted by your functions.
## Related pages
Full conversation analysis across voice and webchat.
The `conv.log` API that powers the Logs diagnosis layer.
Ongoing performance management workflows.
# Conversations
Source: https://docs.poly.ai/analytics/conversations/introduction
Inspect individual conversations to debug agent behavior and audit call quality.
Use **Conversations** to inspect what actually happened on a conversation: transcripts, topic matches, function execution, and latency. When standard or [custom metrics](/analytics/kpis/introduction) show a problem (containment dropping, safety flags rising), use this UI to diagnose the root cause.
Conversations lives under **Analyze > Conversations**. A segmented **Voice | Messaging** toggle in the top right switches between voice calls and messaging sessions (the messaging subview is titled **Messages**), with channel tabs for Voice and Web chat. Both share the same review, annotation, and diagnosis tools.
**Iterate from a conversation with [Wren](/wren/introduction).** After reading a transcript, ask Wren to fix what broke — *"the agent kept asking for the date even after the caller said 'tomorrow'; fix relative-date extraction in the booking flow"*. See [Iterate after a call](/learn/iterate-open-platform).
[Dashboards](/analytics/dashboards/introduction) show aggregate trends; Conversations is where you drill into the individual calls behind those numbers: what topics matched, what functions ran, what the LLM saw on each turn.
For QA workflows, see [QA and analytics](/learn/maintain/qa-analytics). For performance analysis, see [Performance monitoring](/learn/maintain/performance-monitoring).
## Features
Browse, filter, and inspect individual conversation transcripts, both live and completed. Export data for reporting.
Save and share filtered conversation lists as named views for recurring QA workflows.
Flag transcription errors or missing topics during review. Feeds a continuous improvement loop for ASR and your Knowledge area.
Toggle debugging layers to see conversation variables, flow paths, function calls, topic citations, and turn latency.
## Related pages
See aggregate performance trends before drilling into calls
Ask Wren to surface patterns across conversations with natural-language queries
Understand the automated quality score on each conversation
# Review
Source: https://docs.poly.ai/analytics/conversations/review
Browse, filter, and inspect voice and webchat conversations from a single workspace.
Use **Conversation Review** to see exactly what happened on every voice call and webchat session — the full transcript, the topics the agent matched, the tools it called, and how it handled each turn. It is the primary tool for debugging agent behavior and auditing conversation quality.
For end-to-end QA workflows, see [QA and analytics](/learn/maintain/qa-analytics). For programmatic export, see [Call data](/call-data/introduction).
## How the page is organized
The page is built around three layers: **channel scopes** in the sidebar, your **views** as pinned tabs, and the **conversations table** that opens an integrated side panel when you click a row.
### Voice and webchat scopes
Conversations are split by channel. Open **Analyze > Conversations** in the sidebar, then use the segmented **Voice | Messaging** toggle in the top right to switch scope (the messaging subview is titled **Messages**), with channel tabs for Voice and Web chat:
* **Voice** — voice calls logged automatically
* **Web chat** — webchat sessions logged automatically
Each scope has its own column layout, its own set of views, and its own default view, so voice and webchat workflows stay completely separate.
### Views
Each scope opens on a built-in **System View** by default:
* **Production** — live environment conversations only
* **Test** — sandbox, draft, and pre-release conversations
System Views can't be renamed or deleted. To save your own filter and column combinations as pinned tabs, see [Views](/analytics/conversations/views).
### Conversations table
Each row in the table is one conversation. Default visible columns are **Start time** (always shown), **Contact**, **Summary**, **Duration**, and **PolyScore**. Additional columns can be toggled on from the **Column** button.
* **Live calls** appear at the top with a duration indicator and update as new turns arrive. See [Live conversations](#live-conversations) below.
* **Sort** any sortable column by clicking its header.
* **Select rows** to create a [test case](/testing/simulation-tests) batch from the toolbar.
* **Click a row** to open the conversation in the review side panel.
### Live conversations
When any conversation is currently in progress, an **"X conversations in progress"** banner appears above the table with a green dot. Click the banner to open the **Live conversations** side panel.
The Live conversations panel:
* Lists every live conversation in the current scope (Voice or Web chat), with contact and elapsed duration.
* Updates in real time as new conversations start and existing ones end.
* Includes a **refresh** button to force an immediate poll.
* **Details →** on any row opens that conversation in the standard review side panel, where you can watch the transcript stream in turn by turn.
Use the Live conversations panel during launches, A/B rollouts, or incident response to watch live traffic without leaving the Conversations workspace. The panel respects the active scope and filter chips, so a Test-scope view shows only sandbox traffic in progress.
Viewing live conversations requires [PII-level access](/tools/classes/conv-log#pii) — in-progress transcripts can't be redacted in real time, so users without PII access get an access-denied response.
### Review side panel
Clicking a row opens the **Conversation Review** panel on the right. The panel header shows the timestamp, channel type, and detected language. For webchat sessions, the contact email also appears.
The panel has four tabs:
| Tab | What you see |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Transcription** | Turn-by-turn transcript, audio waveform (voice only), matched topics, retrieved sources, and the **Diagnosis** toggle group |
| **Scores** | [PolyScore](/analytics/polyscore) and [CSAT](/analytics/csat/introduction) badges with per-dimension breakdowns |
| **Details** | Call info (status, environment, version), pre-built metrics (handoff target and reason, function calls, SMS events, A/B group, group ID, QA), and links to latency visualization, data logs, and outbound calls |
| **Metrics** | Searchable, sortable list of any [custom metrics](/analytics/kpis/introduction) written on the conversation |
A **display-settings gear** dropdown in the panel controls how the transcript is rendered — for example a **Topic citations** toggle that shows or hides matched-topic tags beneath each turn.
Use the **link icon** next to "Conversation Review" in the panel header to copy a deep link to the conversation. The **Open in new tab** icon (top right of the panel toolbar) opens the same conversation in the [full-page review](#full-page-review) — useful for in-depth analysis without the table in the background. Press **Esc** or click **×** to close the panel and return to the table.
Webchat sessions do not have AI-generated summaries. The panel displays a **"No summary due to conversation being via chat"** notice where the summary would otherwise appear.
### Full-page review
Conversation Review can also be opened as a dedicated full-page view — either by clicking **Open in new tab** from the side panel, or by following a deep-linked URL (for example, a link shared from Slack or a ticket).
* **Conversation tabs**: Switch between *Live*, *Ended*, and *All* to focus on active or completed calls.
* **Conversation turns**: Shows agent and user utterances in chronological order. Live calls will appear here and they will update automatically as new turns arrive. Each turn may display matched [Knowledge](/knowledge/faqs/introduction) topics where relevant.
* **Caller identity**: Displays the user email address if available (for [Webchat](/messaging-channel/introduction)), or phone number if applicable. Personally identifiable details are shown only to users with [PII-level access](/tools/classes/conv-log#pii). For users without PII access, caller phone numbers are automatically redacted as `REDACTED` in the conversations list – inbound calls hide the `from_number`, and outbound calls hide the `to_number`. The opposite number is preserved so you can still tell which line was used.
* **Environment and metadata**: Includes information such as [variant name](/knowledge/variants/introduction), timezone, timestamp, [environment](/environments-and-versions/introduction) (e.g. Sandbox or Production), and version ID.
Active calls display a **live duration indicator** beside the environment label.
* **Matched topics**: When topics from the Knowledge area are referenced or triggered, they are shown as tags beneath the corresponding utterance.
* **Sources**: When the agent retrieves content from [Sources](/knowledge/sources/introduction) sources, a **Sources** tag appears beneath the turn. Click a source name to open an inline preview panel showing the retrieved content. Enable this layer from the **Diagnosis** dropdown.
The full-page view shows everything the side panel exposes via tabs, but laid out side-by-side instead:
* **Header** — back arrow to return to the previous page, conversation title, and a reload button to fetch any new turns.
* **Summary banner** — the AI-generated [Call summary](/analytics/polyscore) at the top, expandable for the full text.
* **Left column** — stacked cards for **PolyScore**, **CSAT**, **Conversation details**, and **Metrics** (pre-built and custom). Each card collapses independently.
* **Right column** — the full transcript with the **Diagnosis** toggle group (cog icon) controlling which layers overlay each turn.
Use the full-page view when you want to scan PolyScore, CSAT, metrics, and the transcript together without flipping between tabs — for example, during a deep QA pass or when sharing a single conversation with a teammate.
## Columns
Click **Column** in the table toolbar to configure which columns are shown and in what order.
### Visibility
The **Visibility** tab lists all available columns with a toggle for each. **Start time** is always visible and cannot be hidden. All other columns under **Pre-built metrics** can be turned on or off: Contact, PolyScore, Summary, Duration, Language, Call SID, Variant, QA, Handoff To, A/B Group, and Environment.
When a metric has multiple values for one conversation (e.g. several QA topic matches), the cell shows them comma-joined (e.g. `billing, handoff`) with the most recent timestamp. Filtering and sorting still operate on the individual values.
### Order
The **Order** tab lets you drag columns into your preferred display order. Start time is locked as the first column.
Column changes apply to the active view only. Save the view after adjusting columns to make the layout persistent.
## Filters
Click **Filter** to open the filter builder. You can add up to **10 filters** per view.
Available filter types include:
* **Start date** — date range picker (timezone offset shown)
* **PolyScore** — numeric comparison: less than, greater than, equals
* **Duration** — time comparison (more than / less than), entered as hours, minutes, seconds
* **Environment** — include or exclude specific environments
* **Contact** — filter by caller or recipient phone number. Matches against either the `from_number` or `to_number` on the conversation, so a single value finds both inbound and outbound calls involving that number. Supports *equals*, *contains*, and *not contains* — use *contains* to match a partial number such as the last four digits.
* **CSAT score** — filter by the [CSAT survey](/analytics/csat/introduction) rating (1–5) the caller gave at the end of the conversation. Supports *equals*, *less than*, *greater than*, range comparisons, and *exists* to find every conversation that has (or doesn't have) a recorded score.
* **Function calls** — filter by whether any function ran, or by a specific function name (supports *exists*, *equals*, *contains*, *not contains*)
* **SMS events** — filter by whether any SMS was sent, or by a specific SMS event name (supports *exists*, *equals*, *contains*, *not contains*)
* **Custom metrics** — filter by any [custom metric](/analytics/kpis/introduction) recorded on the conversation. String metrics support *equals*, *exists*, *contains*, and *not contains*; numeric metrics support *equals*, *less than*, *greater than*, and range comparisons.
#### Substring matching with contains and not contains
Use **contains** and **not contains** to match any conversation whose field value includes the text you enter as a substring. Matching is case-insensitive, so `pizza` matches `Pizza Palace` and `THE PIZZERIA`.
* **contains: `pizza`** — keeps conversations where the value includes `pizza` anywhere in the string.
* **not contains: `pizza`** — excludes conversations where the value includes `pizza`, and keeps everything else (including conversations where the field is empty).
For example, filtering the `RESTAURANT_NAME` custom metric with **contains: `pizza`** surfaces every booking taken for a restaurant with "pizza" in its name, while **not contains: `sms`** on the **Channel** field hides any conversation routed through an SMS channel.
Click **Apply** to run the filters. Active filters appear as chips above the table showing a short summary of each condition — for example, **PolyScore \< 3** and **Duration > 1m 0s**. Click **×** on a chip to remove that filter.
Combine PolyScore and Duration filters to build a focused QA queue — for example, all calls with a PolyScore of 1 or 2 that ran for at least a minute.
### Share filters as a link
Active filters are encoded in the page URL. To share a filtered view with a teammate, copy the URL from your browser's address bar after applying the filters and paste it into Slack, email, or a ticket. When the recipient opens the link, Conversation Review loads with the same scope, view, and filter conditions pre-applied (subject to their access permissions).
Shared links reflect filter state only — they don't pin column visibility or order. To share a full layout, save a [Custom View](/analytics/conversations/views) and share the link to that view instead.
## Export
Click **Export** in the table toolbar to download the conversations in the current view as a file. The export reflects the active view — its filters, columns, and channel scope — so refine the view first if you want a subset.
Choose one of two options from the dropdown:
* **Export with annotations** — includes any [annotations](/analytics/conversations/annotations) (wrong transcription, missing topic) flagged on the conversations in the current view. Use this when you want reviewer feedback to travel with the data — for example, when sharing low-quality calls with the team responsible for ASR or Knowledge tuning.
* **Export without annotations** — exports the conversations only. Use this when you just need the raw transcripts and metadata, or when sharing data with stakeholders who don't need the QA layer.
For programmatic export and the underlying API, see [Call data](/call-data/introduction).
## Caller identity and PII
The panel header displays the caller's email (webchat) or phone number (voice) when available. Personally identifiable details are visible only to users with [PII-level access](/tools/classes/conv-log#pii). When PII masking is enabled for your account, contact details are redacted in both the table and the panel.
### Audio playback and PII
Audio playback in the Transcription tab follows the same [PII-level access](/tools/classes/conv-log#pii) rules as the rest of the panel. Users with PII access hear the original recording by default and can opt in to the redacted version. Users without PII access always hear the redacted recording — sensitive segments are muted server-side, and the option to fetch the unredacted audio is not available. The same rule applies to the public [Conversations API](/api-reference/conversations/introduction) `/audio` endpoint: requests from API keys without PII access return redacted audio regardless of the `redacted` query parameter.
## Matched topics and sources
When the agent matches [Knowledge](/knowledge/faqs/introduction) topics or retrieves content from [Sources](/knowledge/sources/introduction) sources, both surfaces appear under the relevant turn:
* **Matched topics** — tags listing the topics most relevant to the user's utterance.
* **Sources** — clickable source names. Clicking a source opens an inline preview; use **Open in Knowledge** to jump to the source file.
Enable both layers from the **Diagnosis** toggle group on the Transcription tab. See [Diagnosis](/analytics/conversations/diagnosis) for all available layers.
## PolyScore at a glance
Every eligible conversation displays a [PolyScore](/analytics/polyscore) badge in the table and on the **Scores** tab — a 1–5 quality rating generated automatically by AI. The badge is color-coded (green 5, amber 3–4, red 1–2) and expands to show per-dimension breakdowns.
Use PolyScore in Conversation Review to:
* **Prioritize review** — sort or filter by PolyScore to surface the lowest-quality conversations first.
* **Understand failure modes** — expand the breakdown to see whether a low score reflects poor flow, an unresolved task, or caller frustration.
* **Spot-check high scores** — confirm that well-scored calls followed the right process before using them as benchmarks.
Save a [view](/analytics/conversations/views) filtered to low PolyScore to build a persistent QA queue.
## Conversation status
The end of every conversation is visually marked with an **"Ended conversation"** indicator. Live calls show an **"In progress"** badge until they close automatically.
## Default view
Each scope opens on the **Production** System View. If you are working in Sandbox or haven't yet deployed to production, switch to the **Test** System View, or create a Custom View with your preferred filters and set it as your default.
## Data retention
Transcripts are removed once they fall outside your contracted retention period. Retention varies by contract — export anything you need to keep longer.
## Related pages
Save filter and column combinations as pinned tabs.
Inspect tool calls, flows, latency, and topic citations for a specific turn.
Flag transcription errors and missing topics to drive continuous improvement.
Understand how conversations are scored and what each dimension measures.
# Views
Source: https://docs.poly.ai/analytics/conversations/views
Save filter and column combinations as private pinned tabs to make conversation review repeatable.
A **view** is a saved combination of filters and column settings on the [Conversations](/analytics/conversations/review) table. You could add a view tab for all calls lasting over a minute, or all calls that trigger a safety filter. Views are private to you and can't be shared with your team.
Views are kept separate per channel — Voice and Web chat each have their own pinned tabs.
## Concepts
* **View** — a saved combination of filters and column settings, private to your account.
* **System View** — a built-in view (Production or Test) that can't be edited or deleted.
* **Custom View** — a view you create and pin as a tab.
* **Filters** — conditions that narrow the conversations table. You can add up to **10 filters per view**.
* **Column configuration** — which columns are visible and their display order.
You can save up to **50 Custom Views per project**, per scope. View names are limited to **25 characters**.
## System Views
Every project starts with two System Views per scope. They cannot be renamed, edited, or deleted:
| System View | Filter | Use it for |
| -------------- | -------------------------------------------- | ----------------------------------------------------- |
| **Production** | Live environment | Monitoring real customer conversations |
| **Test** | Sandbox, draft, and pre-release environments | Working in Sandbox, debugging branches, pre-launch QA |
If you spend most of your time in Test, save your filters as a Custom View and **set it as default** so the page opens there automatically.
System View tabs are marked with a **pin icon** and can't be unpinned, renamed, or deleted. Right-click (or open the ⋮ menu on) a System View to access a reduced action set — just **Duplicate** (⌘ D / Ctrl D) and **Share filters as link** (⌘ L / Ctrl L).
Use **Duplicate** to start a new Custom View pre-loaded with the System View's filters — the most common way to create a Production- or Test-scoped view of your own.
## Create a Custom View
Click the **+** tab to the right of your existing view tabs. A dialog appears with two starting options:
* **Blank view** — start with no filters applied.
* **Duplicate current view** — start with the filters from your current view already applied.
Enter a name (up to 25 characters — a counter shows how many you have left) and click **Add view**. The new view appears as a pinned tab.
## Manage views
Open the **⋮ menu** on any Custom View tab to access the full set of view actions (System Views show only Duplicate and Share filters as link — see [System Views](#system-views)):
| Action | Shortcut | What it does |
| ------------------------- | ---------------------------------- | ------------------------------------------------------------------ |
| **Rename view** | F2 | Update the view name in place. |
| **Duplicate** | ⌘ D / Ctrl D | Create a new Custom View with the same filters and columns. |
| **Set as default** | — | Open this view automatically when you visit the page. |
| **Share filters as link** | ⌘ L / Ctrl L | Copy a URL encoding the current filter state to your clipboard. |
| **Delete view** | — | Remove the Custom View permanently. System Views can't be deleted. |
## Share filters with your team
Use **Share filters as link** to copy a URL that encodes your current filter configuration. When a teammate opens the link, the filters are applied temporarily to their view. They can dismiss the filters to return to their own setup, or save them as a new Custom View.
Sharing a link never silently overwrites someone else's existing setup, and the view itself stays private to your account.
## Set a default view
Open the ⋮ menu on any tab and choose **Set as default**. That view loads automatically the next time you open the Conversations page. Only one Custom View per scope can be the default at a time.
## Column configuration
Click **Column** in the table toolbar to open the Columns panel. It has two tabs:
* **Visibility** — toggle columns on or off. **Start time** is always visible and cannot be hidden. Available columns include Contact, PolyScore, Summary, Duration, Language, Call SID, Variant, QA, Handoff To, A/B Group, and Environment.
* **Order** — drag columns into your preferred display order. Start time is locked as the first column.
Column changes apply to the active view. Save the view after adjusting columns to make the layout persistent.
## Limits
* View names are capped at **25 characters**.
* **50 Custom Views maximum** per project, per scope.
* **10 filters maximum** per view.
* Views are **private** — visible only to you. Use **Share filters as link** to share filter configurations with teammates.
* System Views (Production, Test) cannot be renamed, edited, or deleted.
* Views are scoped per channel — a Voice view doesn't appear on the Web chat scope and vice versa.
## Related pages
Browse, filter, and inspect individual conversations.
Toggle diagnostic layers to debug agent behavior at each turn.
# CSAT surveys
Source: https://docs.poly.ai/analytics/csat/introduction
Measure customer satisfaction with in-call voice CSAT surveys and webchat CSAT surveys.
Use CSAT surveys to measure whether callers are satisfied with how the agent handled their request. Don't just rely on proxy metrics (containment, duration) that won't tell you how the caller actually felt.
CSAT surveys collect feedback from customers at the end of their conversation. The agent asks customers to rate their experience on a 1–5 scale, and responses are tracked in your analytics dashboards and exported data.
PolyAI currently supports voice and webchat CSAT surveys. Voice CSAT is an in-call survey where the agent asks customers to rate their experience before ending the conversation; webchat CSAT shows a similar rating prompt in the chat widget.
## Configuring voice CSAT
Voice CSAT is configured from the CSAT settings under the **Analyze** section of the sidebar.
### Step 1: Enable the survey
At the top of the CSAT settings page, toggle **Enable in-call voice survey** to on. This activates the CSAT feature for your project. Until enabled, no surveys run.
### Step 2: Connect to your end function
Add `conv.goto_csat_flow()` to your end function in **Tools**. This instructs the agent to route users into the survey flow before ending the conversation.
Here is the complete function definition:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def goodbye_and_hang_up(conv: Conversation):
conv.goto_csat_flow()
```
Required — without this, the survey never triggers, even with the toggle on. End the function with `conv.goto_csat_flow()` and return nothing.
If you want a custom final message before the survey, set the utterance in `conv.state.csat_output_args` before calling `conv.goto_csat_flow()`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def goodbye_and_hang_up(conv: Conversation):
conv.state.csat_output_args = {"utterance": "Custom final thank you message"}
conv.goto_csat_flow()
```
Without this, the agent uses the default lead-in message configured in your CSAT settings.
### Step 3: Define survey content
Configure the two text fields in the **Content** section:
What the agent says before asking the question. Keep it short and clear to prevent hang-ups.
**Example:** "Please stay on the line to answer a quick survey about your experience."
The actual rating question. You must include a 1–5 scale in the wording – the system does not infer a scale.
**Example:** "On a scale of 1 to 5, how well was your issue resolved today?"
Once configured, every time your end function executes, the user routes into the CSAT flow before the conversation ends.
## Configuring webchat CSAT
Webchat CSAT shows a star-rating survey (1–5, with a skip option) in the chat widget at the end of a conversation. It's configured from the **CSAT survey** card on your webchat channel's settings. Unlike voice, no end-function wiring is required — once enabled with title and question text configured, the survey shows automatically at the end of a conversation.
### Step 1: Enable the survey
Toggle **Enable webchat survey** at the top of the CSAT survey card. This activates the CSAT feature for your project's webchat channel. Until enabled, no surveys run.
### Step 2: Define survey content
Configure the survey text for each of your agent's languages — one section per language, with the default language required and additional languages optional:
The heading shown at the top of the survey. Maximum 60 characters.
**Default:** "Rate your chat"
The rating question shown to the customer, answered on a 1–5 star scale.
Maximum 200 characters.
**Default:** "How would you rate your experience?"
## How CSAT works
### Voice
1. **Customer completes conversation** - The agent finishes helping the customer
2. **Lead-in message** - Agent introduces the survey
3. **Survey question** - Agent asks the 1–5 rating question
4. **Customer responds** - Customer provides a rating
5. **Data collection** - Rating is stored and appears in your dashboards
### Webchat
1. **Customer ends the conversation** - The conversation reaches its end
2. **Survey appears** - A star-rating modal opens in the widget, showing your configured title and question
3. **Customer responds** - The customer picks a 1–5 star rating, or skips the survey. If they don't respond, the survey is automatically skipped
4. **Data collection** - The rating (or skip) is stored and appears in your dashboards
## CSAT best practices
**Tips for effective surveys:**
* **Keep it short** - Customers are more likely to respond to brief surveys
* **Be clear about the scale** - Explicitly state what 1 and 5 mean in your survey question
* **Natural language** - Write messages that sound conversational, not robotic
* **Test thoroughly** - Try different phrasings in sandbox before deploying
## Viewing CSAT data
### Agent Studio dashboards
CSAT scores appear in your analytics dashboards within Agent Studio:
1. Navigate to **Analyze > Enterprise dashboards**
2. Select the **CSAT** dashboard
3. View metrics including:
* Average CSAT score
* Score distribution (1-5)
* CSAT trends over time
* Response rate
* CSAT correlation with containment and resolution rates
### Conversation-level data
Individual CSAT scores are also visible in conversation records:
1. Go to **Analyze > Conversations** and select the **Voice** scope
2. Filter the conversations table to show only conversations with CSAT responses
3. Select a conversation
4. View CSAT metrics in the conversation details
### Conversations API
CSAT scores are included as structured fields in [Conversations API](/api-reference/conversations) responses. You can export this data for:
* Integration with your existing analytics or reporting tools
* Building custom dashboards
* Ingesting into client systems
### Wren
You can query CSAT data by asking Wren for custom analysis and reporting. Wren has access to all CSAT metrics and can generate custom reports.
### Tracked metrics
The following metrics are automatically tracked for CSAT surveys:
#### Voice CSAT metrics
* `CSAT_OFFERED` - Whether the survey was offered (boolean)
* `CSAT_ACCEPTED` - Whether the customer accepted the survey (boolean)
* `CSAT_DENIED` - Whether the customer declined the survey (boolean)
* `CSAT_SCORE` - Customer rating (numeric 1-5)
* `CSAT_FREE_FEEDBACK` - Verbatim customer feedback (string, if collected)
* `CSAT_COMPLETED` - Whether the survey was completed (boolean)
#### Webchat CSAT metrics
* `CSAT_OFFERED` - Whether the survey was shown (boolean)
* `CSAT_ACCEPTED` - Whether the customer rated the survey (boolean)
* `CSAT_DENIED` - Whether the customer skipped the survey (boolean)
* `CSAT_SCORE` - Customer rating (numeric 1-5)
* `CSAT_COMPLETED` - Whether the survey was completed (boolean)
Webchat CSAT is a star-rating survey only – `CSAT_FREE_FEEDBACK` does not apply.
These metrics are attached to the original conversation record and can be used for analysis and reporting.
## Rating scale
The standard CSAT scale is 1-5:
| Rating | Interpretation |
| ------ | ----------------- |
| 5 | Very satisfied |
| 4 | Satisfied |
| 3 | Neutral |
| 2 | Dissatisfied |
| 1 | Very dissatisfied |
### CSAT calculation methods
CSAT scores can be calculated in different ways depending on your reporting needs:
**Standard average**
Simple average of all CSAT scores (e.g., 4.06 out of 5.0). This is what PolyAI dashboards report by default.
**Net Promoter style (client-side)**
Some organizations apply a Net Promoter Score-style calculation to their exported CSAT data. This is not calculated by PolyAI – you would apply it to your own data exports:
* CSAT Score = (Promoters - Detractors) / Total responses
* Promoters: Scores of 4-5
* Detractors: Scores of 1-2
* Neutral: Score of 3 (not included in calculation)
Example: If 70% of customers rate 4-5 and 10% rate 1-2, the CSAT score would be 60%.
## Troubleshooting
* Check that `conv.goto_csat_flow()` is called in your end function
* Ensure the end function is being executed (check conversation logs)
* Simplify your survey messages
* Make the scale clearer (explicitly state what 1 and 5 mean)
* Consider the timing - surveys work best after successful resolutions
* Allow up to 15 minutes for data to appear
* Verify the conversation completed successfully
* Check that the customer actually provided a rating
## Limitations
* The rating scale is fixed at 1-5
* Only one CSAT survey can be triggered per conversation
* Survey must be triggered from the end function (voice only)
For custom survey requirements or multi-question surveys, contact your PolyAI representative to discuss advanced options.
## Related pages
Add conv.goto\_csat\_flow() to trigger surveys at conversation end.
View CSAT scores alongside containment and other key metrics.
Ask Wren to query CSAT data and surface satisfaction trends.
# Managed dashboards
Source: https://docs.poly.ai/analytics/dashboards/custom
Dashboards that PolyAI builds and maintains for your organisation.
Managed dashboards are dashboards that PolyAI builds and maintains for your organisation.
Find them in Agent Studio under **Analyze > Dashboards**.
Use a managed dashboard when you need reporting that goes beyond what you build yourself. To build and edit dashboards without PolyAI, see [Self-serve dashboards](/analytics/dashboards/introduction).
## Managed or self-serve?
| | Managed dashboards | Self-serve dashboards |
| ----------------- | ----------------------------------------------------------------------------------------- | ------------------------------------- |
| Where | **Analyze > Dashboards** | **Analyze > Analytics** |
| Who builds it | PolyAI, with your team. Your team can also build them if you enable QuickSight Authoring. | You, or Wren |
| How you change it | Request a change from your account manager, or use QuickSight Authoring | Edit it yourself at any time |
| Best for | Bespoke KPIs, complex data joins, board reporting | Day-to-day metrics and fast iteration |
Both types are embedded in Agent Studio. You do not need a separate login or an external tool.
## Find your dashboards
1. In the left menu, select **Dashboards** under **ANALYZE**.
2. Select the dashboard you want to open.
Each dashboard appears as a card with its name. Cards that PolyAI builds and maintains are marked **View only · by PolyAI**.
If your project has no dashboards yet, the page shows **No dashboards configured**. Contact your PolyAI team to set them up.
## What a managed dashboard includes
A managed dashboard can track any combination of metrics relevant to your use case. Common examples include:
| Metric | Description |
| ----------------------------- | ---------------------------------------------------------------------------------------------------- |
| **Containment rate** | Percentage of queries resolved by the agent without escalation or [handoff](/voice-channel/handoffs) |
| **Booking performance** | Success rates, conversion funnels, and failure reasons for booking interactions |
| **Call resolution breakdown** | Categorized call outcomes (resolved, transferred, abandoned) with drill-down capability |
| **Call flows and outcomes** | Visual maps of customer journeys showing which paths lead to successful resolution |
| **Revenue impact** | Bookings completed, upsells, or payments processed by the agent |
| **Custom function metrics** | Performance data from your specific [functions](/tools/introduction) and integrations |
Managed dashboards run on **Amazon Quick** (formerly Amazon QuickSight). PolyAI manages that connection for you.
## How to get a managed dashboard
Managed dashboards are available to enterprise customers.
1. Contact your PolyAI account manager.
2. Agree the metrics and KPIs you need.
3. PolyAI designs and builds the dashboard.
4. Review the dashboard in your Sandbox environment.
5. PolyAI deploys it for your team.
## Advanced settings
Select **Advanced settings** to see the dashboard connection for your project. PolyAI configures this during setup, so most teams never need to open it. It is where a dashboard gets linked, given a URL name, and scoped to a single project.
If a dashboard fails to load, the page reports one of two problems:
* The dashboard is not available on Amazon Quick. Check that it still exists.
* The dashboard filters are configured incorrectly on Amazon Quick.
Contact your PolyAI team if you see either message.
### QuickSight Authoring
QuickSight Authoring lets your team create and edit QuickSight dashboards directly. Use it when you want to change a managed dashboard without a request to your account manager.
Two conditions apply:
* QuickSight Authoring is available on demand to enterprise customers. To enable it, contact PolyAI.
* Only users with admin permissions can use it. If you do not have admin permissions, you do not see the tab.
To open it:
1. In the left menu, select **Dashboards** under **ANALYZE**.
2. Select **Advanced settings**.
3. Select the **QuickSight Authoring** tab.
## Related pages
Dashboards you build yourself, under **Analyze > Analytics**.
Define the metrics that power dashboard widgets.
Ask Wren about your conversation data in natural language for ad-hoc analysis.
# Self-serve dashboards
Source: https://docs.poly.ai/analytics/dashboards/introduction
Build and customise your own dashboards on the Analytics page.
The Analytics page shows how your agent performs. You can customise it yourself or have [Wren](/wren/analyze) do it for you.
Open it from **Analyze > Analytics** in the left menu.
For dashboards that PolyAI builds and maintains for you, see [Managed dashboards](/analytics/dashboards/custom) under **Analyze > Dashboards**.
For quick workflows using dashboards, see [QA and analytics](/learn/maintain/qa-analytics). For in-depth performance analysis, see [Performance monitoring](/learn/maintain/performance-monitoring).
## Dashboards and tabs
Each tab is one dashboard. The **Overview** tab is the default dashboard.
To add a dashboard, select **+** next to the tabs. You then pick one of these:
| Option | What it gives you |
| ---------------- | ---------------------------------------------------------------- |
| Overview | Conversation volume, containment, PolyScore and handle time. |
| Inbound voice | Call volume, containment and transfer reasons for inbound calls. |
| Outbound voice | Connect rate, completion, callback outcomes and talk time. |
| Web chat | Engaged conversations, messages, containment and PolyScore. |
| Custom dashboard | An empty dashboard that you build yourself. |
The first four options are **templates**. A template is a set of tiles that the platform creates for you. After you create the dashboard, the tiles belong to you. You can change, move, or delete any of them. A template saves you the setup work. It does not lock you in.
To manage a dashboard, select the **⋮** icon on its tab. You can rename it, edit it, duplicate it, set it as the default, or delete it.
To try a change safely, select **Duplicate** first. Edit the copy and keep the original.
## The default Overview tiles
A new Overview dashboard starts with five headline numbers. Each one shows the change against the previous period of the same length.
| Tile | What it shows by default |
| ------------------- | ----------------------------------------------------------------------------------------- |
| Total conversations | The number of conversations in the selected period. |
| Containment rate | The percentage of conversations that the agent completed without a transfer to a human. |
| Average PolyScore | A quality score from 0 to 5 for each conversation. See [PolyScore](/analytics/polyscore). |
| Cost savings | An estimate of the money saved against a human agent. |
| Average handle time | The average conversation length in seconds. |
It also starts with four charts: **Conversation distribution (daily)**, **Evolution of containment rate**, **Evolution of PolyScore** and **Top 10 topics**.
Treat these as defaults, not as rules. Each tile is a metric, an aggregation and a format that you can open and change. For example, the **Cost savings** tile multiplies the total AI-only call duration by a rate per second. That rate is a field on the tile. If your agents cost more or less, open the tile and change it.
## Filters
The toolbar above the tiles filters every tile at the same time.
* **Channels** — voice or chat.
* **Deployment** — the environment, for example `Live`.
* **Variants** — the agent version or site.
* **Group ID** — the [A/B test](/environments-and-versions/ab-testing) deployment.
* **Groups** — the arm of the A/B test, either the control group or a test variant.
* **Date range** — a preset period, for example `Last 7 days`, or a custom range.
Use **Group ID** and **Groups** together to compare a test variant against the control.
These filters do not change the dashboard. They change only what you look at right now. To narrow one tile permanently, use a tile filter instead.
## Edit a dashboard
### Open the editor
1. Select the **⋮** icon on the dashboard tab.
2. Select **Edit dashboard**.
The header changes to **Edit dashboard**. You now see three controls:
* **←** returns to the dashboard.
* **+ Tile** adds a new tile.
* **Save dashboard** saves your changes. This button stays inactive until you make a change.
Your changes go live only when you select **Save dashboard**. If you leave the editor without saving, the dashboard stays as it was.
### Rearrange and resize tiles
* To move a tile, drag the **⠿** handle to the left of the tile title.
* To resize a tile, drag the grip in the bottom right corner of the tile.
### Add, edit and delete tiles
* To add a tile, select **+ Tile**, configure it, then select **Add to dashboard**.
* To edit a tile, select the **⋮** icon on the tile, then select **Edit**. Select **Update** to apply your changes.
* To delete a tile, select the **⋮** icon on the tile, then select **Delete**.
The dashboard behind the panel updates as you change settings. Use it as a live preview. Select **Cancel** to close the panel and discard the tile changes.
## What you can customise on a tile
The tile panel has three parts: **Visualize as**, the **Data** tab and the **Format** tab.
### Visualize as
Pick the tile type: **Line**, **Bar**, **Donut**, **Table**, **Heatmap**, **KPI tile** or **Pie**. You can change the type at any time. Your data settings stay in place. Switch between types to see which one reads best.
### Data tab
**Metric** — the value the tile measures. Select the **Metric** field to open the list of pre-built metrics, for example `Call duration` or `Chat abandoned`. Use the search box to find a metric by name. To chart your own outcomes, define them first — see [Custom metrics](/analytics/kpis/introduction).
**Aggregation** — how the platform combines the values. The options are:
| Aggregation | Use it for |
| ------------------ | ---------------------------------------------------------------------- |
| Count | The number of records. |
| Conversation count | The number of distinct conversations. |
| Sum | The total of all values. |
| Average | The mean value. |
| Min / Max | The lowest or highest value. |
| P50 / P95 / P99 | Percentiles. P95 shows the value that 95% of conversations stay below. |
Averages hide outliers. If you track call duration, add a P95 tile next to the average.
**Breakdown** — splits the tile by a dimension. Set:
* **Group by** — the dimension to split by, for example channel or variant.
* **Sort** — `None`, `Ascending` or `Descending`.
* **Limit results** — the maximum number of groups to show. Use it for a top-10 chart.
**Filter (optional)** — narrows the tile to a subset of conversations. Select **+ Filter**, then build a `Where` condition. Add more conditions to narrow the tile further. This filter applies to this tile only. It does not change the other tiles.
For example, to count only conversations that the agent finished alone, filter on `Where Handoff to Is not present`.
### Format tab
The options change with the tile type.
For charts (line, bar, pie, donut, heatmap):
* **Chart title** — the name shown on the tile. Rename tiles to match the words your team uses.
* **Show area fill** — fills the area under a line.
* **Show gridlines** — adds horizontal guide lines.
* **Empty buckets** — choose `Show as zero` or `Connect points` for periods with no data.
* **Chart color** — pick one of eight colours. Use the same colour for related tiles.
* **Divided by** — select a second metric to show the result as a rate.
* **Y-axis label** — the label on the vertical axis.
For a KPI tile:
* **Chart title** — the name shown on the tile.
* **Show change vs. previous period** — adds the percentage change badge.
* **Show as** — `Number`, `Percent (%)` or `Currency`.
* **Multiply result by** — a multiplier. Use it to apply a rate or convert a unit.
* **Value suffix** — a unit to show after the value, for example `s`.
* **Divided by** — select a second metric to show the result as a rate.
To build a rate, set **Divided by** to the base metric and set **Show as** to `Percent (%)`. The tile then shows the first metric as a percentage of the second.
### Table tiles
A table tile has its own settings on the **Data** tab:
1. Under **Row**, pick what each row represents: `Channel`, `Deployment`, `Value` or `Variant`.
2. Under **Column**, configure each column:
* **Metric** — the value in the column.
* **Measured as** — the aggregation.
* **Show as** — `Number`, `Percent (%)` or `Duration`.
* **Divided by** — an optional second metric, to show a rate.
* **Filters (optional)** — counts only conversations that match every filter you add.
3. Select **+ Column** to add more columns.
Use a table when you compare the same metrics across sites, variants or channels.
## Wren integration
Each chart has **Generate insights** and **Create analysis** buttons that open [Wren](/wren/analyze) with a pre-populated prompt based on that chart's data.
Wren can also build dashboards for you. Ask it in natural language instead of using the editor.
## Related pages
Dashboards that PolyAI builds and maintains for your organisation.
Define the metrics that power your dashboards.
Drill into individual calls behind your dashboard metrics.
Ask Wren to investigate trends with natural-language queries.
How PolyAI scores conversation quality.
Extract structured fields from a transcript after the call ends.
# Custom metrics
Source: https://docs.poly.ai/analytics/kpis/introduction
Define and manage custom metrics for your agent.
Use custom metrics to measure the outcomes that matter to your business – whether callers are being authenticated, bookings completed, or issues resolved without escalation. Without custom metrics, you are limited to generic call statistics and cannot track whether the agent is achieving its actual purpose.
Configure metrics on the Analytics page. Open **Analyze > Analytics**, select **Edit**, then select **Metrics**. Metrics are not a separate item in the sidebar. A metric is recorded against a conversation only when your function logic calls [`conv.write_metric`](/tools/classes/conv-object#write-metric) – the agent does not write metrics on its own. Once written, metric values flow into [Dashboards](/analytics/dashboards/introduction), [Wren](/wren/analyze), and the [Conversations API](/api-reference/conversations/introduction).
### Metric types
Metrics can be **boolean** (true/false) or **categorical** depending on your configuration. Boolean metrics answer a yes/no question – for example, "Was the caller authenticated?" – while categorical metrics assign a label from a set of options. When creating a metric, be clear about which type you're defining so that results are consistent and easy to interpret.
## Creating a metric
Go to **Analyze > Analytics**, select **Edit**, then select **Metrics**.
Click **Add metric** and provide:
* **Name** – a short, descriptive label (e.g. "Booking completed", "Caller authenticated")
* **Definition** – a clear description of what this metric measures and how it should be evaluated
Save your metric. It becomes active after you publish and will begin appearing in conversation data going forward.
## Naming conventions
Consistent naming makes metrics easier to find, compare, and reference across dashboards, Wren, and API exports.
* **Use a consistent format** – pick one casing style (e.g. `Booking completed` or `BOOKING_COMPLETED`) and apply it across all metrics in a project
* **Avoid synonyms** – choose one term and stick with it. For example, use `Handoff` consistently rather than alternating between "handoff", "transfer", and "escalation"
* **Be descriptive but concise** – the name should make sense at a glance in a dashboard or filter dropdown
* **Prefix related metrics** – if you have multiple metrics for one flow, group them with a shared prefix (e.g. `Auth: verified`, `Auth: failed`, `Auth: partial`)
## Writing effective definitions
The metric definition is a note for whoever is building or maintaining the project. It is **not** seen by the agent at runtime and is **not** used by [Wren](/wren/analyze) when evaluating conversations – metric values are recorded only via [`conv.write_metric`](/tools/classes/conv-object#write-metric). A clear definition still matters because it gives builders a shared, unambiguous understanding of what the metric represents and when it should be written.
### Be specific about the outcome
| Less effective | More effective |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| "Check if the call went well" | "The caller's issue was fully resolved without a handoff to a human agent" |
| "Booking metric" | "A reservation was successfully created, confirmed with the caller, and an SMS confirmation was sent" |
| "Authentication" | "The caller was verified using at least two identifying details (e.g. name and date of birth, or account number and postcode)" |
### Define success and failure
Define what counts as failure as well as what counts as success.
For example, an authentication metric might specify:
* **Success**: the caller provided their name and date of birth, and the system confirmed a match
* **Failure**: the caller could not provide sufficient details, or the system could not verify the information
* **Partial**: the caller provided one identifier but the second could not be confirmed
### Specify when the metric applies
The definition should make clear at what point in the conversation the metric is meant to be written, so builders know where in the flow to call `write_metric`.
For example:
* "Write this metric only after the agent determines the caller is eligible for the loyalty program" – not from the start of every call
* "Write this metric once the booking flow has completed" – not based on whether the caller mentioned a booking
### Additional tips
* If the metric depends on a specific flow or function, mention it by name in the definition
* For boolean metrics, state the exact condition that should make the metric `true`
* Keep definitions self-contained – avoid referencing other metric definitions
## Metrics and Wren
[Wren](/wren/analyze) can sample conversations based on the metric values you have written via `write_metric` and use those values when answering natural-language questions. Wren does not see the metric definition itself – the quality of its insights depends on whether the right metrics are being written, with the right values, at the right point in the conversation.
Once metrics are being written reliably, you can:
* Sample conversations where a specific metric succeeded or failed, rather than relying on random sampling
* Ask questions like *"Why are calls failing the authentication metric?"* and get targeted answers
* Track metric trends across recent conversations without writing queries
See [Analyze conversations](/wren/analyze) for more on deep sampling and example prompts.
## Logging metrics from functions
Custom metrics are not written automatically by the agent at runtime. They are only recorded when you explicitly call [`conv.write_metric`](/tools/classes/conv-object#write-metric) from a [function](/tools/introduction). This is useful when a metric depends on function output – for example, confirming that an API call succeeded or a payment was processed.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.write_metric("booking_completed", True)
conv.write_metric("call_outcome", "resolved", write_once=True)
```
See the [conversation object reference](/tools/classes/conv-object#write-metric) for the full parameter list.
## Where metrics appear
Once defined, custom metrics are available across several parts of the platform:
| Location | How metrics are used |
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Dashboards](/analytics/dashboards/introduction) | Visualize metric trends over time. Custom metrics appear in [Standard](/analytics/dashboards/introduction) and [Custom](/analytics/dashboards/custom) dashboards. |
| [Wren](/wren/analyze) | Sample conversations by metric value. Ask natural-language questions about metric performance across recent calls. |
| [Conversations API](/api-reference/conversations/introduction) | The `metrics` field on each conversation object contains your custom metric results, available for export to external analytics pipelines. |
| [Conversation review](/analytics/conversations/review) | Filter and browse conversations by metric outcomes. |
## Enabling metrics for API responses
Custom metrics only appear in [Conversations API](/api-reference/conversations/introduction) responses if they are explicitly enabled in the project configuration. To enable them:
1. Go to the **API Keys** tab on the workspace homepage and select **Configuration**.
2. Find your project and select the response metrics you want included in API responses. You can choose **All metrics** or pick individual metrics such as `CALL_IN_PROGRESS`, `CALL_COMPLETED`, `HANDOFF_TO`, and `HANDOFF_REASON`. You can also toggle **Conversation transcript** access.
These settings apply at the **project level**, not per API key. For data to appear in an API response, both conditions must be met: the API key must have the relevant permission **and** the project must have the metric enabled. See [API keys](/secrets/api-keys) for key setup details.
## Related pages
* [Dashboards](/analytics/dashboards/introduction) – visualize your metrics
* [Analyze conversations](/wren/analyze) – ask Wren about your conversation data in natural language
* [Conversation diagnosis](/analytics/conversations/diagnosis) – LLM-powered call categorization
* [Conversations API](/api-reference/conversations/introduction) – export metric data programmatically
* [Conversation object](/tools/classes/conv-object) – log metrics from functions
* [`prompt_llm`](/tools/classes/conv-utils#prompt_llm) – run LLM extraction queries over the transcript after a call
# PolyScore
Source: https://docs.poly.ai/analytics/polyscore
An automated 1–5 conversation quality score for voice, messaging, and email.
**PolyScore** is the automated quality score assigned to every eligible conversation with your agent — a **1–5 rating** backed by an evaluation rubric that works across voice, messaging, and email, for both inbound and outbound conversations.
Scores appear on your charts on the [Analytics](/analytics/dashboards/introduction) page and on individual conversations in [Conversation review](/analytics/conversations/review).
## How the score works
Every conversation is evaluated on two questions:
| Dimension | Question | Outcomes |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- |
| **Agent quality** | Did the agent handle the exchange competently — understanding the user, avoiding forced repeats, not causing frustration through its own faults? | Good / Fair / Poor |
| **Task success** | Did the conversation deliver on its objective, so the user won't need to make contact again for the same reason? | Good (Completed) / Fair (Handoff or decline honored) / Poor (Not completed) |
The 1–5 scale mirrors CSAT, so PolyScore reads naturally alongside the customer-satisfaction metrics you already use.
### Which conversations get scored
Conversations need to meet two criteria to be scored:
* **Is the user engaged?** That is, was it possible for the agent to do its job. Spam calls, silent calls, or conversations where a user instantly requests a human count as not engaged.
* **Have there been more than 3 user turns in the conversation?** This only scores conversations where some interaction took place.
When a conversation isn't scored, the reason is shown explicitly on the conversation.
### How the two dimensions combine
| PolyScore | Typical conversation |
| --------- | ------------------------------------------------------------------------------------------------------------------------ |
| **5** | Understood cleanly and fully resolved (or a clear self-service path given) |
| **4** | Strong on one dimension — for example, handled well but ended in a handoff, or resolved despite a minor misunderstanding |
| **3** | Middling on both — for example, some friction and a handoff |
| **2** | Weak on both dimensions |
| **1** | The agent got stuck or repeatedly misunderstood, and the conversation ended unresolved with no handoff |
The overall score is shown as a color-coded badge in [Conversation review](/analytics/conversations/review):
| Range | Label | Color |
| ----- | ------ | ----- |
| 5 | High | Green |
| 3–4 | Medium | Amber |
| 1–2 | Low | Red |
## How to read the score
These rubric decisions matter most when interpreting a score:
* **Handoffs result in a neutral Task Success outcome.** From the user's perspective the outcome is identical: they were routed to a person. A handoff is never scored as "not completed" — that rating is reserved for genuine dead-ends where the user got nothing and nobody. A handoff due to a struggling agent is penalized through the Agent Quality sub-score instead.
* **Self-service paths score as completed.** If the agent gives the user a concrete path they can complete themselves — *"you can reset your PIN any time at acme.com/pin"* — that scores as fully completed, the same as resolving it in-conversation. For many agents, routing users to self-service *is* the designed job; penalizing it would punish the configuration you chose.
* **Frustration only counts against the agent when the agent caused it.** Unhappiness with a policy or outcome doesn't penalize the agent; being stuck in a loop does.
* **Design choices aren't penalized.** To the extent that this can be inferred from the transcript, if your agent is configured to deflect or decline certain requests, executing that correctly scores as competent handling.
* **Outbound declines result in a neutral Task Success.** A polite *"not interested, remove me,"* honored cleanly, is scored as the agent doing its job.
## Where PolyScore appears
* **Conversation review** — score badge at the top of each transcript, with expandable dimension breakdowns.
* **Conversations table** — sortable PolyScore column for quick quality scanning.
* **Home page** — average PolyScore trend chart under Quick Insights.
* **Wren** — use PolyScore as a sampling criterion or query PolyScore tables directly via SQL.
* **Conversations API** — PolyScore data is available in the API response when the conversation has been scored.
## Limitations
PolyScore evaluates conversations based on the transcript alone. It does not have access to your knowledge base, flows, external systems, or expected outcomes.
This means:
* PolyScore **cannot verify whether an action was actually completed** in an external system (for example, a booking made, an appointment canceled). It can only assess whether the conversation *appeared* to resolve the task based on what was said.
* PolyScore does not know what the agent *should* have said — only what it *did* say. If the agent confidently gave an incorrect answer, PolyScore may still rate the conversation highly.
* Scores reflect conversational quality, not business accuracy. Use PolyScore alongside your own QA processes and [custom metrics](/analytics/kpis/introduction) for a complete picture.
PolyScore is available for conversations from **28 July 2026** onwards. Earlier conversations were scored on the previous 0–10 scale and no longer carry a PolyScore.
***
Questions? Reach out to your PolyAI account team.
## Related pages
View per-dimension PolyScore breakdowns alongside transcripts.
Ask Wren to query PolyScore data and sample conversations by score.
Access transcripts and call summaries.
# Create agent
Source: https://docs.poly.ai/api-reference/agents/endpoint/agents/create-agent
POST /v1/accounts/{accountId}/agents
# Delete agent
Source: https://docs.poly.ai/api-reference/agents/endpoint/agents/delete-agent
DELETE /v1/agents/{agentId}
# Duplicate agent
Source: https://docs.poly.ai/api-reference/agents/endpoint/agents/duplicate-agent
POST /v1/agents/{agentId}/duplicate
# List agents
Source: https://docs.poly.ai/api-reference/agents/endpoint/agents/list-agents
GET /v1/accounts/{accountId}/agents
# Bulk delete cached audio entries
Source: https://docs.poly.ai/api-reference/agents/endpoint/audio-cache/bulk-delete-audio-cache
POST /v1/agents/{agentId}/audio-cache/bulk-delete
Delete multiple audio cache entries by ID in a single request. Operates best-effort — returns which IDs were successfully deleted and which failed, so partial failures can be retried. Maximum 20 IDs per request.
# Delete a cached audio entry
Source: https://docs.poly.ai/api-reference/agents/endpoint/audio-cache/delete-audio-cache-entry
DELETE /v1/agents/{agentId}/audio-cache/{entryId}
Delete a cached audio entry and its associated audio file from storage. The entry is permanently removed and cannot be recovered.
# Download cached audio file
Source: https://docs.poly.ai/api-reference/agents/endpoint/audio-cache/download-audio-file
GET /v1/agents/{agentId}/audio-cache/{entryId}/file
Download the cached audio file as a WAV binary. Returns the raw audio bytes for the given cache entry ID.
# List cached audio entries
Source: https://docs.poly.ai/api-reference/agents/endpoint/audio-cache/list-audio-cache
GET /v1/agents/{agentId}/audio-cache
List cached TTS audio entries for an agent with metadata including transcript, provider, voice, duration, and hit count.
# Replace cached audio file
Source: https://docs.poly.ai/api-reference/agents/endpoint/audio-cache/replace-audio-file
PATCH /v1/agents/{agentId}/audio-cache/{entryId}/file
Replace the audio file for an existing cache entry. Send raw WAV bytes as the request body with `Content-Type: audio/wav`. Optionally include an `X-Filename` header to name the file, otherwise defaults to `.wav`. Maximum file size is 6 MB.
# Synthesize audio preview
Source: https://docs.poly.ai/api-reference/agents/endpoint/audio-cache/synthesize-audio-preview
POST /v1/agents/{agentId}/audio-cache/{entryId}/synthesize
Generate a TTS audio preview using an existing cache entry's voice and provider configuration. Returns WAV audio without saving to cache, allowing you to preview how different text or tuning settings would sound before committing changes.
# Replace audio and voice tuning settings
Source: https://docs.poly.ai/api-reference/agents/endpoint/audio-cache/update-audio-cache-details
PUT /v1/agents/{agentId}/audio-cache/{entryId}/details
Replace both the audio file and voice tuning settings for a cache entry in a single request. Sent as `multipart/form-data` with a `file` part containing the WAV audio (max 6 MB) and a `settings` part containing a JSON object with `text` and `config` fields.
# Get agent behavior rules
Source: https://docs.poly.ai/api-reference/agents/endpoint/behavior/get-agent-behavior-rules
GET /v1/agents/{agentId}/branches/{branchId}/behavior
# Update agent behavior rules
Source: https://docs.poly.ai/api-reference/agents/endpoint/behavior/update-agent-behavior-rules
PATCH /v1/agents/{agentId}/branches/{branchId}/behavior
# Create branch
Source: https://docs.poly.ai/api-reference/agents/endpoint/branches/create-branch
POST /v1/agents/{agentId}/branches
# Delete branch
Source: https://docs.poly.ai/api-reference/agents/endpoint/branches/delete-branch
DELETE /v1/agents/{agentId}/branches/{branchId}
# List branches
Source: https://docs.poly.ai/api-reference/agents/endpoint/branches/list-branches
GET /v1/agents/{agentId}/branches
# Merge branch
Source: https://docs.poly.ai/api-reference/agents/endpoint/branches/merge-branch
POST /v1/agents/{agentId}/branches/{branchId}/merge
# Batch get connectors by ID
Source: https://docs.poly.ai/api-reference/agents/endpoint/connectors/batch-get-connectors-by-id
POST /v1/agents/{agentId}/telephony/connectors/batch
# Batch update connectors
Source: https://docs.poly.ai/api-reference/agents/endpoint/connectors/batch-update-connectors
PATCH /v1/agents/{agentId}/telephony/connectors/batch
# Delete a connector and its phone numbers
Source: https://docs.poly.ai/api-reference/agents/endpoint/connectors/delete-a-connector-and-its-phone-numbers
DELETE /v1/agents/{agentId}/telephony/connectors/{connectorId}
# Get a connector by ID
Source: https://docs.poly.ai/api-reference/agents/endpoint/connectors/get-a-connector-by-id
GET /v1/agents/{agentId}/telephony/connectors/{connectorId}
# List all connectors for a project
Source: https://docs.poly.ai/api-reference/agents/endpoint/connectors/list-all-connectors-for-a-project
GET /v1/agents/{agentId}/telephony/connectors
# Look up connector by phone number
Source: https://docs.poly.ai/api-reference/agents/endpoint/connectors/look-up-connector-by-phone-number
POST /v1/agents/{agentId}/telephony/connectors/lookup
# Update a connector
Source: https://docs.poly.ai/api-reference/agents/endpoint/connectors/update-a-connector
PATCH /v1/agents/{agentId}/telephony/connectors/{connectorId}
# Get active deployment per environment
Source: https://docs.poly.ai/api-reference/agents/endpoint/deployments/get-active-deployment-per-environment
GET /v1/agents/{agentId}/deployments/active
# List deployments for an environment
Source: https://docs.poly.ai/api-reference/agents/endpoint/deployments/list-deployments-for-an-environment
GET /v1/agents/{agentId}/deployments
# Promote a deployment to the next environment
Source: https://docs.poly.ai/api-reference/agents/endpoint/deployments/promote-a-deployment-to-the-next-environment
POST /v1/agents/{agentId}/deployments/{deploymentId}/promote
# Publish the current draft to an environment
Source: https://docs.poly.ai/api-reference/agents/endpoint/deployments/publish-the-current-draft-to-an-environment
POST /v1/agents/{agentId}/deployments/publish
# Rollback to a previous deployment
Source: https://docs.poly.ai/api-reference/agents/endpoint/deployments/rollback-to-a-previous-deployment
POST /v1/agents/{agentId}/deployments/{deploymentId}/rollback
# Create knowledge base topic
Source: https://docs.poly.ai/api-reference/agents/endpoint/knowledge-base/create-knowledge-base-topic
POST /v1/agents/{agentId}/branches/{branchId}/knowledge-base/topics
# Delete knowledge base topic
Source: https://docs.poly.ai/api-reference/agents/endpoint/knowledge-base/delete-knowledge-base-topic
DELETE /v1/agents/{agentId}/branches/{branchId}/knowledge-base/topics/{topicId}
# Get knowledge base topic
Source: https://docs.poly.ai/api-reference/agents/endpoint/knowledge-base/get-knowledge-base-topic
GET /v1/agents/{agentId}/branches/{branchId}/knowledge-base/topics/{topicId}
# List knowledge base topics
Source: https://docs.poly.ai/api-reference/agents/endpoint/knowledge-base/list-knowledge-base-topics
GET /v1/agents/{agentId}/branches/{branchId}/knowledge-base/topics
# Update knowledge base topic
Source: https://docs.poly.ai/api-reference/agents/endpoint/knowledge-base/update-knowledge-base-topic
PATCH /v1/agents/{agentId}/branches/{branchId}/knowledge-base/topics/{topicId}
# Get the status of an outbound call
Source: https://docs.poly.ai/api-reference/agents/endpoint/outbound-calls/get-outbound-call-status
GET /v1/agents/{agentId}/telephony/outbound-calls/{callSid}/status
Retrieves the current status of an outbound call. Call status data is retained for approximately 2 hours after the call ends.
# Trigger an outbound call
Source: https://docs.poly.ai/api-reference/agents/endpoint/outbound-calls/trigger-outbound-call
POST /v1/agents/{agentId}/telephony/outbound-calls
Triggers the PolyAI voice agent to dial a phone number. The agent will call the provided number and conduct the conversation autonomously. Returns a callSid that can be used to poll status.
# Batch get phone numbers
Source: https://docs.poly.ai/api-reference/agents/endpoint/phone-numbers/batch-get-phone-numbers
POST /v1/agents/{agentId}/telephony/phone-numbers/batch
# Delete a single phone number
Source: https://docs.poly.ai/api-reference/agents/endpoint/phone-numbers/delete-a-single-phone-number
DELETE /v1/agents/{agentId}/telephony/phone-numbers/{phoneNumber}
# Delete phone numbers from a project
Source: https://docs.poly.ai/api-reference/agents/endpoint/phone-numbers/delete-phone-numbers-from-a-project
DELETE /v1/agents/{agentId}/telephony/phone-numbers
# Get a specific phone number
Source: https://docs.poly.ai/api-reference/agents/endpoint/phone-numbers/get-a-specific-phone-number
GET /v1/agents/{agentId}/telephony/phone-numbers/{phoneNumber}
# Import a single phone number
Source: https://docs.poly.ai/api-reference/agents/endpoint/phone-numbers/import-a-single-phone-number
POST /v1/agents/{agentId}/telephony/phone-numbers/{phoneNumber}
# Import phone numbers into a project
Source: https://docs.poly.ai/api-reference/agents/endpoint/phone-numbers/import-phone-numbers-into-a-project
POST /v1/agents/{agentId}/telephony/phone-numbers
# List all phone numbers for a project
Source: https://docs.poly.ai/api-reference/agents/endpoint/phone-numbers/list-all-phone-numbers-for-a-project
GET /v1/agents/{agentId}/telephony/phone-numbers
# Reassign a phone number to a different connector
Source: https://docs.poly.ai/api-reference/agents/endpoint/phone-numbers/reassign-a-phone-number-to-a-different-connector
PATCH /v1/agents/{agentId}/telephony/phone-numbers/{phoneNumber}
# Get a config page by environment
Source: https://docs.poly.ai/api-reference/agents/endpoint/real-time-configs/get-a-config-page-by-environment
GET /v1/agents/{agentId}/real-time-configs/{clientEnv}
# List all config pages
Source: https://docs.poly.ai/api-reference/agents/endpoint/real-time-configs/list-all-config-pages
GET /v1/agents/{agentId}/real-time-configs
# Update config variables for an environment
Source: https://docs.poly.ai/api-reference/agents/endpoint/real-time-configs/update-config-variables-for-an-environment
PATCH /v1/agents/{agentId}/real-time-configs/{clientEnv}/variables
# Upsert the JSON Schema for a config page
Source: https://docs.poly.ai/api-reference/agents/endpoint/real-time-configs/upsert-the-json-schema-for-a-config-page
PUT /v1/agents/{agentId}/real-time-configs/{clientEnv}/schema
# Create a secret
Source: https://docs.poly.ai/api-reference/agents/endpoint/secrets/create-a-secret
POST /v1/agents/{agentId}/secrets
Create a secret for the account this agent belongs to. Use for credentials the agent needs at runtime. Secrets are scoped to the account/workspace, not the individual agent.
## Scoping
Secrets belong to the **account (workspace)** that owns this agent, not the agent itself. Any other agent in the same account can be granted access to the same secret. The `agentId` in the path only determines which account the secret is created under and which agent gets initial access; it does not scope lookups.
PolyAI does not enforce an expiry on secrets. If you need to rotate credentials on a schedule (e.g. an annual refresh policy), call [Update a secret](/api-reference/agents/endpoint/secrets/update-a-secret); PolyAI does not track or enforce rotation cadence itself.
# Delete a secret
Source: https://docs.poly.ai/api-reference/agents/endpoint/secrets/delete-a-secret
DELETE /v1/agents/{agentId}/secrets/{secretName}
Delete a secret. This cannot be undone.
Any function or integration still configured to use this secret will start failing until reconfigured. This action cannot be undone.
Secrets are looked up by `secretName` within the account (workspace), the same scoping as [Create a secret](/api-reference/agents/endpoint/secrets/create-a-secret).
# Update a secret
Source: https://docs.poly.ai/api-reference/agents/endpoint/secrets/update-a-secret
PUT /v1/agents/{agentId}/secrets/{secretName}
Update a secret's value. Value is a required parameter and will always replace the existing value
## Rotation
This is the endpoint to call for credential rotation. The new value takes effect immediately for every function and agent that reads this secret. There's no staged or per-consumer rollout.
Secrets are looked up by `secretName` within the account (workspace), the same scoping as [Create a secret](/api-reference/agents/endpoint/secrets/create-a-secret). A 404 means no secret with this name exists in the account, not that it exists under a different agent.
There is no grace period. As soon as this call succeeds, any function or integration still using the old value will fail until it is updated with the new one.
# Create attribute
Source: https://docs.poly.ai/api-reference/agents/endpoint/variants/create-attribute
POST /v1/agents/{agentId}/branches/{branchId}/attributes
# Create variant
Source: https://docs.poly.ai/api-reference/agents/endpoint/variants/create-variant
POST /v1/agents/{agentId}/branches/{branchId}/variants
# Delete attribute
Source: https://docs.poly.ai/api-reference/agents/endpoint/variants/delete-attribute
DELETE /v1/agents/{agentId}/branches/{branchId}/attributes/{attributeId}
# Delete variant
Source: https://docs.poly.ai/api-reference/agents/endpoint/variants/delete-variant
DELETE /v1/agents/{agentId}/branches/{branchId}/variants/{variantId}
# List attributes
Source: https://docs.poly.ai/api-reference/agents/endpoint/variants/list-attributes
GET /v1/agents/{agentId}/branches/{branchId}/attributes
# List variants
Source: https://docs.poly.ai/api-reference/agents/endpoint/variants/list-variants
GET /v1/agents/{agentId}/branches/{branchId}/variants
# Update attribute
Source: https://docs.poly.ai/api-reference/agents/endpoint/variants/update-attribute
PATCH /v1/agents/{agentId}/branches/{branchId}/attributes/{attributeId}
# Update variant
Source: https://docs.poly.ai/api-reference/agents/endpoint/variants/update-variant
PATCH /v1/agents/{agentId}/branches/{branchId}/variants/{variantId}
# Get a voice
Source: https://docs.poly.ai/api-reference/agents/endpoint/voice-library/get-voice
GET /v1/accounts/{accountId}/voice-library/{voiceId}
# List voices
Source: https://docs.poly.ai/api-reference/agents/endpoint/voice-library/list-voices
GET /v1/accounts/{accountId}/voice-library
# Register a voice
Source: https://docs.poly.ai/api-reference/agents/endpoint/voice-library/register-voice
POST /v1/accounts/{accountId}/voice-library
# Synthesize a voice sample
Source: https://docs.poly.ai/api-reference/agents/endpoint/voice-library/synthesize-voice-sample
GET /v1/accounts/{accountId}/voice-library/{voiceId}/sample
# Update a voice
Source: https://docs.poly.ai/api-reference/agents/endpoint/voice-library/update-voice
PATCH /v1/accounts/{accountId}/voice-library/{voiceId}
# Agents API
Source: https://docs.poly.ai/api-reference/agents/introduction
Build, configure, and deploy PolyAI agents programmatically. Manage agents, branches, knowledge bases, telephony, and deployments from your own systems.
The Agents API is a set of REST endpoints for building and shipping PolyAI agents without the UI. It covers the full agent lifecycle: create the agent, branch for parallel work, edit behavior and knowledge, provision telephony, publish through environments, and tweak runtime values after launch.
Use it when you want to automate agent setup from your own tooling — CRM workflows, CCaaS provisioning, internal platforms, or coding agents — instead of clicking through Agent Studio.
Also known as the **builder APIs** or **Agent Studio APIs**. This reference covers the same surface area.
## How it differs from the Conversations API
The Agents API is the **build and deploy** layer. The [Conversations API](/api-reference/conversations/introduction) is the **read and analyze** layer. They're separate services with separate base URLs, auth, and audiences.
| | Agents API | Conversations API |
| --------------- | ---------------------------------- | ---------------------------------------- |
| **Purpose** | Build and deploy agents | Read call data and transcripts |
| **Direction** | Write (create, update, deploy) | Read (query, retrieve) |
| **Base URL** | `api.{region}.poly.ai/v1/agents/…` | `api.{region}.platform.polyai.app/v3/…` |
| **Auth** | API key (workspace-scoped) | API key (project-scoped) |
| **Who uses it** | Developers automating agent builds | Analysts, integrations pulling call data |
If you want to ship an agent change, use the Agents API. If you want to know what happened in a call, use the Conversations API. See [Getting started](/api-reference/introduction) for the full distinction across all API families.
## Resource model
The Agents API is organized around a small set of nested resources:
```
account
├── agent # top-level agent resource (created/listed under account)
└── voice-library # account-scoped voice catalog (register, list, sample)
agent
├── branch # isolated working copy
│ ├── behavior # system prompt and interaction rules
│ ├── knowledge-base # topics and RAG
│ ├── attributes # variant dimensions
│ └── variants # site-specific overrides
├── deployment # published version per environment
├── telephony
│ ├── connector # voice-infra binding
│ └── phone-number # E.164 number routed to a connector
└── real-time-config # runtime values editable without publishing
```
Agents and voice library entries are scoped to an **account**. Everything inside an agent except deployments, telephony, and real-time configs is scoped to a **branch**. The default branch is `main`; create additional branches for parallel development and merge them back.
## Base URL
The Agents API uses the same regional base URL family as [Alerts](/api-reference/alerts/introduction) and [Webhooks](/api-reference/webhooks/introduction):
| Region | Base URL |
| ------ | ---------------------------- |
| US | `https://api.us.poly.ai` |
| EU | `https://api.eu.poly.ai` |
| UK | `https://api.uk.poly.ai` |
| Studio | `https://api.studio.poly.ai` |
Most Agents API paths are prefixed with `/v1/agents/{agentId}/…`. Account-scoped endpoints (listing/creating agents, voice library) are prefixed with `/v1/accounts/{accountId}/…`.
Do not use `https://api.poly.ai` without a region prefix — it returns an error. Always include the region.
## Authentication
All endpoints authenticate with an API key sent in the `x-api-key` header. Keys are scoped to a workspace.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl https://api.us.poly.ai/v1/accounts/ACCOUNT_ID/agents \
-H "x-api-key: YOUR_API_KEY"
```
Create a workspace-scoped key from the **API Keys** tab on your workspace homepage in Agent Studio — see [API keys](/secrets/api-keys). Copy it when it's shown; the full value only appears once.
## Identifiers
Agent Studio labels and API parameters use different names for the same things. `ACCOUNT_ID` above is your **account ID** — Agent Studio's UI calls it the **Workspace ID** and shows it prefixed (`ws-xxxxxxxx`):
| Agent Studio label | API parameter | Example |
| --------------------- | ------------- | ------------------ |
| Workspace ID | `accountId` | `ws-fd112d8f` |
| Project ID / Agent ID | `agentId` | `PROJECT-58RP822I` |
**Agent ID is the same value as Project ID.** "Agent" is the current product name; "Project" is the legacy term still surfaced in some Agent Studio screens and URLs, and used by the Conversations and Chat APIs. Use the value you see in Agent Studio directly — no transformation needed.
## Quick start
`main` is read-only — behavior and knowledge base writes go to a **branch**, which you then merge into `main`. Merging publishes to Sandbox automatically. For the full walkthrough (create → configure → test → deploy → observe), see the [API quickstart](/api-reference/quickstart).
### 1. Create an agent
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api.us.poly.ai/v1/accounts/ACCOUNT_ID/agents \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Support Agent",
"description": "Handles Tier 1 support for the US market"
}'
```
The response includes an `agentId` and a default `main` branch.
### 2. Create a working branch
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/branches \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "branchName": "quickstart" }'
```
The response returns a `branchId` — use it for the edits below.
### 3. Update the behavior and add a topic
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PATCH https://api.us.poly.ai/v1/agents/AGENT_ID/branches/BRANCH_ID/behavior \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"behavior": "You are a friendly, concise support agent..."
}'
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/branches/BRANCH_ID/knowledge-base/topics \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Password reset",
"content": "Users can reset their password by..."
}'
```
### 4. Merge to `main` (publishes to Sandbox)
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/branches/BRANCH_ID/merge \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "deploymentMessage": "Initial support agent" }'
```
### 5. Promote to pre-release, then live
Fetch the active Sandbox deployment's `id` (`GET /v1/agents/AGENT_ID/deployments/active`), then promote it. Each promote returns the next environment's `deployment.id`:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/deployments/DEPLOYMENT_ID/promote \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "targetEnvironment": "pre-release" }'
```
## Environments
Deployments run in one of three environments:
| Environment | Purpose |
| ------------- | --------------------------------------------------------- |
| `sandbox` | Safe space for testing without affecting production calls |
| `pre-release` | Staging for final validation before going live |
| `live` | Production — serves real customer traffic |
Merging a branch into `main` publishes the result to `sandbox`; from there, `promote` moves it through `pre-release` and `live`, and `rollback` reverts a deployment. (The standalone `publish` endpoint deploys the current `main` draft to `sandbox` — redundant right after a merge, which already does this.) See [Environments](/environments-and-versions/introduction) for the full model.
## Branches
Branches are isolated working copies of an agent. Because `main` can't be written to directly, **every** behavior, knowledge base, or variant change starts on a branch:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/branches \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "branchName": "experiment-new-greeting" }'
```
Merge back to `main` when you're ready — this also publishes the result to Sandbox:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/branches/BRANCH_ID/merge \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "deploymentMessage": "Update greeting" }'
```
## Real-time configs
Real-time configs let you change runtime values (opening hours, seasonal messaging, holiday flags) without a publish cycle. Updates take effect immediately.
Define a JSON Schema for a config page, then PATCH variables against it:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PATCH https://api.us.poly.ai/v1/agents/AGENT_ID/real-time-configs/live/variables \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "variables": { "open_hour": 8, "close_hour": 22 } }'
```
## Variants
Variants let you run one agent across many sites — hotel chains, restaurant groups, franchises — with site-specific values (phone number, address, hours). Define attributes (dimensions), then create variants (combinations).
See [Variants](/knowledge/variants/introduction) for the conceptual model.
## Error responses
| Status | Description |
| ------ | ------------------------------------------------------ |
| 400 | Validation error - check request body or parameters |
| 401 | Missing or invalid API key |
| 403 | API key lacks permission for the workspace or resource |
| 404 | Resource not found |
| 409 | Conflict - e.g. merging a branch with diverged state |
| 422 | Malformed ID or schema validation error |
| 500 | Internal server error |
### Example error response
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"message": "Validation failed",
"errors": [
{
"field": "name",
"message": "must be between 1 and 128 characters"
}
]
}
```
## Related
How sandbox, pre-release, and live environments fit together.
Run one agent across many sites with attribute-driven variants.
Provision phone numbers and route them to connectors.
How knowledge base topics power retrieval and actions.
# Create an alert rule
Source: https://docs.poly.ai/api-reference/alerts/endpoint/create-alert-rule
POST /v1/alert-rules
Create a new alert rule. The `account_id` is derived from the `X-API-KEY` header.
# Delete an alert rule
Source: https://docs.poly.ai/api-reference/alerts/endpoint/delete-alert-rule
DELETE /v1/alert-rules/{rule_id}
Delete an alert rule by its ID.
# Get an alert rule
Source: https://docs.poly.ai/api-reference/alerts/endpoint/get-alert-rule
GET /v1/alert-rules/{rule_id}
Get a single alert rule by its ID.
# List active alerts
Source: https://docs.poly.ai/api-reference/alerts/endpoint/list-active-alerts
GET /v1/alerts
List alert rules that are currently in the `alert` state.
# List alert rules
Source: https://docs.poly.ai/api-reference/alerts/endpoint/list-alert-rules
GET /v1/alert-rules
List alert rules with optional filters.
# Update an alert rule
Source: https://docs.poly.ai/api-reference/alerts/endpoint/update-alert-rule
PATCH /v1/alert-rules/{rule_id}
Update an alert rule. Only the fields included in the request body are changed.
# Alerts API
Source: https://docs.poly.ai/api-reference/alerts/introduction
Manage alert rules and query active alerts firing on your PolyAI agents.
The Alerts API lets you monitor your voice agents in real time and react to performance issues. Configure rules to watch key metrics like latency, errors, and call volume, then list which rules are currently firing.
## Key features
* **Alert rules** - Monitor metrics like turn latency, API errors, function errors, call crashes, and call volume
* **Active alerts** - Query which rules are currently in the `alert` state
* **Project scoping** - Scope alerts to specific projects or monitor account-wide
## Limits
| Resource | Maximum per account |
| ----------- | ------------------- |
| Alert rules | 10 |
Requests to create an alert rule beyond the limit return a `409 Conflict` error.
## Available metrics
All count-based metrics represent **absolute counts in the evaluation window**, not rates. For example, if you set `window_duration: "5m"` and `threshold_value: 10` for `api_errors`, the alert triggers when there are 10 or more API errors in the 5-minute window.
| Metric | Description | Unit | Typical threshold guidance |
| ------------------ | --------------------------------------------- | ------------ | ------------------------------------- |
| `turn_latency_p50` | Median turn latency | milliseconds | 800-1500ms depending on use case |
| `turn_latency_p95` | 95th percentile turn latency | milliseconds | 1500-3000ms depending on use case |
| `api_errors` | Number of API errors in window | count | Consider 0 for critical flows |
| `function_errors` | Number of function execution errors in window | count | Consider 0 for critical functions |
| `call_crashes` | Number of call crashes in window | count | Typically 0 (any crash is concerning) |
| `call_volume` | Number of calls in window | count | Depends on expected traffic patterns |
When setting thresholds, consider the metric type and business impact:
* **Error metrics** (`api_errors`, `function_errors`, `call_crashes`): Consider setting threshold to 0 for critical flows where any error requires attention
* **Latency metrics**: Use longer windows (5-15 minutes) to smooth out transient spikes
* **Volume metrics**: Base thresholds on historical traffic patterns and expected business hours
## Alert states
| State | Description | When it occurs |
| --------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `ok` | The metric is within the configured threshold | After successful evaluation shows metric below threshold |
| `alert` | The metric has breached the threshold and the alert is firing | Metric exceeds threshold during evaluation |
| `no_data` | No data is available for evaluation | No metric data reported during the evaluation window (e.g., no calls) |
| `unknown` | The state could not be determined | The alert rule has not yet been evaluated or the evaluation could not complete |
**`no_data` vs `unknown`:** Use `no_data` to detect when your agents aren't receiving traffic (which may itself be a problem). `unknown` indicates that the state could not be determined — for example, the rule has not yet been evaluated. New alert rules default to the `ok` state until their first evaluation.
## Quick start
### 1. Create an alert rule
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api.us.poly.ai/v1/alert-rules \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "High turn latency (p95)",
"project_id": "proj_abc123",
"metric": "turn_latency_p95",
"operator": ">=",
"threshold_value": 1500,
"window_duration": "5m"
}'
```
Replace `api.us.poly.ai` with the base URL for your deployment region. See [Getting started](/api-reference/introduction#pick-your-region) for the full list of regional base URLs.
### 2. Check active alerts
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl https://api.us.poly.ai/v1/alerts \
-H "x-api-key: YOUR_API_KEY"
```
## Authentication
All Alerts API endpoints use API key authentication with the `x-api-key` header. Resources are automatically scoped to your account.
Create a key from the **API Keys** tab in Agent Studio — see [API keys](/secrets/api-keys).
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl https://api.us.poly.ai/v1/alerts \
-H "x-api-key: YOUR_API_KEY"
```
## Error responses
| Status | Description |
| ------ | -------------------------------------------------------------------- |
| 400 | Validation error - check request body or parameters |
| 401 | Missing or invalid API key |
| 404 | Resource not found |
| 409 | Resource limit exceeded (e.g. maximum number of alert rules reached) |
| 422 | Validation error or malformed resource ID |
| 500 | Internal server error |
| 502 | Monitoring backend sync failure |
### Example error response
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"message": "Validation failed",
"errors": [
{
"field": "threshold_value",
"message": "must be a non-negative integer"
},
{
"field": "metric",
"message": "must be one of: turn_latency_p50, turn_latency_p95, api_errors, function_errors, call_crashes, call_volume"
}
]
}
```
## Pagination
List endpoints currently return all results without pagination. For accounts with many alert rules, consider filtering by `project_id`, `metric`, or `enabled` status to reduce response size.
## Update request format
PATCH requests use flat JSON, the same format as POST/create requests. Include only the fields you want to update:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"threshold_value": 20,
"enabled": false
}
```
## Window duration
The `window_duration` field accepts the following values:
| Value | Duration |
| ----- | ------------------- |
| `5m` | 5 minutes |
| `10m` | 10 minutes |
| `60m` | 60 minutes (1 hour) |
Recommended usage:
* **Latency alerts:** `5m` or `10m` to smooth out transient spikes
* **Error count alerts:** `5m` for faster detection
* **Volume alerts:** `10m` or `60m` depending on traffic patterns
# Close a chat
Source: https://docs.poly.ai/api-reference/chat/endpoint/close-a-chat
PUT /{version}/{account_id}/{project_id}/chat/close
Closes an active chat conversation and marks it as complete.
# Create a chat
Source: https://docs.poly.ai/api-reference/chat/endpoint/create-a-chat
POST /{version}/{account_id}/{project_id}/chat/create
Creates a new chat conversation and returns the conversation ID along with the agent's initial response.
## Custom parameters
The Chat API supports additional parameters for advanced use cases:
Custom metadata passed from external integrations. This data is accessible in functions through `conv.integration_attributes`.
**Example**:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"integration_attributes": {
"customer_id": "12345",
"source": "mobile_app"
}
}
```
Specify which variant to use for this conversation. Useful for multi-site deployments.
Channel identifier for the conversation (e.g., `"webchat"`, `"sms"`).
Language code for speech recognition (e.g., `"en-US"`, `"es-ES"`).
Language code for text-to-speech (e.g., `"en-US"`, `"es-ES"`).
# Respond to a chat
Source: https://docs.poly.ai/api-reference/chat/endpoint/respond-to-a-chat
POST /{version}/{account_id}/{project_id}/chat/respond
Sends a user message to an existing conversation and returns the agent's response.
## Custom parameters
The respond endpoint supports additional parameters for advanced use cases:
Custom metadata to pass to the conversation. This data is accessible in functions through `conv.integration_attributes`.
**Example**:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"integration_attributes": {
"page_url": "https://example.com/checkout",
"cart_value": "99.99"
}
}
```
Language code for speech recognition (e.g., `"en-US"`, `"es-ES"`).
Language code for text-to-speech (e.g., `"en-US"`, `"es-ES"`).
## Custom parameters
Custom metadata for this specific message. Accessible in functions during this turn.
**Example**:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"metadata": {
"user_action": "clicked_button",
"button_id": "confirm_booking"
}
}
```
Language code for speech recognition (e.g., `"en-US"`, `"es-ES"`).
Language code for text-to-speech (e.g., `"en-US"`, `"es-ES"`).
# Chat API
Source: https://docs.poly.ai/api-reference/chat/introduction
Use the Chat API to power webchat, in-browser widgets, web SDK implementations, and SMS conversations.
Use the Chat API to integrate PolyAI agents into non-voice channels. It provides a simple set of REST endpoints for starting conversations, sending and receiving messages, and handling handoffs–ideal for webchat widgets, web SDKs, and SMS platforms.
The Chat API powers webchat integrations including in-browser widgets, web SDK implementations, and SMS.
If you need real-time streaming responses, typing indicators, or in-session live-agent handoff events over a persistent connection, use the [Messaging API](/api-reference/messaging/introduction) instead. See [Messaging API vs Chat API](/api-reference/messaging/introduction#messaging-api-vs-chat-api) for guidance on choosing.
## Chatting with the API
1. **Start a conversation**
Call `POST /{version}/{account_id}/{project_id}/chat/create`.
The response includes a `conversation_id` and the agent's initial `response`.
You can optionally pass `integration_attributes` to provide custom context to the agent, and `variant_id` to target a specific variant.
2. **Send and receive messages**
Call `POST /{version}/{account_id}/{project_id}/chat/respond` with the `conversation_id`.
`message` is optional. The response includes the agent's `response`, an `end_conversation` flag, and may include a `handoff` object.
3. **Close a conversation**
Call `PUT /{version}/{account_id}/{project_id}/chat/close` with the `conversation_id` in the body.
The response returns `{ "success": true }` on success.
## Endpoints
### Base URLs
| Region | Base URL |
| -----: | ------------------------------------------------------------------------------ |
| US | [https://api.us-1.platform.polyai.app](https://api.us-1.platform.polyai.app) |
| UK | [https://api.uk-1.platform.polyai.app](https://api.uk-1.platform.polyai.app) |
| EUW | [https://api.euw-1.platform.polyai.app](https://api.euw-1.platform.polyai.app) |
**Endpoint format:**
`/{version}/{account_id}/{project_id}/chat/{operation}`
* `version`: API version (for example `v1`)
* `account_id`: Your PolyAI account ID (for example `poly-scs-uk` or `ws-xxxxxxxx`)
* `project_id`: Your PolyAI project ID (for example `PROJECT-191bfa2a`)
* `operation`: `create`, `respond`, or `close`
### Finding your `account_id` and `project_id`
Your `account_id` and `project_id` are the first two path segments of your Agent Studio URL, immediately after the host:
```
https://studio..poly.ai///...
```
**Agent Studio is region-specific.** Each Studio host serves one region and pairs with the matching API host. Replace `` with the subdomain for your tenant:
| Studio URL | API host |
| --------------------------- | ----------------------------------------- |
| `https://studio.us.poly.ai` | `https://api.us-1.platform.polyai.app` |
| `https://studio.uk.poly.ai` | `https://api.uk-1.platform.polyai.app` |
| `https://studio.eu.poly.ai` | `https://api.euw-1.platform.polyai.app` |
| `https://studio.poly.ai` | `https://api.studio.poly.ai` (self-serve) |
A workspace lives in exactly one region — log in to the Studio host for that region and call the matching API host.
For example, if your Studio URL is `https://studio.uk.poly.ai/acme-uk/acme-team-4/agent`, then:
* `` = `uk` (so calls go to `api.uk-1.platform.polyai.app`)
* `account_id` = `acme-uk`
* `project_id` = `acme-team-4`
Account and project identifiers are also shown in prefixed form in Agent Studio — both the slug form (visible in the URL) and the prefixed form are accepted in API paths:
| Path parameter | Slug form (from URL) | Prefixed form (shown in Agent Studio as **Workspace ID** / **Agent ID**) |
| -------------- | -------------------- | ------------------------------------------------------------------------ |
| `account_id` | `acme-uk` | `ws-xxxxxxxx` (for example `ws-fd112d8f`) |
| `project_id` | `acme-team-4` | `PROJECT-xxxxxxxx` |
The `account_id` path parameter is shown as **Workspace ID** in Agent Studio and uses a `ws-` prefix — not `ACCOUNT-`. Older docs may still call it `ACCOUNT-xxxxxxx`; the value you see in Agent Studio (prefixed with `ws-`) is the correct one to use. If you only have the prefixed form, your PolyAI representative can confirm the matching slug.
The same IDs are exposed inside Agent Studio functions as `conv.account_id` and `conv.project_id` — see the [Conversation object reference](/tools/classes/conv-object#account_id).
## Authentication
All requests must include the following headers (case-sensitive):
| Header | Description |
| -------------- | -------------------------------------- |
| `X-API-KEY` | Your API key for PolyAI |
| `X-TOKEN` | Agent authentication token (connector) |
| `Content-Type` | Must be `application/json` |
### Getting your credentials
Both `X-API-KEY` and `X-TOKEN` are provisioned by PolyAI — there is no self-serve endpoint to create a Chat API connector or generate a connector token.
To request access, contact your PolyAI representative with:
* Your `account_id`
* Your `project_id`
* The client environment you need (`live`, `pre-release`, or `sandbox`)
* Your `variant_id` (optional, only if targeting a specific variant)
PolyAI will provision a Chat API connector for your project and return:
* An **API key** to use in the `X-API-KEY` header
* A **connector token** to use in the `X-TOKEN` header
The Chat API connector is distinct from the voice/telephony connectors managed under the [Agents API](/api-reference/agents/introduction). You do not need SIP details, phone numbers, or language codes to provision a Chat API connector.
## Example: Create chat
**POST** `/v1/{account_id}/{project_id}/chat/create`
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST \
"https://api..platform.polyai.app/v1/ws-xxxxxxxx/PROJECT-xxx/chat/create" \
-H "X-API-KEY: YOUR_API_KEY" \
-H "X-TOKEN: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"variant_id": "VARIANT-xxxxxxxx",
"integration_attributes": {
"user_id": "12345",
"customer_tier": "premium"
}
}'
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import requests
base_url = "https://api..platform.polyai.app"
headers = {
"X-API-KEY": "YOUR_API_KEY",
"X-TOKEN": "YOUR_TOKEN",
"Content-Type": "application/json",
}
response = requests.post(
f"{base_url}/v1/ws-xxxxxxxx/PROJECT-xxx/chat/create",
headers=headers,
json={
"variant_id": "VARIANT-xxxxxxxx",
"integration_attributes": {
"user_id": "12345",
"customer_tier": "premium",
},
},
)
data = response.json()
conversation_id = data["conversation_id"]
print(data["response"])
```
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const baseUrl = "https://api..platform.polyai.app";
const headers = {
"X-API-KEY": "YOUR_API_KEY",
"X-TOKEN": "YOUR_TOKEN",
"Content-Type": "application/json",
};
const res = await fetch(
`${baseUrl}/v1/ws-xxxxxxxx/PROJECT-xxx/chat/create`,
{
method: "POST",
headers,
body: JSON.stringify({
variant_id: "VARIANT-xxxxxxxx",
integration_attributes: {
user_id: "12345",
customer_tier: "premium",
},
}),
}
);
const data = await res.json();
const conversationId = data.conversation_id;
console.log(data.response);
```
**Response**
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"conversation_id": "CONV-1234567890",
"response": "Hi, how can I help you today?"
}
```
## Example: Send message
**POST** `/v1/{account_id}/{project_id}/chat/respond`
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST \
"https://api..platform.polyai.app/v1/ws-xxxxxxxx/PROJECT-xxx/chat/respond" \
-H "X-API-KEY: YOUR_API_KEY" \
-H "X-TOKEN: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": "CONV-1234567890",
"message": "What'\''s your return policy?"
}'
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = requests.post(
f"{base_url}/v1/ws-xxxxxxxx/PROJECT-xxx/chat/respond",
headers=headers,
json={
"conversation_id": "CONV-1234567890",
"message": "What's your return policy?",
},
)
data = response.json()
print(data["response"])
if data.get("end_conversation"):
print("Conversation ended")
if data.get("handoff"):
print(f"Handoff to: {data['handoff']['destination']}")
```
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const respondRes = await fetch(
`${baseUrl}/v1/ws-xxxxxxxx/PROJECT-xxx/chat/respond`,
{
method: "POST",
headers,
body: JSON.stringify({
conversation_id: "CONV-1234567890",
message: "What's your return policy?",
}),
}
);
const respondData = await respondRes.json();
console.log(respondData.response);
if (respondData.end_conversation) {
console.log("Conversation ended");
}
if (respondData.handoff) {
console.log(`Handoff to: ${respondData.handoff.destination}`);
}
```
**Response**
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"conversation_id": "CONV-1234567890",
"response": "Our return policy is 30 days with proof of purchase.",
"end_conversation": false
}
```
**Response with handoff**
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"conversation_id": "CONV-1234567890",
"response": "Transferring you to a live agent.",
"end_conversation": true,
"handoff": {
"destination": "live_agent_queue",
"reason": "billing_question"
}
}
```
## Example: Close chat
**PUT** `/v1/{account_id}/{project_id}/chat/close`
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PUT \
"https://api..platform.polyai.app/v1/ws-xxxxxxxx/PROJECT-xxx/chat/close" \
-H "X-API-KEY: YOUR_API_KEY" \
-H "X-TOKEN: YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"conversation_id": "CONV-1234567890"}'
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = requests.put(
f"{base_url}/v1/ws-xxxxxxxx/PROJECT-xxx/chat/close",
headers=headers,
json={"conversation_id": "CONV-1234567890"},
)
print(response.json())
```
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const closeRes = await fetch(
`${baseUrl}/v1/ws-xxxxxxxx/PROJECT-xxx/chat/close`,
{
method: "PUT",
headers,
body: JSON.stringify({
conversation_id: "CONV-1234567890",
}),
}
);
const closeData = await closeRes.json();
console.log(closeData);
```
**Response**
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"success": true
}
```
## Passing custom data with `integration_attributes`
The `integration_attributes` field lets you pass custom data when creating a chat. These attributes are accessible in your project functions at `conv.integration_attributes`.
### When to use
* Pass user context (user ID, session ID, authentication status)
* Include customer information (tier, preferences, history)
* Send external system references (CRM ID, ticket number)
* Provide A/B test parameters or feature flags
### Accessing in project functions
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start(conv):
# Get integration attributes (always check for None)
attrs = conv.integration_attributes or {}
user_id = attrs.get("user_id")
customer_tier = attrs.get("customer_tier", "standard")
# Store in state for use throughout the conversation
if user_id:
conv.state.user_id = user_id
# Customize greeting based on tier
if customer_tier == "premium":
return "Welcome back! As a premium member, how can I assist you today?"
return "Hello! How can I help you?"
```
Pass `integration_attributes` when creating the chat. They're set on the first turn and available throughout the conversation. Store values in `conv.state` to access them in later turns.
## Targeting a variant with `variant_id`
Pass `variant_id` in the `create` request body to route the conversation to a specific variant. This is useful for multi-site deployments where different variants serve different brands, languages, or regions.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"variant_id": "VARIANT-xxxxxxxx"
}
```
`variant_id` is only accepted on `POST /chat/create`. It cannot be changed after the conversation starts.
## Notes
* `create` accepts an optional request body with `variant_id` and `integration_attributes`.
* `respond` requires `conversation_id`; `message` is optional.
* `handoff` may appear in the `respond` response with `destination` and `reason`.
* `close` requires a JSON body containing `conversation_id`.
* Header names are case-sensitive: `X-API-KEY`, `X-TOKEN`.
* Use the regional base URL closest to your deployment.
# Get max concurrent call numbers
Source: https://docs.poly.ai/api-reference/concurrent-calls/endpoint/get-max-concurrent
GET /v1/{account_id}/{project_id}/conversations/concurrency
Returns the maximum number of concurrent conversations in several 5-minute intervals based on the given parameters.
# Concurrent Calls API
Source: https://docs.poly.ai/api-reference/concurrent-calls/introduction
Track concurrent conversations over time for capacity planning and monitoring.
The Concurrent Calls API lets you measure concurrent conversations and analyze traffic patterns over time. It returns a time series of 5-minute intervals with peak concurrency, filtered by environment, variant, language, phone number, and more–useful for capacity planning and monitoring.
## Endpoint
`GET https://api.{region}.platform.polyai.app/v1/{account_id}/{project_id}/conversations/concurrency`
Where:
* region is one of: us-1, uk-1, euw-1
* account\_id is your PolyAI account ID
* project\_id is your PolyAI project ID
| Region | Base URL |
| -----: | ------------------------------------------------------------------------------ |
| US | [https://api.us-1.platform.polyai.app](https://api.us-1.platform.polyai.app) |
| UK | [https://api.uk-1.platform.polyai.app](https://api.uk-1.platform.polyai.app) |
| EUW | [https://api.euw-1.platform.polyai.app](https://api.euw-1.platform.polyai.app) |
Example:
`GET https://api..platform.polyai.app/v1/ws-t53y16r3/PROJECT-f76d75c2/conversations/concurrency`
## Identifiers
`account_id` is your **account ID** — Agent Studio's UI calls this the **Workspace ID** and shows it prefixed (`ws-xxxxxxxx`, e.g. `ws-t53y16r3` above). `project_id` is the same value as the **Agent ID** shown in Agent Studio (prefixed `PROJECT-xxxxxxxx`); "Project" is the legacy term for the same resource. Both the slug form from the Agent Studio URL and the prefixed form work in API calls.
## Required query parameters
* `start_time`
Start of the reporting window in ISO8601 format. Rounded down to the nearest 5-minute mark.
* `end_time`
End of the reporting window in ISO8601 format. Rounded up to the nearest 5-minute mark.
start\_time and end\_time must be no more than one week apart.
## Optional filters
* `client_env`
Filter by environment: sandbox, pre-release or live. Defaults to live.
* `variant_id` / `variant_name`
Filter to a single variant by ID or name (only one may be supplied).
* `language`
Filter to a specific language code (for example `en-GB`).
* `phone_number`
Filter to a specific phone number.
## Slicing results
Use the `slice_by` parameter to break down the time series:
| Value | Description |
| --------- | ---------------------------------------------------------------- |
| `variant` | Separate series per variant with `variant_id` and `variant_name` |
Additional slicing options (such as `language` and `phone_number`) may be added in the future.
When slice\_by is set, each interval in the response includes additional context fields such as variant\_id and variant\_name.
## Authentication
All requests must include an API key in the x-api-key header. The key must be authorized for the Conversations and analytics endpoints for the specified account and project.
Example header:
x-api-key: YOUR\_API\_KEY
## Response structure
On success, the API returns:
* `account_id`
The account that was queried.
* `project_id`
The project that was queried.
* `intervals`
An array of 5-minute intervals in ascending chronological order. Each interval includes:
* time: lower bound of the 5-minute window (UTC, ISO8601)
* max\_concurrent\_conversations: maximum concurrent calls in that window after filters
* variant\_id and variant\_name (when sliced by variant)
* optional language or phone\_number fields for future slicing support
You can use this time series to plot concurrency over time, to validate concurrency limits, or to correlate spikes with marketing campaigns and operational changes.
## Error handling
Typical error responses include:
* 400 Bad Request\
Missing or invalid parameters (such as an invalid time range).
* 401 Unauthorized / 403 Forbidden\
Missing, invalid, or unauthorized API key.
* 404 Not Found\
Account or project not found.
* 500 or 501\
Internal errors or unimplemented combinations such as `slice_by` set to `language` or `phone_number`.
Log the `error_message` field in the response body to help diagnose issues.
# Conversations API
Source: https://docs.poly.ai/api-reference/conversations/introduction
Retrieve conversation transcripts, metadata, and performance metrics for analytics, compliance, and integration.
The Conversations API provides programmatic access to conversation records generated by your PolyAI agents. Query transcripts, turn-by-turn metadata, handoff information, and performance metrics in a structured format–ideal for analytics pipelines, compliance reporting, and downstream integrations.
## API versions
Only the Conversations API is versioned. Other APIs are currently unversioned.
### v3 – current and recommended
v3 is the fully supported release of the Conversations API.
It runs on PolyAI's event-sourced data platform and improves on v1 by offering:
* reliable, scalable ingestion and querying
* consistent ISO8601 timestamps
* empty strings ("") instead of `null` for empty text fields
* additional turn-level metadata such as `latency`, `translated_user_input`, and `english_agent_response`
Authentication for v3 is managed by PolyAI.
Customers must request v3 access through their PolyAI representative.
Example base path:
`https://api.{region}.platform.polyai.app/v3/{account_id}/{project_id}/conversations`
### v2
v2 uses the same schema and event-sourced backend as v3, but retains the legacy authentication model. Existing customers can continue using v2, but new customers should use v3
### v1 – legacy
v1 uses an earlier data pipeline and the legacy authentication system.
**Deprecation timeline**
* From **2 March 2026**, v1 moves to best-effort support (no SLA).
* From **31 August 2026**, v1 remains available but is no longer supported.
Customers on v1 should migrate to v3 before support ends. Contact your PolyAI representative for migration assistance.
## Regional base URLs
The Conversations API uses **`api..platform.polyai.app`** — *not* `api..poly.ai` (that's the [Agents API](/api-reference/agents/introduction)). Note the regional hosts include a `-1` suffix (for example `api.us-1.platform.polyai.app`); mixing them up with `api.us.poly.ai` is a common source of 404/DNS errors.
| Region | Base URL |
| -----: | --------------------------------------- |
| US | `https://api.us-1.platform.polyai.app` |
| UK | `https://api.uk-1.platform.polyai.app` |
| EUW | `https://api.euw-1.platform.polyai.app` |
Endpoint pattern:
`https://api.{region}.platform.polyai.app/{version}/{account_id}/{project_id}/conversations`
## Authentication
The Conversations API uses API key authentication.
### v3 keys
v3 keys are project- and region-scoped and are issued by PolyAI.
Your PolyAI representative will confirm the appropriate scope and provision access.
## Identifiers
`account_id` above is your **account ID** — Agent Studio's UI calls this the **Workspace ID** and shows it prefixed (`ws-xxxxxxxx`). `project_id` is the same value as the **Agent ID** shown in Agent Studio (prefixed `PROJECT-xxxxxxxx`); "Project" is the legacy term for the same resource.
| Agent Studio label | API parameter | Example |
| --------------------- | ------------- | ------------------ |
| Workspace ID | `account_id` | `ws-fd112d8f` |
| Project ID / Agent ID | `project_id` | `PROJECT-58RP822I` |
Both the slug form from the Agent Studio URL and the prefixed form work in API calls.
## Retrieval modes (optional)
The v3 endpoint supports three optional retrieval modes powered by the same tooling Wren uses: **transcript search**, **semantic search**, and **random sampling**. When no retrieval mode is specified, the endpoint returns all conversations matching the given filters (default behavior). Only one mode may be used per request.
| Mode | Parameter | Requires `query` | Description |
| ----------------- | ----------------------- | :--------------: | ---------------------------------------------------------------------------------------------------------- |
| Transcript search | `query_type=transcript` | Yes | Full-text search over conversation transcripts. Optionally scoped to user or agent turns with `turn_type`. |
| Semantic search | `query_type=semantic` | Yes | Vector similarity search. Returns conversations semantically similar to the query text. |
| Random sampling | `sample=random` | No | Returns a random sample of conversations in the time range. |
`query_type` and `sample` are mutually exclusive – you must specify at most one per request.
## Query parameters
The Conversations API supports several query parameters for filtering and controlling the response:
### Time range parameters
| Parameter | Description | Required |
| ------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `start_time` | Start of time range (ISO8601) | Required when using [retrieval modes](#retrieval-modes-optional) (`query_type` or `sample`). Optional otherwise – if omitted, no lower time bound is applied. |
| `end_time` | End of time range (ISO8601) | Required when using [retrieval modes](#retrieval-modes-optional) (`query_type` or `sample`). Optional otherwise – if omitted, no upper time bound is applied. |
Always set `start_time` and `end_time` — even though they're optional, bounding the range keeps result sets small and queries fast.
### Retrieval mode parameters (optional)
| Parameter | Type | Description |
| ------------ | ------ | -------------------------------------------------------------------------------------------- |
| `query_type` | string | Search mode: `transcript` or `semantic`. Requires `query`. Mutually exclusive with `sample`. |
| `sample` | string | Sampling mode: `random`. Mutually exclusive with `query_type`. |
| `query` | string | The search text used for transcript or semantic search. Required when `query_type` is set. |
| `turn_type` | string | Transcript search only. Filter by `user` or `agent` turns. Defaults to both. |
### Optional filters
| Parameter | Description | Default |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `client_env` | Environment: `sandbox`, `pre-release`, or `live` | `live` |
| `channel` | One or more channels to filter by (e.g. `voice`, `chat`) | – |
| `variant_id` | Filter by variant ID | – |
| `variant_name` | Filter by variant name (URL-encode spaces with `%20`) | – |
| `in_progress` | Filter by conversation status: `true` for in-progress, `false` for finished | all |
| `limit` | Max conversations per request (1–5000) | 5 |
| `offset` | Pagination offset. Prefer `cursor` for large result sets. | 0 |
| `cursor` | Opaque [keyset pagination](#pagination) cursor returned by the previous response. Supported on v3, and on v2 when chunked response streaming is enabled for your project. | – |
### Optional response fields
| Parameter | Description | Default |
| ----------------- | ---------------------------------------- | ------- |
| `include_latency` | Include latency metrics per conversation | `false` |
## Response structure
### Conversation object
| Field | Description |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | Unique conversation ID |
| `account_id` | Customer account ID |
| `project_id` | Project ID |
| `variant_id` | Variant ID |
| `variant_name` | Variant name |
| `environment` | Deployment environment (`live`, `sandbox`, `pre-release`) |
| `started_at` | Conversation start time (ISO8601) |
| `channel` | Conversation medium. Common values: `VOICE-SIP` (voice/telephony), `WEBCHAT` (webchat widget), `CHAT` (agent chat). These are analytics-level labels – for runtime channel detection in custom functions, see [`conv.channel_type`](/tools/classes/conv-object#channel_type). |
| `from_number` | Caller phone number |
| `to_number` | Agent phone number |
| `in_progress` | Whether call is still active |
| `num_turns` | Total number of turns in the conversation (integer count) |
| `total_duration` | Total call duration (seconds) |
| `polyai_duration` | PolyAI-handled duration (seconds) |
| `handoff` | Whether handoff occurred |
| `handoff_reason` | Brief handoff reason |
| `handoff_destination` | Handoff destination |
| `num_silences` | Count of silence turns |
| `num_ood` | Count of out-of-domain turns |
| `metrics` | Custom metrics logged by agent |
| `state` | All [conversation variables](/tools/variables) (`conv.state` values) set during the call, returned as key-value pairs. Includes both built-in keys (e.g. `from_`, `to`, `call_sid`) and any custom variables your agent writes. |
| `turns` | Ordered list of conversation turn objects. May be empty if transcript access is disabled in your project's API configuration (see [transcript visibility](#transcript-visibility)). |
### Turn object
| Field | Description |
| ------------------------- | ------------------------------------------ |
| `user_input` | User's transcribed text |
| `user_input_datetime` | When agent received input (ISO8601) |
| `barge_in` | Whether user interrupted the agent |
| `agent_response` | Agent's response text |
| `agent_response_datetime` | When agent responded (ISO8601) |
| `intents` | Detected intents |
| `entities` | Detected entities (name → value mapping) |
| `is_ood` | Whether turn was out-of-domain |
| `is_silence` | Whether turn was a silence |
| `translated_user_input` | Translated user input (when enabled) |
| `english_agent_response` | English version of response (when enabled) |
## Pagination
The v3 endpoint supports two pagination strategies. Use **cursor-based pagination** for any new integration — it is faster on large result sets and stays consistent when new conversations are written between page requests.
| Strategy | Request param | Response field | When to use |
| -------------------- | ------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Cursor (recommended) | `cursor` | `cursor` | New integrations and any workload that walks more than a few pages. Performance stays constant as you advance through the result set, and pages do not drift when conversations are added concurrently. Available on v3, and on v2 when chunked response streaming is enabled for your project (see [Large response streaming](#large-response-streaming)). |
| Offset (legacy) | `offset` | `next_offset` | Existing integrations that already rely on offsets. Available on all versions. |
You can only use one strategy per request. If both `cursor` and `offset` are present, `cursor` takes precedence.
### Cursor-based pagination
On the first request, omit `cursor`. The response includes a `cursor` field — pass it as the `cursor` query parameter on the next request to fetch the following page. Keep all other filters (`start_time`, `end_time`, `client_env`, `limit`, etc.) identical between requests. When `cursor` is `null`, you have reached the end of the result set.
Cursors are opaque, URL-safe strings — do not parse or generate them yourself. A malformed cursor returns a `400 Bad Request`.
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
# First page
curl -X GET \
"https://api..platform.polyai.app/v3/ws-xxxxxxxx/PROJECT-xxx/conversations?start_time=2026-04-01T00:00:00Z&end_time=2026-04-02T00:00:00Z&limit=100" \
-H "x-api-key: YOUR_API_KEY"
# Next page — pass the `cursor` returned above
curl -X GET \
"https://api..platform.polyai.app/v3/ws-xxxxxxxx/PROJECT-xxx/conversations?start_time=2026-04-01T00:00:00Z&end_time=2026-04-02T00:00:00Z&limit=100&cursor=CURSOR_FROM_PREVIOUS_RESPONSE" \
-H "x-api-key: YOUR_API_KEY"
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
base_url = "https://api..platform.polyai.app"
headers = {"x-api-key": "YOUR_API_KEY"}
params = {
"start_time": "2026-04-01T00:00:00Z",
"end_time": "2026-04-02T00:00:00Z",
"limit": 100,
}
cursor = None
while True:
if cursor:
params["cursor"] = cursor
response = requests.get(
f"{base_url}/v3/ws-xxxxxxxx/PROJECT-xxx/conversations",
headers=headers,
params=params,
)
response.raise_for_status()
page = response.json()
for conv in page["conversations"]:
print(conv["id"])
cursor = page.get("cursor")
if not cursor:
break
```
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const baseUrl = "https://api..platform.polyai.app";
const headers = { "x-api-key": "YOUR_API_KEY" };
let cursor: string | null = null;
do {
const params = new URLSearchParams({
start_time: "2026-04-01T00:00:00Z",
end_time: "2026-04-02T00:00:00Z",
limit: "100",
});
if (cursor) params.set("cursor", cursor);
const res = await fetch(
`${baseUrl}/v3/ws-xxxxxxxx/PROJECT-xxx/conversations?${params}`,
{ headers }
);
const page = await res.json();
for (const conv of page.conversations) {
console.log(conv.id);
}
cursor = page.cursor;
} while (cursor);
```
### Offset-based pagination (legacy)
When results exceed the limit, the response includes `next_offset`. Use this value as the `offset` parameter in your next request to fetch the next page. Offset pagination becomes slower as you advance deep into the result set and pages may shift if conversations are written between requests — use `cursor` instead where possible.
## Large response streaming
By default, the API builds the full JSON response in memory before sending it. For large pages — typically `limit` in the hundreds combined with `include_context_state=true` or `include_turn_metadata=true` — that response can be hundreds of megabytes and slow or unstable to download.
When chunked response streaming is enabled for your project, the API instead sends conversations incrementally using HTTP chunked transfer encoding. The response body is the same JSON shape; only the transport changes. Streaming responses also expose the `cursor` field, so you can combine streaming with [cursor-based pagination](#cursor-based-pagination) to walk through very large result sets without ever buffering a full page.
| Version | Streaming support |
| ------- | ------------------------------------------------------------------- |
| v3 | Enabled when your project has chunked response streaming turned on. |
| v2 | Enabled when your project has chunked response streaming turned on. |
| v1 | Not supported. |
Streaming is gated by a per-project setting managed by PolyAI. To turn it on, ask your PolyAI representative to enable chunked response streaming for the project. No client changes are required — clients that already parse the JSON response work unchanged, and clients that parse incrementally (for example, with a streaming JSON parser) start receiving conversations as soon as the server emits them.
If you query the v2 or v3 endpoint with a large `limit` and run into timeouts or out-of-memory errors on the client, enabling chunked response streaming and switching to `cursor` pagination almost always resolves it.
## Examples
All examples use the v3 endpoint:
`GET https://api.{region}.platform.polyai.app/v3/{account_id}/{project_id}/conversations`
### Retrieve all conversations in a time range
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET \
"https://api..platform.polyai.app/v3/ws-xxxxxxxx/PROJECT-xxx/conversations?start_time=2026-04-01T00:00:00Z&end_time=2026-04-02T00:00:00Z&limit=100" \
-H "x-api-key: YOUR_API_KEY"
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import requests
base_url = "https://api..platform.polyai.app"
headers = {"x-api-key": "YOUR_API_KEY"}
response = requests.get(
f"{base_url}/v3/ws-xxxxxxxx/PROJECT-xxx/conversations",
headers=headers,
params={
"start_time": "2026-04-01T00:00:00Z",
"end_time": "2026-04-02T00:00:00Z",
"limit": 100,
},
)
conversations = response.json()
for conv in conversations:
print(f"{conv['id']} — {conv['started_at']} — {conv['num_turns']} turns")
```
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const baseUrl = "https://api..platform.polyai.app";
const headers = { "x-api-key": "YOUR_API_KEY" };
const params = new URLSearchParams({
start_time: "2026-04-01T00:00:00Z",
end_time: "2026-04-02T00:00:00Z",
limit: "100",
});
const res = await fetch(
`${baseUrl}/v3/ws-xxxxxxxx/PROJECT-xxx/conversations?${params}`,
{ headers }
);
const conversations = await res.json();
for (const conv of conversations) {
console.log(`${conv.id} — ${conv.started_at} — ${conv.num_turns} turns`);
}
```
### Search transcripts for a keyword
Use `query_type=transcript` to find conversations containing specific text. Optionally filter by `turn_type` to search only user or agent turns.
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET \
"https://api..platform.polyai.app/v3/ws-xxxxxxxx/PROJECT-xxx/conversations?start_time=2026-04-01T00:00:00Z&end_time=2026-04-02T00:00:00Z&query_type=transcript&query=cancel%20reservation&turn_type=user&limit=50" \
-H "x-api-key: YOUR_API_KEY"
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = requests.get(
f"{base_url}/v3/ws-xxxxxxxx/PROJECT-xxx/conversations",
headers=headers,
params={
"start_time": "2026-04-01T00:00:00Z",
"end_time": "2026-04-02T00:00:00Z",
"query_type": "transcript",
"query": "cancel reservation",
"turn_type": "user",
"limit": 50,
},
)
results = response.json()
```
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const searchParams = new URLSearchParams({
start_time: "2026-04-01T00:00:00Z",
end_time: "2026-04-02T00:00:00Z",
query_type: "transcript",
query: "cancel reservation",
turn_type: "user",
limit: "50",
});
const searchRes = await fetch(
`${baseUrl}/v3/ws-xxxxxxxx/PROJECT-xxx/conversations?${searchParams}`,
{ headers }
);
const results = await searchRes.json();
```
### Semantic search
Use `query_type=semantic` to find conversations that are semantically similar to your query, even if the exact words don't appear in the transcript.
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET \
"https://api..platform.polyai.app/v3/ws-xxxxxxxx/PROJECT-xxx/conversations?start_time=2026-04-01T00:00:00Z&end_time=2026-04-02T00:00:00Z&query_type=semantic&query=customer%20frustrated%20about%20billing&limit=20" \
-H "x-api-key: YOUR_API_KEY"
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = requests.get(
f"{base_url}/v3/ws-xxxxxxxx/PROJECT-xxx/conversations",
headers=headers,
params={
"start_time": "2026-04-01T00:00:00Z",
"end_time": "2026-04-02T00:00:00Z",
"query_type": "semantic",
"query": "customer frustrated about billing",
"limit": 20,
},
)
similar = response.json()
```
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const semanticParams = new URLSearchParams({
start_time: "2026-04-01T00:00:00Z",
end_time: "2026-04-02T00:00:00Z",
query_type: "semantic",
query: "customer frustrated about billing",
limit: "20",
});
const semanticRes = await fetch(
`${baseUrl}/v3/ws-xxxxxxxx/PROJECT-xxx/conversations?${semanticParams}`,
{ headers }
);
const similar = await semanticRes.json();
```
### Random sampling
Use `sample=random` to retrieve a random subset of conversations in a time range – useful for quality audits.
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET \
"https://api..platform.polyai.app/v3/ws-xxxxxxxx/PROJECT-xxx/conversations?start_time=2026-04-01T00:00:00Z&end_time=2026-04-08T00:00:00Z&sample=random&limit=10" \
-H "x-api-key: YOUR_API_KEY"
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = requests.get(
f"{base_url}/v3/ws-xxxxxxxx/PROJECT-xxx/conversations",
headers=headers,
params={
"start_time": "2026-04-01T00:00:00Z",
"end_time": "2026-04-08T00:00:00Z",
"sample": "random",
"limit": 10,
},
)
sample = response.json()
```
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const sampleParams = new URLSearchParams({
start_time: "2026-04-01T00:00:00Z",
end_time: "2026-04-08T00:00:00Z",
sample: "random",
limit: "10",
});
const sampleRes = await fetch(
`${baseUrl}/v3/ws-xxxxxxxx/PROJECT-xxx/conversations?${sampleParams}`,
{ headers }
);
const sample = await sampleRes.json();
```
### Filter by channel
Use the `channel` parameter to restrict results to a specific medium. You can specify it multiple times to include several channels.
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET \
"https://api..platform.polyai.app/v3/ws-xxxxxxxx/PROJECT-xxx/conversations?start_time=2026-04-01T00:00:00Z&end_time=2026-04-02T00:00:00Z&channel=WEBCHAT&limit=50" \
-H "x-api-key: YOUR_API_KEY"
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = requests.get(
f"{base_url}/v3/ws-xxxxxxxx/PROJECT-xxx/conversations",
headers=headers,
params={
"start_time": "2026-04-01T00:00:00Z",
"end_time": "2026-04-02T00:00:00Z",
"channel": "WEBCHAT",
"limit": 50,
},
)
webchat_convos = response.json()
```
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const channelParams = new URLSearchParams({
start_time: "2026-04-01T00:00:00Z",
end_time: "2026-04-02T00:00:00Z",
channel: "WEBCHAT",
limit: "50",
});
const channelRes = await fetch(
`${baseUrl}/v3/ws-xxxxxxxx/PROJECT-xxx/conversations?${channelParams}`,
{ headers }
);
const webchatConvos = await channelRes.json();
```
A successful response includes:
* complete turn-by-turn transcript
* consistent timestamps
* latency metrics (when `include_latency=true`)
* translation outputs (when enabled)
* handoff metadata (if applicable)
## Understanding `turns` and `num_turns`
These two fields are often confused. Here's how they relate:
| Field | Type | Purpose |
| ----------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `turns` | Array of turn objects | The full ordered list of conversation turns. Included by default, but may be empty if transcript access is disabled in the project's API configuration. |
| `num_turns` | Integer | A count of turns in the conversation. This value reflects the number of turns that occurred, even if the `turns` array is empty due to configuration. |
`include_latency` enriches **existing** turn data – it does not populate an empty `turns` array. If your `turns` array is empty, this parameter will have no effect. The `turns` array is controlled by your project's [transcript visibility](#transcript-visibility) setting, not by this query parameter.
## Transcript visibility
The `turns` array in the API response is controlled by the **Conversation transcript** toggle (underlying field: `show_transcripts`) in your project's API key configuration. When transcript access is disabled, the API returns an empty `turns` array even though `num_turns` still reports the actual count.
This is the most common reason for missing turn data. To check or update this setting:
Go to the **API Keys** tab on the workspace homepage in Agent Studio (next to **Agents**, **Secrets**, and **Users**).
Click the **Configuration** button at the top right of the API Keys page.
Inside the configuration panel, find your project and enable the **Conversation transcript** toggle. You can also select which **Response metrics** to include in API responses (e.g. `CALL_IN_PROGRESS`, `CALL_COMPLETED`, `HANDOFF_TO`, `HANDOFF_REASON`).
These settings apply at the **project level**, not per key. For data to appear in API responses, both conditions must be met: the API key must have **Conversations data** permission enabled, and the project must have the relevant metrics and transcript access enabled in the Configuration panel. See [API keys](/secrets/api-keys) for key setup details.
## Troubleshooting
### `turns` is empty but `num_turns` is greater than zero
This is the most common issue reported by API users. Use this decision tree to diagnose the cause:
```
Empty turns array?
├── Is num_turns > 0?
│ ├── YES → Transcript visibility is disabled (most common cause).
│ │ Go to API Keys > Configuration on the workspace
│ │ homepage and enable "Conversation transcript".
│ └── NO → No turns were recorded. Check your filters
│ (start_time, end_time, client_env).
└── Are you using the v1 endpoint?
└── YES → v1 is deprecated. Migrate to v3 for full field support.
Contact your PolyAI representative for migration help.
```
Go to the **API Keys** tab on the workspace homepage, then click **Configuration** and verify that the **Conversation transcript** toggle is enabled for your project. This is a project-level setting. See [transcript visibility](#transcript-visibility) for details.
Verify your endpoint URL contains `/v3/`. The v1 API is deprecated and may not return full conversation data. v1 moved to best-effort support on **2 March 2026** and will be unsupported from **31 August 2026**.
If `in_progress` is `true`, turn data may still be arriving and may not be fully available yet.
If none of the above applies, this may indicate a data pipeline delay or ingestion issue.
### Only seeing basic fields (`id`, `account_id`, `project_id`, `started_at`)
If the response only contains a subset of expected fields:
1. **Confirm you are on v3.** The full response schema (including `latency`, `translated_user_input`, `english_agent_response`, etc.) is only available on v3. Check that your base URL uses `/v3/`, not `/v1/`. Contact your PolyAI representative for help migrating from v1.
2. **Check your API key permissions.** On the **API Keys** tab of the workspace homepage, verify the API key has **Conversations data** permission enabled. The key must have the correct permission *and* the project must have the relevant metrics enabled.
3. **Check metric configuration.** On the **API Keys** tab, click **Configuration** and verify the response metrics you expect are enabled (e.g. `CALL_IN_PROGRESS`, `CALL_COMPLETED`, `HANDOFF_TO`). If no metrics are selected, the `metrics` object in the response will be empty.
4. **Check field whitelisting (v1 only).** Projects on v1 may have field whitelisting enabled in their project configuration (`enable_whitelist: true`). When active, the API only returns fields explicitly listed in the project's API config — typically just `id`, `started_at`, `account_id`, and `project_id`. If you are on v1 and see this behavior, migrate to v3 where field visibility is controlled through the [API Keys Configuration panel](#transcript-visibility) instead.
### `include_latency` is not adding data
`include_latency=true` adds per-turn latency metrics to each turn object, but enriches **existing** turn data – it does not populate an empty `turns` array.
If `turns` is empty, this parameter has no effect. Fix the underlying cause first (see [transcript visibility](#transcript-visibility) above).
# List all conversations
Source: https://docs.poly.ai/api-reference/conversations/v1/endpoint/get-conversations
GET /v1/{account_id}/{project_id}/conversations
Returns all conversations matching filters.
# Get audio recording for a conversation
Source: https://docs.poly.ai/api-reference/conversations/v3/endpoint/get-conversation-audio
GET /v1/agents/{agentId}/conversations/{conversationId}/audio
Fetch the WAV audio recording for a conversation. Served from api.{region}.poly.ai. Users without PII access automatically receive the redacted recording.
## PII access and redaction
The `redacted` query parameter controls whether personally identifiable information (PII) is muted from the returned audio:
| User PII access | `redacted` query value | Audio returned |
| --------------- | ---------------------- | ----------------------------- |
| Yes | `false` (or omitted) | Raw recording |
| Yes | `true` | Redacted recording |
| No | `false` (or omitted) | Redacted recording (enforced) |
| No | `true` | Redacted recording |
Users without PII access always receive the redacted recording, even when `redacted=false` is passed. This is enforced server-side, so callers cannot bypass redaction by omitting or overriding the query parameter.
PII access is granted through your account's role and permission configuration in Agent Studio. See [PII logging](/tools/classes/conv-log#pii) for related conversation-log behavior.
## Regional base URLs
| Region | Base URL |
| ------ | ---------------------------- |
| US | `https://api.us.poly.ai` |
| EU | `https://api.eu.poly.ai` |
| UK | `https://api.uk.poly.ai` |
| Studio | `https://api.studio.poly.ai` |
This endpoint is served from the Kong-routed host (`api.{region}.poly.ai`), *not* `api.{region}-1.platform.polyai.app` (which serves the list-conversations endpoint). Sending the request to the wrong host will fail.
## Example
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --request GET \
--url "https://api.us.poly.ai/v1/agents/{agentId}/conversations/{conversationId}/audio?direction=combined&redacted=false" \
--header "x-api-key: $POLYAI_API_KEY" \
--output conversation.wav
```
Replace `{agentId}` and `{conversationId}` with real values — the placeholders are not resolved server-side. Only `Accept: audio/wav` is needed; do not send `Content-Type` on this `GET` request (it has no body).
## Common pitfalls
| Symptom | Likely cause | Fix |
| ----------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------- |
| `404 Not Found` or DNS error | Using the wrong host (e.g. `api.us-1.platform.polyai.app`) | Use `api.{region}.poly.ai` |
| `404 Not Found` on the conversation | The literal string `{conversationId}` was left in the URL | Substitute a real conversation ID from the List Conversations endpoint |
| `401 Unauthorized` | Empty or wrong-region API key | Issue a region-scoped key from PolyAI |
# Get a conversation by ID
Source: https://docs.poly.ai/api-reference/conversations/v3/endpoint/get-conversation-by-id
GET /v1/agents/{agentId}/conversations/{conversationId}
Retrieve a single conversation, including turns, metrics, and function events. Served from api.{region}.poly.ai, not the api.{region}-1.platform.polyai.app host.
# List all conversations
Source: https://docs.poly.ai/api-reference/conversations/v3/endpoint/get-conversations
GET /v3/{account_id}/{project_id}/conversations
Retrieves conversations for a given account and project. Supports three retrieval modes: transcript search, semantic search, and random sampling. When no retrieval mode is specified, the endpoint returns all conversations matching the given filters (default behavior). Only one retrieval mode may be specified per request.
# Create a new debug chat session
Source: https://docs.poly.ai/api-reference/debug-chat/create-debug-chat-session
api-reference/debug-chat/openapi.json POST /v1/agents/{agentId}/debug-chat
Start a new debug chat session to interactively test an agent variant outside of production traffic. Served from api.{region}.poly.ai.
# Debug Chat API
Source: https://docs.poly.ai/api-reference/debug-chat/introduction
Create and exchange messages in debug chat sessions to test agent variants outside of production traffic.
The Debug Chat API lets you exercise an agent variant programmatically — create a session against a specific variant and send user turns into it to see the agent's replies, without touching production traffic. Use it for interactive testing, regression checks, and CI-style suites against a branch before you publish.
## What you can do
* Create a debug chat session targeting a specific agent variant.
* Send a user turn into an existing session and read back the agent response.
## Regional base URLs
| Region | Base URL |
| ------ | ---------------------------- |
| US | `https://api.us.poly.ai` |
| EU | `https://api.eu.poly.ai` |
| UK | `https://api.uk.poly.ai` |
| Studio | `https://api.studio.poly.ai` |
## Identifiers
Agent Studio labels and API parameters use different names for the same things. When you read an ID from Agent Studio, map it to the API parameter using this table:
| Agent Studio label | API parameter | Example |
| --------------------- | ------------- | ------------------ |
| Project ID / Agent ID | `agentId` | `PROJECT-58RP822I` |
**Agent ID is the same value as Project ID.** "Agent" is the current product name; "Project" is the legacy term still surfaced in some Agent Studio screens and URLs. Use the value you see in Agent Studio as the `agentId` path parameter — no transformation needed.
## Authentication
The Debug Chat API uses API key authentication with the `X-API-KEY` header. API keys are scoped to account, project, and region. Issue keys from the API keys section of Agent Studio, or request one from your PolyAI representative.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl --request POST \
--url "https://api.us.poly.ai/v1/agents/{agentId}/debug-chat" \
--header "X-API-KEY: $POLYAI_API_KEY"
```
**The Agent Studio instance and API region must match.** A key generated from `studio.poly.ai` only works against `api.studio.poly.ai`. To call `api.us.poly.ai`, you must generate the key from `studio.us.poly.ai` (and likewise for `eu` and `uk`). A mismatch returns `401 Unauthorized`.
| Agent Studio URL | API base URL |
| --------------------------- | ---------------------------- |
| `https://studio.poly.ai` | `https://api.studio.poly.ai` |
| `https://studio.us.poly.ai` | `https://api.us.poly.ai` |
| `https://studio.eu.poly.ai` | `https://api.eu.poly.ai` |
| `https://studio.uk.poly.ai` | `https://api.uk.poly.ai` |
# Send a message to a debug chat session
Source: https://docs.poly.ai/api-reference/debug-chat/send-debug-chat-message
api-reference/debug-chat/openapi.json POST /v1/agents/{agentId}/debug-chat/{conversationId}
Send a user turn into an existing debug chat session and return the agent's response. Served from api.{region}.poly.ai.
# Reserve DNI
Source: https://docs.poly.ai/api-reference/dni/endpoint/dni-reservation
POST /v1/conversations/dni-reservation
Reserves a DNI, associating a temporary set of attributes to a specific virtual agent.
# DNI API
Source: https://docs.poly.ai/api-reference/dni/introduction
Reserve dynamic phone numbers with attached context for marketing, CRM, and web-to-call integrations.
The DNI API reserves dynamic phone numbers that route directly to your PolyAI agent while supplying contextual attributes at call start. Use it for marketing campaigns, CRM integrations, and web-to-call flows to personalize conversations and attribute conversions.
DNI is typically used in marketing, CRM, and web-to-call integrations where a temporary number encodes context about the user journey.
The virtual agent is linked to the API key used in the request. When the caller dials the reserved DNI, they are routed to the agent, which can consume the provided attributes. In most cases, the API key for this endpoint is different from the key used for other API endpoints.
## What the DNI API does
When your system makes a reservation, PolyAI returns a temporary phone number (the DNI). Any caller who dials that number during the active reservation window is automatically routed to the correct agent, with the attached attributes available at the start of the conversation.
Common use cases include:
* Passing shared IDs, customer identifiers, or session tokens
* Injecting CRM or cart details
* Associating prior call history
* Enabling attribution for marketing conversions
* Supplying metadata for routing or state initialisation
Only one endpoint is exposed: a POST request that reserves the DNI and associates attributes.
## Regional base URLs
Choose the base URL that matches your PolyAI deployment region:
| Region | Base URL |
| -----: | ------------------------------------------------------------------------------ |
| US | [https://api.us-1.platform.polyai.app](https://api.us-1.platform.polyai.app) |
| UK | [https://api.uk-1.platform.polyai.app](https://api.uk-1.platform.polyai.app) |
| EUW | [https://api.euw-1.platform.polyai.app](https://api.euw-1.platform.polyai.app) |
The DNI endpoint is:
`https://api.{region}.platform.polyai.app/v1/conversations/dni-reservation`
## Authentication
The DNI API uses API-key authentication, supplied with the x-api-key header.
Each DNI key is tied to a specific virtual agent. Calling the reserved phone number routes directly to that agent and provides the attributes you supplied.
DNI API keys are separate from Conversations API keys. In most deployments, the DNI key is unique and used only for this endpoint.
## Example: Reserve a DNI
POST [https://api.\{region}.platform.polyai.app/v1/conversations/dni-reservation](https://api.\{region}.platform.polyai.app/v1/conversations/dni-reservation)
Send a JSON payload containing any attributes you want associated with the reservation. The response returns a dni value (the temporary phone number). Your attributes become available to the agent as soon as the call begins.
# Error codes
Source: https://docs.poly.ai/api-reference/error-codes
HTTP status codes and platform-specific error codes returned by PolyAI APIs.
When a request fails, PolyAI APIs return a structured JSON error response. This page documents the HTTP status codes, error response format, and platform-specific error codes you may encounter.
64 platform error codes span the categories below — jump straight to yours:
## Error response format
All API errors return a JSON body with the following structure:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"success": false,
"error": "Request body failed schema validation.",
"error_id": "550e8400-e29b-41d4-a716-446655440000",
"error_code": "SCHEMA_VALIDATION_FAILED",
"error_message": "Request body failed schema validation.",
"data": null
}
```
| Field | Type | Description |
| --------------- | ------- | ---------------------------------------------------------------------------------------------------------- |
| `success` | boolean | Always `false` for error responses. |
| `error` | string | Human-readable error message. |
| `error_id` | string | Unique request identifier from the `X-PolyAI-Correlation-Id` header. Include this when contacting support. |
| `error_code` | string | Machine-readable error code in `UPPER_SNAKE_CASE`. |
| `error_message` | string | Human-readable error message. |
| `data` | null | Always `null` for error responses. |
## Standard HTTP status codes
These standard codes apply across all PolyAI APIs. Client errors (4xx) mean fix the request; server errors (5xx) mean retry or contact support.
## Platform error codes
Expand the category you're hitting, or use one of the jump links above.
| Code | ID | HTTP | Description |
| ---------------------------------- | ---- | ---- | -------------------------------------------------- |
| `AUTH_USER_NOT_FOUND` | 3001 | 401 | User not found in the auth system. |
| `AUTH_USER_UNAUTHORISED` | 3002 | 401 | Invalid credentials or expired session. |
| `AUTH_USER_INVALID_TOKEN` | 3003 | 401 | Token is malformed, expired, or revoked. |
| `USER_ACCESS_FORBIDDEN` | 2 | 403 | User lacks permission for the requested action. |
| `EXTERNAL_USER_FORBIDDEN` | 3 | 403 | External user privilege level is too low. |
| `ACCOUNT_INSUFFICIENT_PERMISSIONS` | 1003 | 403 | User lacks permissions for this account operation. |
| Code | ID | HTTP | Description |
| ------------------------------------------ | ---- | ---- | ------------------------------------------------ |
| `CONVERSATIONS_NOT_FOUND` | 5001 | 404 | Conversation ID does not exist. |
| `CONVERSATIONS_VALIDATION_FAILED` | 5002 | 400 | Conversation request body failed validation. |
| `CONVERSATIONS_RECORDING_NOT_FOUND` | 5003 | 404 | Audio recording not found for this conversation. |
| `GET_CONVERSATIONS_FILTER_PARSE_ERROR` | — | 400 | Filter expression syntax is invalid. |
| `GET_CONVERSATIONS_INVALID_SORT_PARAMETER` | — | 400 | Sort field is not in the allowed set. |
| Code | ID | HTTP | Description |
| ------------------------------ | ---- | ---- | ------------------------------------------------------------ |
| `CHAT_DRAFT_NOT_EXIST` | 4001 | 400 | The draft you are attempting to chat with does not exist. |
| `CHAT_TIMEOUT` | 4002 | 400 | Chat session exceeded the timeout limit. |
| `CHAT_DRAFT_DEPLOYMENT_FAILED` | 4003 | 500 | Draft auto-deploy failed before the chat could start. |
| `ChatConversationEnded` | — | 410 | You sent a message to a conversation that has already ended. |
| Code | ID | HTTP | Description |
| ------------------------------------------- | ---- | ---- | -------------------------------------------------------- |
| `DEPLOYMENT_NOT_FOUND` | 6001 | 404 | Deployment ID does not exist. |
| `DEPLOYMENTS_ALREADY_PUBLISHED` | 6009 | 409 | Draft is already published to the target environment. |
| `DEPLOYMENTS_ENVIRONMENT_ALREADY_PUBLISHED` | 6002 | 200 | Environment already has this version (idempotent). |
| `DEPLOYMENTS_VALIDATION_FAILED` | 6007 | 400 | Request body validation failed. |
| `DEPLOYMENTS_INVALID_ENVIRONMENT` | 6008 | 400 | Invalid or missing `environment` query parameter. |
| `ErrPreReleaseNotReady` | — | 412 | You must deploy to pre-release before promoting to live. |
| Code | ID | HTTP | Description |
| ------------------------------------ | ---- | ---- | ----------------------------------------------------- |
| `FLOW_NOT_FOUND` | 8001 | 404 | Flow ID does not exist. |
| `FLOWS_NAME_ALREADY_EXISTS` | 8003 | 409 | A flow with this name already exists. |
| `FLOWS_FUNCTION_NAME_ALREADY_EXISTS` | 8004 | 409 | A function with this name already exists in the flow. |
| `FLOWS_RESERVED_PARAMETER_NAME` | 8005 | 400 | You used a reserved parameter name. |
| `FLOWS_RESERVED_FUNCTION_NAME` | 8006 | 400 | You used a reserved function name. |
| Code | ID | HTTP | Description |
| --------------------------------- | ---- | ---- | ------------------------------------------------------ |
| `FUNCTION_NOT_FOUND` | 9001 | 404 | Function ID does not exist. |
| `FUNCTIONS_DEPLOYMENT_HAS_ERRORS` | 9004 | 400 | Function code contains errors that prevent deployment. |
| `FUNCTION_EXECUTION_FAILED` | 9006 | 400 | Function encountered a runtime execution error. |
| `FUNCTION_FAILED_TO_PARSE` | 9007 | 400 | Function source code has parse errors. |
| `FUNCTION_NAME_ALREADY_EXISTS` | 9009 | 409 | A function with this name already exists. |
| `FUNCTIONS_RESERVED_NAME` | 9010 | 400 | You used a system-reserved function name. |
| Code | ID | HTTP | Description |
| --------------------------------------- | ----- | ---- | ------------------------------------------------------------- |
| `KNOWLEDGE_BASE_TOPIC_ALREADY_EXISTS` | 11001 | 409 | A topic with this name already exists. |
| `KNOWLEDGE_BASE_IMPORT_NO_CSV_FOUND` | 11002 | 400 | No CSV file found in the import request. |
| `KNOWLEDGE_BASE_IMPORT_INVALID_TYPE` | 11003 | 415 | The uploaded file type is not supported. |
| `KNOWLEDGE_BASE_IMPORT_MISSING_COLUMNS` | 11004 | 400 | Required columns are missing from the CSV import. |
| `KNOWLEDGE_BASE_TOPICS_INVALID` | 11005 | 400 | Topic data failed validation. |
| `KNOWLEDGE_BASE_RICH_TEXT_INVALID` | 11006 | 400 | Rich text markup is malformed or references a missing entity. |
| Code | ID | HTTP | Description |
| ------------------------------------------- | ----- | ---- | ----------------------------------------- |
| `PHONE_NUMBERS_NOT_FOUND` | 14006 | 404 | Phone number does not exist. |
| `PHONE_NUMBERS_ALREADY_EXISTS` | 14007 | 409 | Phone number has already been imported. |
| `PHONE_NUMBERS_INVALID_PHONE_NUMBER_FORMAT` | 14003 | 400 | Number is not in valid E.164 format. |
| `PHONE_NUMBERS_CONNECTOR_DOES_NOT_EXIST` | 14005 | 404 | Referenced connector not found. |
| `CONNECTORS_NOT_FOUND` | 14100 | 404 | Connector ID does not exist. |
| `CONNECTORS_VALIDATION_FAILED` | 14101 | 400 | Connector request body failed validation. |
| Code | ID | HTTP | Description |
| ----------------------------------- | ----- | ---- | -------------------------------------------------- |
| `VARIANTS_BAD_ATTRIBUTES_ERROR` | 26001 | 400 | Attribute values do not match the expected schema. |
| `VARIANTS_DUPLICATE_NAME_ERROR` | 26002 | 409 | A variant with this name already exists. |
| `VARIANTS_REMOVE_DEFAULT_ERROR` | 26003 | 400 | You cannot remove the default variant. |
| `VariantsImportInvalidDelimiter` | — | 400 | CSV delimiter not recognized. |
| `VariantImportMissingColumnsHttp` | — | 400 | CSV is missing required columns. |
| `VariantImportDuplicateColumnsHttp` | — | 400 | CSV has duplicate column headers. |
| Code | ID | HTTP | Description |
| --------------------------------------------- | ----- | ---- | --------------------------------------------------------------------- |
| `REAL_TIME_CONFIG_FORBIDDEN_ACCESS` | 29001 | 403 | Not authorized to access real-time configs. |
| `REAL_TIME_CONFIG_INVALID_FOR_SCHEMA` | 29002 | 400 | Config values fail JSON Schema validation. |
| `REAL_TIME_CONFIG_INVALID_SCHEMA` | 29004 | 400 | JSON Schema definition is invalid. |
| `REAL_TIME_CONFIG_UNKNOWN_CLIENT_ENVIRONMENT` | 29006 | 400 | Client environment is not one of `sandbox`, `pre-release`, or `live`. |
| Code | ID | HTTP | Description |
| ------------------- | ----- | ---- | -------------------------------------- |
| `SECRETS_NOT_FOUND` | 21001 | 404 | Secret does not exist in this account. |
| Code | ID | HTTP | Description |
| ---------------------------- | ----- | ---- | ------------------------------------------------------------------------------------ |
| `ACCOUNT_NOT_FOUND` | 1002 | 404 | Account ID does not exist. |
| `ACCOUNT_CREATE_INVALID_ID` | 1004 | 400 | Account ID contains non-alphanumeric characters or is too similar to an existing ID. |
| `ACCOUNT_CREATE_EXISTING_ID` | 1006 | 409 | Account ID is already taken. |
| `PROJECT_NOT_FOUND` | 15001 | 404 | Project ID does not exist. |
| `PROJECT_ID_ALREADY_EXISTS` | 15005 | 409 | Project ID is already taken. |
| `PROJECT_ID_INVALID` | 15006 | 400 | Project ID does not meet format requirements. |
| Code | ID | HTTP | Description |
| ------------------------ | ----- | ---- | ------------------------------- |
| `SMS_TEMPLATE_NOT_FOUND` | 20001 | 404 | SMS template ID does not exist. |
| Code | ID | HTTP | Description |
| ---------------------- | -- | ---- | ----------------------------------------- |
| `MissingTestCasesHttp` | — | 422 | One or more test case IDs were not found. |
| `NoTestCasesHttp` | — | 422 | No test cases exist for this project. |
## WebSocket close codes
If you are using the WebRTC Gateway or webchat WebSocket connections, you may encounter these close codes:
| Code | Meaning | When it occurs |
| ---- | ---------------- | --------------------------------------------------------------------- |
| 1000 | Normal closure | Connection closed cleanly. |
| 1006 | Abnormal closure | Connection dropped unexpectedly (for example, network failure). |
| 408 | Pong timeout | Server did not receive a pong response within the configured timeout. |
## Troubleshooting
Every API response includes an `X-PolyAI-Correlation-Id` header. When contacting PolyAI support, include this value so the team can trace the request through the system.
### Common patterns
Quick diagnostics for the errors you are most likely to hit. Expand a row to see the likely cause and how to resolve it.
**Likely cause** — API key missing or malformed.
**Resolution** — Verify the `x-api-key` header is present and correctly formatted. Check for stray whitespace or missing characters when the key is copied from a secret store.
**Likely cause** — Old key revoked.
**Resolution** — Confirm you are using the new key everywhere it is referenced — including local `.env` files, CI secrets, and any deployed services.
**Likely cause** — Wrong region.
**Resolution** — Verify the base URL matches your account region. See [API getting started](/api-reference/introduction) for the list of regional base URLs.
**Likely cause** — Duplicate identifier.
**Resolution** — Use a different name or ID, or check for an existing resource with the same identifier before retrying.
**Likely cause** — Schema mismatch.
**Resolution** — Verify CSV columns match the expected format. Check the error `data` field for column-level details.
## Related pages
Authentication, base URLs, and API versioning.
Retrieve conversation data and transcripts.
# Bridge ended notification
Source: https://docs.poly.ai/api-reference/external-events/endpoint/bridge-ended
POST /internal/v1/agentic-dial/bridge-ended
Internal endpoint that receives notifications when a call bridge (transfer) completes. Triggered automatically by the telephony system after a bridged call ends, allowing configured project functions to process bridge metadata and call duration information.
## Overview
The bridge-ended endpoint receives notifications when a call bridge (transfer) completes. This endpoint is triggered automatically by the telephony system after a bridged call ends, allowing configured project functions to process bridge metadata and call duration information.
## Use cases
* Log bridge duration for analytics
* Trigger post-transfer workflows
* Update CRM systems with transfer outcomes
* Calculate agent availability based on bridge time
## Request body
The endpoint accepts the following parameters:
| Parameter | Type | Required | Description |
| ------------------------- | ------ | -------- | ------------------------------------------ |
| `conversation_id` | string | Yes | The unique identifier for the conversation |
| `bridge_duration_seconds` | number | Yes | Duration of the bridge/transfer in seconds |
| `call_duration_seconds` | number | Yes | Total call duration in seconds |
## Example request
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"conversation_id": "conv_abc123xyz",
"bridge_duration_seconds": 180,
"call_duration_seconds": 300
}
```
## Behavior
When this endpoint receives a bridge-ended notification:
1. The system extracts the conversation ID and bridge metadata
2. Configured project functions are triggered with the bridge data
3. Functions can access bridge duration and call duration for processing
4. The conversation state is updated to reflect the bridge completion
## Configuration
This endpoint is automatically configured when you set up call transfer/bridge functionality in your project. Contact your PolyAI representative to enable bridge-ended notifications for your project.
# Post external event result
Source: https://docs.poly.ai/api-reference/external-events/endpoint/webhook
POST /v1/external-events/webhook
External providers POST results back to the Agent runtime. PolyAI extracts the external event ID according to the query parameters, binds the data to the active conversation, and (optionally) feeds it to the LLM.
# External Events API
Source: https://docs.poly.ai/api-reference/external-events/introduction
Feed external results (payments, bookings, verifications) back into active conversations asynchronously.
The External Events API lets third-party systems and external processes push results back into active PolyAI conversations. Use it to complete asynchronous workflows–like payment processing, booking confirmations, and eligibility checks–while the user waits.
When the external event arrives, PolyAI links it to the correct conversation and (optionally) feeds the data back into the agent's reasoning and dialogue.
## Endpoint
The External Events API is exposed as a single POST webhook endpoint:
`POST https://api.{region}.platform.polyai.app/v1/external-events/webhook`
Region can be:
* `us-1`
* `uk-1`
* `euw-1`
| Region | Base URL |
| -----: | ------------------------------------------------------------------------------ |
| US | [https://api.us-1.platform.polyai.app](https://api.us-1.platform.polyai.app) |
| UK | [https://api.uk-1.platform.polyai.app](https://api.uk-1.platform.polyai.app) |
| EUW | [https://api.euw-1.platform.polyai.app](https://api.euw-1.platform.polyai.app) |
## Identifying the event
To bind an incoming payload to the correct conversation, PolyAI needs to know where in the request the external event identifier lives.
You configure this per request using two query parameters:
* `event_id_location`
Where the external event ID is found. One of:
* `headers`
* `querystring`
* `payload`
* `event_id_path`
The name or dotted path to the external event ID in that location.\
Examples:
* `cid`
* `path.to.object.property`
You must also include:
* `account_id`: your PolyAI account ID
* `project_id`: your PolyAI project ID
These four query parameters together tell PolyAI how to extract the correct event ID and map the payload to the right conversation.
## Identifiers
`account_id` above is your **account ID** — Agent Studio's UI calls this the **Workspace ID** and shows it prefixed (`ws-xxxxxxxx`). `project_id` is the same value as the **Agent ID** shown in Agent Studio (prefixed `PROJECT-xxxxxxxx`); "Project" is the legacy term for the same resource. Both the slug form from the Agent Studio URL and the prefixed form work in API calls.
## Payload formats
The webhook accepts multiple content types:
* `application/json`
Arbitrary JSON with the event ID somewhere in the structure.
* `application/x-www-form-urlencoded`
Standard form-encoded key–value pairs.
* `application/xml`
XML payload containing the event ID and other fields.
* `text/plain`
Raw text payloads (for example, simple key=value strings).
All of these are treated as opaque data: PolyAI stores the payload and uses the configured event ID location and path to associate it with the conversation.
## Authentication
The External Events API uses a dedicated API key:
* sent in the `X-Api-Key` header
* scoped specifically for external events
You will receive this key from your PolyAI representative.
Example header:
`X-Api-Key: YOUR_EXTERNAL_EVENTS_API_KEY`
## Typical flow
1. The PolyAI agent initiates an external workflow (for example, payment, booking, or verification) and generates an external event ID.
2. Your system or third-party provider completes its task and sends a POST request to the webhook:
* targeting the correct regional base URL
* including `event_id_location`, `event_id_path`, `account_id`, and `project_id` as query parameters
* including the external event ID somewhere in the request, at the agreed location and path
* passing the full result payload in the body
3. PolyAI:
* authenticates the request using X-Api-Key
* extracts the external event ID
* binds the payload to the right conversation
* updates the agent's state so the conversation can continue or complete
## Responses and error handling
On success:
* 201 Accepted\
The event has been received and queued for processing.
Common error responses:
* 400 Malformed request\
For example, the external event ID cannot be found or is invalid.
* 401 Unauthorized\
Missing or invalid API key.
* 500 Internal Server Error\
A transient error occurred while processing the event.
In all error cases, the response body includes an `error_message` field explaining the reason, which you can log or surface in your monitoring.
# Get handoff state
Source: https://docs.poly.ai/api-reference/handoff/endpoint/get-handoff
GET /v1/{account_id}/{project_id}/handoff_state
Returns the stored handoff state for a conversation. At least one of **id** or **shared_id** must be provided as a query parameter. If both are provided, **shared_id** takes precedence.
# Handoff API
Source: https://docs.poly.ai/api-reference/handoff/introduction
Retrieve conversation context and metadata when PolyAI agents hand off to live agents or external systems.
The Handoff API allows downstream platforms to retrieve structured context and metadata when conversations are handed off from PolyAI agents. Use it to enable routing decisions, screen-pops for live agents, and CRM integration.
This endpoint is typically used to support:
* routing decisions
* screen-pops for live agents
* attaching metadata to tickets or workflow systems
* passing through identifiers collected earlier in the call
## What the API returns
A successful response provides:
* `id` – the PolyAI conversation ID
* `shared_id` – an integrator-defined identifier, if one was stored
* `data` – a free-form JSON object containing the handoff metadata written by the agent
The data field may contain:
* customer identifiers
* reasons for handoff
* queue or routing hints
* arbitrary key–value pairs describing caller state
This API is one of three ways the agent passes context to a human at handoff. For SIP headers and Conversations API alternatives, see [Handoff context handover](/voice-channel/handoffs#handoff-context-handover).
## Regional base URLs
| Region | Base URL |
| -----: | ------------------------------------------------------------------------------ |
| US | [https://api.us-1.platform.polyai.app](https://api.us-1.platform.polyai.app) |
| UK | [https://api.uk-1.platform.polyai.app](https://api.uk-1.platform.polyai.app) |
| EUW | [https://api.euw-1.platform.polyai.app](https://api.euw-1.platform.polyai.app) |
Full endpoint structure:
`https://api.{region}.platform.polyai.app/v1/{account_id}/{project_id}/handoff_state`
You must supply either:
* `id` (PolyAI conversation ID), or
* `shared_id` (a custom ID you passed into the system)
If both are supplied, shared\_id takes precedence.
## Authentication
The Handoff API uses API key authentication with the x-api-key header.
API keys are scoped to account, project, and region. Your PolyAI representative will confirm which key is configured for handoff retrieval.
## Identifiers
`account_id` above is your **account ID** — Agent Studio's UI calls this the **Workspace ID** and shows it prefixed (`ws-xxxxxxxx`). `project_id` is the same value as the **Agent ID** shown in Agent Studio (prefixed `PROJECT-xxxxxxxx`); "Project" is the legacy term for the same resource. Both the slug form from the Agent Studio URL and the prefixed form work in API calls.
## Typical flow
A downstream platform receives an inbound call that has just transitioned from the PolyAI agent. It queries the handoff state, then uses the data to populate the agent desktop, route the interaction, or enrich CRM entries.
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET \
"https://api..platform.polyai.app/v1/ws-xxxxxxxx/PROJECT-xxx/handoff_state?id=CONV-1234567890" \
-H "x-api-key: YOUR_API_KEY"
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import requests
response = requests.get(
"https://api..platform.polyai.app/v1/ws-xxxxxxxx/PROJECT-xxx/handoff_state",
headers={"x-api-key": "YOUR_API_KEY"},
params={"id": "CONV-1234567890"},
)
handoff = response.json()
print(handoff["data"]) # Free-form handoff metadata
```
```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const params = new URLSearchParams({ id: "CONV-1234567890" });
const res = await fetch(
`https://api..platform.polyai.app/v1/ws-xxxxxxxx/PROJECT-xxx/handoff_state?${params}`,
{ headers: { "x-api-key": "YOUR_API_KEY" } }
);
const handoff = await res.json();
console.log(handoff.data); // Free-form handoff metadata
```
The endpoint is read-only and returns exactly the state stored by the PolyAI agent at the moment of handoff.
# API reference
Source: https://docs.poly.ai/api-reference/introduction
Build, run, and observe PolyAI agents from your own systems. Three API families, one platform, 99.994% uptime in production.
PolyAI Platform · API Reference
# Build voice agents from code.
Three REST API families cover the full lifecycle — ship agents, run conversations, and observe everything in production. No SDK required.
Task-oriented entry points. Jump straight to a working example.
Full walkthrough: create → branch and configure → merge to Sandbox → test → promote to live.
Query transcripts, metrics, and audio for analytics and compliance pipelines.
Create agents, edit behavior and knowledge, promote sandbox → pre-release → live.
Register a webhook endpoint and receive signed POSTs on conversation and handoff events.
Drive a text-based conversation programmatically with create, respond, close.
Place a programmatic outbound call to a target number and track its status.
Pick your region
Your key is provisioned to a region. The host differs by API family — the platform.polyai.app host carries a -1 suffix, the poly.ai host doesn't.
Do not use `https://api.poly.ai` without a region prefix — it returns an error. Always include the region.
### Finding your `account_id` and `project_id`
Your `account_id` and `project_id` are the first two path segments of your Agent Studio URL:
```
https://studio..poly.ai///...
```
**Agent Studio is region-specific.** Each Studio host serves one region and is paired with the matching API host. Replace `` with the subdomain for your tenant:
| Studio URL | API host |
| --------------------------- | --------------------------------------------------------- |
| `https://studio.us.poly.ai` | `https://api.us-1.platform.polyai.app` |
| `https://studio.uk.poly.ai` | `https://api.uk-1.platform.polyai.app` |
| `https://studio.eu.poly.ai` | `https://api.euw-1.platform.polyai.app` |
| `https://studio.poly.ai` | `https://api.studio.poly.ai` (self-serve / Studio region) |
A workspace lives in exactly one region — use the Studio host you log in to, and the matching API host for calls.
For example, if your Studio URL is `https://studio.uk.poly.ai/acme-uk/acme-team-4/agent`, then `account_id` is `acme-uk` and `project_id` is `acme-team-4`.
Both the slug form (visible in the URL) and the prefixed form are accepted in API paths:
| Path parameter | Slug form (from URL) | Prefixed form |
| -------------- | -------------------- | ------------------ |
| `account_id` | `acme-uk` | `ws-xxxxxxxx` |
| `project_id` | `acme-team-4` | `PROJECT-xxxxxxxx` |
The `account_id` path parameter corresponds to the **workspace** in Agent Studio. Its prefixed form starts with `ws-` (for example `ws-fd112d8f`) — not `ACCOUNT-`. Older docs may still refer to the prefixed form as `ACCOUNT-xxxxxxx`; the value you see in Agent Studio (prefixed with `ws-`) is the correct one to use.
Authenticate
Every PolyAI API uses an API key sent in the x-api-key header. Create keys from the API Keys tab in Agent Studio (see API keys) — runtime and build keys are separate.
# Best practices
Source: https://docs.poly.ai/api-reference/messaging/best-practices
Recommendations for connection management, message handling, UX, and security.
## Connection management
* **Send heartbeats regularly.** Use the interval from `capabilities.heartbeat_interval_seconds` in the `SESSION_START` event (fall back to 30 seconds). Without heartbeats, the session times out after 10 minutes.
* **Implement reconnection with backoff.** Connections can drop due to network issues or server-side timeouts. Reconnect with exponential backoff (start at 1 second, cap at 30 seconds) using the same `session_id` and a `cursor` set to your last seen `sequence`.
* **Don't create a new session on reconnect.** Reuse the existing `session_id` to preserve the conversation. Only create a new session for a genuinely new conversation.
## Message handling
* **Deduplicate by `id`.** The same event may arrive more than once — during replay after a reconnect, or if replay and live delivery overlap. Use the event `id` to detect duplicates.
* **Order by `sequence`.** Events may arrive out of order during reconnection. Sort by `sequence` for display.
* **Flatten `EVENT_TYPE_EVENT_BATCH`.** On connect, the server sends history as batch events. Extract the `events` array into your local event list.
* **Process streaming chunks in order.** Accumulate chunks by `message_id` in `chunk_index` order. The message is complete when `is_complete` is `true`.
## User experience
* **Show typing indicators.** Display when you receive `POLY_AGENT_THINKING` or `LIVE_AGENT_TYPING`. Clear when the message arrives.
* **Show response suggestions as buttons.** When a message includes `response_suggestions`, display them as tappable buttons. When tapped, send the suggestion's `message_text` as a `USER_MESSAGE`.
* **Handle handoff gracefully.** Inform the user they're being connected to a human agent. Show queue status updates if available.
* **Handle session end.** When you receive `SESSION_END`, disable the message input and show an appropriate message based on the reason.
## Security
* **Treat the access token as sensitive.** Do not log it, store it in `localStorage`, or expose it to third-party scripts. It is passed as a WebSocket query parameter — ensure connections use `wss://` (TLS).
* **Tokens are short-lived.** Obtain a fresh token for each new session. Do not cache tokens across sessions.
* **Never embed your connector token in client-side code.** The connector token must be kept on a trusted backend that mints access tokens for your clients. Exposing it lets anyone create sessions against your project.
# Enable Verified Context Injection
Source: https://docs.poly.ai/api-reference/messaging/enable-verified-context-injection
Enable and configure verified context injection for your Messaging API sessions.
## Before you start
Three things must be true, and all three are on PolyAI's side.
| Precondition | How to check |
| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| A signing key is provisioned for the project | Talk to your PolyAI representative about getting a key provisioned. |
| The backend mints the context id in the session start event | Visible in the widget console on the session start payload |
| **The agent-side read path is enabled for the project** | Set per project in experimental configuration. **Off by default** — ask your PolyAI representative to enable it. |
The integrator needs: a **key ID** and a **secret** from PolyAI, a backend endpoint that can sign a JWT, and a few lines of JavaScript on the page.
## Step 1 - Register the callback
`onContextRequired` takes a handler receiving `{ sessionId, contextId }` and returning a signed JWT string, or a promise of one.
```jsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
WebchatAPI.onReady(function () {
WebchatAPI.onContextRequired(async function ({ sessionId, contextId }) {
const res = await fetch('/api/polyai/sign-context', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ context_id: contextId }),
});
const { token } = await res.json();
return token; // signed JWT, sub = contextId
});
});
```
The handler is read **at request time**, not snapshotted at init, so registering inside `onReady` is comfortably in time. The effective deadline is `SESSION_START`.
Return-value traps. Only a non-empty string is treated as a token. `undefined` , `null` , `''` , a number, or an object like `{ token: '...' }` are all treated as declines — you get a `console.war`n and the conversation proceeds without context.
Other behaviours worth knowing:
* **One handler slot, not a list.** Registering twice silently overwrites - last call wins, no warning. There is no way to unregister; `WebchatAPI.off()` does not apply here.
* **`destroy()` clears the handler.** Re-initialising the widget means re-registering.
* **`contextId` can be `undefined`.** The SDK reads it defensively off the payload. Signing `sub = undefined` will fail the vault's binding check - guard for it.
* **A synchronous throw is caught.** The handler is invoked inside a promise chain, so a raw `throw` becomes a decline rather than stranding the widget.
## Step 2 - Sign the token on your backend
HS256, signed with the project's key. Never in the browser.
```jsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
import jwt from 'jsonwebtoken';
app.post('/api/polyai/sign-context', (req, res) => {
const { context_id } = req.body;
const user = getAuthenticatedUser(req); // from YOUR session — never trust the client
const token = jwt.sign(
{
sub: context_id, // MUST equal the contextId the widget provided
ctx: { customer_id: user.id, account_tier: user.tier, is_premium: user.tier === 'premium' },
},
process.env.POLYAI_KEY_SECRET,
{ algorithm: 'HS256', keyid: process.env.POLYAI_KEY_ID, expiresIn: '30s' }
);
res.json({ token });
});
```
The secret is used as a UTF-8 string, not decoded from hex — pass it to your JWT library exactly as issued. If you decode it to bytes first `(Buffer.from(secret, 'hex')` or equivalent), every token you mint will fail signature verification with an opaque 401.
### Token contract
| Field | Where | Rule |
| ------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `alg` | header | Must be `HS256` |
| `kid` | header | Your key ID. Stable across rotation. |
| `sub` | payload | Must exactly equal the `contextId` passed to your callback |
| `iat` / `exp` | payload | `exp − iat` must be **≤ 30s**. Watch for server clock skew. |
| `ctx` | payload | **Flat** object, primitive values only (string / number / boolean / null), **≤ 8 KB**. A nested object or array anywhere in `ctx` is rejected — the most common integration mistake. The 8 KB is measured on the re-serialized decoded claim, not your original JSON. Whole token capped separately at 16 KB. |
The token is **signed, not encrypted** - `ctx` is readable by anyone who sees the token. It proves origin, not confidentiality.
## Step 3 - Refresh mid-conversation
```jsx theme={"theme":{"light":"github-light","dark":"github-dark"}}
WebchatAPI.refreshVerifiedContext(); // e.g. from your login success handler
```
Re-runs the **same** callback against the live conversation and submits a fresh token. The vault set is idempotent - full replacement, last write wins - and the conversation is not interrupted.
**It returns** `undefined`**, not a promise.** Fire-and-forget, with no way to await it or learn the outcome. It silently no-ops in three separate places: no handler registered, no session or no `context_id` yet, or a context request already in flight. If your handler isn't invoked, you cannot tell which of the three happened from the host page.
## The timing budget
Three timeouts interact, and the tightest one is the one that matters.
| Limit | Value | What it governs |
| ----------------------- | ------ | ------------------------------------------------------------ |
| SDK cap on your handler | **4s** | Your callback, end to end, including your backend round-trip |
| Widget backstop | 5s | Covers the SDK never replying at all |
| Token lifetime | 30s | `exp − iat` ceiling enforced by the vault |
**A signing backend slower than 4 seconds always declines**, no matter how long the token's TTL is. Budget your endpoint against the 4s cap, not the 30s TTL.
## Failure behaviour
Verified context is best-effort and fails open. The conversation **always** starts.
| Situation | User experience | Context attached? |
| ------------------------------------------------------------- | ------------------------------- | ----------------- |
| Valid token, vault accepts | Normal conversation | Yes |
| No handler registered | Normal conversation, zero delay | No |
| Handler returns nothing / non-string / throws | Normal conversation | No |
| Handler exceeds 4s | Proceeds after the timeout | No |
| Token rejected (signature, `sub` mismatch, expired, oversize) | Normal conversation | No |
**No success or failure signal ever reaches the host page.** There is no `onContextSet` / `onContextFailedcallback` — the full context-related public surface is `onContextRequired` and `refreshVerifiedContext`. If the vault rejects your token, the widget logs it, resolves internally as `failed`, and the agent joins without context. Your page is not told. Budget for widget-console and network-tab debugging, and design the agent to degrade gracefully.
## Related pages
What Verified Context Injection is, when to use it, and how it works end to end.
# Errors
Source: https://docs.poly.ai/api-reference/messaging/errors
HTTP status codes and system-message error catalog.
## HTTP errors
| Status | Meaning |
| ------ | --------------------------------------------------------------------------------- |
| `400` | Bad request — missing required headers or malformed body |
| `401` | Unauthorized — invalid or expired access token, or connector token not recognised |
| `404` | Session not found |
| `429` | Rate limit exceeded — check the `Retry-After` header for when to retry |
| `500` | Server error — retry with exponential backoff |
## WebSocket errors
If you send an invalid event (wrong type, malformed JSON, or a server-only event type), the server responds with `EVENT_TYPE_SYSTEM_MESSAGE` at level `SYSTEM_MESSAGE_LEVEL_ERROR`. The message describes what went wrong. Your WebSocket connection remains open — fix the issue and continue.
## Rate limiting
Both HTTP endpoints and WebSocket messages are rate limited per session.
* **HTTP:** returns `429 Too Many Requests` with a `Retry-After` header
* **WebSocket:** messages may be dropped
Under normal usage you should not hit rate limits.
## Access token errors
Returned from `POST /api/v1/access-token`.
| Error | HTTP Status | Cause | Resolution |
| -------------------------------- | ----------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Missing authentication headers` | 401 | `X-Token` or `X-Host` header missing | Include both headers. |
| `Connector token does not exist` | 401 | `X-Token` is not a valid connector token | Use a connector token provisioned by PolyAI (the self-serve **Messaging → API Configuration** page in Agent Studio is not fully built out yet). |
| `Failed to validate connector` | 401 | `X-Host` doesn't match the registered domain | Ensure `X-Host` matches the domain or app namespace you registered. Subdomains match — `app.yourcompany.com` matches `yourcompany.com`. |
| `Connector missing host domain` | 401 | The connector has no host domain configured | Contact your PolyAI representative to register a host domain against the connector token (self-serve configuration in Agent Studio is not fully built out yet). |
| `Failed to create access token` | 500 | Internal error generating the JWT | Retry. If persistent, contact PolyAI. |
## Session creation errors
Returned from `POST /api/v1/sessions`.
| Error | HTTP Status | Cause | Resolution |
| -------------------------------- | ----------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `Missing access token` | 400 | No `Authorization` header | Include `Authorization: Bearer `. |
| `Missing authentication headers` | 401 | `Authorization` header is not in `Bearer ` format | Use the format `Authorization: Bearer `. |
| `Invalid access token` | 401 | Access token is malformed, expired, or tampered with | Obtain a fresh access token. |
| `Error parsing request` | 400 | Request body contains invalid JSON | Send valid JSON (e.g. `{}` or `{"streaming_enabled": true}`). The body is optional — omit it entirely if you don't need streaming. |
| `Failed to create session` | 500 | Internal error | Retry. If persistent, contact PolyAI. |
## WebSocket connection errors
Returned as HTTP errors during the WebSocket handshake, not as WebSocket events.
| Error | HTTP Status | Cause | Resolution |
| --------------------------------------- | ----------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `session not found` | 404 | The `session_id` doesn't exist or has expired | Create a new session. Sessions expire after \~10 minutes of inactivity. |
| `authentication failed` | 401 | The `access_token` is invalid, expired, or doesn't match the session | Obtain a fresh access token and create a new session. |
| `connection limit exceeded for session` | 429 | Too many simultaneous WebSocket connections (default 10) | Close unused connections before opening new ones. |
| `invalid connection parameters` | 400 | Missing `session_id` or `access_token` | Include both query parameters in the WebSocket URL. |
An invalid `cursor` value (for example, a non-numeric string) does not reject the connection. The server logs a warning and defaults to `0`, replaying the full conversation history.
## Invalid event errors
These arrive as `EVENT_TYPE_SYSTEM_MESSAGE` events. The connection remains open.
| Error message | Cause | Resolution |
| ---------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `Unknown event type or invalid message format` | JSON could not be parsed, or event type is not recognised | Check that your JSON is well-formed and `type` is a valid `EVENT_TYPE_*` value. |
| `Event type not allowed: EVENT_TYPE_*` | You sent an event type that only the server can send | Only send the 5 allowed [client events](/api-reference/messaging/events-send). |
## Agent errors
These arrive as `EVENT_TYPE_SYSTEM_MESSAGE` during the conversation. They indicate a server-side issue — your client did nothing wrong.
| Error message | Cause | What to do |
| ------------------------------------------------------------------------------ | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `Our system is experiencing high load. Please try again in a moment.` | Agent service is temporarily overloaded | Display the message. Retry after a few seconds. |
| `Sorry, I'm unable to start a conversation right now. Please try again later.` | Agent failed to start a conversation | Display the message. The user can try `REQUEST_POLY_AGENT_JOIN` again or start a new session. |
| `Sorry, I'm having trouble responding right now. Please try again.` | Agent failed to generate a response | Display the message. The user can re-send their message. |
| `Sorry, I encountered an error. Please try again.` | Error during streaming (stream interrupted or invalid data) | Display the message. The user can re-send. |
## Session state errors
These usually mean the session has expired or the server has lost track of it.
| Error message | Cause | What to do |
| --------------------------------------------- | ------------------------------------------------- | --------------------------------------------------------------------- |
| `Error: Session not found` | The session no longer exists | Start a new session. |
| `Error: Unable to retrieve session` | Internal lookup error | Transient — retry. If persistent, start a new session. |
| `Error: Connector not found` | The connector is no longer valid | Contact PolyAI — indicates a configuration issue. |
| `Error: Conversation not found` | The session exists but has no active conversation | Send `REQUEST_POLY_AGENT_JOIN`. |
| `Error: Conversation ID not found in session` | The conversation was not properly initialised | Send `REQUEST_POLY_AGENT_JOIN`. If it persists, create a new session. |
## Handoff errors
| Error message | Cause | What to do |
| ---------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `Sorry, we could not connect you to an agent. Please try again.` | Handoff provider could not create a live agent session | Display the message. The user may try again or use an alternative channel. |
| `Failed to forward message to live agent` | A user message could not be delivered during an active handoff | Display the message. The user can re-send. If persistent, the handoff may have silently failed. |
| `Sorry, handoff could not be initiated.` | Handoff configuration is invalid or the system rejected the request | Display the message. Usually a configuration issue — contact PolyAI if it persists. |
## General debugging tips
* **Check `metadata.custom`.** Custom metadata you sent is echoed back in error messages too — useful for correlating errors with specific user actions.
* **Watch for `SESSION_END` after errors.** Some errors are followed by a `SESSION_END` event. Always handle session end gracefully.
* **"High load" errors are transient.** They self-resolve — implement a brief retry (2–5 seconds) before showing a permanent error.
* **Connection dropped ≠ session ended.** Reconnect with the same `session_id` and a `cursor` to resume.
* **Log the full event.** When reporting issues to PolyAI, include the complete JSON of the error event (including `id`, `timestamp`, and `metadata`).
# Event format
Source: https://docs.poly.ai/api-reference/messaging/events
The common JSON envelope used for every WebSocket event.
All communication over the WebSocket uses JSON events with a common structure.
## Sending events (client → server)
When sending events, include only `type` and `payload`. The server assigns `id`, `sequence`, and `timestamp` — do not include them.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "EVENT_TYPE_USER_MESSAGE",
"payload": {
"text": "Hello, I need some help."
}
}
```
You can optionally include a `metadata` field with custom key-value pairs. The server echoes these back, useful for correlating sent messages with their echoes:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "EVENT_TYPE_USER_MESSAGE",
"payload": {
"text": "Hello, I need some help."
},
"metadata": {
"custom": {
"local_id": "draft_abc123"
}
}
}
```
## Receiving events (server → client)
Events from the server include additional fields:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"id": "018d91c7-7b12-7474-9a84-7e8c0e9e4f1a",
"sequence": 5,
"timestamp": "2024-02-12T12:00:00Z",
"type": "EVENT_TYPE_POLY_AGENT_MESSAGE",
"payload": {
"message_id": "msg_abc123",
"text": "Hello! How can I help you today?",
"attachments": [],
"response_suggestions": [
{ "message_text": "I have a question about my order" },
{ "message_text": "I need to make a booking" }
]
}
}
```
| Field | Type | Description |
| ----------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | string | Unique identifier for this event (UUID). Use to deduplicate. |
| `sequence` | integer or null | Monotonically increases with each event in the session. Use to order events. `null` for transient events (e.g. typing indicators) that are not part of the permanent history. |
| `timestamp` | string | When the server processed this event (ISO 8601). |
| `type` | string | The event type. |
| `payload` | object | Event-specific data. |
| `metadata` | object | Present when `custom` data was included. Contains your key-value pairs echoed back. |
## Echo behavior
When you send certain events, the server echoes them back with the server-assigned `id`, `sequence`, and `timestamp` added. This confirms the server received and processed your event. The following events are echoed:
* `EVENT_TYPE_USER_MESSAGE` — the echo includes a server-assigned `message_id` in the payload
* `EVENT_TYPE_USER_END_SESSION`
* `EVENT_TYPE_HEARTBEAT`
* `EVENT_TYPE_REQUEST_POLY_AGENT_JOIN`
Since your client receives both its own echoed events and server-originated events on the same WebSocket, use the `id` field to deduplicate.
## Using echoes as delivery receipts
Echoes act as server-side acknowledgements — when you receive the echo of a message you sent, the server has received and processed it. Use this to build delivery confirmation and retry logic.
Add a unique `client_event_id` to `metadata.custom` when sending. The server echoes this value back, letting you match the echo to the original message:
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
// Track pending messages
const pending = new Map();
function sendMessage(text) {
const clientEventId = crypto.randomUUID();
const event = {
type: "EVENT_TYPE_USER_MESSAGE",
payload: { text },
metadata: { custom: { client_event_id: clientEventId } },
};
pending.set(clientEventId, { event, sentAt: Date.now(), retries: 0 });
ws.send(JSON.stringify(event));
// Retry if no echo arrives in 5 seconds
setTimeout(() => retryIfPending(clientEventId), 5000);
}
function handleEvent(msg) {
// Check if this is an echo of something we sent
const clientEventId = msg.metadata?.custom?.client_event_id;
if (clientEventId && pending.has(clientEventId)) {
pending.delete(clientEventId);
updateMessageStatus(clientEventId, "sent");
}
}
function retryIfPending(clientEventId) {
const entry = pending.get(clientEventId);
if (!entry) return;
if (entry.retries < 3) {
entry.retries++;
ws.send(JSON.stringify(entry.event));
setTimeout(() => retryIfPending(clientEventId), 5000 * entry.retries);
} else {
pending.delete(clientEventId);
updateMessageStatus(clientEventId, "failed");
}
}
```
This pattern gives you optimistic delivery tracking (show "sending..." → "sent" → "failed") without additional server-side support.
# Server events
Source: https://docs.poly.ai/api-reference/messaging/events-receive
Events your client receives over the WebSocket.
The server sends these events to your client. Group them into four categories: PolyAI agent, live agent, handoff, and system.
## PolyAI agent events
### `EVENT_TYPE_POLY_AGENT_JOINED`
The PolyAI agent has joined the session. Sent in response to your `REQUEST_POLY_AGENT_JOIN`.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"payload": {
"agent_name": "Ada",
"agent_avatar_url": "https://example.com/avatars/ada.png"
}
}
```
| Field | Type | Description |
| ------------------ | ------ | ----------------------------------------------- |
| `agent_name` | string | The agent's display name — show this in your UI |
| `agent_avatar_url` | string | URL of the agent's avatar image |
### `EVENT_TYPE_POLY_AGENT_THINKING`
The agent is composing a response. Show a typing indicator in your UI.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{ "payload": {} }
```
### `EVENT_TYPE_POLY_AGENT_MESSAGE`
A complete message from the agent. Sent when `streaming_enabled` is `false`.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"payload": {
"message_id": "msg_abc123",
"text": "I'd be happy to help you book a table! What restaurant are you interested in?",
"attachments": [
{
"title": "Our locations",
"preview_image_url": "https://example.com/thumb.png",
"content_url": "https://example.com/locations",
"content_type": "ATTACHMENT_CONTENT_TYPE_URL",
"call_to_action_text": "View locations"
}
],
"response_suggestions": [
{ "message_text": "The Italian place on Main Street" },
{ "message_text": "Show me all available restaurants" }
]
}
}
```
| Field | Type | Description |
| ---------------------- | ------ | ----------------------------------------------------------------------------------------- |
| `message_id` | string | Unique identifier for this message |
| `text` | string | The message text |
| `attachments` | array | Rich content cards. See [Attachments](#attachments). |
| `response_suggestions` | array | Quick-reply options. When the user taps one, send its `message_text` as a `USER_MESSAGE`. |
If `streaming_enabled` is `true`, you receive `EVENT_TYPE_POLY_AGENT_MESSAGE_CHUNK` events instead. See [Streaming](/api-reference/messaging/streaming).
### `EVENT_TYPE_POLY_AGENT_LEFT`
The agent has left the session (typically before a handoff to a live agent).
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{ "payload": {} }
```
### `EVENT_TYPE_POLY_AGENT_TRIGGERED_HANDOFF`
The agent has determined a human should take over. Handoff events follow — see [Handoff](/api-reference/messaging/handoff).
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{ "payload": {} }
```
## Live agent events
When a conversation is handed off to a human, you receive these events:
### `EVENT_TYPE_LIVE_AGENT_JOINED`
A human agent has connected to the conversation.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"payload": {
"agent": {
"id": "agent_42",
"name": "Sarah",
"avatar_url": "https://example.com/avatars/sarah.png"
}
}
}
```
### `EVENT_TYPE_LIVE_AGENT_TYPING`
The human agent is typing.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"payload": {
"state": "TYPING_STATE_STARTED",
"agent_id": "agent_42"
}
}
```
### `EVENT_TYPE_LIVE_AGENT_MESSAGE`
A message from the human agent. Same structure as agent messages, plus an `agent_id`.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"payload": {
"message_id": "msg_live_789",
"agent_id": "agent_42",
"text": "Hi! I can see your booking request. Let me check availability.",
"attachments": [],
"response_suggestions": []
}
}
```
### `EVENT_TYPE_LIVE_AGENT_LEFT`
The human agent has left the session.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{ "payload": {} }
```
## Handoff events
These events track the transition from the PolyAI agent to a human agent. See [Handoff](/api-reference/messaging/handoff) for the full flow.
### `EVENT_TYPE_HANDOFF_ACCEPTED`
The live agent system accepted the handoff. The user may be placed in a queue.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{ "payload": {} }
```
### `EVENT_TYPE_HANDOFF_QUEUE_STATUS`
Periodic updates while the user is waiting. Show in your UI (e.g. "You are #3 in the queue").
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"payload": {
"queue_name": "General Support",
"position_in_queue": 3,
"estimated_wait_seconds": 180
}
}
```
All fields are optional — not all live agent systems provide queue position data.
### `EVENT_TYPE_CLIENT_HANDOFF_REQUIRED`
The server cannot handle the handoff automatically and needs your client to take action (e.g. redirect to a different channel or open a third-party widget).
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"payload": {
"reason": "COMPLEX_QUERY",
"queue_name": "Support Queue"
}
}
```
| Field | Type | Description |
| ------------ | ------ | ---------------------------------------------- |
| `reason` | string | `COMPLEX_QUERY`, `AGENT_DECISION`, or `POLICY` |
| `queue_name` | string | Suggested routing destination |
### `EVENT_TYPE_HANDOFF_FAILED`
The handoff could not be completed (e.g. the live agent system is unavailable).
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{ "payload": {} }
```
### `EVENT_TYPE_HANDOFF_TIMEOUT`
The user waited too long in the queue without being connected.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{ "payload": {} }
```
## System events
### `EVENT_TYPE_SESSION_START`
Sent as part of the history replay when the WebSocket connects. Describes session capabilities.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"payload": {
"capabilities": {
"streaming": true,
"max_message_size_bytes": 131072,
"max_reconnect_attempts": 5,
"heartbeat_interval_seconds": 30
}
}
}
```
| Field | Type | Description |
| ---------------------------- | ------- | -------------------------------------------- |
| `streaming` | boolean | Whether agent responses use streaming chunks |
| `max_message_size_bytes` | integer | Maximum size of a single WebSocket message |
| `max_reconnect_attempts` | integer | Suggested number of reconnection attempts |
| `heartbeat_interval_seconds` | integer | How often (in seconds) to send heartbeats |
### `EVENT_TYPE_SESSION_END`
The session has ended. After this, no further events will be sent. Close the WebSocket.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"payload": {
"reason": "REASON_USER_END"
}
}
```
| Reason | Meaning |
| ----------------------- | ------------------------------------------------------------------- |
| `REASON_USER_END` | The user explicitly ended the session |
| `REASON_USER_ABANDONED` | The session timed out — no messages received (including heartbeats) |
| `REASON_NATURAL_END` | The conversation reached a natural conclusion |
### `EVENT_TYPE_SYSTEM_MESSAGE`
A system-level notification — usually an error in response to an invalid event you sent. See [Errors](/api-reference/messaging/errors) for the full catalog.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"payload": {
"level": "SYSTEM_MESSAGE_LEVEL_ERROR",
"message": "Event type not allowed: EVENT_TYPE_POLY_AGENT_MESSAGE"
}
}
```
| Level | When you'll see it |
| ------------------------------ | ----------------------------------------- |
| `SYSTEM_MESSAGE_LEVEL_INFO` | Informational notices |
| `SYSTEM_MESSAGE_LEVEL_WARNING` | Non-critical issues |
| `SYSTEM_MESSAGE_LEVEL_ERROR` | Typically a malformed or disallowed event |
### `EVENT_TYPE_LANGUAGE_CHANGED`
The PolyAI agent has detected the conversation has switched language. Use this to update your UI locale (for example, swap response suggestion translations, adjust input direction, or relabel controls).
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"payload": {
"language_code": "fr-FR"
}
}
```
| Field | Type | Description |
| --------------- | ------ | ---------------------------------------------------------------------------------------------------------------------- |
| `language_code` | string | The new conversation language as a [BCP-47](https://www.rfc-editor.org/info/bcp47) tag (for example `en-US`, `fr-FR`). |
The event is journaled and replayed on reconnect, so your client may receive it more than once. It is idempotent — it carries the absolute language code, not a delta — so just apply the latest value you see.
### `EVENT_TYPE_EVENT_BATCH`
A batch of historical events, sent during connection replay. Contains an array of events in chronological order.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"payload": {
"events": [
{ "id": "...", "sequence": 1, "type": "EVENT_TYPE_SESSION_START", "payload": {} },
{ "id": "...", "sequence": 2, "type": "EVENT_TYPE_POLY_AGENT_MESSAGE", "payload": {} }
]
}
}
```
Flatten the `events` array into your local event list, using `sequence` for ordering and `id` for deduplication.
## Attachments
Attachments are rich content items in agent or live agent messages — typically links or images.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"title": "Our Locations",
"preview_image_url": "https://example.com/thumb.png",
"content_url": "https://example.com/locations",
"content_type": "ATTACHMENT_CONTENT_TYPE_URL",
"call_to_action_text": "View locations"
}
```
| Field | Type | Description |
| --------------------- | ------ | ------------------------------------------------------------------------------- |
| `title` | string | Display title |
| `preview_image_url` | string | URL of a thumbnail or preview image |
| `content_url` | string | URL of the full content (the link the user opens) |
| `content_type` | string | `ATTACHMENT_CONTENT_TYPE_URL` (link) or `ATTACHMENT_CONTENT_TYPE_IMAGE` (image) |
| `call_to_action_text` | string | Button label (e.g. "View locations", "Open image") |
# Client events
Source: https://docs.poly.ai/api-reference/messaging/events-send
Events your client can send over the WebSocket.
Your client can send the following five event types. Any other type is rejected with an `EVENT_TYPE_SYSTEM_MESSAGE` error.
## `EVENT_TYPE_REQUEST_POLY_AGENT_JOIN`
Requests the PolyAI agent to join the session. **You must send this to start the conversation.**
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "EVENT_TYPE_REQUEST_POLY_AGENT_JOIN",
"payload": {}
}
```
After sending this, you receive (in order):
1. An echo of your event
2. `EVENT_TYPE_POLY_AGENT_JOINED` — the agent has joined
3. `EVENT_TYPE_POLY_AGENT_THINKING` — the agent is preparing its greeting
4. `EVENT_TYPE_POLY_AGENT_MESSAGE` (or chunks, if streaming) — the greeting message
## `EVENT_TYPE_USER_MESSAGE`
Send a text message from the user.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "EVENT_TYPE_USER_MESSAGE",
"payload": {
"text": "I'd like to book a table for tomorrow at 7pm."
}
}
```
| Field | Type | Required | Description |
| ------ | ------ | -------- | ---------------- |
| `text` | string | Yes | The message text |
The server echoes this back with a server-assigned `message_id` added to the payload.
## `EVENT_TYPE_USER_TYPING`
Notify the server that the user is typing (or has stopped). Use this to show typing indicators on the live agent side.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "EVENT_TYPE_USER_TYPING",
"payload": {
"state": "TYPING_STATE_STARTED"
}
}
```
| Field | Type | Values |
| ------- | ------ | ------------------------------------------------ |
| `state` | string | `TYPING_STATE_STARTED` or `TYPING_STATE_STOPPED` |
## `EVENT_TYPE_USER_END_SESSION`
Send when the user deliberately ends the conversation (e.g. clicks a "Leave" button).
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "EVENT_TYPE_USER_END_SESSION",
"payload": {}
}
```
This triggers a clean shutdown: the server echoes the event, then sends `EVENT_TYPE_SESSION_END` with reason `REASON_USER_END`.
## `EVENT_TYPE_HEARTBEAT`
Keeps the WebSocket alive. Send at regular intervals (typically every 30 seconds). The server echoes it back.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "EVENT_TYPE_HEARTBEAT",
"payload": {}
}
```
See [WebSocket connection → Keeping the connection alive](/api-reference/messaging/websocket#keeping-the-connection-alive) for details.
# Handoff to live agent
Source: https://docs.poly.ai/api-reference/messaging/handoff
Transfer the conversation from the PolyAI agent to a human agent.
A handoff transfers the conversation from the PolyAI agent to a human agent. This happens automatically when the PolyAI agent determines it cannot handle the user's request.
There are two handoff modes: **server-managed** (PolyAI handles routing internally) and **client-managed** (your app handles routing).
## Server-managed handoff
In the typical flow, PolyAI manages the handoff internally:
```
← POLY_AGENT_TRIGGERED_HANDOFF The agent wants to hand off
← POLY_AGENT_LEFT The agent leaves the session
← HANDOFF_ACCEPTED The live agent system accepted the request
← HANDOFF_QUEUE_STATUS (periodic) User is #3 in the queue
← LIVE_AGENT_JOINED A human agent has connected
... user and human agent chat ...
← LIVE_AGENT_LEFT The human agent leaves
← SESSION_END Session is over
```
During the handoff, the user continues to send `EVENT_TYPE_USER_MESSAGE` events — the messages are routed to the live agent instead of the PolyAI agent.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant Client as Your App
participant PolyAI as PolyAI Agent
participant Live as Live Agent
note over Client,PolyAI: Conversation with PolyAI agent
Client->>PolyAI: USER_MESSAGE
PolyAI-->>Client: POLY_AGENT_MESSAGE
rect rgb(255, 248, 235)
note over Client,Live: Handoff
PolyAI-->>Client: POLY_AGENT_TRIGGERED_HANDOFF
PolyAI-->>Client: POLY_AGENT_LEFT
PolyAI-->>Client: HANDOFF_ACCEPTED
PolyAI-->>Client: HANDOFF_QUEUE_STATUS ("You are #2 in queue")
Live-->>Client: LIVE_AGENT_JOINED ("Sarah has joined")
end
rect rgb(235, 248, 255)
note over Client,Live: Conversation with Live Agent
Client->>Live: USER_MESSAGE
Live-->>Client: echo (USER_MESSAGE + message_id)
Live-->>Client: LIVE_AGENT_TYPING
Live-->>Client: LIVE_AGENT_MESSAGE
end
rect rgb(250, 240, 240)
note over Client,Live: Session End
Live-->>Client: LIVE_AGENT_LEFT
Live-->>Client: SESSION_END (REASON_NATURAL_END)
end
```
## Client-managed handoff
In some configurations, the server cannot handle the handoff and asks your client to do it:
```
← POLY_AGENT_TRIGGERED_HANDOFF The agent wants to hand off
← CLIENT_HANDOFF_REQUIRED Your client needs to handle this
← POLY_AGENT_LEFT The agent leaves the session
```
When you receive `CLIENT_HANDOFF_REQUIRED`, redirect the user to the appropriate support channel (e.g. open a Zendesk widget, redirect to a phone number). The `reason` and `queue_name` fields give context for routing.
| Field | Description |
| ------------ | ---------------------------------------------- |
| `reason` | `COMPLEX_QUERY`, `AGENT_DECISION`, or `POLICY` |
| `queue_name` | Suggested routing destination |
## Failed handoffs
| Event | Meaning | What to do |
| ---------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------- |
| `EVENT_TYPE_HANDOFF_FAILED` | The live agent system rejected or could not complete the handoff | Show an error and let the user try again or end the session |
| `EVENT_TYPE_HANDOFF_TIMEOUT` | The user waited too long in the queue | Show a timeout message and offer alternative support channels |
See [Server events → Handoff events](/api-reference/messaging/events-receive#handoff-events) for the full event payloads.
## Conversation flow (full session)
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant Client as Your App
participant Server as PolyAI
rect rgb(240, 245, 250)
note over Client,Server: Authentication & Session Setup
Client->>Server: POST /api/v1/access-token
Server-->>Client: { access_token, expires_in }
Client->>Server: POST /api/v1/sessions
Server-->>Client: { session_id }
end
rect rgb(240, 250, 240)
note over Client,Server: WebSocket Connection
Client->>Server: WebSocket connect (session_id, access_token)
Server-->>Client: EVENT_BATCH (history incl. SESSION_START)
end
rect rgb(250, 245, 240)
note over Client,Server: Agent Join
Client->>Server: REQUEST_POLY_AGENT_JOIN
Server-->>Client: echo (REQUEST_POLY_AGENT_JOIN)
Server-->>Client: POLY_AGENT_JOINED (agent_name, avatar)
Server-->>Client: POLY_AGENT_THINKING
Server-->>Client: POLY_AGENT_MESSAGE (greeting)
end
rect rgb(245, 240, 250)
note over Client,Server: Conversation (repeats)
Client->>Server: USER_TYPING (started)
Client->>Server: USER_TYPING (stopped)
Client->>Server: USER_MESSAGE
Server-->>Client: echo (USER_MESSAGE + message_id)
Server-->>Client: POLY_AGENT_THINKING
Server-->>Client: POLY_AGENT_MESSAGE (response)
end
rect rgb(250, 240, 240)
note over Client,Server: Session End
Client->>Server: USER_END_SESSION
Server-->>Client: echo (USER_END_SESSION)
Server-->>Client: SESSION_END (REASON_USER_END)
end
```
# Messaging API
Source: https://docs.poly.ai/api-reference/messaging/introduction
Embed real-time text conversations into your application using WebSockets, with handoff to live agents.
The Messaging API lets you embed real-time text conversations into your application. End users chat with a PolyAI agent, and the conversation can be handed off to a live human agent inside the same session when needed.
Communication happens over a WebSocket connection after a short HTTP handshake. All messages are JSON-encoded events.
The Messaging API is a **real-time, event-driven** protocol. If you only need simple turn-based request/response (for example for SMS), use the [Chat API](/api-reference/chat/introduction) instead. See [Messaging API vs Chat API](#messaging-api-vs-chat-api) below.
## Base URL
```
https://messaging.poly.ai
```
For cluster-specific deployments, the pattern is `https://messaging..poly.ai`:
| Cluster | Base URL | Description |
| ------- | --------------------------------- | ----------- |
| `us-1` | `https://messaging.us-1.poly.ai` | US Mainland |
| `uk-1` | `https://messaging.uk-1.poly.ai` | UK |
| `euw-1` | `https://messaging.euw-1.poly.ai` | EU West |
Your PolyAI contact will confirm which base URL to use.
## Prerequisites
Before integrating, you need two values:
| Value | What it is | Where to find it |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| **Connector token** | A secret token that identifies your integration | Provisioned by PolyAI — contact your PolyAI representative to request one |
| **Host identifier** | The domain (e.g. `https://www.yourcompany.com`) or app namespace (e.g. `com.yourcompany.app`) registered against the connector token | Set when the token is provisioned. The server rejects requests where `X-Host` doesn't match. |
The self-serve **Messaging → API Configuration** page in Agent Studio is not fully built out yet. For now, your PolyAI representative will provision a connector token and register the host identifier on your behalf.
If you already have a PolyAI webchat widget configured, you can reuse its credentials:
* **Connector token**: the value between the last `/` and `.js` in the widget embed script URL.
* **Host identifier**: the same domain you set in the widget configuration.
Subdomains are matched against the registered host — for example, a connector registered to `yourcompany.com` accepts requests from `app.yourcompany.com`.
## Getting started
Every conversation follows this sequence:
`POST /api/v1/access-token` with your connector token. Returns a short-lived JWT.
`POST /api/v1/sessions` with the access token. Returns a `session_id`.
`wss:///ws?access_token=...&session_id=...`
Send `EVENT_TYPE_REQUEST_POLY_AGENT_JOIN` to start the conversation.
Send and receive events over the WebSocket.
The PolyAI agent does not join the session automatically. On a **new session**, after opening the WebSocket and receiving the session history, your client must explicitly send `EVENT_TYPE_REQUEST_POLY_AGENT_JOIN` to start the conversation. On a **reconnect** to an existing session, the agent has already joined — check the replayed history for `EVENT_TYPE_POLY_AGENT_JOINED` and skip the join request if present.
### Minimal JavaScript example
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
// 1. Get an access token
const tokenRes = await fetch("https://messaging.us-1.poly.ai/api/v1/access-token", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Token": "",
"X-Host": "https://www.yourcompany.com",
},
body: "{}",
});
const { access_token } = await tokenRes.json();
// 2. Create a session
const sessionRes = await fetch("https://messaging.us-1.poly.ai/api/v1/sessions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${access_token}`,
},
body: JSON.stringify({ streaming_enabled: false }),
});
const { session_id } = await sessionRes.json();
// 3. Open the WebSocket
const ws = new WebSocket(
`wss://messaging.us-1.poly.ai/ws?session_id=${session_id}&access_token=${access_token}`
);
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
console.log(msg.type, msg.payload);
};
ws.onopen = () => {
// Wait for EVENT_TYPE_EVENT_BATCH (history replay), then:
// 4. Request the agent to join
ws.send(JSON.stringify({
type: "EVENT_TYPE_REQUEST_POLY_AGENT_JOIN",
payload: {},
}));
};
```
## Messaging API vs Chat API
PolyAI offers two APIs for non-voice channels. Pick the one that matches your integration model.
| | [Messaging API](/api-reference/messaging/introduction) | [Chat API](/api-reference/chat/introduction) |
| --------------------------------- | ------------------------------------------------------ | -------------------------------------------- |
| **Protocol** | WebSocket (real-time, event-driven) | REST (turn-based request/response) |
| **Streaming responses** | Yes (incremental chunks) | No |
| **Typing indicators** | Yes | No |
| **Live-agent handoff** | Yes (full event flow over the WebSocket) | Yes (via `handoff` in the respond response) |
| **Reconnection / history replay** | Built-in (`cursor` query param) | Client manages turns |
| **Auth** | Connector token + host domain → short-lived JWT | API key + connector token |
| **Best for** | Web apps, iOS/Android apps, custom SDKs | SMS, server-to-server, simple webchat |
## Reference
Obtain an access token and create a session
Connect, reconnect, and keep the connection alive
Common envelope, echo behavior, delivery receipts
Events your client can send
Events your client receives
Receive agent responses as incremental chunks
Server-managed and client-managed handoff flows
Every event in order, from connect to session end
HTTP codes and system-message error catalog
Connection management, dedup, ordering, UX, security
# Session lifecycle
Source: https://docs.poly.ai/api-reference/messaging/lifecycle
Every event in order, from connection through session end.
This page lists every event in order for a complete session — what your client sends and what it receives.
## Connection and agent join
| Step | Direction | Event | What to do |
| ---- | --------- | ------------------------------------ | -------------------------------------------------------------------------------------------------- |
| 1 | ← Server | `EVENT_TYPE_EVENT_BATCH` | History replay. Flatten the `events` array. Contains at least `SESSION_START`. |
| 2 | | ↳ `EVENT_TYPE_SESSION_START` | Read `capabilities` (streaming, heartbeat interval). |
| 3 | Client → | `EVENT_TYPE_REQUEST_POLY_AGENT_JOIN` | Send to start the conversation. **New sessions only** — skip on reconnect if agent already joined. |
| 4 | ← Server | `EVENT_TYPE_REQUEST_POLY_AGENT_JOIN` | Echo of your join request. |
| 5 | ← Server | `EVENT_TYPE_POLY_AGENT_JOINED` | Agent has joined. Read `agent_name` and `agent_avatar_url` for your UI. |
| 6 | ← Server | `EVENT_TYPE_POLY_AGENT_THINKING` | Show a typing indicator. |
| 7 | ← Server | `EVENT_TYPE_POLY_AGENT_MESSAGE` | The agent's greeting. (If streaming: `POLY_AGENT_MESSAGE_CHUNK` events instead.) |
## Conversation turn (repeats)
| Step | Direction | Event | What to do |
| ---- | --------- | ------------------------------------ | -------------------------------------------------------------------------------- |
| 8 | Client → | `EVENT_TYPE_USER_TYPING` (`STARTED`) | Optional. Send when the user begins typing. |
| 9 | Client → | `EVENT_TYPE_USER_TYPING` (`STOPPED`) | Optional. Send when the user stops typing. |
| 10 | Client → | `EVENT_TYPE_USER_MESSAGE` | Send the user's message. |
| 11 | ← Server | `EVENT_TYPE_USER_MESSAGE` | Echo with server-assigned `message_id`. Confirms receipt. |
| 12 | ← Server | `EVENT_TYPE_POLY_AGENT_THINKING` | Show typing indicator. |
| 13 | ← Server | `EVENT_TYPE_POLY_AGENT_MESSAGE` | The agent's response. (If streaming: `POLY_AGENT_MESSAGE_CHUNK` events instead.) |
## Handoff to live agent (if triggered)
| Step | Direction | Event | What to do |
| ---- | --------- | ----------------------------------------- | --------------------------------------------------------- |
| 14 | ← Server | `EVENT_TYPE_POLY_AGENT_TRIGGERED_HANDOFF` | Inform the user they're being connected to a human. |
| 15 | ← Server | `EVENT_TYPE_POLY_AGENT_LEFT` | The PolyAI agent has left. |
| 16 | ← Server | `EVENT_TYPE_HANDOFF_ACCEPTED` | The live agent system accepted the request. |
| 17 | ← Server | `EVENT_TYPE_HANDOFF_QUEUE_STATUS` | Periodic. Show queue position (e.g. "You're #3 in line"). |
| 18 | ← Server | `EVENT_TYPE_LIVE_AGENT_JOINED` | A human agent has connected. Show their name. |
| 19 | Client → | `EVENT_TYPE_USER_MESSAGE` | User messages are now routed to the human agent. |
| 20 | ← Server | `EVENT_TYPE_LIVE_AGENT_TYPING` | Show typing indicator. |
| 21 | ← Server | `EVENT_TYPE_LIVE_AGENT_MESSAGE` | Message from the human agent. |
| 22 | ← Server | `EVENT_TYPE_LIVE_AGENT_LEFT` | The human agent has left. |
## Session end
| Direction | Event | What to do |
| --------- | -------------------------------------------- | --------------------------------------------- |
| Client → | `EVENT_TYPE_USER_END_SESSION` | User clicks "Leave". |
| ← Server | `EVENT_TYPE_USER_END_SESSION` | Echo confirming receipt. |
| ← Server | `EVENT_TYPE_SESSION_END` (`REASON_USER_END`) | Session over. Disable input, close WebSocket. |
## Alternative endings
| Scenario | What happens |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| **User closes browser** | No more heartbeats → session times out after \~10 minutes → `SESSION_END` with `REASON_USER_ABANDONED` |
| **Conversation completes naturally** | Agent finishes → `SESSION_END` with `REASON_NATURAL_END` |
| **Client-managed handoff** | Server sends `CLIENT_HANDOFF_REQUIRED` instead of `HANDOFF_ACCEPTED` — your app routes the user to an alternative channel |
| **Handoff fails** | `HANDOFF_FAILED` or `HANDOFF_TIMEOUT` — show an error and let the user try again or end the session |
| **Connection drops** | Reconnect with `cursor=` using the same `session_id` and `access_token` |
## Background (send throughout)
| Direction | Event | Frequency |
| --------- | ---------------------- | ------------------------------------------------------------------- |
| Client → | `EVENT_TYPE_HEARTBEAT` | Every 30 seconds (or per `capabilities.heartbeat_interval_seconds`) |
| ← Server | `EVENT_TYPE_HEARTBEAT` | Echo of your heartbeat |
## Reconnection flow
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant Client as Your App
participant Server as PolyAI
note over Client: Connection dropped
Client->>Server: WebSocket connect (session_id, access_token, cursor=12)
Server-->>Client: EVENT_BATCH (events after sequence 12)
note over Client: Conversation state restored — continue as normal
```
# Sessions and authentication
Source: https://docs.poly.ai/api-reference/messaging/sessions
Exchange your connector token for a short-lived access token, then create a session.
Before opening a WebSocket, you obtain an access token and create a session.
## Obtain an access token
Exchange your connector token for a short-lived JWT access token.
**`POST /api/v1/access-token`**
### Headers
| Header | Required | Description |
| -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `X-Token` | Yes | Your connector token (provided by PolyAI) |
| `X-Host` | Yes | The domain or app namespace you registered when generating the token (e.g. `https://www.yourcompany.com` or `com.yourcompany.app`). Must match exactly. |
| `Content-Type` | Yes | `application/json` |
### Request body
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{}
```
### Response — `200 OK`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"access_token": "eyJhbGc...",
"expires_in": 86400,
"token_type": "Bearer"
}
```
| Field | Type | Description |
| -------------- | ------- | -------------------------------------- |
| `access_token` | string | JWT to use for all subsequent requests |
| `expires_in` | integer | Seconds until the token expires |
| `token_type` | string | Always `"Bearer"` |
Use this token in two places:
* As a `Bearer` token in the `Authorization` header for [Create session](#create-session)
* As the `access_token` query parameter when [connecting the WebSocket](/api-reference/messaging/websocket)
Treat the access token as sensitive. Do not log it, store it in `localStorage`, or expose it to third-party scripts. Always connect over `wss://` (TLS).
## Create session
A session represents one conversation. Create a session before opening a WebSocket.
**`POST /api/v1/sessions`**
### Headers
| Header | Required | Description |
| --------------- | -------- | ---------------------------------------------- |
| `Authorization` | Yes | `Bearer ` from the previous step |
| `Content-Type` | Yes | `application/json` |
| `User-Agent` | No | The browser or device user agent |
### Request body (optional)
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"platform": "web",
"streaming_enabled": true
}
```
| Field | Type | Default | Description |
| ------------------- | ------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `streaming_enabled` | boolean | `false` | When `true`, agent responses arrive as a series of incremental chunks. When `false`, each response arrives as a single complete message. See [Streaming](/api-reference/messaging/streaming). |
| `platform` | string | `web` | One of `web`, `ios`, `android`, `ios-web`, `android-web`, or `custom`. Use `ios` / `android` from native SDKs that post to this endpoint directly. The webchat script reports `web`, `ios-web`, or `android-web` automatically based on its `data-platform` attribute — you don't need to set this field when embedding the script. |
The request body is optional — if omitted, `streaming_enabled` defaults to `false`.
### Response — `200 OK`
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
```
Keep the `session_id` — you need it to open the WebSocket and to reconnect if the connection drops.
Sessions expire after roughly 10 minutes of inactivity. After expiry, create a new session — you cannot resume an expired session.
# SMS API
Source: https://docs.poly.ai/api-reference/messaging/sms-and-rcs-api/introduction
Send and receive SMS messages through your PolyAI agent.
The SMS API lets you programmatically send SMS messages to end users and handle inbound replies through your PolyAI agent. It supports two directions:
* **Outbound** — your system triggers an SMS to a user via the [Send SMS](/api-reference/sms/endpoint/send-sms) endpoint. Each request creates a new conversation session.
* **Inbound** — users reply to an outbound SMS (or message your agent's number directly). The agent handles the conversation automatically over SMS. No API calls are needed from your side.
## Prerequisites
* An API key provisioned via Agent Studio — create one in the **API keys** tab under your account section (passed as `X-PolyAi-Auth-Token`)
* A connector ID for the target project (passed as `X-TOKEN-ID`). Contact your PolyAI representative.
* At least one Twilio phone number provisioned for the project. The API automatically selects from the project's available numbers for the token's environment (dev, sandbox, live) unless you specify one via `agent_number`.
The SMS API works exclusively with Twilio hosted numbers. The standard path is to purchase the number through Agent Studio, which provisions it under a PolyAI-managed subaccount and wires the inbound webhook automatically. Client-owned numbers ported into Twilio are also supported — see [External Twilio numbers](#external-twilio-numbers) for the extra setup.
## Base URL
Replace `` with the region for your deployment:
| Cluster | Region | Base URL |
| ------- | -------------- | --------------------------------------- |
| `us-1` | United States | `https://api.us-1.platform.polyai.app` |
| `uk-1` | United Kingdom | `https://api.uk-1.platform.polyai.app` |
| `euw-1` | EU West | `https://api.euw-1.platform.polyai.app` |
## Authentication
All requests require two headers for authentication:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api..platform.polyai.app/v1/outbound-sms \
-H "X-PolyAi-Auth-Token: YOUR_API_KEY" \
-H "X-TOKEN-ID: YOUR_CONNECTOR_ID" \
-H "Content-Type: application/json"
```
## How it works
### Outbound flow
1. **Send message** — POST to `/v1/outbound-sms` with the recipient number and message text.
2. **Session created** — The API creates a new conversation session. The message behaves like a greeting message, so it is fully part of the conversation context that the agent can access.
3. **User replies** — If the user replies, the agent handles the conversation as a normal SMS session.
4. **Inactivity timeout** — If the conversation is engaged (the user has replied) and goes idle for 24 hours, a warning message is sent. If the user replies within 10 minutes the session stays open; otherwise it terminates.
### Inbound flow
When a user sends an SMS to one of your provisioned Twilio numbers:
Twilio forwards the message to PolyAI via a pre-configured webhook. This webhook is set up automatically when you provision a number through PolyAI.
If there's an active conversation session with that user's number, the message is added to the existing session. Otherwise, a new session is created and the agent greets the user (or processes the message directly, depending on your agent configuration).
The agent processes the message using the same conversation engine as voice and webchat, and sends a reply via SMS.
Inbound SMS requires Twilio webhook configuration. This is handled automatically when you provision a number through PolyAI. No additional API calls are needed — the agent handles inbound messages through the same conversation engine used for voice and webchat.
Twilio only allows **one inbound webhook per number**. PolyAI-provisioned numbers have that webhook pinned to `/sms`, which the SMS runtime relies on. If you replace it with a different webhook (for example, the `external-events` webhook used by the [SMS data collection during a call](/voice-channel/message-templates) flow), inbound SMS handling will stop working on that number.
If you need both behaviors on the same agent, provision multiple numbers and dedicate each number to one webhook target. Voice and SMS can share the same number — SMS uses the messaging webhook, voice uses the voice webhook.
### Session behavior
| Scenario | Behavior |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **New user** | A new conversation session is created. The agent's start function runs with `conv.channel_type = "sms"`. |
| **Active session exists** | The message is added to the existing conversation. The agent continues where it left off. |
| **Previous session timed out** | A new session is created. |
| **Reply to an outbound SMS** | The message is added to the session created by the [Send SMS](/api-reference/sms/endpoint/send-sms) endpoint. The outbound message is part of the conversation context. |
### Inactivity timeout
Once a conversation is engaged (at least one user reply), an inactivity timer starts:
1. After **24 hours** of no messages, a warning message is sent to the user.
2. If the user replies within **10 minutes**, the session stays open and the timer resets.
3. If no reply, the session terminates.
### Channel detection
In your agent's start function, detect inbound SMS using `conv.channel_type`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start(conv):
if conv.channel_type == "sms":
conv.state.greet_message = "Hi, you've reached [Company]. How can I help?"
elif conv.channel_type == "voice":
conv.state.greet_message = "Thanks for calling. How can I help you today?"
```
## Agent number selection
By default the API picks from the Twilio numbers provisioned for the connector token's environment. You can override this by passing `agent_number` in the request body — the number must be one of the provisioned numbers or the request is rejected with a `400`.
An agent can have multiple numbers attached to it, and different numbers on the same agent can be configured for different purposes (for example, one number wired to `/sms` for 2-Way SMS, another wired to an `external-events` webhook for legacy in-call SMS data collection).
## External Twilio numbers
The standard path is to purchase numbers directly in Agent Studio — PolyAI provisions them under a PolyAI-managed Twilio subaccount and sets the inbound `/sms` webhook automatically.
Client-owned numbers on an external Twilio account (either ported into a PolyAI-managed subaccount or kept on the client's own account) are also supported, but require extra setup.
### Inbound-only from a client-owned number
For **inbound SMS only**, you can point a Twilio number at PolyAI without registering it in Agent Studio:
1. In the Twilio console, set the number's messaging webhook to your project's `/sms` endpoint.
2. Send a test SMS to the number and confirm the conversation appears in Agent Studio's [Conversation Review](/analytics/conversations/review) tab.
The number does not need to exist in Agent Studio for inbound handling to work, but conversations tied to a number that Agent Studio doesn't know about have limited visibility in the platform UI. For full parity (Conversation Review, analytics, handoff routing), add the number to Agent Studio.
### Outbound SMS from a client-owned number
Outbound SMS via `/v1/outbound-sms` requires the number to exist in Agent Studio. Port the client's number into a PolyAI-managed Twilio subaccount (or into a subaccount PolyAI has credentials for) and add it to the target agent before calling the endpoint.
### AWS secret configuration for external subaccounts
When PolyAI sends SMS via a Twilio subaccount that isn't the default platform subaccount, the runtime loads Twilio credentials from an AWS Secrets Manager secret. The secret key follows a fixed structure:
| Scenario | Secret key format |
| ----------------------------------------------- | -------------------------------------------------- |
| Default platform account (no subaccount passed) | `///twilio` |
| Specific subaccount | `////twilio` |
For example, a US production account without a subaccount uses `us-1/prod//twilio`. If a subaccount is specified in the webhook configuration, the key becomes `us-1/prod///twilio`.
Tag the secret following the same structure as existing platform secrets (for example, `uk-1/platform-prod/twilio/platform`) so it is picked up by the correct environment. Contact your PolyAI representative before creating these secrets in a new account — they must line up with the connector configuration.
### A2P 10DLC campaigns and sandbox testing
Twilio 10DLC campaigns are attached to a specific subaccount. Numbers provisioned via Agent Studio for sandbox go into the **PolyAI-Platform** subaccount, which has its own approved campaign — you can attach a sandbox number to that campaign and send test traffic.
Numbers used for live client projects should be registered against the client's own 10DLC campaign, either on their subaccount or on the PolyAI project subaccount they've been assigned to.
## Handoff and conversation review
* **Conversation Review** — all inbound and outbound SMS conversations appear in Agent Studio's [Conversation Review](/analytics/conversations/review) tab, alongside voice and webchat sessions.
* **CCaaS handoff** — SMS conversations can be handed off to a human agent through the same CCaaS integrations used by webchat: NICE, Salesforce, Genesys, Webex, and Amazon Connect.
* **Compliance keywords** — `STOP`, `START`, and `HELP` replies are intercepted before they reach your agent, and outbound messages to opted-out recipients are dropped silently. See [Message templates](/voice-channel/message-templates#what-this-means-for-your-agent) for the full behavior.
## Number availability & compliance
Not every country supports SMS, and the number type you use affects throughput, cost, and lead time. Before sending SMS in a new country, check [Number availability & compliance](/voice-channel/number-availability) for country-by-country details, regulatory links, and the [A2P Campaign Pre-Scanner](https://www.a2pcheck.com/) to validate your setup against carrier requirements.
For US and Canadian numbers, [A2P 10DLC registration](/voice-channel/message-templates#a2p-10dlc-registration-us-and-canada) is required — carriers block messages entirely without it.
## Rate limits
Rate limits are applied per project. If you receive a `429`, back off and retry with exponential backoff.
# RCS API
Source: https://docs.poly.ai/api-reference/messaging/sms-and-rcs-api/rcs
Send and receive rich, interactive messages (images, videos, rich cards, carousels, location pins) over RCS, with automatic SMS fallback.
## Overview
RCS (Rich Communication Services) is a messaging channel that lets your agent send and receive rich, interactive content (images, videos, graphs and location pins) over the same phone number used for SMS. PolyAI supports RCS through Twilio, alongside standard SMS.
If a customer's device or carrier doesn't support RCS, messages automatically fall back to SMS, so no conversation is ever lost.
Learn more about RCS from Twilio directly: [RCS overview](https://www.twilio.com/docs/messaging/channels/rcs) and [RCS onboarding](https://www.twilio.com/docs/messaging/channels/rcs/onboarding).
## How it works
### Starting and holding a conversation
Your customers can reach your agent over RCS in a few ways:
* **Deep link or QR code.** A customer clicks a link or scans a QR code that opens their device's messaging app with a chat to your agent already started. See [Twilio's guide](https://www.twilio.com/docs/messaging/channels/rcs) and [Google's deep link guide](https://developers.google.com/business-communications/rcs-business-messaging/guides/build/deep-links).
* **Existing contact.** If a customer already has your agent's RCS contact saved, they can start a conversation at any time.
* **SMS fallback.** If a customer texts your fallback number over plain SMS, that conversation stays on SMS for its entire duration; it won't switch to RCS mid-conversation.
After the initial message, the conversation is carried between the user and the agent over the same channel.
### Handoff
Handoff is supported on RCS, in the same way as SMS. See [Chat handoff integrations](/integrations/chat/introduction).
Media transfer from the user to the live agent will soon be enabled alongside text communication.
## Sending messages to customers (outbound)
Outbound RCS messages work the same way as [outbound SMS](/api-reference/messaging/sms-and-rcs-api/send-sms), with two differences:
* You must pass the Twilio Messaging Service SID for your RCS sender in the `messaging_service_sid` field.
* The recipient's number in `user_number` must be prefixed with `rcs:`.
If the recipient's device doesn't support RCS, the message automatically falls back to SMS. No extra handling is needed on your end.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api.us-1.platform.polyai.app/v1/outbound-sms \
-H "X-PolyAi-Auth-Token: YOUR_API_KEY" \
-H "X-TOKEN-ID: YOUR_CONNECTOR_ID" \
-H "Content-Type: application/json" \
-d '{
"message": "Hi Jane, your appointment is confirmed for tomorrow at 2pm. Reply to this message if you need to reschedule.",
"user_number": "rcs:+14155551234",
"messaging_service_sid": "YOUR_TWILIO_MESSAGING_SERVICE_SID"
}'
```
## Getting started
SMS must be set up on your agent, and via Twilio before RCS can be enabled. RCS uses the same phone number as SMS and relies on SMS as its automatic fallback channel.
Before RCS can be enabled for your agent, you'll need:
* **An agent number managed by PolyAI's Twilio account.** This number is used as the SMS fallback whenever RCS isn't available.
* **A registered Twilio RCS sender.** This must be submitted to Google for RCS registration. While registration is pending, the sender can be tested using a developer's test number.
* **A defined list of languages.** These are used to provision the message templates that render components correctly in each language. Currently supported: `en`, `fr`, `es`, `de`.
Speak to your PolyAI representative. They'll coordinate the Twilio setup on your behalf. Once complete, you'll receive a Twilio Messaging Service ID to use for outbound messaging, and your agent's Advanced Configuration in Agent Studio will be updated with the RCS settings automatically.
## Sending and receiving rich content in Agent Studio
### Sending attachments to users
Your agent can send rich media to customers, including images, links, suggested responses and hosted videos, using [the same attachments mechanism as other channels](https://docs.poly.ai/tools/classes/conv-object#attachments).
### Image
An image is visualised within a [Twilio’s media widget](https://www.twilio.com/docs/content/twilio-media) which is a frame that contains the image and a caption:
*A media widget with an image*
If the device is not RCS enabled, a text fallback is sent (the device could be smart enough to detect the image inside the link and render it autonomously):
*Serialisation of the media widget with an image*
In Agent Studio, the image is linked to the attachment type `image`.
#### Tool example
**Name:** `show_image`
**LLM description:** *Returns an image attachment*
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def show_image(conv: Conversation):
conv.add_attachments([
Attachment(
content_type="image",
content_url="https://picsum.photos/600/400.jpg#564c8f6fa2ce4f2f99c613cdcf888a89",
title="Image 123",
)
])
return {
"utterance": "Here is the image 123"
}
```
### Video
A video is visualised within a Twilio’s media widget which is a frame that contains the video and a caption:
*A media widget with a video*
If the device is not RCS enabled, a text fallback is sent in the same way as the image serialisation. Remember rich media types like video will incur additional costs to send from Twilio, do contact your Twilio rep when costing.
If the video is a YouTube one, it is always serialised. If the device is smart enough, a preview is shown autonomously:
In Agent Studio, the video is linked to the attachment type `video`.
#### Tool example
**Name:** `show_video`
**LLM description:** *Returns a video attachment*
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def show_image(conv: Conversation):
conv.add_attachments([
Attachment(
content_type="image",
content_url="https://loremipsum.video/vt/powerpoint-1.mp4",
title=f"Video 123",
)
])
return {
"utterance": f"Here is the video 123"
}
```
### Carousel
A carousel widget is a sliding sequence of rich cards, each containing an image to show, a text and a CTA leading the user to an external link.
Depending on the number of items in the list, the RCS client will render the followings:
* A **carousel** if we have from 2 to 10 cards to show (Please note, carousels incur a significant cost to serve - do contact your Twilio rep to support forecast in spend.)
*A carousel widget with some items*
The label of the CTA is hardcoded in the Twilio template.
* A single **rich card** if there's only one attachment
The label of the CTA is hardcoded in the Twilio template.
* Both fall back to plain text automatically if the customer's device doesn't support RCS
The label of the CTA comes from the Agent Studio function - see below.
In Agent Studio, a carousel is linked to the attachment type `weblink`.
Attachments sent to the user are **not visible in Conversation Review**.
#### Tool example
**Name:** `show_carousel`
**LLM description:** *Returns a carousel*
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def show_carousel(conv: Conversation):
conv.add_attachments([
Attachment(
content_type="weblink",
content_url="https://www.example.com",
title="Weblink number 1",
preview_image_url="https://picsum.photos/600/400.jpg",
call_to_action="Read more", # Used when serialising
),
Attachment(
content_type="weblink",
content_url="https://www.example.com",
title="Weblink number 2",
preview_image_url="https://picsum.photos/600/400.jpg",
call_to_action="Read more"
),
Attachment(
content_type="weblink",
content_url="https://www.example.com",
title="Weblink number 3",
preview_image_url="https://picsum.photos/600/400.jpg",
call_to_action="Read more"
)
])
return {
"utterance": "Here is carousel 123"
}
```
## Suggesting responses
The agent is able to suggest a maximum of 3 responses which will render as chips in the RCS UI:
*Response suggestions as chips*
The tap of a chip will be forwarded to the agent as a user message with exactly the text inside the chip.
If the device is not RCS enabled, a text fallback is sent:
*Serialisation of response suggestions*
The user will have to write their message by hand.
n Agent Studio, response suggestions can be added by using the method `set_response_suggestions` of the `Conversation` object (see here).
### Tool example
**Name: `show_response_suggestions`**
**LLM description:** *Returns a list of response suggestions*
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def show_response_suggestions(conv: Conversation):
conv.set_response_suggestions([
"Durian (榴蓮)",
"Dragon Fruit (火龍果)",
"Pineapple (鳳梨)",
])
return {
"utterance": "What's your favourite fruit (你的最喜歡的水果是什麼)?"
}
```
If the customer's device doesn't support RCS, the content falls back to plain text automatically. Attachments sent to the user are not visible in Conversation Review.
More advanced UI components (for example, interactive cards, or richer layouts) are possible but incur an additional cost from Twilio. Speak to your PolyAI representative if this is required.
### Receiving attachments from users
Customers can send your agent images, videos, voice notes, location pins, and button taps.
Attachments reach the agent in various shapes (see below). The agent can access them and handle them accordingly. When media is shared, the user selects the media and, at the moment of sending, the carrier stores it in a bucket and returns a URL pointer. That URL is what the agent sees and can relay upstream or open.
These attachments aren't shown in Conversation Review directly. Instead, a marker is inserted into the conversation to tell the LLM that attachments were sent:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
list of metadata type tags attached
```
The marker can contain any combination of:
* `` — the user sent an image, video, or audio file.
* `` — the user sent a location pin (latitude/longitude).
* `` — the user tapped a call-to-action button on a rich card or carousel.
The underlying data is available on the `Conversation` object at `conv.integration_attributes.get("metadata")`, keyed by metadata type.
#### Examples
**``**
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"media": [
{ "content_type": "image/jpeg", "url": "https://api.twilio.com/.../Media/ME123" },
{ "content_type": "application/pdf", "url": "https://api.twilio.com/.../Media/ME456" }
]
}
```
**``**
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"location": {
"latitude": "51.5074",
"longitude": "-0.1278",
"address": "10 Downing St, London",
"label": "Home"
}
}
```
**``**
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"button": {
"payload": "order_123_confirm",
"text": "Confirm order",
"type": "postback"
}
}
```
A metadata tag like `` would map to:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"metadata": {
"button": { },
"location": { },
"media": [ ]
}
}
```
#### Tool example: `parse_metadata`
**LLM description:** Parse user message when a metadata tag is passed.
**Request parameters:**
| Name | Context description | Type |
| --------- | -------------------------------------------------------------------------- | ------ |
| `message` | The full message comprising of any `` tag and of any extra text. | String |
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import json
METADATA_TAG = ""
def parse_metadata(conv: Conversation, message: str):
if METADATA_TAG not in message:
return {
"utterance": f"No metadata signature found in message: {message}"
}
if not conv.integration_attributes:
return {
"utterance": "No integration_attributes found"
}
metadata = conv.integration_attributes.get("metadata")
if metadata is None:
return {
"utterance": "No metadata found"
}
return {
"utterance": f"{message}\n{json.dumps(metadata)}"
}
```
## Opt-in and opt-out
Users can opt in or out of RCS communication. For RCS, PolyAI enables Twilio's advanced configuration, which allows the agent to receive notifications when the user sends a `STOP` or `START` keyword. By adding specific knowledge or tools, you can customise the agent's behaviour for these cases. The agent always receives the strings `STOP` and `START`, regardless of the actual word used by the user (which must be defined in Twilio, along with any localisation).
The possible cases are:
* **Opt-out.** The user receives the default info message defined in Twilio. The agent receives `STOP`, and any reply from the agent is **not** forwarded to the user, but is recorded in Conversation Review.
* **Opt-in.** The user receives the default info message defined in Twilio. The agent receives `START`, and any reply from the agent **is** forwarded to the user and recorded in Conversation Review. You may want to trigger a tool that drops the utterance so the user does not receive a duplicate message.
* **Between opt-out and opt-in.** Any message sent by the user does not reach either the agent or Conversation Review.
* **Help.** When the user asks for `HELP`, they receive the default info message defined in Twilio. This message does not reach either the agent or Conversation Review.
## Related
* [SMS API](/api-reference/sms/introduction)
* [Send an outbound SMS/RCS](/api-reference/messaging/sms-and-rcs-api/send-sms)
* [Chat handoff integrations](/integrations/chat/introduction)
# Send an outbound SMS/RCS
Source: https://docs.poly.ai/api-reference/messaging/sms-and-rcs-api/send-sms
POST /v1/outbound-sms
Send an outbound SMS or RCS message and open a reply-enabled conversation session.
Sends an outbound message to the specified user number and opens a new conversation session. The endpoint handles both **SMS** and **RCS** on the same route: for SMS, send to a plain E.164 number; for RCS, prefix the recipient number with `rcs:` and pass a Twilio Messaging Service SID for your RCS sender in `messaging_service_sid`. If the recipient's device or carrier doesn't support RCS, delivery automatically falls back to SMS. The message behaves like a greeting, so it is fully part of the conversation context that the agent can access. If the user replies, the agent handles the conversation as a normal SMS or RCS session.
It is your responsibility to ensure that messages are sent in compliance with applicable laws and regulations, including [A2P 10DLC registration](/voice-channel/message-templates#a2p-10dlc-registration-us-and-canada) for US and Canadian numbers, and [RCS Business Messaging registration](/messaging-channel/sms-and-rcs/sms-and-rcs-campaign-registration-compliance-requirements-us) where applicable.
## Request
### Headers
Your PolyAI API key, provisioned in the **API keys** tab under your account section in Agent Studio.
Connector ID for the target project. Contact your PolyAI representative.
Must be `application/json`.
### Body
The plain-text message to send to the user. Delivered as RCS when the recipient supports it, or as SMS otherwise.
Recipient phone number in E.164 format (e.g. `+14155551234`). For **RCS**, prefix the number with `rcs:` (e.g. `rcs:+14155551234`).
Sender phone number in E.164 format. Must be one of the numbers provisioned for your project. If omitted, the API picks one automatically from the numbers available for the connector token's environment. Not used for RCS — RCS senders are selected via `messaging_service_sid`.
**Required for RCS.** The Twilio Messaging Service SID for your RCS sender.
## Response
| Status | Description |
| --------------------------- | -------------------------------------------------------------------------------------------------- |
| `202 Accepted` | Message will be sent and a conversation session will be created. |
| `400 Bad Request` | Invalid body, missing fields, bad E.164 format, or `agent_number` not provisioned for the project. |
| `401 Unauthorized` | Missing or invalid `X-PolyAi-Auth-Token`. |
| `403 Forbidden` | API key account does not match the connector's account. |
| `404 Not Found` | Connector not found, or no numbers provisioned for this project. |
| `429 Too Many Requests` | Per-project rate limit exceeded. |
| `500 Internal Server Error` | Unexpected server error. |
The `202` response has an empty body.
## Example
### SMS
```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api.us-1.platform.polyai.app/v1/outbound-sms \
-H "X-PolyAi-Auth-Token: YOUR_API_KEY" \
-H "X-TOKEN-ID: YOUR_CONNECTOR_ID" \
-H "Content-Type: application/json" \
-d '{
"message": "Hi Jane, your appointment is confirmed for tomorrow at 2pm. Reply to this message if you need to reschedule.",
"user_number": "+14155551234"
}'
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import requests
url = "https://api.us-1.platform.polyai.app/v1/outbound-sms"
headers = {
"X-PolyAi-Auth-Token": "YOUR_API_KEY",
"X-TOKEN-ID": "YOUR_CONNECTOR_ID",
"Content-Type": "application/json",
}
payload = {
"message": "Hi Jane, your appointment is confirmed for tomorrow at 2pm. Reply to this message if you need to reschedule.",
"user_number": "+14155551234",
}
response = requests.post(url, json=payload, headers=headers)
print(response.status_code) # 202
```
```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const response = await fetch(
'https://api.us-1.platform.polyai.app/v1/outbound-sms',
{
method: 'POST',
headers: {
'X-PolyAi-Auth-Token': 'YOUR_API_KEY',
'X-TOKEN-ID': 'YOUR_CONNECTOR_ID',
'Content-Type': 'application/json',
},
body: JSON.stringify({
message:
'Hi Jane, your appointment is confirmed for tomorrow at 2pm. Reply to this message if you need to reschedule.',
user_number: '+14155551234',
}),
}
);
console.log(response.status); // 202
```
### RCS
Outbound RCS uses the same endpoint. Prefix `user_number` with `rcs:` and pass the Twilio Messaging Service SID for your RCS sender in `messaging_service_sid`. If the recipient's device doesn't support RCS, the message automatically falls back to SMS — no extra handling needed.
```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api.us-1.platform.polyai.app/v1/outbound-sms \
-H "X-PolyAi-Auth-Token: YOUR_API_KEY" \
-H "X-TOKEN-ID: YOUR_CONNECTOR_ID" \
-H "Content-Type: application/json" \
-d '{
"message": "Hi Jane, your appointment is confirmed for tomorrow at 2pm. Reply to this message if you need to reschedule.",
"user_number": "rcs:+14155551234",
"messaging_service_sid": "YOUR_TWILIO_MESSAGING_SERVICE_SID"
}'
```
This can lead to a conversation like:
| Direction | Message |
| ----------- | ------------------------------------------------------------------------------------------------------------ |
| **Agent →** | Hi Jane, your appointment is confirmed for tomorrow at 2pm. Reply to this message if you need to reschedule. |
| **← User** | Can I move it to 3pm instead? |
| **Agent →** | Of course, I've rescheduled your appointment to 3pm tomorrow. See you then! |
## Error response format
All error responses return a JSON object:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"detail": "A human-readable description of the error."
}
```
## Notes
* **SMS vs RCS delivery** — SMS is sent to a plain E.164 `user_number`. RCS requires an `rcs:` prefix on `user_number` **and** a `messaging_service_sid`. If the recipient can't receive RCS, delivery automatically falls back to SMS.
* **Agent number selection** — by default the API picks from the numbers provisioned for your project for the environment linked to the connector token. Pass `agent_number` to override; it must be one of the provisioned numbers or the request is rejected with a `400`.
* **Rate limits** are applied per project. If you hit a `429`, back off and retry.
* **Inactivity timeout** — if the conversation is engaged (the user has replied), after 24 hours of inactivity a warning message is sent. If the user replies within 10 minutes, the conversation stays open and a new 24-hour inactivity timer starts. Otherwise the session terminates.
* **Number availability** — not every country supports SMS or RCS. Check [Number availability & compliance](/voice-channel/number-availability) for country-by-country details, throughput limits, and regulatory links before sending to a new region.
# Streaming
Source: https://docs.poly.ai/api-reference/messaging/streaming
Receive agent responses as incremental chunks, like ChatGPT's typing effect.
When `streaming_enabled` is `true` on session creation, agent responses arrive incrementally as `EVENT_TYPE_POLY_AGENT_MESSAGE_CHUNK` events instead of a single `EVENT_TYPE_POLY_AGENT_MESSAGE`. This lets you display the response as it's generated.
## Chunk format
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "EVENT_TYPE_POLY_AGENT_MESSAGE_CHUNK",
"payload": {
"message_id": "msg_abc123",
"text": "I'd be happy to ",
"attachments": [],
"response_suggestions": [],
"chunk_index": 1,
"is_complete": false
}
}
```
| Field | Type | Description |
| ---------------------- | ------- | -------------------------------------------------------------- |
| `message_id` | string | Same across all chunks of one message — use this to group them |
| `text` | string | A fragment of text to append to the message |
| `attachments` | array | Attachment(s) to append to the attachment list |
| `response_suggestions` | array | Suggestion(s) to append to the suggestions list |
| `chunk_index` | integer | 1-based index. Process chunks in this order. |
| `is_complete` | boolean | `true` on the final chunk — the message is now complete |
## Reassembling chunks
To reconstruct the full message, concatenate `text` and append `attachments` / `response_suggestions` from each chunk in order:
| Chunk | Received | Accumulated message |
| ----- | ---------------------------------------------------------- | -------------------------------------------------------------------------------- |
| 1 | `text: "Hello "`, `attachments: [A1]`, `suggestions: [S1]` | `text: "Hello "`, `attachments: [A1]`, `suggestions: [S1]` |
| 2 | `text: "there, "`, `attachments: []`, `suggestions: []` | `text: "Hello there, "`, `attachments: [A1]`, `suggestions: [S1]` |
| 3 | `text: "how can I help?"`, `is_complete: true` | `text: "Hello there, how can I help?"`, `attachments: [A1]`, `suggestions: [S1]` |
The final chunk (`is_complete: true`) may have empty `text`. It signals that the message is complete and may carry attachments or response suggestions that were only available after the full response was generated.
## Example: handling chunks in JavaScript
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const streamingMessages = {};
function handleChunk(payload) {
const { message_id, text, attachments, response_suggestions, chunk_index, is_complete } = payload;
if (!streamingMessages[message_id]) {
streamingMessages[message_id] = { text: "", attachments: [], suggestions: [] };
}
const msg = streamingMessages[message_id];
msg.text += text;
msg.attachments.push(...attachments);
msg.suggestions.push(...response_suggestions);
updateMessageInUI(message_id, msg);
if (is_complete) {
finalizeMessageInUI(message_id, msg);
delete streamingMessages[message_id];
}
}
```
## Streaming flow
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant Client as Your App
participant Server as PolyAI
Client->>Server: USER_MESSAGE
Server-->>Client: echo (USER_MESSAGE)
Server-->>Client: POLY_AGENT_THINKING
rect rgb(240, 248, 255)
note over Client,Server: Streaming chunks (same message_id)
Server-->>Client: POLY_AGENT_MESSAGE_CHUNK (chunk_index: 1, text: "I'd be happy to ")
Server-->>Client: POLY_AGENT_MESSAGE_CHUNK (chunk_index: 2, text: "help you with ")
Server-->>Client: POLY_AGENT_MESSAGE_CHUNK (chunk_index: 3, text: "your booking!", is_complete: true)
end
note over Client: Reassembled: "I'd be happy to help you with your booking!"
```
# WebSocket connection
Source: https://docs.poly.ai/api-reference/messaging/websocket
Connect, reconnect, and keep the WebSocket alive.
After [creating a session](/api-reference/messaging/sessions), open a WebSocket to send and receive events in real time.
## Connecting
**`wss:///ws`**
### Query parameters
| Parameter | Required | Description |
| -------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `session_id` | Yes | The session ID from the Create Session response |
| `access_token` | Yes | Your access token |
| `cursor` | No | Resume from a specific point in the conversation. Pass the sequence number as a non-negative integer (e.g. `cursor=12`). Default: `0` (full history). The `seq:` prefix is also accepted for backwards compatibility. Invalid values fall back to `0` instead of rejecting the connection. |
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const ws = new WebSocket(
`wss://messaging.us-1.poly.ai/ws?session_id=${sessionId}&access_token=${accessToken}`
);
```
The access token is passed as a query parameter (not a header) because WebSocket connections do not support custom headers during the handshake. All connections use TLS encryption.
### Connection limits
| Constraint | Value |
| ---------------------------------- | ---------- |
| Idle timeout (no messages) | 10 minutes |
| Maximum message size | 128 KB |
| Concurrent connections per session | 10 |
## What happens on connect
When the WebSocket opens, the server immediately sends the session history as one or more `EVENT_TYPE_EVENT_BATCH` events. This lets your client reconstruct the conversation state — especially useful when reconnecting after a network drop.
The first batch always contains at least the `EVENT_TYPE_SESSION_START` event, which tells your client about the session's capabilities (e.g. whether streaming is enabled).
After processing the history:
* **New session** (no `EVENT_TYPE_POLY_AGENT_JOINED` in the replayed events): send `EVENT_TYPE_REQUEST_POLY_AGENT_JOIN` to start the conversation.
* **Reconnect**: the agent has already joined — skip the join request.
## Keeping the connection alive
Send `EVENT_TYPE_HEARTBEAT` events at regular intervals to prevent the connection from timing out. The server echoes each heartbeat back.
If the server receives no messages (including heartbeats) for the idle timeout period (10 minutes), the connection is closed and the session will eventually end.
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
// Send a heartbeat every 30 seconds
setInterval(() => {
ws.send(JSON.stringify({ type: "EVENT_TYPE_HEARTBEAT", payload: {} }));
}, 30000);
```
Read `capabilities.heartbeat_interval_seconds` from the `EVENT_TYPE_SESSION_START` event and use that interval. Fall back to 30 seconds if it's not set.
## Reconnecting
If the connection drops, reconnect using the same `session_id` and `access_token`. Pass the `cursor` parameter to avoid replaying events you've already received:
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const ws = new WebSocket(
`wss://messaging.us-1.poly.ai/ws?session_id=${sessionId}&access_token=${token}&cursor=${lastSequence}`
);
```
Set `lastSequence` to the highest `sequence` number your client has seen. The server replays only events after that point.
If the `cursor` value can't be parsed (e.g. non-numeric), the server logs a warning and defaults to `0` (full replay) instead of rejecting the connection. Your client stays connected — you just receive the full history.
**Connection dropped ≠ session ended.** If the WebSocket closes unexpectedly, the session is still alive on the server. Reconnect with the same `session_id` and a `cursor` to resume. Do not create a new session on reconnect.
### Recommended reconnect strategy
* Implement exponential backoff: start at 1 second, cap at 30 seconds
* Use `capabilities.max_reconnect_attempts` from `SESSION_START` as a hint for how many times to retry
* After exhausting retries, surface an error and let the user start a new session
# Get call status
Source: https://docs.poly.ai/api-reference/outbound/endpoint/get-call-status
GET /v1/outbound-calling/{call_sid}/status
Retrieve the current status of an outbound call
Retrieves the current status for an outbound call. Use this endpoint to monitor call progress after triggering a call.
Call status data is retained for approximately **2 hours** after the call ends. After this period, the endpoint will return a 404 Not Found error.
## Request
### Path parameters
The unique call identifier returned from the trigger endpoint (prefixed with `OUT-`).
### Headers
Authentication token provided by your PolyAI representative
## Response
Current status of the call. One of:
* `queued` – Call has been queued for processing
* `calling` – Call is being placed to the destination
* `success` – Call completed successfully
* `failure` – Call failed to connect or was not answered
Human-readable reason for the current status. Typically populated for terminal failure states (e.g., `"call queueing: max timeout limit exceeded"`); otherwise an empty string.
## Example
```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "https://api.us-1.platform.polyai.app/v1/outbound-calling/OUT-550e8400-e29b-41d4-a716-446655440000/status" \
-H "X-PolyAi-Auth-Token: YOUR_AUTH_TOKEN"
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import requests
call_sid = "OUT-550e8400-e29b-41d4-a716-446655440000"
url = f"https://api.us-1.platform.polyai.app/v1/outbound-calling/{call_sid}/status"
headers = {
"X-PolyAi-Auth-Token": "YOUR_AUTH_TOKEN"
}
response = requests.get(url, headers=headers)
status_data = response.json()
print(f"Call status: {status_data['status']}")
```
```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const call_sid = 'OUT-550e8400-e29b-41d4-a716-446655440000';
const response = await fetch(
`https://api.us-1.platform.polyai.app/v1/outbound-calling/${call_sid}/status`,
{
headers: {
'X-PolyAi-Auth-Token': 'YOUR_AUTH_TOKEN'
}
}
);
const data = await response.json();
console.log('Call status:', data.status);
```
## Response example
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"status": "success",
"reason": ""
}
```
Failure example:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"status": "failure",
"reason": "call queueing: max timeout limit exceeded"
}
```
## Error responses
Invalid call SID format.
Invalid or missing authentication token.
Call SID not found. This may occur if:
* The call does not exist
* More than 2 hours have passed since the call ended (status data expired)
Failed to retrieve call status. Retry with exponential backoff.
## Polling recommendations
When monitoring call status:
* Poll every 2-5 seconds during the `queued` and `calling` phases
* Stop polling once status reaches a terminal state (`success` or `failure`)
* Implement exponential backoff if you receive errors
* **Store the final status** before the 2-hour retention window expires if you need long-term records
## Notes
* Call status is updated in real-time as the call progresses.
* Terminal statuses (`success`, `failure`) are final and will not change.
* The `reason` field is typically populated only for failure states – successful and in-progress calls return an empty string.
* **Status data is retained for approximately 2 hours after the call ends** – poll and store data if you need longer retention.
# Trigger an outbound call
Source: https://docs.poly.ai/api-reference/outbound/endpoint/trigger-call
POST /v1/outbound-calling
Initiate an outbound call to a phone number
Triggers a new outbound call to the specified phone number. When the request is accepted, the call is placed close to immediately.
The API should only be called to make a call. The call is always attempted, and the [start tool](/tools/start-tool) (`start_function`) is executed **after** the call connects.
It is your responsibility to ensure that calls are placed in compliance with applicable laws and regulations.
## Request
### Headers
Authentication token provided by your PolyAI representative
Must be `application/json`
### Body
Phone number to call. Provide it in E.164 format (e.g., `+14155552671`), or supply `country_code` and pass a national-format number for that country.
Optional ISO 3166-1 alpha-2 country code (e.g., `US`, `GB`) used to parse a non-E.164 `to_number`. Leave unset when `to_number` is already in E.164 format.
Arbitrary key/value pairs to attach to the call. **All values must be strings.** The base64 representation of the metadata must be less than 26 KB.
Each key is delivered to the agent as a SIP header and can be read inside `start_function` (or any tool) with:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.sip_headers.get('key_name')
```
Telephony encryption type for the outbound leg. One of:
* `TLS/SRTP` (default)
* `TLS/RTP`
* `UDP/SRTP`
* `UDP/RTP`
## Response
Unique identifier for the triggered call. Always prefixed with `OUT-`. Use this to check call status.
## Example
```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api.us-1.platform.polyai.app/v1/outbound-calling \
-H "X-PolyAi-Auth-Token: YOUR_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to_number": "+16176451984",
"metadata": {
"patient_first_name": "Paul",
"patient_last_name": "Smith",
"patient_dob": "1981-August-26",
"referred_by_name": "Dr. Alex Deerman",
"variant": "Inpatient at St Charles",
"patient_address_street": "13 Main Street",
"patient_city_state": "Colonia, New Jersey",
"patient_zip_code": "07676"
}
}'
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import requests
url = "https://api.us-1.platform.polyai.app/v1/outbound-calling"
headers = {
"X-PolyAi-Auth-Token": "YOUR_AUTH_TOKEN",
"Content-Type": "application/json"
}
payload = {
"to_number": "+16175551212",
"metadata": {
"patient_first_name": "Paul",
"patient_last_name": "Smith",
"patient_dob": "1981-August-26",
"referred_by_name": "Dr. Alex Deerman",
"variant": "Inpatient at St Charles",
"patient_address_street": "13 Main Street",
"patient_city_state": "Colonia, New Jersey",
"patient_zip_code": "07676"
}
}
response = requests.post(url, json=payload, headers=headers)
call_sid = response.json()["call_sid"]
print(f"Call triggered: {call_sid}")
```
```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const response = await fetch(
'https://api.us-1.platform.polyai.app/v1/outbound-calling',
{
method: 'POST',
headers: {
'X-PolyAi-Auth-Token': 'YOUR_AUTH_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
to_number: '+16175551212',
metadata: {
patient_first_name: 'Paul',
patient_last_name: 'Glynn',
patient_dob: '1981-August-26'
}
})
}
);
const data = await response.json();
console.log('Call triggered:', data.call_sid);
```
## Response example
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"call_sid": "OUT-550e8400-e29b-41d4-a716-446655440000"
}
```
## Error responses
Invalid request parameters – for example, an unparseable `to_number`, a metadata value that isn't a string, or metadata that exceeds the 26 KB base64 size limit.
Invalid or missing authentication token, or the connector behind the token is not configured for outbound calling.
Failed to place the outbound call. Retry with exponential backoff.
## Notes
* Phone numbers must be in E.164 format unless you also pass `country_code`.
* All `metadata` values must be strings; nested objects, numbers, and booleans are rejected.
* Use the [Get call status](/api-reference/outbound/endpoint/get-call-status) endpoint to monitor call progress.
* Call status data is retained for approximately 2 hours after the call ends.
# Outbound Calling API
Source: https://docs.poly.ai/api-reference/outbound/introduction
Trigger outbound calls and monitor status for customer outreach, reminders, and notifications.
**New integrations should use the [Agents API's outbound calls endpoints](/api-reference/agents/endpoint/outbound-calls/trigger-outbound-call).** Those endpoints are the official, supported way to trigger and monitor outbound calls for an agent going forward. The standalone Outbound Calling API documented on this page is the legacy endpoint — existing integrations continue to work, but new work should target the Agents API.
The Outbound Calling API lets you programmatically initiate outbound calls and monitor their status. Use it for proactive customer outreach, appointment reminders, and automated notifications.
## Prerequisites
* An active PolyAI project with outbound calling enabled
* An authentication token provided by your PolyAI representative
* Your base URL (provided per project by PolyAI)
Contact your PolyAI representative to enable outbound calling for your project and obtain the necessary credentials.
## Base URL
Your base URL is provided by PolyAI when outbound calling is configured for your project. Regional endpoints include:
| Region | Base URL |
| ------ | --------------------------------------- |
| US | `https://api.us-1.platform.polyai.app` |
| UK | `https://api.uk-1.platform.polyai.app` |
| EUW | `https://api.euw-1.platform.polyai.app` |
## Endpoint paths
Outbound calling endpoints use the following path structure:
```
/{version}/outbound-calling/...
```
## Authentication
All outbound calling endpoints require authentication using the `X-PolyAi-Auth-Token` header:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api.{region}.platform.polyai.app/v1/outbound-calling \
-H "X-PolyAi-Auth-Token: YOUR_AUTH_TOKEN" \
-H "Content-Type: application/json"
```
## Call flow
1. **Trigger call** - POST to `/v1/outbound-calling` with the destination phone number (and optional `metadata` and `encryption`)
2. **Receive call SID** - API returns a unique call identifier (`call_sid`, prefixed with `OUT-`)
3. **Check status** - GET `/v1/outbound-calling/{call_sid}/status` to monitor call progress
4. **Handle completion** - Call status updates to `success` or `failure`
## Call statuses
| Status | Description |
| --------- | ------------------------------------------ |
| `queued` | Call has been queued for processing |
| `calling` | Call is being placed to the destination |
| `success` | Call completed successfully |
| `failure` | Call failed to connect or was not answered |
## Status data retention
Call status data is retained for approximately **2 hours** after the call ends. After this period, the status endpoint will return a 404 Not Found error. If you need to retain call data longer, poll and store the status data before it expires.
## Rate limits
Outbound calling is subject to rate limits based on your account configuration. Contact your PolyAI representative to adjust limits for your use case.
## Best practices
* **Validate phone numbers** - Ensure numbers are in E.164 format before triggering calls
* **Handle failures gracefully** - Implement retry logic with exponential backoff
* **Monitor status promptly** - Poll the status endpoint within 2 hours of call completion
* **Store status data** - If you need call status beyond 2 hours, store it in your own system
* **Respect time zones** - Schedule calls during appropriate hours for the destination region
# API quickstart
Source: https://docs.poly.ai/api-reference/quickstart
Build a PolyAI agent from the API: create it, configure its behavior and knowledge, test it in a live conversation, and ship it to production.
This guide builds an agent from scratch through the API alone — no flow-building in the Agent Studio UI. By the end you'll have created an agent, given it a behavior and a knowledge base topic, held a test conversation with it, and promoted it to production.
Every step below has a **curl** and a **Python** tab. The Python snippets run top to bottom as one script — each reuses variables (`agent_id`, `branch_id`, …) set by the step before — so copy them in order.
For an overview of the API families, see the [API overview](/api-reference/introduction). This page uses the [Agents](/api-reference/agents/introduction) API to build and ship, and the [Debug Chat](/api-reference/debug-chat/introduction) API to test — the same API key and host work for both.
## What you'll build
Stand up a new agent with a greeting.
Branch, add a behavior and a knowledge base topic, then merge.
Hold a live conversation with it in Sandbox.
Promote through Pre-release into Live traffic.
Pull back the conversations it has.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
stateDiagram-v2
[*] --> Branch: Create agent + branch
Branch --> Sandbox: Edit, then merge to main
Sandbox --> Sandbox: Test (debug chat)
Sandbox --> PreRelease: Promote
PreRelease --> Live: Promote
note right of Branch: Steps 1-2 — edits live on a branch, never on main directly
note right of Sandbox: Step 3 — merging to main publishes to Sandbox automatically
note right of Live: Step 4 — real callers
```
## Prerequisites
You build agents inside a PolyAI workspace, so you need access to one first.
* **Enterprise customers** — PolyAI provisions your workspace during onboarding; your PolyAI representative sets it up and grants you access. Enterprise workspaces are region-specific (US, UK, or EU).
* **Getting started via the website** — sign up at [poly.ai](https://poly.ai) to create a self-serve workspace, which lives in the Studio region.
Create a **workspace-scoped API key** from the **API Keys** tab on your workspace homepage in Agent Studio (see [API keys](/secrets/api-keys)). Copy the value when it's shown — the full key only appears once. The same key authenticates the Agents API and the Data API's debug chat — everything through step 4 of this guide.
* Step 5 (pulling call data back out) uses the [Conversations v3](/api-reference/conversations/introduction) API, which needs a separate project-scoped key. The [Chat API](/api-reference/chat/introduction) needs its own connector token too. Request either only once you're integrating a real channel or data pipeline — you don't need them for steps 1–4.
Treat the key like a password. Don't commit it or put it in client-side code.
Open Agent Studio. Your account ID is the first path segment in the URL:
```
https://studio.{region}.poly.ai/{account_id}/{project_id}/agent
```
For example, `https://studio.uk.poly.ai/acme-uk/acme-team-4/agent` → `account_id=acme-uk`.
**"Account ID" and "Workspace ID" are the same thing.** Agent Studio's UI calls this the **Workspace ID** and shows it in a prefixed form (`ws-xxxxxxxx`). The API parameter is named `accountId` (Agents and Data APIs) or `account_id` (Conversations, Chat, Webhooks, and most other APIs) — same value, different casing convention depending on which API family you're calling. Both the slug form from the URL (`acme-uk`) and the prefixed form (`ws-xxxxxxxx`) work in API calls.
The Agents and Data APIs — everything in steps 1–4 of this guide — share one regional host family:
| Region | Base URL |
| ------ | ---------------------------- |
| US | `https://api.us.poly.ai` |
| UK | `https://api.uk.poly.ai` |
| EU | `https://api.eu.poly.ai` |
| Studio | `https://api.studio.poly.ai` |
The Conversations v3 API (used in step 5) is on a *different* host — `api.{region}-1.platform.polyai.app`, with a `-1` suffix. Mixing these up is the most common cause of `404`s. See [base URLs](/api-reference/introduction#pick-your-region) for the full table across every API family.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export POLYAI_API_KEY="your_api_key_here"
export POLYAI_BASE_URL="https://api.us.poly.ai"
export POLYAI_ACCOUNT_ID="ws-xxxxxxxx"
```
All examples below assume these are set.
## Step 1: Create an agent
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$POLYAI_BASE_URL/v1/accounts/$POLYAI_ACCOUNT_ID/agents" \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Support Agent",
"responseSettings": {
"greeting": "Hi, thanks for calling Acme Corp. How can I help?"
}
}'
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os
import requests
base_url = os.environ["POLYAI_BASE_URL"]
account_id = os.environ["POLYAI_ACCOUNT_ID"]
headers = {"x-api-key": os.environ["POLYAI_API_KEY"]}
response = requests.post(
f"{base_url}/v1/accounts/{account_id}/agents",
headers=headers,
json={
"name": "Support Agent",
"responseSettings": {
"greeting": "Hi, thanks for calling Acme Corp. How can I help?",
},
},
)
response.raise_for_status()
agent_id = response.json()["agentId"]
print(agent_id)
```
**Response**
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"accountId": "ws-xxxxxxxx",
"agentId": "PROJECT-58RP822I",
"agentName": "Support Agent",
"createdAt": "2026-07-02T10:00:00.000Z",
"updatedAt": "2026-07-02T10:00:00.000Z",
"branchCount": 1
}
```
Save `agentId` — every remaining call in this guide uses it. The agent starts with one branch, `main`.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export POLYAI_AGENT_ID="PROJECT-58RP822I" # use the agentId from your response
```
## Step 2: Configure it
You can't edit `main` directly — the API rejects writes to it with `422 Cannot directly update main branch`. Instead, create a **working branch**, make your edits there, then merge it back into `main`. This is the same branch-and-merge model Agent Studio and Wren use.
**Create a working branch:**
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$POLYAI_BASE_URL/v1/agents/$POLYAI_AGENT_ID/branches" \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "branchName": "quickstart" }'
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = requests.post(
f"{base_url}/v1/agents/{agent_id}/branches",
headers=headers,
json={"branchName": "quickstart"},
)
response.raise_for_status()
branch_id = response.json()["branchId"]
print(branch_id)
```
The response returns the branch's ID — save it, every edit below targets it:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{ "branchId": "BRANCH-F9F1WNN8", "sequenceId": "1" }
```
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export POLYAI_BRANCH_ID="BRANCH-F9F1WNN8" # use the branchId from your response
```
### Set the behavior
The behavior is the system prompt that governs how the agent responds.
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PATCH "$POLYAI_BASE_URL/v1/agents/$POLYAI_AGENT_ID/branches/$POLYAI_BRANCH_ID/behavior" \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"behavior": "You are a friendly, concise support agent for Acme Corp. Answer questions using the knowledge base. If you cannot help, offer to hand off to a human agent."
}'
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
requests.patch(
f"{base_url}/v1/agents/{agent_id}/branches/{branch_id}/behavior",
headers=headers,
json={
"behavior": "You are a friendly, concise support agent for Acme Corp. "
"Answer questions using the knowledge base. If you cannot help, "
"offer to hand off to a human agent.",
},
).raise_for_status()
```
### Add a knowledge base topic
Topics are what the agent draws on to answer questions — each one pairs content with example queries that should trigger it.
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$POLYAI_BASE_URL/v1/agents/$POLYAI_AGENT_ID/branches/$POLYAI_BRANCH_ID/knowledge-base/topics" \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Password reset",
"content": "Users can reset their password at acme.com/reset. Resets take effect immediately and any active sessions are logged out.",
"exampleQueries": {
"queries": ["How do I reset my password?", "I forgot my password"]
}
}'
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
requests.post(
f"{base_url}/v1/agents/{agent_id}/branches/{branch_id}/knowledge-base/topics",
headers=headers,
json={
"name": "Password reset",
"content": "Users can reset their password at acme.com/reset. Resets take "
"effect immediately and any active sessions are logged out.",
"exampleQueries": {
"queries": ["How do I reset my password?", "I forgot my password"],
},
},
).raise_for_status()
```
See [Knowledge base](/api-reference/agents/endpoint/knowledge-base/create-knowledge-base-topic) for the full schema, including `actions` and `isActive`.
### Merge the branch into `main`
Merging applies the branch's edits to `main` **and publishes them to Sandbox in one step** — there's no separate publish call.
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$POLYAI_BASE_URL/v1/agents/$POLYAI_AGENT_ID/branches/$POLYAI_BRANCH_ID/merge" \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "deploymentMessage": "Initial support agent" }'
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = requests.post(
f"{base_url}/v1/agents/{agent_id}/branches/{branch_id}/merge",
headers=headers,
json={"deploymentMessage": "Initial support agent"},
)
response.raise_for_status()
print(response.json()["message"])
```
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{ "sequence": "2", "message": "Branch merged to main and deployed to sandbox", "testRunIds": [] }
```
## Step 3: Test it
Your merge in step 2 already published to Sandbox, so the agent is live there and ready to talk. Hold a test conversation using the [Debug Chat API](/api-reference/debug-chat/introduction) — it authenticates with the same key and host as the Agents API, so there's no extra credential to request.
**Edits reach an environment only when you merge or promote into it.** If debug chat replies with stale behavior, you edited the branch but haven't merged it to `main`. Merge again to push the change into Sandbox.
Prefer a UI? Agent Studio has a built-in **Test** panel that talks to a branch directly, no merge required — see [Test your agent](/environments-and-versions/introduction#testing-your-agent).
**Start a session:**
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$POLYAI_BASE_URL/v1/agents/$POLYAI_AGENT_ID/debug-chat" \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "clientEnv": "sandbox" }'
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = requests.post(
f"{base_url}/v1/agents/{agent_id}/debug-chat",
headers=headers,
json={"clientEnv": "sandbox"},
)
response.raise_for_status()
conversation_id = response.json()["conversationId"]
print(response.json()["response"]) # the greeting
```
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"conversationId": "AS_CHAT_106b4f9a-2541-4924-9ea1-fe5ced3c1eec",
"userInput": "",
"response": "Hi, thanks for calling Acme Corp. How can I help?",
"metadata": { "citedTopic": "", "retrievedTopics": [], "nodeTrace": [] },
"conversationEnded": false,
"delayedResponse": false
}
```
(`metadata` has more fields than shown — trimmed here for readability. The `conversationId` is an `AS_CHAT_…` string; the curl examples below hard-code the one from this response — swap in your own.)
**Send a message** — try the knowledge base topic you just added:
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$POLYAI_BASE_URL/v1/agents/$POLYAI_AGENT_ID/debug-chat/AS_CHAT_106b4f9a-2541-4924-9ea1-fe5ced3c1eec" \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"clientEnv": "sandbox",
"message": "I forgot my password"
}'
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = requests.post(
f"{base_url}/v1/agents/{agent_id}/debug-chat/{conversation_id}",
headers=headers,
json={"clientEnv": "sandbox", "message": "I forgot my password"},
)
response.raise_for_status()
body = response.json()
print(body["response"])
print(body["metadata"]["retrievedTopics"]) # ['Password reset']
```
The agent answers from the topic you added:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"conversationId": "AS_CHAT_106b4f9a-2541-4924-9ea1-fe5ced3c1eec",
"response": "No problem — you can reset your password at acme.com/reset. It takes effect immediately and any active sessions are logged out.",
"metadata": { "retrievedTopics": ["Password reset"] },
"conversationEnded": false
}
```
To confirm the topic was actually used, check `metadata.retrievedTopics` — it lists the knowledge base topics the agent pulled in (`["Password reset"]` here), a more reliable signal than scanning the reply text for `acme.com/reset`. Keep sending messages against the same `conversationId` to continue the conversation, matching `clientEnv` to whichever environment you're checking.
Building a real webchat, SMS, or in-app integration instead of a one-off test? Use the [Chat API](/api-reference/chat/introduction) — it's built for driving conversations from an end-user-facing client and requires its own connector token.
## Step 4: Deploy it
Sandbox is for testing, not customer traffic. Promote the Sandbox deployment through Pre-release and into Live — that's what real callers hit. See [Environments](/environments-and-versions/introduction) for the full model.
Promotion works on a deployment ID, and the merge in step 2 didn't return one — so fetch the active Sandbox deployment first:
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$POLYAI_BASE_URL/v1/agents/$POLYAI_AGENT_ID/deployments/active" \
-H "x-api-key: $POLYAI_API_KEY"
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = requests.get(
f"{base_url}/v1/agents/{agent_id}/deployments/active",
headers=headers,
)
response.raise_for_status()
deployment_id = response.json()["activeDeployments"]["sandbox"]["id"]
print(deployment_id)
```
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"activeDeployments": {
"sandbox": { "id": "019f380c-4235-7ea5-8426-1edf249cd6f3", "environment": "sandbox" },
"pre-release": null,
"live": null
}
}
```
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export POLYAI_DEPLOYMENT_ID="019f380c-4235-7ea5-8426-1edf249cd6f3" # activeDeployments.sandbox.id
```
Then promote it up the chain. Each promote returns a **new** deployment under `deployment.id` for the target environment — feed that into the next call:
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
# sandbox -> pre-release
curl -X POST "$POLYAI_BASE_URL/v1/agents/$POLYAI_AGENT_ID/deployments/$POLYAI_DEPLOYMENT_ID/promote" \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "targetEnvironment": "pre-release" }'
# take deployment.id from the response above:
export POLYAI_PRERELEASE_DEPLOYMENT_ID="..."
# pre-release -> live
curl -X POST "$POLYAI_BASE_URL/v1/agents/$POLYAI_AGENT_ID/deployments/$POLYAI_PRERELEASE_DEPLOYMENT_ID/promote" \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "targetEnvironment": "live" }'
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# sandbox -> pre-release
response = requests.post(
f"{base_url}/v1/agents/{agent_id}/deployments/{deployment_id}/promote",
headers=headers,
json={"targetEnvironment": "pre-release"},
)
response.raise_for_status()
pre_release_id = response.json()["deployment"]["id"]
# pre-release -> live
requests.post(
f"{base_url}/v1/agents/{agent_id}/deployments/{pre_release_id}/promote",
headers=headers,
json={"targetEnvironment": "live"},
).raise_for_status()
```
Omit `targetEnvironment` and the promote defaults to the next stage in sequence (sandbox → pre-release → live). A Sandbox deployment can also promote straight to `live` — going through Pre-release first is a safety choice, not an API requirement.
Made a mistake? [Roll back](/api-reference/agents/endpoint/deployments/rollback-to-a-previous-deployment) to the previous deployment in any environment.
## Step 5: Work with the calls it takes
Once your agent is live (or you've made test calls), pull the data back with the [Conversations API](/api-reference/conversations/introduction). This uses a different host and a project-scoped key — see [prerequisites](#prerequisites) above.
The `project_id` this API expects is the same value as the `agentId` you've used throughout this guide — "Agent" and "Project" are the current and legacy names for the same resource.
This is the one step on the `-1.platform.polyai.app` host with its own project-scoped key, so set that key first:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export POLYAI_CONVERSATIONS_API_KEY="your_conversations_api_key_here"
```
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET \
"https://api.us-1.platform.polyai.app/v3/$POLYAI_ACCOUNT_ID/$POLYAI_AGENT_ID/conversations?limit=5" \
-H "x-api-key: $POLYAI_CONVERSATIONS_API_KEY"
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
platform_url = "https://api.us-1.platform.polyai.app"
conversations_headers = {"x-api-key": os.environ["POLYAI_CONVERSATIONS_API_KEY"]}
response = requests.get(
f"{platform_url}/v3/{account_id}/{agent_id}/conversations",
headers=conversations_headers,
params={"limit": 5},
)
response.raise_for_status()
conversations = response.json()["conversations"]
```
This returns a `conversations` array and a `cursor` for pagination. Fetch one by ID to get its full turn-by-turn transcript:
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET \
"https://api.us-1.platform.polyai.app/v3/$POLYAI_ACCOUNT_ID/$POLYAI_AGENT_ID/conversations/CONVERSATION_ID" \
-H "x-api-key: $POLYAI_CONVERSATIONS_API_KEY"
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conversation_id = conversations[0]["id"]
response = requests.get(
f"{platform_url}/v3/{account_id}/{agent_id}/conversations/{conversation_id}",
headers=conversations_headers,
)
response.raise_for_status()
transcript = response.json()
```
From here:
Voice calls have a recording available on a separate binary endpoint.
Get a signed POST when a call completes instead of polling for it.
## Clean up
Built this agent just to try the API? Delete it when you're done — this removes the agent along with all its branches and deployments, and can't be undone.
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X DELETE "$POLYAI_BASE_URL/v1/agents/$POLYAI_AGENT_ID" \
-H "x-api-key: $POLYAI_API_KEY"
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
requests.delete(
f"{base_url}/v1/agents/{agent_id}",
headers=headers,
).raise_for_status()
```
A successful delete returns `204 No Content`.
## Troubleshooting
| Symptom | Likely cause | Fix |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `401 Unauthorized` | Missing or wrong `x-api-key`, or a key from the wrong region. | Confirm the key was issued for the region in your base URL — see [region mismatches](/api-reference/agents/introduction#authentication). |
| `403 Forbidden` | Key lacks permission for this account or agent. | Confirm the key was provisioned for the account ID / agent ID you're using. |
| `400` on create agent | Missing `responseSettings.greeting`, which is required. | Include a non-empty `greeting` — it's the agent's opening line. |
| `422 Cannot directly update main branch` | You sent a behavior or knowledge base write to `branches/main`. | Create a working branch, make edits on it, then merge to `main` — see step 2. `main` is read-only. |
| Debug chat replies with the old behavior/topic | You edited a branch but haven't merged it into `main` yet. | Merge the branch again — edits reach Sandbox only once they're merged into `main`. |
| `404` on debug-chat message | Wrong or expired `conversationId`. | Start a new session with `POST /debug-chat` and use the returned `conversationId`. |
| `409 Deployments already published` | You called `publish` after a merge, which already deployed to Sandbox. | Skip the explicit publish — merging a branch publishes to Sandbox for you. Move on to promote. |
| `404` on promote | Wrong `deploymentId`. | Use the `deployment.id` from the most recent publish/promote response, not an old one. |
| `400` on promote | `targetEnvironment` isn't a later stage than the deployment's current one. | Sandbox can promote to `pre-release` or `live`; pre-release can only promote to `live`. |
| `404` on Conversations endpoint (step 5) | Wrong base URL — usually the build host (`api.us.poly.ai`) instead of the platform host. | Conversations v3 uses `api.{region}-1.platform.polyai.app`, with the `-1` suffix. |
| Empty `conversations` array | No calls yet in the time window, or wrong `client_env`. | Place a test call via debug chat, widen the window, or try `client_env=sandbox`. |
| `429 Too Many Requests` | Rate limit hit. | Back off per the `Retry-After` header; use cursor pagination for large pulls. |
See [Error codes](/api-reference/error-codes) for the full reference.
## Next steps
Branches, telephony, real-time configs, and variants for multi-site agents.
Wire a real webchat, web SDK, or SMS integration into a live conversation.
Full schema, pagination, and retrieval modes for call data.
Event types, retries, and signature verification.
Have your new agent place a real call out — needs outbound enabled on the project first.
Talk to your agent by voice from a browser tab instead of typing.
# Create a webhook endpoint
Source: https://docs.poly.ai/api-reference/webhooks/endpoint/create-webhook-endpoint
POST /v1/webhook-endpoints
# Delete a webhook endpoint
Source: https://docs.poly.ai/api-reference/webhooks/endpoint/delete-webhook-endpoint
DELETE /v1/webhook-endpoints/{endpoint_id}
# Get a webhook endpoint
Source: https://docs.poly.ai/api-reference/webhooks/endpoint/get-webhook-endpoint
GET /v1/webhook-endpoints/{endpoint_id}
# List webhook endpoints
Source: https://docs.poly.ai/api-reference/webhooks/endpoint/list-webhook-endpoints
GET /v1/webhook-endpoints
# Rotate webhook signing secret
Source: https://docs.poly.ai/api-reference/webhooks/endpoint/rotate-webhook-signing-secret
POST /v1/webhook-endpoints/{endpoint_id}/rotate-secret
Generates a new signing secret for the webhook endpoint. The previous secret is immediately invalidated.
# Update a webhook endpoint
Source: https://docs.poly.ai/api-reference/webhooks/endpoint/update-webhook-endpoint
PATCH /v1/webhook-endpoints/{endpoint_id}
# Webhooks API
Source: https://docs.poly.ai/api-reference/webhooks/introduction
Register webhook endpoints to receive real-time signed notifications for PolyAI events.
Use webhooks when your systems need to react to PolyAI events in real time–for example, triggering incident response when an alert fires or updating a dashboard. Webhooks include HMAC-SHA256 signatures and automatic retries, with support for secret rotation.
The Webhooks API lets you register HTTP endpoints that receive real-time notifications when events occur in your PolyAI account. Webhooks are currently used by the [Alerts API](/api-reference/alerts/introduction) and will expand to other services in the future.
## Key features
* **Signed delivery** - Every webhook includes an HMAC-SHA256 signature you can verify
* **Automatic retries** - Failed deliveries retry with exponential backoff
* **Secret rotation** - Rotate signing secrets without recreating the endpoint
## Limits
| Resource | Maximum per account |
| ----------------- | ------------------- |
| Webhook endpoints | 10 |
Requests to create a webhook endpoint beyond the limit return a `409 Conflict` error.
## Event types
| Event | Description |
| ------------------ | ---------------------------------------------- |
| `alerts.triggered` | An alert rule transitioned into a firing state |
| `alerts.resolved` | A firing alert transitioned back to `ok` |
## Webhook headers
Each webhook request includes these headers:
| Header | Description |
| -------------------- | -------------------------------------------------- |
| `X-PolyAI-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
| `X-PolyAI-Signature` | HMAC-SHA256 signature for verification |
| `X-PolyAI-Event-ID` | Unique event identifier for deduplication |
## Retry policy
Failed webhook deliveries are retried with exponential backoff:
| Attempt | Delay | Cumulative time |
| ------- | ---------- | --------------- |
| 1 | Immediate | 0 |
| 2 | 1 minute | 1 minute |
| 3 | 5 minutes | 6 minutes |
| 4 | 15 minutes | 21 minutes |
| 5 | 1 hour | \~1.5 hours |
| 6 | 4 hours | \~5.5 hours |
Retried failures:
* Timeout
* Network error
* HTTP 408, 429, 5xx
Not retried:
* Other 4xx errors
## Signature verification
Verify webhook signatures to ensure requests are from PolyAI.
**Algorithm:** HMAC-SHA256
**Signed message format:** `{timestamp}.{raw_request_body}`
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import hmac
import hashlib
import time
def verify_webhook(payload: bytes, timestamp: str, signature: str, secret: str) -> bool:
# Reject requests older than 5 minutes
if abs(time.time() - int(timestamp)) > 300:
return False
# Compute expected signature
message = f"{timestamp}.{payload.decode('utf-8')}"
expected = hmac.new(
secret.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Constant-time comparison
return hmac.compare_digest(expected, signature)
```
```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const crypto = require('crypto');
function verifyWebhook(payload, timestamp, signature, secret) {
// Reject requests older than 5 minutes
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
return false;
}
// Compute expected signature
const message = `${timestamp}.${payload}`;
const expected = crypto
.createHmac('sha256', secret)
.update(message)
.digest('hex');
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
```
```go theme={"theme":{"light":"github-light","dark":"github-dark"}}
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"math"
"strconv"
"time"
)
func verifyWebhook(payload []byte, timestamp, signature, secret string) bool {
// Reject requests older than 5 minutes
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return false
}
if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
return false
}
// Compute expected signature
message := timestamp + "." + string(payload)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(message))
expected := hex.EncodeToString(mac.Sum(nil))
// Constant-time comparison
return hmac.Equal([]byte(expected), []byte(signature))
}
```
Use `X-PolyAI-Event-ID` for deduplication since retries can deliver the same event more than once.
## Authentication
All Webhooks API endpoints use API key authentication with the `x-api-key` header. Resources are automatically scoped to your account.
Create a key from the **API Keys** tab in Agent Studio — see [API keys](/secrets/api-keys).
# WebRTC Gateway API
Source: https://docs.poly.ai/api-reference/webrtc-gateway/introduction
Enable in-browser voice conversations with PolyAI agents using WebRTC and WebSocket signaling.
The WebRTC Gateway enables real-time voice communication between a web browser and a PolyAI voice agent, using WebSocket signaling and bidirectional WebRTC audio.
It provides two integration layers:
* [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) signaling for session setup, [SDP](https://datatracker.ietf.org/doc/html/rfc8866) exchange, and [ICE](https://datatracker.ietf.org/doc/html/rfc8445) candidate exchange
* [WebRTC](https://webrtc.org/getting-started/overview) media for bidirectional audio once the connection is established
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant Browser
participant WS as WebSocket (Signaling)
participant PolyAI as PolyAI Agent
Browser->>WS: Connect to signaling endpoint
Browser->>WS: Send offer (SDP + auth token)
WS->>PolyAI: Create session
PolyAI-->>WS: Answer (SDP + sessionId)
WS-->>Browser: Forward answer
Browser->>WS: ICE candidates
WS-->>Browser: ICE candidates
Note over Browser,PolyAI: WebRTC peer connection established
Browser<<->>PolyAI: Bidirectional audio (Opus)
```
## Prerequisites
* A WebRTC-capable browser
* Microphone permissions enabled
* A PolyAI authentication token
## Quick start
1. Open a WebSocket connection to the signaling endpoint
2. Create a WebRTC peer connection and collect microphone audio
3. Send an offer message containing SDP and your auth token
4. Receive an answer message containing SDP and a session identifier
5. Exchange ICE candidates until the connection is established
6. Audio flows bidirectionally
## Authentication token
The `authToken` you send in the `offer` message is the same credential used to authenticate SIP traffic — your **connector token** (the value provided by PolyAI for the `X-PolyAi-Auth-Token` SIP header). The gateway accepts two token types:
| Token | Source | When to use |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| **Connector token** | Provisioned by PolyAI per project (same token used on the SIP connector for [Five9](/integrations/voice/sip/five9), [Genesys](/integrations/voice/sip/genesys), and other telephony integrations) | **Recommended for all integrations outside Agent Studio.** Durable across redeploys. |
| **Studio-minted JWT** | Generated automatically by Agent Studio for in-Studio calls | Only when calling from the Agent Studio UI. Goes stale on redeploy, so it isn't suitable for external clients. |
For your own WebRTC client (widgets, native apps, custom browser integrations), use the connector token.
## Signaling endpoint
Signaling URL (WebSocket):
`wss://webrtc-gateway.us-1.platform.polyai.app/api/v1/webrtc/signal`
All signaling messages are JSON objects sent over the WebSocket connection.
## Message structure
All signaling messages follow the same top-level structure.
| Field | Required | Description |
| ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
| `type` | Yes | Message type: `offer`, `answer`, `ice-candidate`, `error`, `close` |
| `sessionId` | Yes | Empty string when creating a new session |
| `data` | No | Message-specific payload (SDP, ICE candidate, or error) |
| `authToken` | Offer | Authentication token for the voice agent. Required for every offer, including studio draft and preview calls |
| `mode` | No | Agent mode: `end-to-end` (default), `traditional`, or `echo` (debug only) |
| `callSid` | No | Unique call identifier (distinct from the outbound REST API's `call_sid`) |
| `caller` | No | Calling number |
| `callee` | No | Called number |
| `accountId` | No | Account identifier |
| `projectId` | No | Project identifier |
| `variantId` | No | Optional variant override |
| `agentVersionOverride` | No | Optional `{ artifactVersion, lambdaDeploymentVersion }` to pin a specific agent build |
## Message types
### Offer (client to server)
Starts a new session.
Send with an empty sessionId.
Example message:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "offer",
"sessionId": "",
"data": {
"type": "offer",
"sdp": "v=0 o=- 4611731400430051336 2 IN IP4 127.0.0.1"
},
"authToken": "your-auth-token",
"callSid": "call-unique-id",
"caller": "+14155551234",
"callee": "+14155555678"
}
```
### Answer (server to client)
Sent in response to an offer.
Contains the SDP answer and the assigned sessionId.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "answer",
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"type": "answer",
"sdp": "v=0 o=- 4611731400430051336 2 IN IP4 192.168.1.1"
}
}
```
Store the `sessionId` and use it for all subsequent messages.
### ICE candidate (bidirectional)
Sent by both client and server to establish network connectivity.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "ice-candidate",
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"candidate": "candidate:1 1 UDP 2130706431 192.168.1.1 54321 typ host",
"sdpMid": "0",
"sdpMLineIndex": 0
}
}
```
### Close (client to server)
Terminates the session gracefully.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "close",
"sessionId": "550e8400-e29b-41d4-a716-446655440000"
}
```
### Error (server to client)
Sent when an error occurs.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "error",
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"code": "UNAUTHORIZED",
"message": "Invalid authentication token"
}
}
```
Error codes:
| Code | Description |
| ---------------------- | ------------------------------------------------------------------- |
| `UNAUTHORIZED` | Invalid or missing authentication token |
| `INVALID_ARGUMENT` | Request field has an invalid value (for example, an unknown `mode`) |
| `INVALID_MESSAGE` | Malformed or unsupported message format |
| `HANDLER_ERROR` | Error processing the signaling message |
| `MEDIA_BRIDGE_FAILURE` | Failed to establish media connection |
| `AGENT_FAILURE` | Error connecting to the PolyAI agent |
## WebRTC configuration
### Audio codec
The gateway requires Opus audio.
* MIME type: audio/opus
* Sample rate: 48 kHz
* Channels: stereo
### ICE servers
Configure your peer connection with a STUN server.
TURN is recommended for restrictive networks.
Example STUN server:
`stun.l.google.com:19302`
## Browser support
* Chrome 72 or newer
* Firefox 60 or newer
* Safari 14.1 or newer
* Edge 79 or newer
## Troubleshooting
### Unauthorized error
Ensure the authentication token is valid and included in the offer message. Every offer requires an `authToken`, including studio draft and preview calls that pin a specific build with `agentVersionOverride`. Offers without a token are rejected with an `UNAUTHORIZED` error and the message `Auth token required`.
### No audio
* Confirm microphone permissions are granted
* Verify Opus is negotiated successfully
### ICE connection fails
* Corporate firewalls may require TURN
* Ensure UDP traffic is allowed
* Configure TURN over TCP if needed
## Useful links
* [WebRTC overview](https://webrtc.org/getting-started/overview) -- Getting started with WebRTC
* [MDN WebRTC API](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API) -- Browser API reference
* [MDN WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) -- WebSocket reference
* [RFC 8866 (SDP)](https://datatracker.ietf.org/doc/html/rfc8866) -- Session Description Protocol specification
* [RFC 8445 (ICE)](https://datatracker.ietf.org/doc/html/rfc8445) -- Interactive Connectivity Establishment specification
# WebSocket signaling
Source: https://docs.poly.ai/api-reference/webrtc-gateway/ws/signaling
WebSocket signaling protocol for SDP exchange, ICE candidates, and session management.
The signaling channel is a WebSocket connection used to exchange SDP offers/answers and ICE candidates between your client and the WebRTC Gateway.
## Endpoint
```
wss://webrtc-gateway.us-1.platform.polyai.app/api/v1/webrtc/signal
```
## Message format
All messages are JSON objects with a common top-level structure.
Message type. One of `offer`, `answer`, `ice-candidate`, `error`, `close`.
Session identifier. Send an empty string (`""`) when creating a new session with an offer.
Message-specific payload. Structure depends on the message type.
Authentication token. Required in every `offer` message, including studio draft and preview calls. Offers without a valid `authToken` are rejected with an `UNAUTHORIZED` error (`Auth token required`). Setting `agentVersionOverride`, `accountId`, or `projectId` does not exempt a call from authentication.
The gateway accepts two token types:
* **Connector token** — the same token used on your project's SIP connector (the `X-PolyAi-Auth-Token` value provided by PolyAI). Use this for any client outside Agent Studio. It is durable across redeploys.
* **Studio-minted JWT** — issued automatically by Agent Studio for in-Studio calls. It goes stale on redeploy and is not suitable for external integrations.
For widgets, native apps, and custom WebRTC clients, use the connector token.
Agent mode for the session. Defaults to `end-to-end` when omitted.
| Value | Description |
| ------------- | -------------------------------------------------------------------------------- |
| `end-to-end` | End-to-end mode. A single speech-to-speech model handles audio input and output. |
| `traditional` | Traditional mode. Audio cascades through separate ASR, LLM, and TTS stages. |
Unknown values are rejected with an `INVALID_ARGUMENT` error. Legacy values (`agent`, `agent_v1`, `agent_v2`, `cascaded`, `normal`, `realtime`) are still accepted but resolve to a canonical value and may be removed in a future release. Update integrations to use the canonical names.
**Echo mode is restricted to debug environments.** Echo mode (`mode: "echo"`) loops your audio back to the client and is intended for connectivity testing. Production gateways reject `echo` offers with a `FORBIDDEN` error before any session is created. Use `agent` mode (the default) for normal voice agent traffic.
Unique call identifier. This is distinct from the [Outbound Calling API](/api-reference/outbound/endpoint/trigger-call)'s `call_sid` field.
Calling number.
Called number.
Account identifier.
Project identifier.
Optional variant override.
Optional pinning of a specific agent build. Both fields are required when set:
* `artifactVersion` (string)
* `lambdaDeploymentVersion` (string)
Setting `agentVersionOverride` does not bypass authentication. A valid `authToken` is still required.
## Operations
### Send offer
**Direction: client to server**
Starts a new session. Send with an empty `sessionId` and include your `authToken`.
The `data` field contains the SDP offer:
Must be `"offer"`.
Full SDP string from your local peer connection.
```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "offer",
"sessionId": "",
"data": {
"type": "offer",
"sdp": "v=0\r\no=- 4611731400430051336 2 IN IP4 127.0.0.1..."
},
"authToken": "your-auth-token",
"callSid": "call-unique-id",
"caller": "+14155551234",
"callee": "+14155555678"
}
```
### Receive answer
**Direction: server to client**
Sent by the server in response to a valid offer. Contains the SDP answer and the assigned `sessionId`. Store the `sessionId` and use it for all subsequent messages.
The `data` field contains the SDP answer:
Must be `"answer"`.
Full SDP string from the server.
```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "answer",
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"type": "answer",
"sdp": "v=0\r\no=- 4611731400430051336 2 IN IP4 192.168.1.1..."
}
}
```
### Exchange ICE candidates
**Direction: bidirectional**
Sent by both client and server to exchange network connectivity candidates. Continue exchanging until the WebRTC connection is established.
The `data` field contains the ICE candidate:
ICE candidate string.
Media stream identification tag.
Zero-based index of the media description in the SDP.
```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "ice-candidate",
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"candidate": "candidate:1 1 UDP 2130706431 192.168.1.1 54321 typ host",
"sdpMid": "0",
"sdpMLineIndex": 0
}
}
```
### Close
**Direction: client to server**
Terminates the session gracefully. Send when you want to end the call.
```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "close",
"sessionId": "550e8400-e29b-41d4-a716-446655440000"
}
```
### Error
**Direction: server to client**
Sent when the server encounters an error during the session.
The `data` field contains the error details:
Error code identifying the failure type.
Human-readable error description.
```json Example theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "error",
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"data": {
"code": "UNAUTHORIZED",
"message": "Invalid authentication token"
}
}
```
#### Error codes
| Code | Description |
| ---------------------- | ------------------------------------------------------------------- |
| `UNAUTHORIZED` | Invalid or missing authentication token |
| `FORBIDDEN` | The requested action is not permitted on this gateway |
| `INVALID_ARGUMENT` | Request field has an invalid value (for example, an unknown `mode`) |
| `INVALID_MESSAGE` | Malformed or unsupported message format |
| `HANDLER_ERROR` | Error processing the signaling message |
| `MEDIA_BRIDGE_FAILURE` | Failed to establish the media connection |
| `AGENT_FAILURE` | Error connecting to the PolyAI agent |
## Connection flow
Connect to the signaling endpoint using a WebSocket client.
Create a local `RTCPeerConnection`, add your microphone track, generate an SDP offer, and send it with your `authToken`.
The server responds with an SDP answer and a `sessionId`. Set the remote description on your peer connection.
Forward ICE candidates from your `onicecandidate` handler. Add incoming candidates from the server to your peer connection.
Once ICE negotiation completes, bidirectional audio streams between the browser and the PolyAI agent.
Send a `close` message when the conversation ends.
# Agent
Source: https://docs.poly.ai/behavior/general/agent
Define your agent's personality and role.
Use the Agent page to control your agent's character – its **personality** and **role**. These two fields affect how responses sound at their most basic level.
Configure personality and role in **Behavior > General**. The same page also contains the [Behavior](/behavior/general/rules) section for hard rules and constraints.
**Where is the greeting?** Configured per channel — **Voice > Advanced settings** or **Messaging > Advanced > Chat configuration**. Legacy projects without channel-specific settings still configure it on this page.
## Greeting
The agent's opening line is configured per-channel:
* **Voice greeting** – set under **Voice > [Voice configuration](/voice-channel/advanced/call-settings)**.
* **Webchat greeting** – set under **Messaging > Advanced > Chat configuration**.
The greeting goes directly to [TTS](https://en.wikipedia.org/wiki/Speech_synthesis) without LLM processing — write it exactly as you want it spoken. See [conversation flow](/essentials/order).
You can include [tool calls](/tools/introduction) and [variant attributes](/knowledge/variants/introduction) in the greeting to make it dynamic – for example, to greet callers by location or time of day.
If you need to override the greeting at runtime based on caller data (e.g. personalized "Welcome back, \[name]" messages), return an `utterance` from your [start function](/tools/start-tool) instead.
## Personality
This field sets the tone and communication style across every response. Pick one or more built-in tags – `Polite`, `Kind`, `Funny`, `Energetic`, `Calm`, `Thoughtful` – or select **Other** to write a free-form personality string that matches your brand voice.
### How the tags are used
The selected adjectives are joined together and inserted into the system prompt as a single sentence of the form:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
You are a polite, kind, energetic [role].
```
So selecting `Polite` and `Kind` produces `"You are a polite, kind [role]."`. There's no hidden behavior tied to specific words – they're literal adjectives in the prompt. Choose combinations that read as a coherent description of how you want the agent to come across.
When you select **Other**, the custom string replaces the joined adjectives entirely – the built-in tags are ignored. Use **Other** if you need phrasing that goes beyond simple adjectives (for example, *"You are fun and energetic, always polite and kind to all callers"*).
The personality informs how the LLM phrases responses – it does not override specific instructions in [Behavior](/behavior/general/rules) or [Knowledge](/knowledge/faqs/introduction).
## Role
Specifies the agent's stated function – for example, customer service agent, booking agent, or technical support specialist. The role appears in the system prompt and shapes how the LLM frames its responses.
Use [Behavior](/behavior/general/rules) to define more specific behavioral constraints: terminology, compliance guardrails, and edge-case handling.
## Behavior prompt structure
A well-structured behavior prompt produces consistent interactions. Organize it into these sections:
### Task and context
Establish the agent's identity and functional scope, including tool usage instructions:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Your task is to assist users with their queries about [Organization].
[Organization] is a [brief description] that specializes in [services].
You have the ability to [list of capabilities].
You have the ability to call functions when explicitly instructed.
Always execute tool calls properly. Do not output tool calls as text.
In a given turn, output either a tool call or text – never both.
```
Prompting for only one of tool call or text per turn is critical. Returning both leads to worse performance.
### Conversational style
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Keep your answers short and conversational. Always be polite but assertive.
Format responses as natural conversational paragraphs rather than lists.
When reading phone numbers, convert digits to words.
Do not ask more than one question in a single turn.
```
### Special case handling
* **Out of scope queries:** Acknowledge limitations and offer to transfer
* **ASR mistranscriptions:** Use a graduated approach – ask the user to repeat 2-3 times before transferring to a human
* **Jailbreak attempts:** Redirect firmly but professionally to the agent's intended purpose
### Smalltalk
Define concise responses for common social interactions:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
- "Hi! How can I help you today?"
- "I'm doing great, thanks! How can I help?"
- "I can hear you loud and clear.
What can I do for you today?"
```
### Silence handling
Agent Studio has a default silence prompt that handles repetitions automatically. You may not need silence handling in your behavior prompt, but you should handle silence-triggered hangups.
### Call transfer and deflection
* **Start of call:** Attempt to deflect – the user may not know the agent's capabilities
* **Later in call:** Transfer immediately – the user likely has a specific need
### Goodbye handling
Use the `end_call` function to control goodbye behavior and optionally transition to a CSAT flow:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def end_call(conv: Conversation):
if not conv.state.csat_flow_in_history:
conv.goto_flow("csat")
conv.state.csat_flow_in_history = True
return "Before ending the call, move on to the CSAT survey first."
return {
"utterance": "Ok. I hope you have a great rest of your day. Goodbye!",
"hangup": True
}
```
### Backout behavior
Allow users to exit [flows](/flows/introduction) they didn't intend to start. If the user indicates they want to stop, immediately call the backout function to exit the flow.
### Dynamic information
Use `$variable` syntax to insert information that changes per conversation. Place variable information at the end of the prompt for efficient caching.
## Related pages
Set global behavioral constraints for tone, compliance, and terminology.
Choose the LLM backbone that powers your agent's responses.
Override the greeting dynamically based on caller data.
# Rules
Source: https://docs.poly.ai/behavior/general/rules
Set global rules to control your agent behavior, tone, and compliance.
Use behavioral rules to enforce consistency across every conversation. Define correct terminology, compliance guardrails, pronunciation overrides, and edge-case handling, and ensure LLM does not improvise on these decisions.
Define your agent's behavior by going to **[Behavior > General](/behavior/introduction)** and then scrolling to the **Behavior** section.
**Example:**
For a museum agent that always refers to "exhibits" instead of "artworks":
*"Always refer to 'artworks' as **exhibits**. Do not use the term 'artworks' in any context."*
### Types of rules
#### 1. Behavior and interaction guidelines
Specify how the agent interacts with users:
* **Tone**: Choose formal, casual, empathetic, or calm tones.
* **Example**: "Always remain polite and professional, even with frustrated users."
* **Language style**: Simplify language or avoid jargon as needed.
* **Example**: "Use clear, simple language suitable for non-technical users."
* **Consistency**: Align responses with branding and messaging.
* **Example**: "Always address visitors as 'guests' rather than 'customers.'"
#### 2. Task execution
* **Explicit instructions**: Clearly define actions.
* **Example**: "If asked about upcoming events, provide the event details and offer to send them in a text message."
* **Response scope**: Limit responses to specific tasks or topics.
* **Example**: "Only answer questions related to museum exhibits. Avoid general queries outside this domain."
#### 3. Content restrictions
Set boundaries for what the agent can or cannot say:
* **Sensitive topics**: Avoid prohibited subjects. For details, see the [Self-serve dashboards](/analytics/dashboards/introduction).
* **Example**: "Do not discuss politics, religion, or personal opinions."
* **Accuracy**: Avoid fabricated or uncertain answers.
* **Example**: "If unsure, direct the user to a staff member or a verified source."
### Best practices
1. **Be specific**: Avoid ambiguity.
* **Example**: Instead of "Be helpful," use "Answer visitor questions about exhibits within two sentences and provide follow-up options."
2. **Provide examples**: Demonstrate expected interactions and responses.
* **Example**:
* Visitor: "What time does the museum close?"
* Agent: "The museum closes at 6 PM. Would you like a list of activities available before closing?"
3. **Plan for edge cases**: Handle emergency or high-risk scenarios.
* **Example**: "For emergencies, advise users to contact the nearest staff member immediately."
4. **Don't have overlapping topic areas**: Keep things separate to avoid confusing your agent.
* **Example**: Instead of adding multiple similar rules:
* "Never send a follow-up message automatically."
* "If a follow-up message is available, always offer it."
* "Never send a follow-up message without user consent."
Use a single rule:
* "Only send follow-ups if the user agrees."
5. **Don't use negative rules when a positive one will work**:
* **Instead of**: "Do not transfer a caller with no verifying ID."
* **Use**: "Always verify ID before transferring."
6. **Test and iterate**: Regularly review and refine rules.
### Example behavior
* **Handoff to a staff member**
* **Example**: "If visitors ask for a staff member or seem confused, notify the front desk and provide directions."
* **When handling sensitive queries**
* **Example**: "For questions about controversial exhibits, respond: 'I'm sorry, I can't provide additional context. Please contact our curator for more information.'"
* **Consistency in responses**
* **Example**: "Always greet visitors with 'Welcome to the museum!' before answering their question."
### Scope rules to a channel or language
You can scope rules (and any prompt content) to specific channels or languages using conditional tags. Each opening tag requires a matching closing tag (`` or ``):
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Always confirm the caller's name before proceeding.
Offer clickable links instead of reading URLs aloud.
Reply STOP to unsubscribe.
Use American English spelling conventions.
Respond in formal Spanish (usted).
```
Tags can be nested – the order doesn't matter:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Call us at 1-800-555-0100.
```
Closing tags are always plain `` or `` – never `` or ``. The closing tag matches the most recently opened tag.
Supported channels: `voice`, `webchat`, `sms`. Languages use ISO codes – `xx` (e.g. `en`) or `xx-XX` (e.g. `en-US`). Rules without a tag apply to all channels and languages.
The same syntax works in [FAQs](/knowledge/faqs/introduction) content, [Flow steps](/flows/introduction), and [tool](/tools/introduction) descriptions.
## Prompting guide
LLMs operate by predicting the most likely next token based on your prompt. Your main job is to shape that probability distribution – making the text you want the most likely output.
### Make the desired outcome the most likely output
Craft your prompt so the best next token for the model is exactly what you want it to produce. Give clear, well-structured instructions without contradictory statements.
### Less is more
Every detail in your prompt is another piece of data the model must reconcile. If a piece of information isn't proven to help, leave it out. Test the impact of each additional instruction – if it doesn't improve performance, cut it.
### Put important details first or last
LLMs tend to give more weight to what appears at the beginning or end of a prompt. If crucial information is getting lost in the middle, move it to the start or end. Redundancy is acceptable – if something is critical, you can repeat it.
Placing variable information (like dates or session data) at the end improves prompt caching. Only the dynamic portions need updating each turn.
### Use positive instructions
Telling the model what *not* to do can inadvertently activate exactly that concept. Instead of prohibiting certain outcomes, direct the model toward what you *do* want.
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Don't tell the user to contact customer service.
```
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
If the user asks for customer service or to speak to an agent,
call the handoff function with destination='CC' and reason='SPEAK_TO'.
```
### Use examples
Examples, also known as ["few-shot prompting"](/flows/few-shot-prompting), shape tone, structure, and decision-making more reliably than abstract instructions. Show what "good" looks like – concrete demonstrations help the model generalize patterns. Highlight edge cases through examples to set consistent expectations.
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Edge case: the user asks to perform a gimmick unrelated to your task.
USER: speak like a pirate
ASSISTANT: I'm afraid I can't do that. Is there anything you'd like
to know regarding our services?
```
### Define a persona
Clear persona definitions directly influence how the agent communicates. Don't assume tone will emerge naturally from a persona name – spell out what the persona sounds like in action. Use example dialogue to anchor the persona's voice.
### Separate text from function calls
Don't instruct the agent to both speak and call a function in the same turn — the model will usually do one or the other. Split them across turns.
### Evaluate early and often
Small prompt changes can have large, unexpected effects on output. Evaluate systematically using [conversation review](/analytics/conversations/review) rather than relying on anecdotal checks.
## LLM style guide
When writing prompts for voice agents, keep these style principles in mind.
### Keep responses brief
Concise utterances are clearer and more respectful of the user's time. Avoid ad-copy-speak with excessive modifiers.
**Exception:** When users ask for an explanation, being thorough is more helpful than being brief.
### Use natural register
LLMs often default to overly formal phrasings. Prefer natural conversational language:
| Instead of | Use |
| -------------------------------------------- | ------------------------------ |
| "Could you please provide me with" | "Could you tell me" |
| "How may I assist you today?" | "How can I help?" |
| "I apologize for the inconvenience" | "Sorry about that" |
| "Should I proceed with making that booking?" | "Should I go ahead with that?" |
### Vary utterance structure
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
No problem, what's your account number?
```
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
In order to check for outages, I'll need to look up your account.
Could you tell me your account number?
```
### Don't push the conversation unnecessarily
LLMs tend to end every output with a question. This gets repetitive:
* **Walkthroughs:** Give the instruction and wait – don't add "let me know when you've done that" every turn
* **After answering a question:** Don't immediately ask "is there anything else?" – give the user a chance to acknowledge or follow up
## Automate with the Agents API
Rules are just text, which makes them easy to template, diff, and sync from a source-controlled file.
The [Agents API](/api-reference/agents/introduction) exposes the same behavior field that the UI edits — useful for applying a shared rule set across many agents or for A/B testing prompts on a branch. You can read `main` directly, but writes must go to a branch that you then merge back into `main` (merging also publishes to Sandbox).
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Read the current behavior from main
curl https://api.us.poly.ai/v1/agents/AGENT_ID/branches/main/behavior \
-H "x-api-key: $POLYAI_API_KEY"
# To change it: create a branch, update behavior on it, then merge to main
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/branches \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "branchName": "update-tone" }' # response includes { "branchId": "BRANCH-…" }
curl -X PATCH https://api.us.poly.ai/v1/agents/AGENT_ID/branches/BRANCH_ID/behavior \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"behavior": "Always refer to artworks as exhibits. Use a warm, curious tone."
}'
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/branches/BRANCH_ID/merge \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "deploymentMessage": "Update tone" }'
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os, requests
BASE = "https://api.us.poly.ai"
HEADERS = {"x-api-key": os.environ["POLYAI_API_KEY"]}
# Apply the same behavior rules across a fleet of agents.
# main is read-only, so each edit goes on a branch that's then merged.
AGENT_IDS = [...]
BEHAVIOR = open("rules/support-agent.md").read()
for agent_id in AGENT_IDS:
branch_id = requests.post(
f"{BASE}/v1/agents/{agent_id}/branches",
headers=HEADERS,
json={"branchName": "sync-rules"},
).json()["branchId"]
requests.patch(
f"{BASE}/v1/agents/{agent_id}/branches/{branch_id}/behavior",
headers=HEADERS,
json={"behavior": BEHAVIOR},
)
requests.post(
f"{BASE}/v1/agents/{agent_id}/branches/{branch_id}/merge",
headers=HEADERS,
json={"deploymentMessage": "Sync behavior rules"},
)
```
## Related pages
Set the greeting, personality, and role that shape first impressions.
Choose the LLM that interprets and applies your behavioral rules.
Define topic-level behavior.
Read and update behavior rules via the Agents API.
# Guardrails
Source: https://docs.poly.ai/behavior/guardrails/introduction
Platform safety guardrails that protect your agent in production, with observability in conversation transcripts.
Platform guardrails are pre-built safety protections that PolyAI applies to every conversation. Each one targets a common production risk. All five are enabled by default and can be toggled off at any time.
The **Behavior** page is organized into three tabs — **General**, **Language**, and **Guardrails**. Manage in **Advanced behavior settings**, where the input/output [safety filters](/behavior/guardrails/safety-filters) live.
Platform guardrails are applied automatically, standardized across projects, and maintained by PolyAI — no per-agent prompt engineering required.
## The five guardrails
The underlying prompts are managed by PolyAI and are not currently visible or editable in Agent Studio.
| Guardrail | What it does |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Jailbreak & Prompt Defence** | Blocks attempts to extract your agent's instructions, override its behavior, or impersonate a different AI. |
| **Scope & Hallucination Control** | Restricts the agent to its knowledge base. Prevents fabrication of phone numbers, prices, or policies. |
| **AI Identity & Confidentiality** | Prevents the agent from disclosing which LLM, provider, or platform powers it. |
| **Emergency & Crisis Escalation** | Escalates immediately if a caller expresses suicidal ideation, self-harm, threats, or a medical emergency. Catches conversational distress signals that content filters miss. |
| **Tool Call Integrity** | Prevents the agent from speaking internal function calls or tool names aloud. |
## Enable or disable a guardrail
1. Open **Behavior** and select the **Guardrails** tab.
2. Toggle a guardrail off or on. Disabling prompts you to confirm.
3. Test with **Chat with Agent** before promoting to a higher environment.
## Observe when guardrails fire
Guardrail events are recorded on every conversation.
* **In a transcript:** open a conversation in [Conversations](/analytics/conversations/review), open transcript display options, and toggle on **Guardrails**. Each turn where a guardrail fired is annotated inline.
* **Across conversations:** filter by guardrail in the **QA category** of the conversation filters.
* Guardrails are stored per-project and travel through [environments and versions](/environments-and-versions/introduction) – the configuration is part of each published version.
## How guardrails fit with safety filters
Platform guardrails are **prompt-level** instructions to the LLM. They run alongside the input/output [safety filters](/behavior/guardrails/safety-filters):
* **Safety filters** classify each user input and agent output against hate, violence, sexual, and self-harm categories at the model layer. Configure thresholds per category in **Advanced behavior settings** (via the **Advanced settings** button on the Behavior page) and override per channel.
* **Jailbreak detection** is always-on at the model layer and is independent of the Jailbreak & Prompt Defence guardrail. The guardrail tells the LLM how to respond; the detector blocks input upstream.
* **Emergency & Crisis Escalation** catches conversational distress signals that content filters miss – for example, "I don't want to be here anymore" said in a measured tone.
Use guardrails and safety filters together. They protect different layers.
## Related pages
Add custom rules for terminology, tone, and edge cases on top of the platform guardrails.
See guardrail events inline in transcripts and filter by guardrail in QA category.
Per-channel content filters for hate, sexual, violence, and self-harm.
Build a dashboard to track jailbreak attempts and other safety signals.
Validate guardrail behavior in the preview before promoting a version.
# Safety filters
Source: https://docs.poly.ai/behavior/guardrails/safety-filters
Per-channel content filters that classify user input and agent output across hate, sexual, violence, and self-harm – and always-on jailbreak detection.
Safety filters are PolyAI's content classifiers. They inspect every user input and every agent output in real time and block anything that exceeds the severity threshold you set for each category. Filters combine PolyAI's models with third-party services such as [Azure OpenAI content safety](https://learn.microsoft.com/en-us/azure/ai-services/content-safety/).
Configure project-wide defaults in **Advanced behavior settings** — open the **Behavior** page and select the **Advanced settings** button (top-right) to reach the **Safety filters** section. From there you can override the defaults per channel in **Voice > Advanced settings** and **Messaging > Advanced > Chat configuration**.
## How filters run
Filters run on both sides of every turn:
* **User input** – classifies what the caller or visitor says before the agent sees it. Blocked input is replaced with a safe fallback response.
* **Agent output** – classifies what the agent is about to say. Blocked output is suppressed and the agent recovers with a safe response.
Filtering is independent of the LLM prompt – it's a separate model layer, so it works regardless of agent behavior, flow, or prompt changes.
## Categories and severity levels
Each category has four severity levels. Pick the level per category that matches your use case and compliance requirements.
| Severity | Behavior |
| ------------ | --------------------------------------------- |
| **Off** | Category is not enforced. |
| **Lenient** | Block high severity content only. |
| **Moderate** | Block medium and high severity content. |
| **Strict** | Block low, medium, and high severity content. |
Content that attacks or discriminates based on race, ethnicity, nationality, religion, gender identity, sexual orientation, disability, or appearance. Includes bullying, harassment, and slurs.
Content involving explicit anatomy, sexual acts, or romantic/erotic themes – including abusive or exploitative content.
Physical harm, threats, weapons, terrorism, and other violent acts or intimidation.
Mentions of suicide, self-injury, eating disorders, or content about hurting oneself.
**Jailbreak detection is always on.** A separate jailbreak attack filter watches for attempts to bypass or disable safety features. It can't be turned off and is independent of the per-channel severity sliders.
## Project defaults vs. channel overrides
Safety filters are configured on a **per-channel basis** with project-wide defaults as a fallback.
* **Project defaults** – set in **Advanced behavior settings** (the **Advanced settings** button on the **Behavior** page). Apply to any channel that does not have its own overrides enabled.
* **Voice channel** – override in **Voice > Advanced settings**. See [Voice configuration → Safety filters](/voice-channel/advanced/call-settings#safety-filters).
* **Chat channel** – override in **Messaging > Advanced > Chat configuration**. See [Chat configuration → Safety filters](/messaging-channel/advanced/chat-configuration#safety-filters).
When a channel has its own filters enabled, the channel values win for that channel. When a channel has filters disabled, the project defaults apply.
Review your use case and compliance requirements before relaxing any category.
## Edit filters
1. Open **Behavior**, select **Advanced settings** (top-right), and find the **Safety filters** section on **Advanced behavior settings** to set the project-wide baseline.
2. To override for a specific channel, open the channel's configuration page (**Voice** or **Messaging**), enable safety filters, and adjust the sliders.
3. Save. Voice and project-level changes follow the standard [environment branching](/environments-and-versions/introduction); chat changes take effect immediately on save.
4. Test with **Chat with Agent** or a sandbox phone number before promoting.
## Monitor filter activity
Every filter trigger is recorded on the conversation. Monitor across conversations from the [Self-serve dashboards](/analytics/dashboards/introduction):
* **Calls managed for risk** – how often filters fired (count and percentage).
* **Caller utterance category** – breakdown by hate, sexual, violence, and self-harm.
* **Caller utterance risk level** – risk distribution of incoming messages.
* **Distribution of flagged calls** – trend over time.
To inspect a single conversation, open it in [Conversation review](/analytics/conversations/review) – flagged turns are annotated inline. Filter the conversations list by safety category in the **QA category** filter.
## Language support
Filters have been trained and tested in English, German, Japanese, Spanish, French, Italian, Portuguese, and Chinese. Other languages are supported but performance may vary – test thoroughly in your target language before going live.
## How safety filters fit with Guardrails
Safety filters and [platform Guardrails](/behavior/guardrails/introduction) protect different layers and are designed to run together:
* **Safety filters** classify each input and output against hate, sexual, violence, and self-harm before/after the LLM. They block content at the model layer.
* **Guardrails** are prompt-level instructions that shape how the LLM responds – for example, refusing to disclose its identity or escalating on a crisis signal.
* **Jailbreak detection** (always-on filter) blocks malicious input upstream; the **Jailbreak & Prompt Defence** guardrail tells the LLM how to respond if anything slips through.
* **Emergency & Crisis Escalation** (a guardrail) catches conversational distress signals that the self-harm filter misses – for example, "I don't want to be here anymore" said in a measured tone.
Use both. They're complementary, not redundant.
## Best practices
* **Test thoroughly** – run your own tests to validate filter behavior against representative content from your domain.
* **Don't default to Strict** – find the level that prevents harm without over-filtering legitimate calls. Over-filtering causes safe fallbacks that hurt CX.
* **Be consistent across variants** – when running [A/B tests](/testing/ab-testing) or multiple agents, keep filter levels aligned so reporting is comparable.
* **Review flagged calls weekly** – use the [Self-serve dashboards](/analytics/dashboards/introduction) to catch drift before it becomes a compliance issue.
## Related pages
Platform-level prompt protections that run alongside safety filters.
Build a dashboard to monitor flagged conversations and filter trigger trends.
Per-channel filter overrides for voice.
Per-channel filter overrides for chat.
# Behavior
Source: https://docs.poly.ai/behavior/introduction
Set and edit your agent's global rules, model, personality, and fixed greeting line.
Configure your agent's greeting, personality, model, and rules in **Behavior > General**.
## When to configure each setting
| Setting | When to use | Impact |
| --------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| **[Agent](/behavior/general/agent)** | For the initial setup or when rebranding | Personality (tone) and role. The greeting is configured per-channel under **Voice/Chat configuration**. |
| **[Model](/behavior/models/model-use)** | New agent setup, or optimizing for cost/latency/quality | LLM selection. [Raven](/behavior/models/raven) recommended, but other models are also available. |
| **[Behavior](/behavior/general/rules)** | Consistent terminology, compliance, edge-case handling | Add your global constraints – tone, language style, compliance guardrails, pronunciation overrides. |
| **[Guardrails](/behavior/guardrails/introduction)** | Safety protections against jailbreaks, hallucination, crisis escalation, and more | Toggle the five built-in platform guardrails. Lives under **Behavior > Guardrails**. |
## Configuration sections
Personality and role.
Raven (recommended), OpenAI, Amazon Bedrock, or your own endpoint.
Language style, compliance, edge cases, and pronunciation overrides.
Built-in platform protections against jailbreaks, hallucination, and crisis events.
Multilingual settings, language coverage, and translation overrides.
## Automate with the Agents API
If you spin up new agents from a reference template — for example, one agent per client or market — you can create and duplicate them from code.
The [Agents API](/api-reference/agents/introduction) has endpoints for [creating](/api-reference/agents/endpoint/agents/create-agent) and [duplicating](/api-reference/agents/endpoint/agents/duplicate-agent) agents.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Create a new agent from scratch
curl -X POST https://api.us.poly.ai/v1/agents \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Support Agent", "description": "Tier 1 support, US" }'
# Duplicate an existing agent as a starting point
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/duplicate \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Support Agent - UK" }'
```
## Related pages
Create, duplicate, and configure agents from code.
Publish and promote versions through Sandbox, Pre-release, and Live.
# Language coverage
Source: https://docs.poly.ai/behavior/language/language-coverage
Languages and models supported across LLM, ASR, and TTS on the PolyAI platform.
This page is the canonical reference for which languages PolyAI supports and which models cover them across LLM, speech recognition (ASR), and text-to-speech (TTS).
For setup and runtime behavior, see [Multi-language](/behavior/language/multilingual). For model selection, see [Model](/behavior/models/model-use) and [Raven](/behavior/models/raven).
## At a glance
| Layer | Coverage | Notes |
| ----------------------- | ------------------ | ---------------------------------------------------------------------------------------------------- |
| LLM response languages | 73 (table below) | Compiled list of BCP 47 codes accepted by the platform. |
| Raven (recommended LLM) | 24 languages | Conversation-tuned. Some languages outside this list (for example Danish) require a third-party LLM. |
| Third-party LLMs | Provider-dependent | Pick a model that supports your target language; channel availability varies. |
| ASR (speech-to-text) | Provider-dependent | Routed automatically per language with fallback. |
| TTS (text-to-speech) | Provider-dependent | Voice availability varies by provider and language. |
## LLM language coverage
### Raven (recommended)
[Raven](/behavior/models/raven) is purpose-built for customer service across voice and chat. Raven 3.5 supports the following 24 languages:
Arabic, Bulgarian, Cantonese, Croatian, Czech, Dutch, English, French, German, Greek, Hindi, Hindi (Romanized/Hinglish), Italian, Japanese, Korean, Mandarin (PRC), Mandarin (Taiwan), Polish, Portuguese (Brazil), Portuguese (Portugal), Serbian, Spanish (US), Swedish, Turkish.
**Strongest performance** relative to general-purpose models: Cantonese, Italian, Korean, Mandarin (China), Mandarin (Taiwan), Spanish (US).
You can keep prompts and knowledge in English and set the response language to your target language – Raven responds consistently in the target language. Quality improves further if you translate prompts and add examples in the target language.
### Third-party LLMs
When you select an OpenAI or Amazon Bedrock model on the [Model](/behavior/models/model-use) page, language coverage matches that provider's official support. Use a third-party LLM when:
* Your target language is not on the Raven list above, or
* You need a capability only the third-party model provides.
### Full list of supported response languages
PolyAI accepts the BCP 47 codes below as response languages. Pass the code in the UI or via `conv.set_language()`.
| Language | Code |
| --------------------- | ----------- |
| Albanian | sq-AL |
| Amharic | am-ET |
| Arabic | ar |
| Armenian | hy-AM |
| Bengali | bn-BD |
| Bosnian | bs-BA |
| Bulgarian | bg-BG |
| Burmese | my-MM |
| Cantonese | yue-Hant-HK |
| Catalan | ca-ES |
| Chinese (China) | zh-CN |
| Chinese (Taiwan) | zh-TW |
| Croatian | hr-HR |
| Czech | cs-CZ |
| Danish | da-DK |
| Dutch (Belgium) | nl-BE |
| Dutch (Netherlands) | nl-NL |
| English (Australia) | en-AU |
| English (Canada) | en-CA |
| English (New Zealand) | en-NZ |
| English (Singapore) | en-SG |
| English (UK) | en-GB |
| English (US) | en-US |
| Estonian | et-EE |
| Finnish | fi-FI |
| French (Belgium) | fr-BE |
| French (Canada) | fr-CA |
| French (France) | fr-FR |
| Georgian | ka-GE |
| German (Germany) | de-DE |
| Greek | el-GR |
| Gujarati | gu-IN |
| Hebrew | he-IL |
| Hindi | hi |
| Hungarian | hu-HU |
| Icelandic | is-IS |
| Indonesian | id-ID |
| Italian (Italy) | it-IT |
| Japanese | ja-JP |
| Kannada | kn-IN |
| Kazakh | kk-KZ |
| Korean | ko-KR |
| Latvian | lv-LV |
| Lithuanian | lt-LT |
| Macedonian | mk-MK |
| Malay | ms-MY |
| Malayalam | ml-IN |
| Marathi | mr-IN |
| Mongolian | mn-MN |
| Norwegian | nb-NO |
| Persian (Farsi) | fa-IR |
| Polish | pl-PL |
| Portuguese (Brazil) | pt-BR |
| Portuguese (Portugal) | pt-PT |
| Punjabi | pa-IN |
| Romanian | ro-RO |
| Russian | ru-RU |
| Serbian | sr-RS |
| Slovak | sk-SK |
| Slovenian | sl-SI |
| Somali | so-SO |
| Spanish (Spain) | es-ES |
| Spanish (US) | es-US |
| Swahili | sw-KE |
| Swedish | sv-SE |
| Tagalog (Filipino) | tl-PH |
| Tamil | ta-IN |
| Telugu | te-IN |
| Thai | th-TH |
| Turkish | tr-TR |
| Ukrainian | uk-UA |
| Urdu | ur-PK |
| Vietnamese | vi-VN |
Serbian uses `sr-RS` (Republic of Serbia). If you were previously using the non-standard `sr-SP` code, update your project configuration to `sr-RS`.
## Models
For full model descriptions and selection guidance, see [Model](/behavior/models/model-use).
### LLM models
| Provider | Model | Channel | Regions | Notes |
| ------------------------------ | ---------------------------- | ---------------- | ------------------ | ---------------------------------------------------------------------------------------------- |
| PolyAI | Raven 3.5 | Voice + Chat | All | Recommended. Auto-reasoning, out-of-domain detection, custom style following, built-in safety. |
| PolyAI | Raven V3 | Voice | All | Legacy. Superseded by Raven 3.5. No chat support. |
| PolyAI | Raven V2 | Voice | All | Legacy. Retained for existing deployments. |
| OpenAI (Azure / OpenAI direct) | GPT-5 | Voice + Chat | All | Strong reasoning. |
| OpenAI (Azure / OpenAI direct) | GPT-5 mini | Voice + Chat | All | Lower latency for mid-complexity workloads. |
| OpenAI (Azure / OpenAI direct) | GPT-5 nano | Voice + Chat | All | Fast, lightweight responses. |
| OpenAI (Azure / OpenAI direct) | GPT-5 chat | Chat | All | Optimized for extended dialogue. |
| OpenAI (Azure) | GPT-5.2 chat | Chat | All | Latest chat-optimized model. |
| OpenAI (Azure / OpenAI direct) | GPT-4.1 | Voice + Chat | All | Strong reasoning with improved cross-task performance. |
| OpenAI (Azure / OpenAI direct) | GPT-4.1 mini | Voice + Chat | All | Cost-effective, latency-focused. |
| OpenAI (Azure / OpenAI direct) | GPT-4.1 nano | Voice + Chat | All | Minimal compute, high throughput. |
| OpenAI (Azure / OpenAI direct) | GPT-4o | Voice + Chat | All | Balanced reasoning, speed, and cost. |
| OpenAI (Azure / OpenAI direct) | GPT-4o mini | Voice + Chat | All | High-volume / everyday workloads. |
| OpenAI | GPT realtime / realtime-mini | End-to-end voice | All | Speech-to-speech; behaves differently from text LLMs. |
| Amazon Bedrock | Claude Sonnet 4 | Voice + Chat | US-1, EU-W-1, UK-1 | Strong reasoning and safety alignment. |
| Amazon Bedrock | Claude 3.5 Haiku | Voice + Chat | US-1 only | Predictable tasks with strong safety alignment. Not currently configured in EU-W-1 / UK-1. |
| Amazon Bedrock | Nova Micro | Voice + Chat | All | Efficient general-purpose performance. |
| Custom | Fine-tuned OpenAI | Voice + Chat | All | Per-account fine-tuned OpenAI deployments. |
| Custom | Bring your own model | Voice + Chat | All | See [Bring your own model](/behavior/models/byom). |
### ASR providers
ASR providers wired into the platform today:
* Deepgram
* Google Cloud Speech-to-Text (v1 and v2)
* NVIDIA Riva
* Amazon Transcribe
* OpenAI (Whisper)
* Fano
* NVIDIA NeMo
The platform routes requests to the best-fit provider per language and use case, with automatic fallback. See [Keyphrases](/voice-channel/advanced/call-settings#keyphrases).
### TTS providers
TTS voice availability varies by provider and language. Browse what's available per language in the [Voice library](/voice-channel/voice-library).
* ElevenLabs
* Amazon Polly
* Azure Speech
* Cartesia
* Google Cloud Text-to-Speech
* Hume
* MiniMax
* Neuphonic
* OpenAI
* PlayHT
* Rime
* Custom TTS integrations
## Choosing a language and model
1. **Pick the response language** from the table above using its BCP 47 code.
2. **Check Raven coverage** – if your language is on the 24-language Raven list, Raven 3.5 is the recommended LLM.
3. **If Raven does not cover it** (for example Danish), select a third-party LLM in [Voice configuration](/voice-channel/advanced/call-settings) or [Chat configuration](/messaging-channel/advanced/chat-configuration). Verify the chosen model officially supports the language.
4. **Confirm regional availability** for Bedrock models if you are deploying outside US-1.
5. **Confirm a voice exists** for the language in the [Voice library](/voice-channel/voice-library). Prefer native voices over multilingual fallbacks.
6. **Configure ASR** – defaults usually work; for domain terms see [Keyphrases](/voice-channel/advanced/call-settings#keyphrases) and ASR biasing.
## Related pages
* [Multi-language](/behavior/language/multilingual) – set up, test, and route multilingual agents.
* [Model](/behavior/models/model-use) – compare LLM options and configure per channel.
* [Raven](/behavior/models/raven) – PolyAI's proprietary LLM family.
* [Bring your own model](/behavior/models/byom) – connect a custom LLM endpoint.
* [Keyphrases](/voice-channel/advanced/call-settings#keyphrases) – ASR settings and biasing.
* [Voice library](/voice-channel/voice-library) – browse voices per language and provider.
# Multi-language
Source: https://docs.poly.ai/behavior/language/multilingual
Configure your agent to handle conversations in multiple languages from a single project.
Use multi-language support when your agent serves callers who speak different languages. A multilingual agent detects the caller's language, switches mid-conversation if needed, and uses language-appropriate voices and content.
Languages are now managed directly in Agent Studio. This replaces the older `start_function` approach and one-project-per-language setups.
## Setting up multi-language support
Go to **Behavior** and find the **Additional languages** field. Select up to 10 additional languages from the dropdown. The **Response language** set during project creation becomes the **main language**.
Each language has its own voice configuration. Go to **Voice > Settings** where you'll see a voice card for each configured language. The main language card is tagged as "Main language".
* Select a voice for each language from the [Voice Library](/voice-channel/voice-library)
* You can configure separate voices for **Agent voice** and **Disclaimer** per language
* Multi-voice is supported per language, so you can assign multiple voices to a single language
If you need manual translation overrides for specific content, use the [Translations](/behavior/language/translations) page under **Behavior > Language**.
Use the **language dropdown** in Agent Chat to select a language and test your agent's behavior in each configured language.
### Supported languages
PolyAI supports **73 response languages** for multilingual agents, covering the full union of languages officially supported by the underlying LLM providers. Pass the BCP 47 code (e.g. `es-ES`) when setting the language in the UI or via [`conv.set_language()`](/tools/classes/conv-object#set_language).
| Language | Code |
| --------------------- | ------------- |
| Albanian | `sq-AL` |
| Amharic | `am-ET` |
| Arabic | `ar` |
| Armenian | `hy-AM` |
| Bengali | `bn-BD` |
| Bosnian | `bs-BA` |
| Bulgarian | `bg-BG` |
| Burmese | `my-MM` |
| Cantonese | `yue-Hant-HK` |
| Catalan | `ca-ES` |
| Chinese (China) | `zh-CN` |
| Chinese (Taiwan) | `zh-TW` |
| Croatian | `hr-HR` |
| Czech | `cs-CZ` |
| Danish | `da-DK` |
| Dutch (Belgium) | `nl-BE` |
| Dutch (Netherlands) | `nl-NL` |
| English (Australia) | `en-AU` |
| English (Canada) | `en-CA` |
| English (New Zealand) | `en-NZ` |
| English (Singapore) | `en-SG` |
| English (UK) | `en-GB` |
| English (US) | `en-US` |
| Estonian | `et-EE` |
| Finnish | `fi-FI` |
| French (Belgium) | `fr-BE` |
| French (Canada) | `fr-CA` |
| French (France) | `fr-FR` |
| Georgian | `ka-GE` |
| German (Germany) | `de-DE` |
| Greek | `el-GR` |
| Gujarati | `gu-IN` |
| Hebrew | `he-IL` |
| Hindi | `hi` |
| Hungarian | `hu-HU` |
| Icelandic | `is-IS` |
| Indonesian | `id-ID` |
| Italian (Italy) | `it-IT` |
| Japanese | `ja-JP` |
| Kannada | `kn-IN` |
| Kazakh | `kk-KZ` |
| Korean | `ko-KR` |
| Latvian | `lv-LV` |
| Lithuanian | `lt-LT` |
| Macedonian | `mk-MK` |
| Malay | `ms-MY` |
| Malayalam | `ml-IN` |
| Marathi | `mr-IN` |
| Mongolian | `mn-MN` |
| Norwegian | `nb-NO` |
| Persian (Farsi) | `fa-IR` |
| Polish | `pl-PL` |
| Portuguese (Brazil) | `pt-BR` |
| Portuguese (Portugal) | `pt-PT` |
| Punjabi | `pa-IN` |
| Romanian | `ro-RO` |
| Russian | `ru-RU` |
| Serbian | `sr-RS` |
| Slovak | `sk-SK` |
| Slovenian | `sl-SI` |
| Somali | `so-SO` |
| Spanish (Spain) | `es-ES` |
| Spanish (US) | `es-US` |
| Swahili | `sw-KE` |
| Swedish | `sv-SE` |
| Tagalog (Filipino) | `tl-PH` |
| Tamil | `ta-IN` |
| Telugu | `te-IN` |
| Thai | `th-TH` |
| Turkish | `tr-TR` |
| Ukrainian | `uk-UA` |
| Urdu | `ur-PK` |
| Vietnamese | `vi-VN` |
Serbian uses `sr-RS` (Republic of Serbia). If you were previously using the non-standard `sr-SP` code, update your project configuration to `sr-RS`.
Voice (TTS) coverage depends on the provider you select for each language. Some languages support fewer voices – see the [Voice Library](/voice-channel/voice-library) for what's available per language.
## How multilingual agents work
Multilingual agents can:
* **Detect the caller's language** automatically with ASR
* **Switch languages mid-conversation** if the caller changes language
* **Automatically switch voices** when the language changes, if a voice is configured for that language
* **Maintain language-specific knowledge** using language variants on FAQs
* **Filter content by language** using `` tags in prompts
* **Handle mixed-language queries** (code-switching)
Auto voice switching only works for the main language and configured additional languages. If the conversation changes to an unsupported language, the voice stays the same.
## Forcing a language by dialled number (DNIS routing)
If you serve different markets on different phone numbers and want to bypass auto-detection, set the language explicitly in `start_function` based on `conv.callee_number` (the number the caller dialled).
The language you pass to `conv.set_language()` must already be added under **Behavior > Additional languages** (or be the main language). If the code is not configured on the project, the agent falls back to the default language.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start_function(conv: Conversation):
spanish_dnis = {"+34911234567", "+34931234567"}
if conv.callee_number in spanish_dnis:
conv.set_language("es-ES")
else:
conv.set_language("en-US")
return str()
```
Use the full IETF locale code (e.g. `"es-ES"`, `"en-US"`, `"fr-FR"`), not the short form (`"es"`, `"en"`). See [`conv.set_language`](/tools/classes/conv-object#set_language) and [`conv.callee_number`](/tools/classes/conv-object#callee_number) for the full reference.
## Configuring voices per language
When multilingual support is enabled, the **Agent Voice** page shows separate voice sections organized by language.
1. Go to **Voice > Settings**
2. You'll see an **Agent** tab and a **Disclaimer** tab
3. On the **Agent** tab, each language has its own voice card
4. Click into a language card to select or change the voice
5. To assign multiple voices to a language, add them from the voice card – multi-voice is supported per language
**Voice quality tips:**
* Use native voices – don't use an English voice for Spanish
* Match regional accents – use Mexican Spanish for Mexico, Castilian for Spain
* Test pronunciation for language-specific characters
* Multilingual TTS models are convenient but may have slightly lower quality than language-specific models
You can also configure voices programmatically. See [Voice classes](/tools/classes/voice) for available providers including ElevenLabs, Cartesia, Hume, Rime, Minimax, PlayHT, and Google TTS.
## Conditional content filtering
Use `` tags to serve language-specific content within a single prompt, without needing separate variants:
```
Please hold while I check your account.
Por favor espere mientras reviso su cuenta.
```
Closing tags are always plain `` (or ``) – never ``. The closing tag matches the most recently opened tag.
This works in:
* [Behavior rules](/behavior/general/rules)
* [FAQs](/knowledge/faqs/introduction) content
* [Flow steps](/flows/introduction)
* [Function](/tools/introduction) descriptions
## Language variants on FAQs
FAQs support **language variants** so you can manage multilingual knowledge base content within a single agent. Each topic can have language-specific versions of its content and sample questions.
1. Go to **Knowledge > FAQs**
2. Create or edit a topic
3. Add **language variants** for each supported language
4. Translate sample questions and content for each variant
Sample questions must be in the same language as caller inputs – they are compared with user inputs during the retrieval process.
## Language-specific pronunciation rules
Pronunciation rules in [Advanced voice settings](/voice-channel/advanced/call-settings#pronunciation) are organized by language. Each language has its own set of rules, displayed as separate collapsible cards. Rules within a language card only apply to responses in that language. Rules with no language specified apply globally.
## What to translate
Some project content needs translation, and some does not:
| Area | Element | Translate? | Notes |
| --------------- | ------------------------------------ | :-------------------: | ---------------------------------------------------------------------- |
| **Knowledge** | Sample questions | | Must match user input language for retrieval |
| | Content | | Translate for brand accuracy and better output |
| | Topic names and actions | | Keep in English (used internally, not user-facing) |
| **SMS** | SMS content | | Translate anything user-facing |
| **ASR & Voice** | ASR keywords and corrections | | Leave in native language – these may differ significantly from English |
| | Response control and pronunciations | | Leave in native language – these may differ significantly from English |
| **Functions** | Python code | | Leave in English |
| | Function names and descriptions | | Leave in English |
| | Hard-coded responses and LLM prompts | | Translate only user-facing content (e.g., utterances) |
## General rules
* Keep **instructions** in English (e.g., "Ask for the user's phone number")
* Translate **example utterances** or scripted responses
* If it's directed at the **agent**, keep it in English. If it's going to be spoken aloud directly to the customer, translate it.
> `Ask the user for their number by saying "¿Me puedes dar tu número de teléfono?"`
### Function examples
If you're using a function with a hard-coded response, translate the user-facing string:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"utterance": "Respuesta fija en español aquí"
}
```
If you're re-prompting the LLM, you only need to translate example responses:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"content": "Inject prompt here"
}
```
### Accessing the current language in functions
You can access the caller's detected language in functions:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def dynamic_response():
current_language = conv.language
if current_language == "es":
return {"utterance": "Respuesta en español"}
else:
return {"utterance": "Response in English"}
```
### Accessing translations in functions
For hard-coded utterances that need language-specific versions, use the translations object:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.translations.tn_name
```
Or for translation keys with special characters:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
getattr(conv.translations, "name with special chars!!!!")
```
## Testing multilingual agents
The Agent Chat panel includes a **language dropdown** that lets you select a language to test with – similar to how you select variants.
1. Open Agent Chat
2. Select a language from the dropdown (defaults to the main language on the first turn)
3. Interact with your agent and verify it responds correctly
4. Switch languages mid-conversation to test detection and voice switching
## Reviewing multilingual conversations
Language information surfaces across Agent Studio so you can review and filter multilingual traffic:
* **Conversations table** – add the **Language** column from the **Column** menu to see which language was used in each conversation, then sort or scan for patterns.
* **Filter by language** – open **Filter** and add a `Language is …` condition to narrow the table to a specific language. The condition shows as a chip above the table and can be saved into a [Custom View](/analytics/conversations/views).
The filter builder supports combining language with other conditions (for example, **Environment includes Draft, Sandbox, Pre-release** AND **Language is French**) so you can carve out a per-language QA queue.
* **Review side panel** – the panel header shows the detected language for the conversation, and per-turn language information appears alongside the transcript when the agent switched languages mid-call.
* **Audio management** – cached audio files include language metadata so you can identify and manage TTS audio per language.
## Related pages
Manually override auto-translations for specific content in your agent's responses.
Maintain and optimize your multilingual agent over time.
Browse and select voices per language for your agent.
Configure language-specific pronunciation rules for natural speech.
# Translations
Source: https://docs.poly.ai/behavior/language/translations
Manually override auto-translated content for multilingual agents.
The Translations page is only visible for [multilingual projects](/behavior/language/multilingual) with additional languages configured. Translations apply agent-wide – to both **voice** and **chat** channels.
Agent Studio can build agents in English that respond to callers in other languages – the LLM handles most translation automatically. However, some content can't be auto-translated well: hard-coded utterances in functions, delay control responses, and phrases where cultural nuances matter. The Translations page gives you a central place to manage these overrides.
You create **translation cards** that store language-specific versions of content, then reference them throughout your project using a translation key.
You don't need to add every utterance to the Translations page. Only use it for content where the auto-translation isn't good enough. If you're satisfied with the LLM's translations, you don't need this page at all.
## When to use translations
Translations are useful when:
* Auto-translated phrasing sounds unnatural – for example, "Please hold" auto-translates correctly to British English but "Please bear with me" sounds more natural
* Concepts don't exist in the target language – for example, "Spell the name" has no direct equivalent in Mandarin or Japanese, so a direct translation sounds unnatural
* Domain-specific terms need precise translations that the LLM gets wrong
* Cultural nuances require different phrasing – extra politeness levels in Japanese, gender-based formality in Hindi or Polish
* Hard-coded utterances in functions need language-specific versions
* Greeting or disclaimer messages need manual translation
## How it works
1. Create a **translation card** with the content you want translated
2. The card auto-translates to all configured languages on save
3. Manually edit any translation that needs improvement – these are marked as "Manually Translated"
4. Reference the card in your project using its **Translation Key**
## Creating a translation card
1. Go to **Behavior > Language > Translations**
2. Click **Add translation**
3. Enter the content in your main language
4. Set a **Translation Key** – this is how you'll reference the card elsewhere
5. Save – auto-translations are generated for all configured languages
## Manually overriding translations
After a card is created, each language shows a status:
* **Auto-Translated** – generated automatically on save
* **Manually Translated** – you've edited the translation for this language
To override a translation, click into the language field and edit the text directly.
## Using translations in your project
### In prompts and content
Insert a translation using the action menu (similar to inserting a variant) in:
* Greeting and disclaimer messages
* [Behavior rules](/behavior/general/rules) (style guides)
* [Prompts](/knowledge/faqs/introduction) in FAQs
* [Delay control](/tools/delay-control) responses
* SMS templates
### In functions
For hard-coded utterances in functions, access translations using the `conv.translations` object:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.translations.tn_greeting
```
For translation keys with special characters:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
getattr(conv.translations, "key with special chars")
```
## Language-specific style guides
Translation cards handle specific phrases, but broader behavioral differences across languages are best managed in [Behavior rules](/behavior/general/rules). Add language-specific style guides to make the agent behave more naturally in each language:
* Japanese – extra politeness and honorific language
* Hindi and Polish – gender-based formality
* German – formal "Sie" vs informal "du" based on context
* Spanish – regional variation between Latin America and Spain
Use `` tags in behavior rules to scope style guides to specific languages.
## Best practices
* **Only translate what needs overriding.** If auto-translation works, don't add it to the Translations page.
* **Use descriptive translation keys.** Keys like `tn_hold_message` are easier to manage than `tn_1`.
* **Test with native speakers** before going live – auto-translations may be technically correct but sound unnatural.
* **Keep the main language content up to date.** When you edit the main language text on a card, auto-translations regenerate for languages that haven't been manually overridden.
## Related pages
Control how your agent pronounces specific terms.
Block or log specific phrases in agent responses.
Configure languages and voices for your agent.
# Bring your own model (BYOM)
Source: https://docs.poly.ai/behavior/models/byom
Connect your own LLM endpoint to PolyAI.
PolyAI supports **bring-your-own-model (BYOM)** with a simple API integration. If you run your own LLM, expose an endpoint that follows the OpenAI [`chat/completions`](https://platform.openai.com/docs/api-reference/chat/create) schema and PolyAI will treat it like any other provider.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant Agent as PolyAI Agent
participant Endpoint as Your BYOM Endpoint
Agent->>Endpoint: POST /chat/completions (OpenAI format)
Note over Endpoint: Your model processes the request
Endpoint-->>Agent: Response (OpenAI format)
Note over Agent: Agent uses response in conversation
```
## Overview
Accept and return data in the OpenAI `chat/completions` format.
PolyAI can send either an `x-api-key` header **or** a Bearer token.
Support streaming responses using `stream: true` for lower latency.
## API endpoint
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"model": "your-model-id",
"messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "What's the weather today?" }
],
"temperature": 0.7,
"top_p": 1.0,
"stream": false
}
```
You might receive extra OpenAI-style fields such as `frequency_penalty`, `presence_penalty`, etc.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1712345678,
"model": "your-model-id",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "It's sunny today in London."
},
"finish_reason": "stop"
}
]
}
```
If `stream` is `true`, send Server-Sent Events (SSE) mirroring OpenAI's format:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
data: {
"id": "...",
"object": "chat.completion.chunk",
"choices": [{
"delta": { "content": "Hello" },
"index": 0,
"finish_reason": null
}]
}
data: {
"choices": [{
"delta": {},
"index": 0,
"finish_reason": "stop"
}]
}
data: [DONE]
```
## Authentication
| Method | Header sent by PolyAI |
| ----------- | ---------------------------------- |
| **API Key** | `x-api-key: YOUR_API_KEY` |
| **Bearer** | `Authorization: Bearer YOUR_TOKEN` |
Configure your server to accept **one** of the above.
## Sample implementation (Python / Flask)
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from flask import Flask, request, jsonify
import time, uuid
app = Flask(__name__)
@app.route('/chat/completions', methods=['POST'])
def chat_completions():
data = request.json
messages = data.get('messages', [])
user_input = messages[-1]['content'] if messages else ''
# Replace with your model inference logic
reply = f'You said: {user_input}'
return jsonify({
'id': f'chatcmpl-{uuid.uuid4().hex}',
'object': 'chat.completion',
'created': int(time.time()),
'model': 'my-llm',
'choices': [{
'index': 0,
'message': { 'role': 'assistant', 'content': reply },
'finish_reason': 'stop'
}]
})
```
## Final checklist
Before going live, verify all of the following:
* [ ] Endpoint reachable with **POST**.
* [ ] Request/response match **OpenAI `chat/completions`** schema.
* [ ] Authentication header configured (API Key **or** Bearer token).
* [ ] (Optional) Streaming supported if needed.
**Send the following to your PolyAI representative to complete setup:**
* **Endpoint URL**
* **Model ID**
* **Auth method & credential**
## Related pages
Choose which LLM powers your agent.
Set global behavior rules interpreted by the model.
# Model
Source: https://docs.poly.ai/behavior/models/model-use
Choose which LLM powers your agent – including PolyAI Raven, our proprietary model family built for conversational AI.
The model directly affects response quality, latency, and cost – a wrong choice can make your agent slow, expensive, or inaccurate.
**Recommended: [Raven 3.5](/behavior/models/raven).** PolyAI's purpose-built conversational model — 24+ languages, sub-300 ms latency, powers both voice and chat.
## Available models
PolyAI's proprietary **[Raven](/behavior/models/raven)** model family is specialized for customer service across voice and chat. Raven eliminates the trade-off between speed and accuracy – delivering sub-300ms latency, fewer errors, and more natural responses than general-purpose LLMs.
**Raven 3.5** is the recommended model for all deployments. It supports voice and chat, 24+ languages, and has powerful auto-reasoning, out-of-domain detection, custom style following, and built-in safety.
Full details on capabilities, supported languages, and why Raven is recommended for most deployments.
| Model | Good for |
| ---------------- | --------------------------------------------------------------- |
| **GPT-5.2** | High-quality interactions requiring nuance and strong reasoning |
| **GPT-5.2 chat** | Extended dialogue and conversational stability |
| **GPT-5 mini** | Lower latency and reduced cost for mid-complexity use cases |
| **GPT-5 nano** | Simple tasks and fast-response workloads |
| **GPT-4o** | Versatile balance of reasoning, speed, and cost |
| **GPT-4o mini** | Everyday queries and high-volume deployments |
| **GPT-4.1** | Strong reasoning with improved cross-task performance |
| **GPT-4.1 mini** | Cost-effective, latency-focused for lighter workloads |
| **GPT-4.1 nano** | Minimal compute and high throughput |
See [OpenAI model documentation](https://platform.openai.com/docs/models) for detailed specifications.
| Model | Good for |
| --------------------- | ----------------------------------------------------------------- |
| **Claude Opus 4.8** | Highest-capability reasoning for complex, multi-step interactions |
| **Claude Sonnet 4.6** | Balanced quality and speed for general-purpose conversations |
| **Claude Haiku 4.5** | Low-latency responses for high-volume, predictable tasks |
| **Claude 3.5 Haiku** | Simple, predictable tasks with strong safety alignment |
| **Nova Micro** | Efficiency with strong general-purpose performance |
See [Anthropic Claude docs](https://docs.anthropic.com/) and [Amazon Nova docs](https://docs.aws.amazon.com/ai/responsible-ai/nova-micro-lite-pro/overview.html) for more details.
## Configuring the model
Navigate to **Voice > [Voice configuration](/voice-channel/advanced/call-settings)** or **Messaging > [Chat configuration](/messaging-channel/advanced/chat-configuration)** to select the model for each channel.
Choose the desired model from the dropdown.
Click **Save** to apply your changes.
## Related pages
Full details on PolyAI's proprietary model family – capabilities, versions, and supported languages.
Connect your own LLM endpoint to PolyAI.
Select the model for your voice channel.
Select the model for your chat channel.
# Raven
Source: https://docs.poly.ai/behavior/models/raven
PolyAI's proprietary LLM family, built for customer service across voice and chat.
Raven is PolyAI's proprietary LLM, built for real-time customer conversations. Sub-300ms latency across 24+ languages. Raven runs the majority of PolyAI deployments.
Select it in [Voice configuration](/voice-channel/advanced/call-settings) or [Chat configuration](/messaging-channel/advanced/chat-configuration), or compare it with other models on the [Model](/behavior/models/model-use) page.
## Why Raven
General-purpose models (GPT, Claude) are trained for broad text tasks. They can handle customer service, but require heavy prompting to behave reliably and add latency a voice channel cannot absorb.
Raven is built specifically for customer conversations:
* **Sub-300ms latency.** Fast enough for live voice with consistent response times.
* **Trained on phone conversations.** Handles interruptions, mispronunciations, and partial utterances.
* **Conversational behavior built in.** Staying on topic, grounding answers in your knowledge, and asking for clarification when unsure do not require prompting.
Built for customer service across voice and chat. Add raw information to your knowledge – Raven converts it into natural conversational responses without extra prompting.
Sub-300ms median latency. Consistent response times – no long-tail spikes.
Higher accuracy on PolyAI's customer service benchmarks. Fewer errors in tool calling and knowledge grounding.
24+ languages with near-perfect language consistency. Set the response language – Raven speaks it, even with English-only prompts.
### Additional capabilities
**Date and time logic** – handles relative dates, scheduling, and format conversions that trip up general-purpose models.
**Reliable tool calling** – trained on real Agent Studio projects. Calls functions with correct parameters; doesn't confuse responding with acting.
**No hallucination** – grounded in your knowledge. Says "I don't know" rather than inventing answers.
**Agent Studio native** – understands topics, flows, and PolyAI's knowledge retrieval patterns by default.
## Raven 3.5
Latest Raven model. Supports voice and chat. Recommended for all new deployments.
* **Auto-reasoning** – automatically decides when to think deeper before responding, improving accuracy on complex tasks like date calculations without adding latency on simple turns
* **Out-of-domain detection** – identifies when a request falls outside the agent's scope, enabling cleaner handoffs and knowledge gap tracking
* **Built-in safety** – guardrails against misuse, with built-in protection against hallucinations
* **Custom style following** – respects custom persona and style instructions, including emotion tags for TTS, formatting rules, and channel-specific tone
* **24+ languages** – more natural multilingual outputs than earlier Raven versions, with near-perfect language consistency
**Raven V3 is deprecated.** Older Raven versions now route to Raven 3.5 automatically. Existing deployments keep working, but you should select **Raven 3.5** directly in your agent configuration.
## Supported languages
Raven supports the following languages:
Arabic, Bulgarian, Cantonese, Croatian, Czech, Dutch, English, French, German, Greek, Hindi, Hindi (Romanized/Hinglish), Italian, Japanese, Korean, Mandarin (China), Mandarin (Taiwan), Polish, Portuguese (Brazil), Portuguese (Portugal), Serbian, Spanish (US), Swedish, Turkish
These languages show particularly strong performance relative to general-purpose models: Cantonese, Italian, Korean, Mandarin (China), Mandarin (Taiwan), Spanish (US)
You can keep all your prompts and knowledge in English and set the response language to your target language. Raven responds consistently in the target language. Quality improves further if you translate prompts and add examples in the target language.
## Getting started
Select **Raven 3.5** in [Voice configuration](/voice-channel/advanced/call-settings) or [Chat configuration](/messaging-channel/advanced/chat-configuration).
## Related pages
Compare Raven with OpenAI and Amazon Bedrock models.
Transparency on datasets used to train Raven.
Connect your own LLM endpoint to PolyAI.
# Handoff States
Source: https://docs.poly.ai/call-data/conversations-api/handoff-states
Retrieve the handoff state and context written by your agent when a conversation transitions to a live agent.
Use the Handoff States API to retrieve the handoff state recorded by your agent at the moment of transition to a live agent, along with the context and metadata needed to synchronize downstream systems. The endpoint is read-only — handoffs themselves are triggered from the agent (via [Call Handoffs](/voice-channel/handoffs), [FAQs actions](/knowledge/faqs/actions/handoff), or `conv.call_handoff` in code), not from this API.
For detailed API specifications, refer to the [Handoff API documentation](/api-reference/handoff/introduction).
## Related handoff documentation
* **[Call Handoffs overview](/voice-channel/handoffs)** - Configure handoff destinations in the UI
* **[Handoff actions in FAQs](/knowledge/faqs/actions/handoff)** - Trigger handoffs from Knowledge topics
* **[Handoff API reference](/api-reference/handoff/introduction)** - Retrieve handoff context programmatically
* **[Twilio handoff integration](/voice-channel/numbers/twilio/how-to-handoff)** - Twilio-specific handoff setup
## When to use handoff states
The **handoff states** API reads back the state your agent recorded for a conversation at handoff. Use it to:
* Retrieve the state of a handoff, such as `handoff_initiated`, `handoff_completed`, or `handoff_failed`, or custom topic-specific states (e.g., `customer_refund` or `complaint_escalation`).
* Synchronize metadata with systems operated by human agents so they have full conversational context.
The API does not trigger handoffs or change conversation state — it only returns what the agent stored. To trigger or configure handoffs, see [Call Handoffs](/voice-channel/handoffs) (UI) or `conv.call_handoff` (code).
## Handoff states overview
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
stateDiagram-v2
[*] --> handoff_initiated: Agent triggers handoff
handoff_initiated --> handoff_completed: Live agent accepts
handoff_initiated --> handoff_failed: Transfer fails
handoff_failed --> handoff_initiated: Retry
handoff_completed --> [*]: Call ends
handoff_failed --> [*]: Fallback / call ends
```
The **handoff states** API provides key triggers for managing transitions between automated and live agents. These states act as signals indicating the outcome of a handoff process rather than continuously updating during the call.
Some example states you could use include:
| State | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------------- |
| `customer_refund` | The call was escalated to a live agent to process a refund request. |
| `complaint_escalation` | The call was handed off due to a complaint requiring live agent resolution. |
| `successfully_identified` | The system successfully verified the customer's identity before transitioning the call to a live agent. |
Example API response:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"id": "0bba04d7-38b3-4fd3-a1a8-329c34517fc1",
"shared_id": "acme_inc_sdklfasdklfjasbdfklabs",
"data": {
"customer_id": "12345",
"handoff_reason": "successfully_identified"
}
}
```
## Accessing handoff state data
Use the [Call Handoffs page](/voice-channel/handoffs) to configure handoff destinations in the UI, or the [handoff action in FAQs](/knowledge/faqs/actions/handoff) to trigger handoffs from Knowledge topics.
### API endpoint
The **Handoff API** retrieves the current handoff state of a conversation using either:
* **Shared IDs (`shared_id`)**: Used in both the PolyAI and client systems to keep them in sync.
* **PolyAI conversation IDs (`id`)**: Generated automatically by PolyAI for each conversation.
When both IDs are provided, the API prioritizes the `shared_id`.
For full details on parameters, headers, and error codes, refer to the [Handoff API documentation](/api-reference/handoff/introduction).
## Webchat and SMS handoff via the Chat API
For webchat and SMS integrations using the [Chat API](/api-reference/chat/introduction), the agent signals a handoff on the `chat/respond` response: `end_conversation` is set to `true` and a `handoff` object is included alongside the agent's final message.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"conversation_id": "CONV-1234567890",
"response": "Transferring you to a live agent.",
"end_conversation": true,
"handoff": {
"destination": "live_agent_queue",
"reason": "billing_question"
}
}
```
When your widget or SMS connector sees `handoff` in the response:
1. Stop calling `chat/respond` for this conversation.
2. Use `destination` to route the session to the right queue or skill in your live-chat platform.
3. Call the [Handoff API](/api-reference/handoff/introduction) with the `conversation_id` (or your `shared_id`) to retrieve the full `data` payload — customer identifiers, verification status, collected entities — for a screen-pop on the live agent's desktop.
4. Optionally call `chat/close` once the human has accepted the session.
For voice handoffs, the equivalent signal is a SIP REFER, INVITE, or BYE — see [Call handoff](/voice-channel/handoffs#adding-a-handoff-destination).
## SIP header handoff
Some deployments include handoff metadata in [SIP](https://en.wikipedia.org/wiki/Session_Initiation_Protocol) headers when calls are passed back to the contact center. SIP headers can provide critical context quickly, because they package agent IDs and handoff states into lightweight metadata.
### Considerations
* **Customization**: SIP header metadata varies by deployment. Review your deployment-specific SIP header documentation or contact your PolyAI representative for details on the fields and formats available in your setup.
* **Completeness**: Ensure the SIP headers in your deployment include all context your contact center agents need for handoff handling (e.g., handoff reason, customer ID).
## Best practices
1. **Prioritize shared IDs**: Use `shared_id` for consistency with your internal systems. If both `id` and `shared_id` are provided, the API defaults to the `shared_id`.
2. **Monitor handoff failures**: Track the `handoff_failed` state (or its equivalent in your deployment) to implement automated retries or fallback workflows.
3. **Use topic-specific states**: Implement custom states (e.g., `customer_refund` or `complaint_escalation`) for better tracking and reporting on specific interaction types.
***
## Related pages
Retrieve metadata for conversations programmatically.
Access detailed transcripts for compliance and analytics.
Automate large-scale transcript and metadata transfers.
# List conversations
Source: https://docs.poly.ai/call-data/conversations-api/list-conversations
Retrieve conversation transcripts and metadata programmatically using the Conversations API.
Use the [Conversations API](/api-reference/conversations/introduction) to retrieve conversation transcripts with metadata, timestamps, and states programmatically. Each conversation response includes a `state` field containing all [conversation variables](/tools/variables) (`conv.state` values) set during the call – both built-in keys and custom variables your agent writes.
## Push and pull
PolyAI supports both **push** and **pull API** models, offering flexible synchronization options:
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
flowchart LR
subgraph Pull["Pull model"]
direction LR
A[Your system] -->|Periodic request| B[PolyAI API]
B -->|Conversation data| A
end
subgraph Push["Push model"]
direction LR
C[PolyAI] -->|Real-time webhook| D[Your system]
end
```
* **Pull API**: On-demand metadata retrieval through the List Conversations endpoint. Ideal for periodic data collection or workflows where updates are less time-sensitive.
* **Push API**: Automatically sends updates (new conversations or completed interactions) to your system in real time. Best suited for dashboards requiring prompt updates.
For triggering **instant** communication updates for real-time use cases, like determining where to route a call, it
is recommended to use [handoff states](/call-data/conversations-api/handoff-states).
### Best practices
1. **Pull**: Use this model for scheduled reporting or when real-time updates are unnecessary.
2. **Push**: Use push updates for real-time synchronization, such as live agent dashboards.
3. If you need real-time updates, use push. If periodic batch retrieval is sufficient, use pull.
## Integration with other workflows
The **List Conversations API** can fit into a larger data-sharing workflow:
1. [Manual access using the studio](/call-data/studio-transcripts): View richly-detailed conversation transcripts and recordings directly in the PolyAI platform.
2. **End of call metadata retrieval**: Automate metadata syncing after conversations using push or pull APIs.
3. [**Handoff metadata integration**](/call-data/conversations-api/handoff-states): Provide your live customer agent with a quick identifier indicating what kind of call they have just received.
# Recordings and transcripts
Source: https://docs.poly.ai/call-data/introduction
Access call recordings, transcripts, and metadata for compliance, QA, and performance analysis.
Choose the method that fits your scale – Studio for manual review, the API for programmatic access, or S3 sync for bulk storage. Use call data to access recordings, transcripts, and conversation metadata for compliance, QA, and performance analysis.
For API-based data export, handoff state management, and S3 sync setup, expand **API and export** in the sidebar. These guides require technical implementation. You can also find call data content in the **Developer** tab.
## Methods
Manage real-time state sharing for conversation handoffs between automated systems and live agents.
Retrieve filtered lists of conversation transcripts with metadata and identifiers through the Conversations API.
Access conversation transcripts and recordings directly in [Agent Studio](https://studio.us.poly.ai/).
Sync transcripts and recordings to your own [AWS S3 bucket](https://aws.amazon.com/s3/) for long-term storage.
## Use case table
| **Tool** | **Purpose** | **Provides audio?** | **Provides text (transcripts)?** | **Best use cases** |
| ---------------------- | ----------------------------------------------------------------------------------------------- | ------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| **Handoff States** | Monitor and trigger real-time state transitions for handoffs between automated and live agents. | No | No | Passing context during handoffs. Not for audio or transcript retrieval, but critical for conversation flow. |
| **List Conversations** | Retrieve filtered lists of conversations with metadata and transcript IDs. | No | Yes | Ideal for monitoring, reporting, or linking transcripts to external systems for further analysis. |
| **Transcripts** | Access and review conversation transcripts and recordings directly in the PolyAI UI. | Yes (No download) | Yes | Quick manual review for quality assurance, compliance, and performance analysis. Not suitable for bulk handling. |
| **AWS S3 Integration** | Sync recordings and metadata to your AWS S3 environment for storage and analysis. | Yes (Downloadable) | Yes | Large-scale or long-term storage, especially for compliance-heavy industries requiring both audio and transcripts. |
* **Audio only**: Use transcripts for quick access or a customized AWS S3 integration with downloadable audio files.
* **Text only**: Use list conversations to retrieve transcript IDs and link them to conversation data. Studio transcripts also provides manual access to text.
# AWS S3 integration
Source: https://docs.poly.ai/call-data/s3-to-s3
Automatically sync call recordings and transcripts to your S3 bucket for long-term storage and compliance.
Automatically sync call transcripts and audio recordings to your own [AWS S3](https://aws.amazon.com/s3/) bucket for long-term storage, compliance archiving, and integration with internal data pipelines.
## What gets synced
| Data type | Format | Description |
| ---------------- | ------- | --------------------------------------------------------------------------------- |
| Transcripts | JSON | Full conversation transcripts with turn-level detail |
| Audio recordings | WAV/MP3 | Complete call recordings |
| Metadata | JSON | Conversation metadata including duration, environment, variant, and handoff state |
## When to use S3 sync
* You need to retain call data for compliance (HIPAA, GDPR, PCI-DSS)
* You want to run your own analytics or ML pipelines on conversation data
* You require downloadable audio files (the [Studio UI](/call-data/studio-transcripts) supports playback but not download)
* You process high call volumes and need scalable, automated data transfer
## How to get started
Reach out to your PolyAI account manager or [contact PolyAI](https://poly.ai/contact/) to discuss your data transfer requirements.
Share your AWS account ID, target S3 bucket ARN, and preferred region with your PolyAI representative.
PolyAI will provide an IAM policy template. Apply it to your bucket to grant PolyAI write access.
PolyAI runs test transfers to your bucket. Verify the data format and file structure meet your needs.
Once validated, PolyAI enables continuous sync for your production environment.
## Related pages
Retrieve conversation metadata programmatically through the API.
Review transcripts and play recordings directly in Agent Studio.
# Studio transcripts
Source: https://docs.poly.ai/call-data/studio-transcripts
Access conversation transcripts and recordings directly in Agent Studio for compliance, QA, and analysis.
Access transcripts and recordings in **Analytics > Conversations > Voice** (for voice calls) or **Analytics > Conversations > Web chat** (for webchat sessions) for manual review of conversation quality and customer interactions. For API access, see [List Conversations](/call-data/conversations-api/list-conversations).
See [Conversation review](/analytics/conversations/review) for detailed analysis workflows.
## Accessing transcripts in the PolyAI Studio
To view conversation transcripts:
1. Go to **Analytics > Conversations > Voice** (for voice calls) or **Analytics > Conversations > Web chat** (for webchat sessions) in PolyAI Studio.
2. Click **Filter** and add filters based on your search criteria. Available filters include:
* **Call handoff** -- Identify conversations involving handoff events.
* **Duration** -- Filter calls by duration length.
* **Tool calls** -- Search interactions where specific tools were invoked.
* **Function calls** -- Find conversations where a specific function ran (filter by function name, or filter by *exists* to find any conversation that ran a function).
* **SMS events** -- Find conversations where an SMS was sent. Filter by SMS event name, or use *exists* to surface every call that triggered an SMS.
* **Phone number** -- Filter by the caller's or recipient's phone number.
* **Safety flag** -- Identify flagged conversations.
* **Start date** -- Set a date range for the search.
* **Environment** -- Filter by environment type, such as Sandbox or Production.
3. Select a conversation from the results to open its details, including transcript and audio playback.
### Transcript details
Each transcript includes the following details:
* **Metadata**
* Start and end times, call duration, and environment (e.g., Sandbox or Production).
* Call SID and phone numbers for both caller and recipient.
* **Speaker segmentation**
* Clear differentiation between customer and agent speech, with timestamps for each utterance.
* **Handoff information**
* Transition details for live agent handoffs, such as handoff state and timestamps.
* **Audio playback**
* Play and review call recordings directly in **Conversation review**.
* **Conversation analysis**
* Includes toggles for viewing **Tool calls**, **Topic citations**, **Flows and steps**, and **Variables** for deeper interaction insights.
## Related pages
Retrieve metadata for conversations programmatically.
Monitor and manage live agent transitions.
Set up bulk synchronization of recordings and transcripts.
## PolyScore
**PolyScore** is an automated 1–5 quality rating shown for every eligible conversation (voice, messaging, and email). It evaluates agent quality and task success to help identify strong and weak conversations.
For full details on how scoring works, dimensions, limitations, and interpretation guidance, see the dedicated [PolyScore](/analytics/polyscore) page.
## Call summaries
Call summaries are AI-generated overviews of conversation content, key topics discussed, and outcomes. They appear alongside the transcript in Conversation Review.
# Compare versions
Source: https://docs.poly.ai/environments-and-versions/diffs
Use side-by-side diffs to compare versions and track changes across your entire project.
Use version comparison before promoting changes to catch unintended edits. A diff shows exactly what changed between any two versions – across knowledge, functions, flows, variants, and settings – so you can promote with confidence.
## When to use it
* **Before promoting from sandbox to pre-release or live** – Review all changes to confirm nothing unexpected is included.
* **After a model upgrade** – Verify that no unintended changes were introduced alongside the upgrade.
* **When you find a draft you didn't expect** – Understand what's in it, who made the changes, and whether to keep or discard it.
* **In branching workflows** – When multiple people work on the same project, use the diff to verify a teammate's changes before merging or promoting. For example, if a customer needs a quick fix while your team is building on a separate version, compare versions to confirm the fix is isolated.
## Key features
* **Side-by-side comparison** across sandbox, pre-release, and live environments
* **Full platform coverage** – diffs apply to knowledge, tools, flows, variants, and settings
* **Change attribution** – each change shows who made it and when, which is useful for compliance, regulated industries, and multi-team projects
* **Content diff** for responses, sample questions, and configuration
## How it works
1. **Select versions to compare:** Choose two versions of the project from the dropdown menu. You can compare across environments (for example, sandbox vs. live) or between versions within the same environment.
2. **Review differences:** Changes are highlighted in a structured format:
* ** Additions** – a new item was added.
* ** Deletions** – an existing item was removed.
* ** Edits** – an existing item was modified.
3. **Check attribution:** Each change is annotated with the user who made it. Use this to audit who changed what – especially useful when multiple people have access to the same project.
4. **Assess impact:** Use the comparison view to evaluate whether changes are ready to promote.
5. **Exit comparison mode:** Return to the editor or switch to another version for further review.
## Using diffs with branching
The diff tool is most valuable in collaborative and branching workflows. A common pattern:
1. A customer or teammate needs to make a quick change to the live agent.
2. They create a new version from the current live version and make the change there.
3. Before publishing, they use **Compare versions** to confirm only the intended change is present – nothing from an in-progress build is included.
4. They publish the isolated change to live.
This workflow keeps in-progress work separate from urgent fixes. The diff tool is how you verify the separation.
## Related pages
* [Project history](/environments-and-versions/project-history) – View a chronological record of all project events, including published versions, draft branch merges, and deleted drafts. Use Project history to find versions to compare, audit when changes went live, and roll back if needed.
* [Environments](/environments-and-versions/introduction) – Understand the deployment pipeline (sandbox, pre-release, live).
# Deployments
Source: https://docs.poly.ai/environments-and-versions/introduction
Learn how to ship safely: branch, promote, A/B test, and roll back from one timeline.
**New to environments?** Start with the [Environments tutorial](/learn/guides/get-started/environments) for a hands-on introduction. For detailed workflows and best practices, see [Version management](/learn/maintain/version-management).
**Wren edits on branches.** Changes from [Wren](/wren/introduction) land on a branch off `Main`. Once merged, they flow through this same Draft → Sandbox → Pre-release → Live pipeline. Promotion is always manual.
**Saving is not the same as going live.** Saved changes are drafts. Drafts must be **published** to Sandbox, then **promoted** through Pre-release to Live. Unpublished changes don't appear in any environment.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
stateDiagram-v2
[*] --> Draft: Make changes
Draft --> Sandbox: Publish
Sandbox --> PreRelease: Promote
PreRelease --> Live: Promote
Live --> PreRelease: Rollback
PreRelease --> Sandbox: Rollback
note right of Draft: Development
note right of Sandbox: Testing
note right of PreRelease: UAT staging
note right of Live: Production
```
## Creating a version
A draft version
is created whenever changes are made to an agent. A draft banner appears at the top of the page, allowing you to:
* **Delete**: Revert to the most recent published version.
* **Publish**: Save the draft as a version, optionally adding a description highlighting changes made and any notes for future
collaborators.
Once published, the version becomes your active Sandbox deployment
and you can access it from **Deployments** in the sidebar.
## Working on branches
Some areas of Agent Studio — such as **Knowledge** and **Tools** — use a git-style branching workflow so you can develop changes in isolation before they reach the shared version line. A **branch selector** dropdown (top-left) lists your branches alongside **Main**; switch branches to work on a set of changes without affecting `Main`.
When a branch is ready, open the **Merge to main branch** popover, add a commit message describing the change, and select **Merge**. Once merged into `Main`, the changes join the shared version line and can be promoted through the Draft → Sandbox → Pre-release → Live pipeline described below.
## Promoting a version
Promotion moves a version from one environment to the next. The environments include Sandbox,
Pre-release,
and Live.
### Pre-release
Staging environment for user acceptance testing (UAT).
1. Go to **Deployments** in the sidebar.
2. Click the **Options Menu** next to the desired version.
3. Select **Promote to Pre-release**.
### Live
Production. Changes affect all active calls immediately.
1. Go to the **Pre-release** tab in **Deployments**.
2. Click the overflow menu (three vertical dots) next to the version.
3. Select **Promote to Live**.
4. Confirm your selection by checking the box and clicking **Promote**.
## Comparing versions and environments
Before promoting changes, you can compare versions across environments using a side-by-side diff view.
1. Go to the **Deployments** section and open **Environments** or **Project History**.
2. Select a version and click **Compare** to view differences between **Sandbox**, **Pre-release**, and **Live**.
3. Versions appear in **reverse chronological order** (newest first) for easier navigation.
For detailed information on tracking changes between versions, see [Tracking changes](/environments-and-versions/diffs).
## Rolling back to a previous version
Roll back to a previous version if needed:
1. Go to **Deployments** in the sidebar.
2. Select the **Options Menu** for the desired version.
3. Click **Rollback**.
4. Confirm the rollback.
The system confirms when the rollback is complete.
## Testing your agent
Main page: [Quickstart: test your agent](/get-started/quickstart#test-your-agent)
Test your agent in any environment:
1. Click the **Play Chat** icon in the top-right corner of the screen.
2. Select the environment containing the version you want to test.
## Assigning phone numbers
Each environment can have its own phone number. To assign:
1. Go to **Voice > Numbers** in the sidebar.
Assign phone numbers and SIP headers per version.
## Automate with the Agents API
The same pipeline is available programmatically, which is useful for wiring deployments into CI or orchestrating releases across many agents.
The [Agents API](/api-reference/agents/introduction) exposes [publish](/api-reference/agents/endpoint/deployments/publish-the-current-draft-to-an-environment), [promote](/api-reference/agents/endpoint/deployments/promote-a-deployment-to-the-next-environment), and [rollback](/api-reference/agents/endpoint/deployments/rollback-to-a-previous-deployment) as the CI-friendly equivalents of the UI actions above. Note that merging a branch into `main` already publishes to Sandbox, so a standalone `publish` call is only needed when you have an undeployed `main` draft. Both `publish` and `promote` return the resulting deployment under a `deployment` key.
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Publish the current draft to sandbox
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/deployments/publish \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "environment": "sandbox" }'
# Promote a sandbox deployment to pre-release
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/deployments/DEPLOYMENT_ID/promote \
-H "x-api-key: $POLYAI_API_KEY"
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os, requests
BASE = "https://api.us.poly.ai"
HEADERS = {"x-api-key": os.environ["POLYAI_API_KEY"]}
# Publish the current draft to sandbox
resp = requests.post(
f"{BASE}/v1/agents/{AGENT_ID}/deployments/publish",
headers=HEADERS,
json={"environment": "sandbox"},
)
deployment_id = resp.json()["deployment"]["id"]
# Promote to pre-release once sandbox checks pass
requests.post(
f"{BASE}/v1/agents/{AGENT_ID}/deployments/{deployment_id}/promote",
headers=HEADERS,
)
```
## Related pages
Side-by-side diff of any two versions before promoting.
Audit trail of all published versions and changes.
Run regression tests against Draft or Sandbox.
Publish, promote, and rollback from the Agents API.
# Conversation flow
Source: https://docs.poly.ai/essentials/order
How PolyAI agents process conversations.
How a PolyAI agent processes conversations from input to response.
The agent's greeting is sent directly without LLM processing or behavioral rules. For voice, the greeting text is converted to speech; for webchat, it's displayed as text. Make sure to write the greeting in the language your users expect. Behavioral rules and agent logic only apply starting from the second turn of the conversation.
## Processing stages
A conversation moves through the following stages:
* **User**: The user provides input–speech (voice) or text (webchat/SMS).
* **Input capture**: For voice, the audio stream is captured and sent for transcription. For webchat/SMS, text is received directly.
* **ASR Provider** (voice only): The system receives the raw audio.
* **[ASR Service](/voice-channel/advanced/call-settings#keyphrases)** (voice only): Converts the audio into text using [automatic speech recognition](https://en.wikipedia.org/wiki/Speech_recognition).
* **ASR Processing** (voice only): Searches for transcription issues and applies any relevant corrections.
* **Transcript/Text → Processed Input**: The processed input is passed to [Retrieval](/knowledge/faqs/RAG/introduction).
* **Retrieval**: Pulls relevant **topics retrieved** from the [Knowledge area](/knowledge/faqs/introduction) using [RAG (retrieval-augmented generation)](https://en.wikipedia.org/wiki/Retrieval-augmented_generation) to provide context for the response.
* **Compute Prompt**: The system builds an [LLM](https://en.wikipedia.org/wiki/Large_language_model) prompt using retrieved topics, system knowledge, and conversation history.
* **Run LLM**: The LLM processes the request and determines whether to return:
* **Returned Text**: A direct text response.
* **Returned Function**: A tool call (if applicable).
* **Execute Function (if applicable)**: Runs the function and passes the result back to the LLM.
* **LLM Refinement**: If a function result is returned, the LLM updates its response before proceeding.
* **Chunk LLM Output**: The response is broken into chunks for delivery.
* **Postprocess Chunks**: Applies rules such as [stop keywords](/voice-channel/advanced/call-settings#stop-keywords) to remove unnecessary phrases.
* **Stream Partial Responses**: The system sends chunks as soon as they are ready, rather than waiting for the full response.
* **TTS Service** (voice only): Converts text chunks into speech using [text-to-speech synthesis](https://en.wikipedia.org/wiki/Speech_synthesis). Configure voices in [voice settings](/voice-channel/introduction).
* **Response delivery**: For voice, synthesized speech is streamed to the user. For webchat/SMS, text responses are sent directly.
* **Live Handoff (if applicable)**: If escalation is needed, the agent triggers a [live handoff](/voice-channel/handoffs). For voice, this transfers the call; for webchat, this can route to a live chat agent.
* **Conversation Logs**: The system stores conversation history and logs for [analytics](/analytics/conversations/introduction).
* **Final Response**: The user receives the completed response as it streams, without waiting for the entire message.
## Advanced: How response streaming works
PolyAI agents don't wait for the full response before speaking. Instead, responses are processed and streamed **in real time**:
* **LLM Streaming**: Words are generated and sent continuously.
* **Chunking**: Responses are broken into chunks for controlled delivery.
* **Postprocessing**: [Stop keywords](/voice-channel/advanced/call-settings#stop-keywords) remove unnecessary phrases before delivery.
* **Response Streaming**: For voice, users hear speech as soon as it's processed via TTS. For webchat, text appears progressively as it's generated.
### Watch it in action
This video visualizes the conversation flow, showing how responses are processed, chunked, and streamed:
## Next steps
Understand system components and data flow
Configure your agent's personality and behavior
Add FAQs and knowledge sources
Tune ASR and input processing
# Agent Development Kit (ADK)
Source: https://docs.poly.ai/extend/adk
Python CLI for building, managing, and deploying PolyAI agents locally with a Git-like pull-edit-push workflow.
The Agent Development Kit (ADK) is a Python CLI for building PolyAI agents locally. Pull your Agent Studio project as YAML and Python files, edit in your IDE (or with a coding assistant like Claude), version with Git, and push from the command line.
Installation, tutorials, CLI reference, and examples.
## How it works
ADK uses a Git-like workflow: init → pull → branch → edit → validate → push → review → merge.
Link a local directory to an Agent Studio project with `poly init`. Select your region, account, and project interactively or pass them as flags.
Run `poly pull` to download your project as structured YAML and Python files organized by resource type.
Use `poly branch switch ` to work on a branch, keeping your changes isolated from the live agent.
Edit with any tool — VS Code, Cursor, or AI coding assistants. YAML handles configuration, Python handles functions.
Run `poly validate` to check your changes locally, then `poly push` to diff against the remote state and send updates to Agent Studio.
Run `poly chat` to start an interactive chat session with your agent and verify behavior before merging.
ADK is fully compatible with Agent Studio and the APIs. Switch between surfaces at any time.
## Prerequisites
ADK uses [uv](https://docs.astral.sh/uv/) to manage Python and virtual environments.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
# macOS
brew install uv
# Linux / WSL
curl -LsSf https://astral.sh/uv/install.sh | sh
```
**Self-serve accounts** — run `poly start` to sign in automatically.
**Enterprise accounts** — run `poly login --region `, or export your API key directly:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export POLY_ADK_KEY=
```
Generate API keys from your account at [studio.poly.ai](https://studio.poly.ai).
## CLI commands
| Command | Description |
| --------------- | ----------------------------------------------------------- |
| `poly init` | Link a local directory to an Agent Studio project |
| `poly pull` | Download the current project state as YAML and Python files |
| `poly push` | Diff local changes against remote and send updates |
| `poly status` | View changed, new, and deleted files |
| `poly diff` | Show differences between local and remote |
| `poly validate` | Validate project configuration locally |
| `poly branch` | Manage project branches (`switch`, `delete`, `merge`) |
| `poly chat` | Start an interactive chat session with your agent |
| `poly format` | Format project resources |
| `poly review` | Create a GitHub gist for reviewing changes |
| `poly revert` | Revert local changes |
| `poly docs` | Print documentation for any ADK resource type |
Run `poly --help` for the full list, or `poly --help` for flags and options.
## Project structure
After pulling, your local directory contains human-readable files organized by resource type:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
agent_settings/ # Identity, behavior, personality, rules
role.yaml
personality.yaml
rules.txt
topics/ # Knowledge base content (RAG-retrieved)
Frequently Asked Questions.yaml
Billing Issues.yaml
functions/ # Python business logic
check_billing.py
flows/ # Multi-step guided conversations
greeting/
flow_config.yaml
flow_step_welcome.yaml
flow_step_billing_check.yaml
config/ # Entities, handoffs, SMS templates, translations
entities/
billing_amount.yaml
handoffs/
sms_templates.yaml
variant_attributes.yaml
voice/ # Voice channel settings
chat/ # Chat channel settings
test_suite/ # Conversation tests
```
Each YAML file represents a single resource. For example, a topic file:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
enabled: true
actions: ""
content: |
We accept Visa, Mastercard, and PayPal.
Refunds take 3–5 business days.
example_queries:
- What payment methods do you accept?
- How long do refunds take?
- Can I pay with PayPal?
```
## Resource references
Cross-references between resources use human-readable placeholder tags. ADK resolves these automatically at push time.
| Syntax | Reference type |
| ------------------------- | ----------------------- |
| `{{fn:function_name}}` | Global functions |
| `{{entity:entity_name}}` | Collected entity values |
| `{{attr:attribute_name}}` | Variant attributes |
| `{{ho:handoff_name}}` | Handoff destinations |
| `{{vrbl:variable_name}}` | State variables |
## Resource architecture
ADK resources fall into two categories:
| Category | Resources | Purpose |
| --------------------- | ----------------------- | --------------------------------------------------------------------- |
| **Knowledge / facts** | Topics | Subject-specific content retrieved via RAG when contextually relevant |
| **Behavior / logic** | Rules, flows, functions | Define what the agent does and when |
**Quick guide:** Always-true instructions belong in `rules.txt`. Subject-specific information belongs in topics. Comparisons, calculations, or API calls belong in functions.
**Common mistakes to avoid:**
* Putting behavioral instructions in topic `content` instead of topic `actions`
* Putting facts in `rules.txt` — this wastes context; keep facts in topics
* Using prose conditionals for branching — use Python functions instead, since models can't reliably detect empty variables
## Next steps
Store API keys and credentials securely for use in your functions
Build flows with transition functions for complex conversation paths
Create, configure, and deploy agents programmatically via REST
Tutorials, examples, and complete CLI reference
# Extend with code
Source: https://docs.poly.ai/extend/introduction
Write Python functions to integrate APIs, validate input, and add custom business logic to your agent.
Write Python functions to extend your agent beyond what the visual interface covers — call APIs, query databases, validate input, and encode custom business logic. The agent invokes your function like any other tool.
**Python required.** Non-technical operators: see [Configure agents in the UI](/get-started/introduction) for the no-code path.
Before writing custom API code, check if a [pre-built integration](/integrations/introduction) already exists for your platform (e.g., Salesforce, Zendesk, OpenTable, Stripe). Pre-built integrations handle authentication, environment configuration, and common operations for you.
## What you can build
Call external APIs to look up bookings, check availability, or retrieve account details
Validate input, calculate pricing, or enforce rules that the LLM cannot handle alone
Create records, update customer profiles, or log interactions in external systems
Route calls to the right team based on real-time data or custom logic
Build code-driven flows with transition functions for complex conversation paths
Define schemas that adapt agent behavior based on external config data
## Get started
Follow the [create a function](/tools/how-to-setup) guide to set up a Python function with parameters, code, and a description the LLM can use to trigger it.
Read the [tools overview](/tools/introduction) to learn how functions integrate with the LLM, how to use `conv` objects, and best practices for naming and triggering.
Read [flows](/flows/introduction), [call handoffs](/voice-channel/handoffs), and [secrets management](/secrets/introduction) to build production integrations.
## Tools and functions
Write Python scripts that run when your agent needs to take action. Functions are the bridge between conversation and your business systems.
Set up a function with naming conventions, parameters, and Python code
Initialize conversation context before the greeting plays
Run post-call processing after a conversation ends
Control agent behavior with string and dictionary returns
Define, update, and persist values across turns
Standard and non-standard libraries available in your functions
Add filler phrases to avoid silence during slow functions
Trigger functions from FAQs
## Reference
Access conversation data and built-in utilities in your functions.
Conversation states, flows, and telephony attributes
Structured diagnostics and PII-scoped logging
Built-in helpers for addresses, cities, and structured data
Call configured API integrations
Persistent data across conversations for repeat callers
VoiceWeighting, TTSVoice, and provider classes
## Advanced capabilities
Store API keys and credentials securely
Configure call transfers and routing rules
Define real-time config schemas for dynamic agent behavior
Access transcripts and recordings through the API
Build flows with transition functions and programmatic control
Use the Agents API to create, configure, and deploy agents from code
Integrate with PolyAI REST APIs for chat, conversations, alerts, and more
# ASR biasing
Source: https://docs.poly.ai/flows/asr-biasing
Configure flow steps to improve speech recognition accuracy for structured inputs like codes and names.
**This page covers flow-level ASR configuration.** For dynamic ASR biasing from Python functions, see [`conv.set_asr_biasing()`](/tools/classes/conv-object). Setting up ASR biasing in the flow editor does not require code.
Automatic Speech Recognition (ASR) biasing helps the agent better understand the type of input it expects in each step. It nudges transcription toward expected patterns — like confirmation codes, personal names, or dates — instead of treating every utterance as free text.
Enable it on any **flow step** that collects structured input.
## When to use it
The user is likely to say a booking code, date, or other structured value.
You see transcription errors on names, codes, or numbers.
The agent is guessing incorrectly from noisy or accented audio.
## Bias options
Pick the bias that matches what the user is about to say — not the wording of the prompt.
| Option | Use for | Example utterance |
| ----------------- | ------------------------------------------ | ---------------------------------- |
| **Alphanumeric** | Booking references, confirmation codes | *"X9C7G2"* |
| **Name** | Full personal names — first, last, or both | *"Aaron Forinton"* |
| **Name spelling** | Phonetically spelled names | *"A for apple, R for Robert…"* |
| **Numeric** | Ages, short numbers | *"forty-two"* |
| **Party size** | Group bookings | *"a table for four"* |
| **Precise date** | Specific calendar dates | *"March 14th"* |
| **Relative date** | Flexible time references | *"next Tuesday"*, *"in two weeks"* |
| **Single number** | One-digit responses, menu selection | *"press 1"* / *"one"* |
| **Time** | Spoken times | *"half past eight"* |
| **Yes/No** | Confirmation responses | *"yeah, that's right"* |
| **Address** | Location names, postcodes, street numbers | *"221B Baker Street, NW1 6XE"* |
Custom keyword fields can also be added to bias transcription toward domain-specific vocabulary — product names, menu items, medical terms, and so on.
Steps with ASR biasing already applied are marked with the ear icon in the Flow Editor.
## Configure it on a step
In the Flow Editor, select the step where you want to bias transcription and open the step editor on the right.
In the **ASR biasing** panel, toggle on the option that matches what the user is expected to say (see the table above).
Add domain-specific vocabulary as custom keywords — product names, menu items, dish names, medical terms. Bias applies on top of the selected input type.
Place a test call and confirm the transcription matches what was said. Iterate on the bias type and keywords if not.
## Example: confirmation code capture
In the [reservation confirmation flow example](./example#e-asr-biasing), the agent collects a booking code. Users say mixed letters and numbers, so **Alphanumeric** biasing is toggled on — this raises the likelihood that utterances like *"X9C7G2"* are transcribed accurately instead of being interpreted as ordinary words.
**Combine with [few-shot prompting](./few-shot-prompting)** when the input format is ambiguous — for example, when users might say a code as *"X nine C seven G two"* or *"X9C7G2"* interchangeably. Bias handles the acoustic model, few-shot prompting handles the LLM's interpretation of the result.
Match the bias to the expected **input**, not the wording of the prompt. Biasing a "yes/no" prompt with **Alphanumeric** hurts recognition of *"yes"* and *"no"*.
# DTMF
Source: https://docs.poly.ai/flows/dtmf
Collect keypad input in flow steps with configurable digit limits, timeouts, and optional recording opt-out.
**This page includes Python code.** The DTMF configuration UI does not require code, but the transition function examples below require Python familiarity.
Use DTMF when callers need to enter numeric input with their phone keypad – phone numbers, account numbers, confirmation codes, or consent responses (e.g., "Press 1 to opt out of recording"). DTMF is usually more reliable than speech for structured numeric input.
**[Dual-Tone Multi-Frequency](https://en.wikipedia.org/wiki/Dual-tone_multi-frequency_signaling)** (DTMF) converts keypad presses into audio tones that telecom systems detect and process.
DTMF configuration is only available on **Advanced** flow steps. Low-code steps do not support DTMF or custom ASR settings. Additionally, DTMF output is not currently supported with the **realtime model** – if your agent uses the realtime model, DTMF collection will not work.
## Configuring DTMF in a flow step
1. Open your **flow** in the editor.
2. Select the **Advanced** step where you want to collect numeric input.
3. On the right-hand configuration panel, toggle **DTMF** on.
4. Configure the following options:
| Setting | Description | Default |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| **Number of digits expected** | How many digits the caller must enter (1–32). Set to `-1` for unlimited. | – |
| **First digit timeout (seconds)** | How long to wait for the first keypress before timing out. | 5 |
| **Inter-digit timeout (seconds)** | How long to wait between subsequent key presses before timing out. | 2 |
| **End key** | Optional key (such as `#` or `*`) to signal the end of input. | None |
| **Collect data while the agent is speaking** | Allow input collection during speech playback. | Off |
| **Mark collected data as PII** | Flag collected values as **Personally Identifiable Information**. When enabled, the collected DTMF digits are redacted from logs and transcripts, and handled according to your organization's PII data-retention policies. | Off |
To open the DTMF menu quickly, click the **app grid icon** in the step's prompt card.
## Behavior and timing
DTMF input is processed **after** the `start` function runs. If your `start` function plays a greeting, that greeting plays before DTMF capture begins. To collect DTMF input before any greeting plays, override the greeting by setting it to an empty string in the `start` function.
Even while a greeting is playing, DTMF input is still captured in the background if **Collect data while the agent is speaking** is enabled. For example, you can say:
> "This call may be recorded. To opt out, press 1. How can I help you today?"
and still capture the caller's keypress while they hear the welcome message.
### ASR and DTMF interaction
When DTMF is enabled, **Automatic Speech Recognition (ASR) remains active**. If the caller speaks instead of pressing a key, the agent processes that speech input normally. This means:
* You should design your flow to handle both spoken and keypad responses.
* If the caller doesn't press a key, there can be a noticeable delay (the inter-digit timeout) before the agent continues.
* Consider adding fallback handling so the agent can prompt the caller again if neither speech nor DTMF input is detected.
### Design considerations
You can use DTMF as either a **fallback** alongside speech or the **primary** input method for a step:
* **Fallback**: The agent asks a question and accepts either a spoken or keypad response. Useful for yes/no confirmations or simple menu selections.
* **Primary input**: The step is dedicated to DTMF collection – for example, entering a credit card number, phone number, or booking reference. In this case, consider creating a **separate step** with DTMF-specific configuration (digit count, end key, timeout) rather than mixing DTMF-heavy collection with speech-primary steps.
## Recording opt-out
One common use of DTMF is giving callers the option to opt out of call recording. You listen for a specific keypress – such as "Press 1 to opt out" – and then call `conv.discard_recording()` to remove the current call's recording.
### The `discard_recording()` function
`conv.discard_recording()` is a built-in method that deletes the audio recording for the current call. It takes no arguments and can be called at any point during the call **except** in the `end` function. Once called, the conversation record shows no recording for that call, and the recording cannot be recovered.
For reliable behavior, call `conv.discard_recording()` after the `start_function` completes – for example, in a dedicated flow step. If you need to discard recordings at the very start of a call, contact your PolyAI representative for guidance.
### Example
At the start of the call, you might say:
> "This call may be recorded for quality and training purposes. To opt out, press 1."
You can play this message either:
* **Before** the greeting (by routing to a DTMF-enabled flow step before the `start` function), or
* **Alongside** your greeting, since DTMF can still be captured while the agent is speaking.
If you want to collect the opt-out keypress *before* any other audio plays, override the greeting in your `start` function with an empty string.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def continue_conversation(conv: Conversation, flow: Flow):
conv.exit_flow()
assert conv.history[-1].role == "user"
if conv.history[-1].text == "1":
conv.discard_recording()
return {
"utterance": "Hello, thanks for calling. How can I help?"
}
```
You can also design the flow so that different keys trigger different actions – for example:
* Press 1 = opt out of recording
* Press 2 = continue with recording enabled
# Example flow
Source: https://docs.poly.ai/flows/example
A complete reservation confirmation flow showing prompts, transitions, and ASR biasing patterns.
**This example includes Python code.** For all code-driven flow guides in one place, visit the **Developer** tab.
You can copy full or partial flows between projects using `Cmd/Ctrl + C` and `Cmd/Ctrl + V` in the Flow Editor.
This flow handles a real-world reservation confirmation scenario. It highlights:
* Step-specific prompts and function references
* ASR biasing for input accuracy
* Clean state transitions between flow steps
## Start step
To help identify some of the key parts of a flow step, the start step is annotated. Click a lettered heading to go to the relevant section.
### A. Flow name (breadcrumb)
The flow name appears at the top of the editor and identifies the current flow – in this case, `reservation confirmation`.
It also acts as a **breadcrumb** for returning to the list of all flows.
### B. Step name and icon
This shows the **current step**: `Collect confirmation code`. The icon indicates it's the **start step** of the flow.
Each step contains:
* A prompt shown to the user
* Transition logic to guide conversation flow
* A set of visible functions the LLM can use.
### C. Transition function reference
Inside the step prompt, the LLM is told to call `save_confirmation_code` if the user provides a valid input.
This example uses both the [conversation](/tools/classes/conv-object) and [flow](./object) objects:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def save_confirmation_code(conv: Conversation, flow: Flow, confirmation: str):
conv.state.confirmation_code = confirmation
flow.goto_step("Collect first name")
return
```
Transition functions are inserted using `/` and appear as underlined blocks in the prompt editor.
See the [transition function](./transition-functions) page for more details.
### D. Add another reference
Clicking the . icon below the prompt lets you:
* Add another function reference
* Insert a transition to another step
* Reference rules or Knowledge topics
Useful for branching logic, but avoid chaining multiple `goto_step()` calls inside a single function. Always `return` after calling `goto_step`.
### E. ASR biasing
This step has [ASR biasing](./asr-biasing) set to **Alphanumeric**, improving recognition of spoken confirmation codes like "B–4–Z–Q–9".
You can enable other biasing types as needed, including:
* Name spelling
* Time and date
* Numbers
* Addresses
See the [ASR biasing](./asr-biasing) page for more details.
### F. Flow toolbar (bottom panel)
At the bottom of the Flow Editor:
* **Flow functions** opens a modal to manage logic used in the flow
* **+ Step** adds a new node to the flow canvas
Transition functions created here are scoped to the flow unless declared globally.
## Middle steps
The agent collects the user's **first** and **last** name using separate steps, each with its own prompt and transition logic.
* If the user has already provided the name, the agent calls the relevant function (e.g. `save_first_name`, `save_last_name`) and proceeds.
* If not, the agent asks for it directly.
Each step uses **few-shot prompting** to improve recognition of non-standard formats:
```plaintext theme={"theme":{"light":"github-light","dark":"github-dark"}}
User: It's Smith.
Johannes: Thanks – that's Smith.
User: My surname is de la Cruz.
Johannes: Got it, de la Cruz.
User: Sure, that's H-O-W-E. Howe.
Johannes: Thanks for spelling it – I've got Howe.
```
[ASR biasing](./asr-biasing) is set to **Name spelling** in the "Collect last name" step to improve recognition of spelled-out inputs.
## End step
The agent attempts to match the user's provided details against active reservations.
* It compares the `confirmation code`, `first name`, and `last name` against entries in the `$reservations` list.
* If a match is found, it calls the `confirm_reservation` function and moves forward.
* If no match is found, it calls `transfer_call` with the following parameters:
* `destination="RESERVATIONS"`
* `reason="RESERVATION_NOT_FOUND"`
* `utterance="Right, let me put you through to someone who can help. Just a moment."`
This is a standard validation step using dynamic values and conditional logic.
## Reminders
* LLMs only see the **current step's prompt and function list** – not previous steps
* Always use `return` after `flow.goto_step()` to prevent silent overrides
* Avoid naming transitions by position (`goto_step_4`) – use intent-based names like `match_reservation` or `retry_lookup`
### Improving date accuracy from speech
Voice transcription (ASR) often produces **unstructured date strings** like `twenty seventh of june twenty twenty five`, which can be hard to parse – especially if you're enforcing a strict format like `DD/MM/YYYY`.
Instead of checking the date format in the step prompt or rejecting invalid inputs immediately:
* Move format enforcement into the **LLM context field** of the parameter.
* Let the LLM interpret and convert the input to the required format.
* Then validate it in the function.
This avoids issues where STT fails to cleanly separate day, month, and year – especially in multi-lingual or noisy environments.
**Example context (LLM parameter):**
> The user will say a date. You must extract the intended date and convert it to DD MM YYYY format. If they use a different format, rewrite it before returning.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
try:
day, month, year = value.split(" ")
except ValueError:
return {
"utterance": "Hmm, I didn't catch a full date. Could you say it again?"
}
```
This code uses Python's [try statement](https://docs.python.org/3/tutorial/errors.html#handling-exceptions) to attempt splitting the date into day, month, and year. If the input isn't in the expected format, it prompts the user to try again.
# Few-shot prompting
Source: https://docs.poly.ai/flows/few-shot-prompting
Guide the LLM with example user inputs and expected agent responses to improve accuracy in flows and topics.
[Few-shot prompting (FSP)](https://www.ibm.com/think/topics/few-shot-prompting) is a technique for guiding the LLM by showing it examples of what users might say – and how the agent should respond. This helps the agent:
* Match vague or unexpected inputs to the correct tool call
* Extract values in non-standard formats (e.g., spelled names, long reference codes)
* Avoid asking unnecessary questions when the value is already present
* Maintain a consistent tone, phrasing, or logic pattern
## Where you can use few-shot prompting
FSP works anywhere the LLM reads a prompt. The most common places in Agent Studio are:
* **Flow step prompts** – the step prompt field in the Flow Editor
* **Topic actions** – the action prompt within a managed topic
* **Agent behavior prompts** – global rules that shape the agent's overall behavior
The examples on this page use flow steps, but the same principles apply wherever you write prompts.
## Why it matters
In a flow, the agent only sees:
* The **current step prompt**
* The **listed functions** (names, descriptions, arguments)
It **does not** see previous step prompts or conversation state unless you surface them.
Each step must stand alone. Few-shot prompting fills in the gaps by giving the model examples to reason from.
Because step prompts are inserted last in the LLM input stack, FSP examples appear directly before the model generates its next turn – making them highly influential.
## Basic structure
Each few-shot example consists of:
* A realistic **user message**
* A matching **agent behavior** – often a response + tool call
Place these inside the prompt, either inline or at the top before your main instructions.
Here's what a set of few-shot examples looks like inside a "Collect last name" step prompt:
```plaintext theme={"theme":{"light":"github-light","dark":"github-dark"}}
User: It's Smith.
Agent: Thanks – that's Smith. [call save_last_name("Smith")]
User: My surname is de la Cruz.
Agent: Got it, de la Cruz. [call save_last_name("de la Cruz")]
User: Sure, that's H-O-W-E. Howe.
Agent: Thanks for spelling it – I've got Howe. [call save_last_name("Howe")]
```
The same pattern looks like this in the Flow Editor:
You don't need dozens of examples – **2–5** is usually enough, especially if you cover:
* A standard, clean input
* An edge case (e.g., multi-word names, spelled-out values)
* A fallback or clarification
* An input that's already been provided earlier in the conversation
Too many examples can make the model too rigid or cause it to overfit to specific cases. If the agent starts parroting your examples word-for-word instead of generalising, reduce the number of examples or make them more varied.
## Tips for strong few-shot examples
* **Use realistic language** – write examples that sound like actual callers, not idealized or overly formal phrasing.
* **Show both success and edge cases** – include at least one non-standard input so the model handles edge cases correctly.
* **Match the agent's persona** – if the agent has a name and tone, use them consistently in the example responses.
* **Pair responses with tool calls** – show the model exactly which function to call and with what arguments.
* **Keep examples independent** – each example should stand alone. Don't build a sequence where example 2 depends on example 1.
## What to avoid
* **Mixing FSP examples with conditional logic** – keep your few-shot examples separate from `if/else` style instructions in the same prompt. Mixing them confuses the model about what's an example versus what's a rule.
* **Using too many examples** – more than 5 examples rarely helps and can cause overfitting. Start with 2–3 and add more only if the agent struggles with specific cases.
* **Copying examples between steps** – each step has different functions and goals. Tailor your examples to the specific step they live in.
* **Using placeholder data** – avoid generic values like "John Doe" or "123". Use realistic but varied values that reflect what real callers say.
## Related reading
* [Prompting Guide: Few-shot](https://www.promptingguide.ai/techniques/fewshot)
* [IBM: Few-shot prompting](https://www.ibm.com/think/topics/few-shot-prompting)
* [Example flow](/flows/example) – see FSP in context within a full reservation confirmation flow
* [Behavior and prompting guide](/behavior/general/rules) – prompting best practices including few-shot examples
# Flows
Source: https://docs.poly.ai/flows/introduction
Use flows to guide callers through structured, multi-step processes with validation and branching logic.
Flows guide callers through structured, multi-step processes: collecting a booking reference, routing by account type, or verifying identity before transferring.
Flows are found under **Flows** in Agent Studio.
Use [FAQs](/knowledge/faqs/introduction) for simple question-and-answer interactions. Use flows when the conversation requires a specific sequence of steps, input validation, or branching logic.
Most flows can be built visually using **Default steps** and entity extraction, without Python. See the [no-code flows guide](/flows/no-code/introduction).
**Use [Wren](/wren/introduction) to scaffold a flow from a description** — *"build a booking flow that collects party size, date, time, and name, then confirms"*. Open **Flows** to review, or keep asking Wren to refine (*"add a confirmation step"*, *"skip the email field if it's already on file"*). Every change lands on a branch.
## How flows work
Each flow is made up of **steps**: self-contained conversation states that ask for something, validate input, and route the conversation forward. Steps are connected by labeled edges that describe when each path should be taken.
There are two ways to build flow logic:
Write natural-language prompts, extract entities, and route by labeled conditions. Best for booking, verification, and data-collection flows.
Use Python to call APIs, apply strict business rules, or perform numeric comparisons. Best when routing depends on custom logic.
## How to trigger a flow
Three ways to start a flow:
1. **From a Managed Topic**, type `/Flow` in the Actions field
2. **From another flow**, use a transition step in the flow editor
3. **From code**, call `conv.goto_flow("Reservation flow")` in a function
See [triggering flows](/flows/triggering-flows) for detailed instructions and examples.
APIs do not trigger flows directly. Flows orchestrate steps, and Function steps can call APIs.
## Connecting steps
In the Flow Editor:
* Use `/Steps` in the prompt to connect to the next step
* Add labeled conditions on the edges between steps to describe when each path should be taken
* Use the Flow Functions modal to see all transitions in one place
Always include an exit step in your flow. Un-exited flows can cause hallucinations.
Use descriptive step and condition names like `check_reservation_match`, not vague ones like `step_two`, this helps the LLM reason correctly.
## Standard entity types
Define the kind of input your agent collects (Alphanumeric, Number, Date, Time, Phone number, Name, Address, Free text, Multiple choice). See [entity types](/flows/no-code/entities) for configuration.
## Developer details
The following sections cover the Python execution model and code-driven techniques. If you are building flows without code, you can skip these.
### LLM interaction model
When the agent is inside a flow step, this is the input order:
1. System prompt (includes [Behavior](/behavior/general/rules) and [Agent](/behavior/general/agent) agent configuration).
2. Any relevant [Knowledge](/knowledge/faqs/introduction) topics (if applicable).
3. The current step's text prompt.
4. A list of available functions with names, descriptions, and arguments.
What the LLM doesn't see:
* Previous step prompts.
* Any system context, unless it's surfaced in the prompt or state.
**Previous steps are not visible to the LLM.** Each prompt must be self-contained.
### Knowledge function visibility in flows
To make global Knowledge functions available while a flow is running, enable this in your agent's **Voice** or **Messaging** advanced settings (under the model configuration section). Contact your PolyAI representative if you need help enabling this setting.
### Code-driven techniques
These techniques require Python. See **Code-driven flows** in the sidebar.
* [Transition functions](/flows/transition-functions) control the flow's routing logic.
* Use [few-shot prompting](/flows/few-shot-prompting) to clarify expected inputs or edge cases.
* Set [ASR biasing](/flows/asr-biasing) to improve voice transcription for structured or ambiguous values like confirmation codes or personal names. Learn more about [ASR (automatic speech recognition)](https://en.wikipedia.org/wiki/Speech_recognition).
* Use [variables](/tools/variables) to store and reference data across steps.
## Next steps
Build flows visually with prompts and entity extraction, no Python required.
Build your first no-code flow step by step.
Configure entity types, validation, and collection in your flows.
ASR biasing, DTMF, and rich text references in advanced steps.
A complete reservation confirmation flow with step-by-step walkthrough.
Write Python logic to control how your agent moves between steps.
# Advanced steps
Source: https://docs.poly.ai/flows/no-code/advanced-steps
Enable ASR biasing, DTMF collection, and function references in flow steps for precise control.
Advanced steps give maximum control over transitions, ASR biasing, and DTMF collection. They are intended for developers who need precise control that Default steps cannot provide.
## ASR biasing options (per-step)
Advanced steps support structured ASR biasing modes for common input types:
| Mode | Use case |
| --------------- | ----------------------------------------- |
| Alphanumeric | Booking references, confirmation codes |
| Name spelling | Phonetically spelled names |
| Numeric | Ages, short numbers |
| Party size | Group booking numbers |
| Precise date | Specific calendar dates |
| Relative date | Flexible time references ("next Tuesday") |
| Single number | One-digit responses |
| Time | Spoken times |
| Yes/No | Confirmation-style responses |
| Address | Postcodes, street names |
| Custom keywords | Your own keyphrases |
See [ASR biasing in flows](/flows/asr-biasing) for full details.
## DTMF configuration (per-step)
Advanced steps can collect DTMF (touch-tone) input. Configure:
| Setting | Description |
| -------------------------- | --------------------------------------------------------------- |
| **Enable DTMF** | Toggle on to accept keypad input |
| **Inter-digit timeout** | How long to wait between digits (default 3 seconds) |
| **Max digits** | Maximum number of digits to collect (1-32) |
| **End key** | Which key signals input is complete (`#`, `*`, or none) |
| **Collect while speaking** | Whether to accept input while the agent is talking |
| **Mark as PII** | Flag the collected input as personally identifiable information |
See [DTMF in flows](/flows/dtmf) for full details.
## Rich text references
Advanced step prompts support inline references using template tags:
| Reference type | Syntax | Purpose |
| ------------------- | ---------------------- | ------------------------------------------ |
| Transition function | `{{ft:functionId}}` | Call a transition function from the prompt |
| Global function | `{{fn:functionId}}` | Call a global function |
| SMS template | `{{sms:templateId}}` | Send an SMS template |
| Handoff | `{{ho:destinationId}}` | Trigger a handoff |
| Variant attribute | `{{va:attributeId}}` | Insert a variant attribute value |
## Related pages
Step types, routing logic, and canvas controls.
Configure entity types and validation for your flows.
Improve transcription accuracy for structured inputs in flow steps.
# Entities
Source: https://docs.poly.ai/flows/no-code/entities
Define and validate structured data types like names, dates, and phone numbers in flow steps.
Entities are structured values the agent can collect and reuse during a flow. Each entity has a **type** that determines what validation is applied. Entity names must be unique across all entities in your project.
Mark an entity as required on a condition so the LLM won't consider that condition satisfied without a valid value. In Function steps, you must enforce this yourself in code.
## Entity types and configuration
Open-ended responses with no validation. Use when you need to capture unstructured input like comments or descriptions.
Numeric values. Configure:
* **Decimal** -- toggle to accept decimal values (float) or restrict to integers
* **Range** -- optionally set a **minimum** and **maximum** value (up to 1,000,000)
Any mix of letters and numbers. Useful for booking references, confirmation codes, or postal codes. Configure:
* **Validation type** -- choose a built-in preset or custom regex:
* **Zip code** -- US zip code format (e.g. `12345` or `12345-6789`)
* **Postal code** -- UK postal code format
* **Custom** -- your own regular expression
Telephone numbers. Configure:
* **Country codes** -- restrict to specific countries (26 supported, including US, CA, GB, AU, DE, FR, and others)
Calendar dates. No additional configuration.
Clock times. Configure:
* **Time range** -- set a start and end time to restrict valid values
Selection from a predefined list. Configure:
* **Options** -- the accepted values the caller can choose from
Personal names (first name, last name, or both). No additional configuration.
Street addresses or locations. No additional configuration.
## Entity visibility in a step
Entities extracted in a step are immediately available in that same step. You can:
* **Reference entities in the prompt** using `{{entity:entity_name}}` – the system resolves the value at runtime, so the prompt can adapt based on what the caller just provided.
* **Use entities in redirect conditions** – mark an entity as a [required entity](/flows/no-code/introduction#conditions) on a condition so the LLM won't activate that path until the entity has been collected **and validated**.
Extraction and evaluation happen in the same step execution – you do not need a separate step to use an entity you just collected.
Entity extraction is a flow-only feature. [FAQs](/knowledge/faqs/introduction) don't support entity extraction directly. To collect structured data when a topic is matched, use the topic's action to [trigger a flow](/flows/triggering-flows) and configure entity extraction in that flow's steps.
## When entities count as "collected"
An entity only counts as collected once it passes validation. If a caller provides a value in the wrong format or outside the allowed range, the agent asks them to try again – and any condition that requires that entity will not activate until a valid value is provided.
This means a condition with **required entities** will only trigger after every required entity has a valid value. If you are debugging why a condition is not firing, check whether the entity passed validation – not just whether the caller said something.
In [Conversation Review](/analytics/conversations/review), system-generated events show entity validation and condition evaluation results, helping you identify whether an entity was rejected or a condition was skipped.
## Validation and retries
Depending on the entity type:
* Valid values allow the flow to continue.
* Invalid values trigger re-asking or fallback routing.
Because there is no automatic cap on retries, always design your flow with a **fallback condition** (e.g. "caller unable to provide") so the conversation can exit the step gracefully if collection stalls.
If a branch depends on an entity being present, always include a clear fallback path for when it is not collected or fails validation.
## Accessing entities in code
Validated entities are available in Function steps two ways:
* `conv.state.entity_name` -- the value coerced to its native Python type. Recommended for new code.
* `conv.entities.entity_name.value` -- the raw string value, with validation metadata on the surrounding object. Use when you need the original string or the [`EntityValidationResult`](/tools/classes/conv-object#entities) fields.
Both are kept in sync. An entity only appears in `conv.state` after it passes validation, so failed extractions never overwrite earlier values.
### Native types in `conv.state`
The type stored in `conv.state` is derived from the entity's configuration:
| Entity type | Stored as | Example |
| ---------------- | --------- | ------------------------------- |
| Number (integer) | `int` | `conv.state.party_size == 4` |
| Number (decimal) | `float` | `conv.state.weight_kg == 12.5` |
| Currency | `float` | `conv.state.amount == 19.99` |
| Everything else | `str` | `conv.state.email == "a@b.com"` |
If coercion fails for any reason, the original string value is stored so your function still receives a value to work with.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Recommended – native int, no cast needed
if conv.state.party_size > 15:
flow.goto_step("Group Booking")
# Still supported – raw string from conv.entities
party_size = int(conv.entities.party_size.value)
if party_size > 15:
flow.goto_step("Group Booking")
```
`conv.entities.entity_name.value` is **always a string**, even for numeric types. Cast to `int()` or `float()` before numeric comparisons, otherwise string comparison rules apply and `"9" > "10"` evaluates to `True`. Use `conv.state.entity_name` to skip the cast.
See [no-code flows – accessing entities in a function](/flows/no-code/introduction#accessing-collected-entities-in-a-function) for more examples.
## Related pages
Step types, routing logic, and canvas controls.
Build your first no-code flow step by step.
ASR biasing, DTMF, and rich text references.
# No-code flows
Source: https://docs.poly.ai/flows/no-code/introduction
Build conversational workflows visually using Default steps and entity extraction without Python.
Use no-code flows when you need structured conversations -- collecting information, routing based on caller input, or confirming details -- without writing Python. Most booking, verification, and data-collection workflows can be built entirely with Default steps.
**Use Default steps (no-code) when** routing can be expressed with labeled branches and entity extraction. **Use [Function steps](#function-step-low-code-still-visual) instead** when you need API calls, strict numeric comparisons, or custom business logic. **Use [code-driven flows](/flows/transition-functions) instead** when the entire flow requires Python control.
A typical no-code flow:
1. Ask for information (intent)
2. Extract structured data (entities)
3. Route based on what was collected (conditions)
4. Continue the conversation in the correct branch
5. End cleanly with an exit flow
You can build most structured interactions using Default steps alone.
**Key concepts**
* **Step (node):** A box in the editor. It represents one moment in the conversation (ask something, confirm something, collect details).
* **Edge:** A line between steps. It represents a possible next path.
* **Condition:** A label on an edge that explains when that edge should be taken.
* **Entity:** A piece of structured information you want to collect (phone number, date, number of passengers).
* **Exit flow:** A terminal end point (finish, handoff, stop).
## Step types
### Choosing a step type
| | Default step | Function step | Advanced step |
| ---------------------- | ------------------------------------------------ | --------------------------------------------- | ----------------------------------------------- |
| **Best for** | Most flows -- collecting info, routing by intent | API calls, business rules, strict comparisons | Full prompt control with code-based transitions |
| **Prompt field** | Yes | No (code only) | Yes |
| **Routing** | LLM evaluates condition labels | Your code calls `flow.goto_step()` | Transition functions called by the LLM |
| **Entity extraction** | Built-in | Access through `conv.entities` in code | Access through code in transition functions |
| **Conditions** | Labels + descriptions guide the LLM | Labels are decorative (code decides) | Transition functions control routing |
| **ASR biasing / DTMF** | No | No | Yes |
| **Requires Python** | No | Yes | Yes |
**Start with Default steps.** Move to a Function step only when you need code (API calls, numeric comparisons, custom validation). Use an [Advanced step](/flows/no-code/advanced-steps) when you need per-step ASR biasing, DTMF collection, or direct control over transition function references in the prompt.
### Default step (no-code)
Use Default steps for most of your flow. They let you:
* Write natural-language instructions (prompt)
* Extract [entities](/flows/no-code/entities)
* Branch to other steps using labeled edges
Most booking, verification, and data-collection flows can be built entirely with Default steps.
A common pattern:
* Step 1: Collect an entity (e.g. number of passengers)
* Step 2: Route to the correct branch (e.g. Individual Booking vs Group Booking)
* Step 3: Continue collecting relevant details
Keep each step focused on one task.
### Function step (low-code, still visual)
Use Function steps only when you need more control, such as:
* Calling an API
* Applying strict business rules
* Performing numeric comparisons
* Writing state changes
Function steps differ from Default steps in several ways:
* They contain **code only** -- there is no prompt field.
* They support conditions for visual routing, but conditions on Function steps **do not have required entities** -- your code handles all validation.
* They do not support ASR biasing, DTMF, or other per-step audio configuration.
If your flow starts to require complex logic, that's when a Function step becomes appropriate.
If you can express the routing clearly using labeled branches, prefer Default steps.
### Exit flow
Use Exit flows to:
* End the conversation cleanly
* Represent a handoff
* Make terminal paths obvious in the editor
Every flow should end in an Exit flow.
## How flow logic actually runs
There are two execution models inside flows. Understanding this prevents most confusion.
### Default steps (LLM-driven) -- no code required
In a Default step:
* The agent collects entities from what the caller says and checks them against the type rules you configured.
* Once an entity passes validation, the agent considers which condition to take. Conditions with **required entities** only activate when those entities have valid values.
* If the caller gives an invalid value (wrong format, out of range, etc.), the agent asks them to try again.
To reference a collected entity inside the step prompt, use rich text `/entities`. This inserts `{{entity:entity_name}}`, which resolves to the entity's value at runtime. Entities are available in the same step they are collected – you do not need a separate step.
Default step prompts support entity and variant attribute references, but do not support function references. To call transition functions or global functions from a prompt, use an [Advanced step](/flows/no-code/advanced-steps).
You do not need to write code for this.
### Function steps (code-driven) -- for developers
**The following section covers writing Python code to control flow logic.** If you are building flows without code, you can skip this section.
Function steps behave differently.
* Conditions are evaluated in your Python code.
* The condition label on the edge is decorative.
* You must explicitly move the flow forward.
#### Accessing collected entities in a function
Validated entities are available two ways:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.state.entity_name # native Python type (recommended)
conv.entities.entity_name.value # raw string value
```
Example:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
email = conv.state.email # str
party_size = conv.state.number_of_passengers # int, no cast needed
# Equivalent using the raw string value:
party_size = int(conv.entities.number_of_passengers.value)
```
`conv.state` stores numeric entities as `int` or `float` based on the entity configuration. `conv.entities..value` is always a string -- cast to `int()` or `float()` before numeric comparisons. See [Entities -- Accessing entities in code](/flows/no-code/entities#accessing-entities-in-code).
#### Moving to another step from a function
A Function step does not automatically transition.
You must call:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
flow.goto_step("step_name", "condition_label")
```
Example:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if conv.state.number_of_passengers > 15:
flow.goto_step("Group Booking", "large party")
else:
flow.goto_step("Individual Booking", "small party")
```
If you do not call `flow.goto_step()`, the flow will not move.
To leave the current flow entirely and enter a different one, use `conv.goto_flow("flow_name")` instead.
## Step naming rules
* Step names must be **unique in a flow** (case-sensitive).
* The **start step** cannot be deleted -- reassign the start step to a different node first.
## Canvas controls
The flow editor provides controls to help you work with complex flows:
* **Zoom in/out** -- adjust the view scale (minimum 25%)
* **Fit to view** -- auto-centers and scales to show all nodes
* **Tidy up** -- auto-arranges nodes in a top-to-bottom layout with consistent spacing
* **Snap to grid** -- nodes align to a 30px grid for clean positioning
### Working with nodes
* **Drag** steps using the drag handle at the top of each node
* **Copy/paste** nodes using keyboard shortcuts
* **Connect** steps by clicking the **+** handle at the bottom of a node and selecting the target step
* **Pan** the canvas by holding the space bar and dragging
### Conditions
When you connect a Default step to another step, a **condition node** appears on the edge. Each condition has:
| Field | Required | Description |
| --------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Label** | Yes | A short name the LLM uses to decide when to take this path (max 50 characters, must be unique in the step) |
| **Description** | Yes | Describe **when** the model should trigger this condition. Provide as much detail as necessary -- this is the primary signal the LLM uses to choose between paths. |
| **Required entities** | No | Entities that must be collected and validated before this condition can activate |
The **description** field is critical for routing accuracy. A label like "large party" tells the LLM *what* the path is; the description tells it *when* to take it -- for example, "The caller has confirmed they need a booking for more than 15 guests."
Write condition labels for humans first -- someone reading the flow should understand the routing logic from labels alone.
## Related pages
Configure entity types, validation, and collection in your flows.
Build your first no-code flow step by step.
ASR biasing, DTMF collection, and rich text references in advanced steps.
# Quickstart
Source: https://docs.poly.ai/flows/no-code/quickstart
Build your first no-code flow step by step with Default steps and routing.
This walkthrough builds a simple booking flow:
* Collect phone number (or fallback to email)
* Collect booking details
* Route large parties differently
* Finish cleanly
## Step 1 -- Create a flow
1. Go to **Flows**
2. Click **+ Create flow**
3. Name it (example: **Make a booking**)
Start with a Default step as your entry point.
## Step 2 -- Add your first Default step (entry point)
1. Add a **Default step**
2. Name it **Collect contact details**
3. In the **Prompt**, write something like:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Ask for the caller's phone number to create or look up the booking.
If they don't want to provide a phone number, ask for an email address instead.
Confirm what you captured in one sentence.
```
## Step 3 -- Add entities to the step
In **Collect contact details**, add:
* Phone number
* Email address (fallback)
This tells the system what structured information to extract and validate. See [Entities](/flows/no-code/entities) for the full list of entity types.
## Step 4 -- Add routing
After collecting information, add labeled edges such as:
* phone collected
* phone missing
* caller refuses
Keep labels short and explicit.
If you later add a step like **Collect Number of Passengers**, you might branch into:
* Individual Booking
* Group Booking
Clear labels make routing easier to understand and maintain.
## Step 5 -- Add a finish/exit step
1. Add an **Exit flow**
2. Name it **Booking complete**
3. Connect your success branches to it
## Writing good condition labels
Good labels are:
* Short
* Unambiguous
* Business-focused
Examples:
* phone collected
* phone missing
* party size above 15
* requires handoff
* unclear
Avoid vague labels like:
* valid
* ok
* continue
If someone else looks at your flow, they should understand the logic immediately from the labels alone.
## Next steps
Configure entity types and validation for your flows.
ASR biasing, DTMF collection, and rich text references.
Understand how flows work and when to use them.
# Flow object
Source: https://docs.poly.ai/flows/object
The Flow object controls step transitions and exposes the current step name in a flow.
**This page requires Python familiarity.** It is a reference for developers writing transition functions inside flows.
The `Flow` object gives you control over how the agent moves between steps in a flow. It is available as a parameter in any flow-scoped function (transition function).
The two companion methods on the [Conversation object](/tools/classes/conv-object) – `conv.goto_flow()` and `conv.exit_flow()` – handle transitions between flows and are documented at the bottom of this page.
## `goto_step(step_name, condition_label)`
Moves the agent to another step in the current flow. This replaces the current step's prompt and functions with those of the target step.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
goto_step(step_name: str, condition_label: str = None) -> None
```
| Parameter | Type | Required | Description |
| ----------------- | ----- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `step_name` | `str` | Yes | The exact name of the target step as it appears in the Flow Editor. **Case-sensitive.** |
| `condition_label` | `str` | No | A label for the transition edge. This is **purely decorative** – it appears on the edge in the Flow Editor for readability but does not affect runtime behavior. |
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Basic transition
flow.goto_step("Confirm Name")
return
# With a condition label (decorative only)
flow.goto_step("Group Booking", "large party")
return
```
**Always call `return` immediately after `goto_step()`.**
The runtime executes your function to completion before honoring the transition. If you call `goto_step()` more than once without returning, each call overwrites the previous transition state – only the final one takes effect. Using `return` after each call prevents accidental overwrites and makes control flow explicit.
If `step_name` doesn't match any step in the flow, the transition **fails silently**. Step names are **case-sensitive**. See [transition functions – debugging](/flows/transition-functions#debugging-tips) for troubleshooting guidance.
### Execution context matters
`flow.goto_step()` behaves differently depending on where it's called:
* **In a transition function (LLM step)** – the LLM decides whether to call the function based on the step prompt and user input. The transition only happens if the LLM invokes the function.
* **In a [Function step](/flows/no-code/introduction)** – the code runs directly without LLM involvement. The transition is deterministic. Here, the `condition_label` parameter controls which edge is highlighted in the visual editor.
### Conditional transition
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if conv.state.has_phone_number:
flow.goto_step("Confirm phone number")
return
flow.goto_step("Collect phone number")
return
```
### Input validation and state update
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def save_user_name(conv: Conversation, flow: Flow, first_name: str, last_name: str):
if not first_name or not last_name:
return "Please make sure we have both first and last name before continuing."
conv.state.first_name = first_name
conv.state.last_name = last_name
flow.goto_step("Confirm Name")
return
```
This is the recommended structure: validate input, update state, transition, then return.
## `current_step`
**Type:** `str`
**Returns:** The name of the current step the agent is in.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if flow.current_step == "Collect Name":
return "You're in the name collection step."
```
Useful for debugging or conditional routing when a single function serves multiple steps.
## Companion methods on the Conversation object
These methods live on the [`Conversation` object](/tools/classes/conv-object), not on `Flow`, but are commonly used alongside `flow.goto_step()` in transition logic.
### `conv.goto_flow(flow_name)`
Transitions to a different flow at the end of the current turn. Can be called from **any function** – flow-scoped or global.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.goto_flow("Identity Verification")
return
```
See [triggering flows](/flows/triggering-flows) for all the ways to start a flow.
### `conv.exit_flow()`
Exits the current flow and returns the agent to its default (non-flow) behavior. Exit steps are visually marked in yellow in the Flow Editor.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def complete_booking(conv: Conversation, flow: Flow):
conv.state.booking_confirmed = True
conv.exit_flow()
return
```
Every flow should have at least one exit path. Un-exited flows can cause the agent to hallucinate or loop. See the [flows overview](/flows/introduction) for more on flow design.
## Next steps
Improve speech recognition accuracy within flow steps
Full reference for conv.goto\_flow() and conv.exit\_flow()
Handle keypad input for menus, PINs, and account numbers
# Transition functions
Source: https://docs.poly.ai/flows/transition-functions
Use transition functions to control how your agent moves between steps and flows
**Prerequisites:** Familiarity with Python and [Flows](/flows/introduction). This page covers writing code to control flow routing.
A transition function is any function that contains `flow.goto_step()` or `conv.goto_flow()`. Transition functions control how your agent moves between steps within a flow – or between flows entirely.
* **`flow.goto_step()`** moves the agent to another step in the current flow. This must be a **flow function** (scoped to the flow).
* **`conv.goto_flow()`** moves the agent to a different flow. This can be called from a flow function **or** a [global function](/tools/introduction).
Transition functions should not be confused with [global functions](/tools/introduction), which are shared across [flows](/flows/introduction), [global rules](/behavior/general/rules), and [Knowledge topics](/knowledge/faqs/introduction).
## Recognizing transition vs global functions in the UI
Global functions are distinguished in flow steps by the function symbol . You can edit them from the flow editor, but changes apply everywhere the function is used in Agent Studio.
Global functions also have profile items in the functions tab.
Transition functions (flow-scoped) only appear in the **Flow Functions** modal – they do not show up in the functions tab.
## Viewing and managing flow functions
To view and manage transition functions, click the ** Flow functions** button in the bottom-right corner of the Flow Editor. This opens a searchable modal showing all flow functions associated with the current flow.
You can manage transition functions in two ways:
* **From a step** – open the step's context menu and select an existing transition or create a new one.
* **From the Flow Functions modal** – view, rename, delete, or connect transitions to steps.
Duplicating a transition auto-generates a unique name. You can then update the logic or connect it to different steps.
## Creating a transition function
1. **Connect two steps** – if no transition exists, linking two steps prompts you to create one.
2. **Name your transition** – you'll be asked to name the new function. Use a [retrospective, intent-based name](#naming-functions).
3. **Handle name conflicts** – if the name is already in use, the UI shows an error. Rename before proceeding.
4. **View usage references** – after creation, the UI shows how many times the function is referenced in prompts.
## `flow.goto_step()` signature
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
flow.goto_step("step_name")
flow.goto_step("step_name", "condition_label")
```
* **`step_name`** (required) – the name of the target step, exactly as it appears in the Flow Editor. This is **case-sensitive**.
* **`condition_label`** (optional) – a label for the transition edge. In [Function steps](/flows/no-code/introduction), this label appears on the edge in the visual editor and can help with readability, but routing is determined by your code, not the label.
## Example: conditional transition logic
A transition function typically checks state and moves to the appropriate step:
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
stateDiagram-v2
[*] --> CheckVerified: check_user_verified()
CheckVerified --> AccountDetails: user_verified = true
CheckVerified --> VerifyIdentity: user_verified = false
AccountDetails --> [*]
VerifyIdentity --> CheckVerified: After verification
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def check_user_verified(conv: Conversation, flow: Flow):
if conv.state.user_verified:
flow.goto_step("Account details")
return
flow.goto_step("Verify identity")
return
```
## Switching to a different flow with `conv.goto_flow()`
Use `conv.goto_flow()` when the conversation needs to leave the current flow entirely – for example, to start an identity verification or escalation flow.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def check_verification(conv: Conversation, flow: Flow):
attempts = conv.state.verification_attempts or 0
if not conv.state.is_verified:
attempts += 1
conv.state.verification_attempts = attempts
if attempts >= 3:
conv.goto_flow("Identity Verification")
return
flow.goto_step("Retry verification")
return
flow.goto_step("Continue booking")
return
```
See [triggering flows](/flows/triggering-flows) for more on `conv.goto_flow()`.
## Naming functions
Function names directly shape LLM behavior. Use **retrospective, intent-based names** that describe what just happened or what was resolved – not where the flow is going next.
* Good: `last_name_given`, `phone_number_collected`, `reservation_confirmed`
* Also good: `save_postcode`, `check_availability`
* Avoid: `goto_next_step`, `continue_flow`, `go_to_collect_phone_number`
Retrospective names anchor the LLM's reasoning around what the user accomplished, which produces more natural responses. Forward-looking names like `start_confirmation` can cause the model to narrate its own flow logic ("Okay, moving on to confirmation now").
Some teams use past-tense verbs for transition functions (`phone_number_given`) and present-tense for global functions (`get_status_of_order`). Pick a convention and keep it consistent across your project.
## Common mistakes
Never chain multiple tool calls in a single step. This increases the failure rate and makes flow behavior unpredictable.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
save_user_input()
check_availability()
flow.goto_step("Next")
```
Consolidate step logic into one function wherever possible.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def save_and_check(conv: Conversation, flow: Flow, value: str):
conv.state.value = value
if check_availability(value):
flow.goto_step("Confirm booking")
return
flow.goto_step("Unavailable")
return
```
## Best practices
* Always use `return` immediately after `flow.goto_step()` or `conv.goto_flow()`. Omitting it can lead to unexpected behavior – only the **last** `goto_step()` call in a function is executed.
* Keep transition functions focused – one decision, one outcome.
* Transition functions are only visible to the LLM if referenced in the current step's prompt.
* Use them to encapsulate branching logic and control step sequencing – not to generate agent responses.
* If your transition function needs to trigger user-facing output, return a message string **instead of** calling `goto_step()`. These are two separate patterns – don't mix them in the same code path.
**Debugging silent transition failures**
If a transition seems to do nothing, the most common cause is a step name mismatch. Because `flow.goto_step()` is **case-sensitive**, `"CollectName"` and `"collectname"` are different targets. A mismatched name fails silently – there is no error message.
To diagnose:
* Open the **Flow Functions** modal and check the exact step name spelling.
* Look for trailing spaces or special characters in step names.
* Use `flow.current_step` in your function to log the current step name for debugging.
When you rename a step in the Flow Editor, Agent Studio automatically updates `flow.goto_step()` and `conv.goto_flow()` references across the project. However, always verify after renaming – especially if you have step names referenced in strings or variables that the auto-rename may not catch.
## Next steps
Python reference for goto\_step() and current\_step.
All the ways to start a flow using conv.goto\_flow().
Default steps vs Function steps and how routing works.
# Triggering flows
Source: https://docs.poly.ai/flows/triggering-flows
Start flows from FAQs, global functions, or transition functions using conv.goto_flow().
**This page requires Python familiarity** for the programmatic examples. Non-technical operators can trigger flows from FAQs actions without writing code – see [Start a flow from a FAQs action](#start-a-flow-from-a-faqs-action) below. All code-focused content is also available in the **Developer** tab.
A flow starts when something calls `conv.goto_flow("Flow name")`. This may happen through a FAQs action or programmatically inside a function.
This page explains how `conv.goto_flow` can be used:
You can call `conv.goto_flow(...)` from any [function](/tools/introduction) in Agent Studio, including:
* A [FAQs action](/knowledge/faqs/actions/introduction)
* A [global function](/tools/classes/conv-object)
* A [transition function inside a flow](/flows/transition-functions)
Example:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start_booking(conv: Conversation):
conv.goto_flow("Make a booking")
return
```
What happens:
* The function runs during the current turn.
* When the turn completes, the agent enters the named flow.
* The flow begins at its configured start step.
## Start a flow from a FAQs action
A Managed Topic can trigger a flow when it matches a user request. This is the primary way to collect [entities](/flows/no-code/entities) from a topic, since FAQs do not support entity extraction directly.
### Using the /Flow shortcut (no code)
Inside a FAQs **Actions** field, type `/Flow` and use the (+) option to create or attach a flow. When the topic matches, the agent enters the selected flow at its start step.
No custom function is required if you just need to enter the flow.
### Using a tool call from a topic
If you need additional logic before entering the flow – for example, storing context so the agent can return to the topic afterward – use a tool call action instead:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start_verification_flow(conv: Conversation, original_faq: str):
"""
Enter the verification flow and remember which topic we came from.
Args:
original_faq: the name of the topic that triggered this function
"""
conv.state.original_topic = original_faq
conv.goto_flow("Verify Identity")
return
```
You can also trigger a flow from a global function by calling `conv.goto_flow()` directly.
### Returning to a topic after a flow
When a topic triggers a flow (e.g. to collect a date or verify identity), the agent can return to the original topic content after the flow exits. To do this:
1. Before entering the flow, store the topic name in state (e.g. `conv.state.original_topic`).
2. In the flow's exit function, check the stored topic and return a prompt that directs the LLM to the relevant topic content:
**What is an "exit function"?** There is no special "exit function" step type. An exit function is just a [transition function](/flows/transition-functions) on the final step of the flow that calls `conv.exit_flow()` and returns a payload (typically `{"content": ...}` or `{"utterance": ...}`). It runs because the step runs — not because the flow registers an "on-exit hook" — and its return value becomes the function output the agent uses next. Mutating `conv.state` from inside it is the same as from any other function: `conv.state["is_verified"] = True` (or `conv.state.is_verified = True`) takes effect immediately and persists across subsequent turns.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def exit_and_resume(conv: Conversation, flow: Flow):
conv.exit_flow()
if conv.state.original_topic:
return {
"content": f"Look for the topic '{conv.state.original_topic}' in your context. "
f"Continue answering the caller's original question."
}
return {"utterance": "Is there anything else I can help with?"}
```
This pattern avoids a generic "Is there anything else?" when the caller's original question has not yet been answered.
### Common mistakes
`conv.goto_flow()` only **queues** a transition — it does not pause your function, run the target flow inline, and resume. The flow runs on subsequent turns. The following anti-patterns all stem from forgetting that.
**Anti-pattern: checking the verification result inline after `goto_flow()`.**
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Don't do this
def identify_and_verify_user(conv: Conversation, account_type: str):
if conv.state.is_verified == False:
conv.state.original_topic = "check_account_balance"
conv.goto_flow("Identify & Verify User")
# Nothing below runs as you'd expect:
# - The verification flow hasn't run yet (it runs next turn).
# - `verification_successful` is undefined.
# - The branch reading `conv.state.is_verified` runs against
# the value from BEFORE the flow.
if verification_successful:
conv.state.is_verified = True
return check_account_balance(conv, conv.state.pending_account_type)
```
**Why it fails**: `conv.goto_flow(...)` returns immediately and the rest of the function executes on the same turn — before the verification flow has had a chance to run. Any code that reads the result of the flow on the same call will see stale or undefined values.
**Correct pattern**: store context, call `conv.goto_flow(...)`, `return` — and resume in the flow's [exit function](/flows/transition-functions):
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def identify_and_verify_user(conv: Conversation, account_type: str):
if not conv.state.get("is_verified"):
conv.state.pending_account_type = account_type
conv.state.original_topic = "check_account_balance"
conv.goto_flow("Identify & Verify User")
return "Tell the user we need to verify their identity first."
# Already verified — proceed with the original task
return check_account_balance(conv, account_type)
```
Inside the verification flow, its steps set `conv.state["is_verified"]` (and any other context). The flow's exit function reads that state and routes back:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def exit_and_resume(conv: Conversation, flow: Flow):
conv.exit_flow()
if conv.state.get("is_verified") and conv.state.get("original_topic") == "check_account_balance":
return {
"content": (
f"The caller has been verified. "
f"Resume the '{conv.state.original_topic}' topic "
f"with account_type='{conv.state.pending_account_type}'."
)
}
return {"utterance": "Tell the user we could not verify their identity."}
```
**Anti-pattern: `if conv.state.is_verified == False:`**
When `is_verified` has never been written, [`conv.state.is_verified` returns `None`](/tools/classes/conv-object#state), not `False`. `None == False` is `False`, so the guard is silently skipped and the unverified user falls through to the protected code.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Don't do this — silently lets unverified callers through
if conv.state.is_verified == False:
...
# Do this — treats missing and falsy the same way
if not conv.state.get("is_verified"):
...
```
**Anti-pattern: mutual recursion between a protected function and its guard.**
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Don't do this
def check_account_balance(conv: Conversation, account_type: str):
if not conv.state.get("is_verified"):
return identify_and_verify_user(conv) # recurses; returns None on cold-start
...
def identify_and_verify_user(conv: Conversation):
if not conv.state.get("is_verified"):
conv.goto_flow("Identify & Verify User")
return "Tell the user we need to verify their identity first."
# Unreachable on the first call — the function above returned already
return check_account_balance(conv, conv.state.pending_account_type)
```
**Why it fails**: on the first (unverified) call, `identify_and_verify_user` queues `goto_flow` and returns a string — but execution never comes back into `check_account_balance` on this turn, so callers reading the return of `check_account_balance` get `None` or the verification string instead of a balance response.
**Correct pattern**: only the entry point calls `goto_flow` + `return`. Post-flow resumption is done in the target flow's exit function, not by calling the original function recursively.
## Preventing verification loops
A protected function that calls `conv.goto_flow("Identify & Verify User")` whenever `conv.state.is_verified` is falsy will keep sending the user back into verification — every turn, forever — unless two things are true:
* The verification flow writes `is_verified = True` on the success path.
* The protected function has a terminal branch for repeated failures.
Skip either one and the caller is trapped in the loop. The walkthrough below sets up both halves of the fix.
The entry point stores any context the flow will need on resume, calls `conv.goto_flow(...)`, and returns. It must not try to read the verification result on the same turn — `goto_flow` only queues the transition.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def check_account_balance(conv: Conversation, account_type: str):
if not conv.state.get("is_verified"):
conv.state.original_topic = "check_account_balance"
conv.state.pending_account_type = account_type
conv.goto_flow("Identify & Verify User")
return "Tell the user we need to verify their identity first."
...
```
Inside the verification flow, a transition function on the verification step compares user input to the expected value. On success it writes `is_verified = True`; on failure it increments an attempts counter and routes to escalation once the counter hits 3.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def check_verification(conv: Conversation, flow: Flow):
if conv.state.get("user_input") == conv.state.get("expected_value"):
conv.state.is_verified = True
flow.goto_step("Verification succeeded")
return
attempts = conv.state.get("verification_attempts", 0) + 1
conv.state.verification_attempts = attempts
if attempts >= 3:
conv.goto_flow("Escalation")
return
flow.goto_step("Retry verification")
return
```
Without the success-path write, the protected function's `is_verified` check stays falsy and the loop repeats on every turn.
Even with the flow setting `is_verified` correctly, the caller still needs its own guard on the attempts counter. Otherwise a user who fails three times and then returns to the original topic re-triggers verification a fourth time.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def check_account_balance(conv: Conversation, account_type: str):
if conv.state.get("verification_attempts", 0) >= 3:
return "Tell the user we cannot continue without successful identity verification."
if not conv.state.get("is_verified"):
conv.state.original_topic = "check_account_balance"
conv.state.pending_account_type = account_type
conv.goto_flow("Identify & Verify User")
return "Tell the user we need to verify their identity first."
...
```
For the in-flow version of the same escalation pattern — when the failure happens inside a flow rather than at a protected entry point — see [Escalating after repeated failed verification](#example-escalating-after-repeated-failed-verification) below.
## Start or switch flows from inside another flow
Once a user is inside a flow, most movement should happen using `flow.goto_step(...)`.
That keeps the user inside the same workflow and branches to another step.
However, sometimes you need to switch to an entirely different workflow.
Examples:
* The user fails identity verification and must enter a verification flow.
* The user requests a human agent and must enter an escalation flow.
* A compliance rule requires a separate structured process.
* The user changes intent entirely (e.g., from booking to cancellation).
In these cases, start a new flow instead of branching.
You do this by calling `conv.goto_flow("Flow name")` from a function inside the current flow.
### Example: Escalating after repeated failed verification
Imagine a booking flow where the user must confirm their date of birth.
If they fail verification too many times, you want to move them into a dedicated verification flow.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def check_verification(conv: Conversation, flow: Flow):
attempts = conv.state.verification_attempts or 0
if not conv.state.is_verified:
attempts += 1
conv.state.verification_attempts = attempts
if attempts >= 3:
conv.goto_flow("Identity Verification")
return
flow.goto_step("Retry verification")
return
flow.goto_step("Continue booking")
return
```
Inside a function, only the last `flow.goto_step(...)` call is executed
## Routing based on API results
**Main article**: [`conv.api`](/tools/classes/conv-api)
A function may call an API using `conv.api`.
After receiving a response, explicitly choose one of two actions:
* Stay in the current workflow using `flow.goto_step(...)`
* Switch workflows using `conv.goto_flow(...)`
Example:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def check_customer_status(conv: Conversation, flow: Flow):
response = conv.api.crm.lookup_user(id=conv.state.user_id)
if response.status_code != 200:
flow.goto_step("API error handling")
return
data = response.json()
if data.get("status") == "requires_verification":
conv.goto_flow("Verification flow")
return
flow.goto_step("Continue booking")
return
```
## Next steps
Understand how flows work and the available trigger methods.
Control routing logic with Python inside flow steps.
Python reference for goto\_step() and current\_step.
# Agent Studio
Source: https://docs.poly.ai/get-started/introduction
Visual interface for building, deploying, and monitoring PolyAI voice agents.
Agent Studio is PolyAI's visual interface for building, deploying, and monitoring voice and chat agents. Configure knowledge, voice, integrations, and conversation flows without writing code. Most agents run on [Raven](/behavior/models/raven), PolyAI's proprietary LLM; [other models](/behavior/models/model-use) are also supported.
If you are a developer looking to extend your agent with Python functions and API integrations, see [Extend with code](/extend/introduction) instead.
For how Agent Studio fits alongside the ADK and REST APIs, see the [platform overview](/platform/introduction).
## Get started
[Quickstart guide](/get-started/quickstart) – create an account, build an agent, add knowledge, test, and deploy.
[PolyAcademy](/learn/guides/introduction) – structured training from basic setup to advanced configuration.
Explore the sections below for full configuration options.
## Navigate the interface
The sidebar organizes Agent Studio into the sections below. Use ⌘ K / Ctrl K to search from any screen. Coming from the old Build/Channels/Configure layout? See [What's new](/get-started/whats-new).
Some sections are permission-gated or feature-flagged. If a section is missing, check your [role and permissions](/user-management/access-control-scope).
### Home
[Wren](/wren/introduction), our natural-language builder. Describe what you want to do in plain language and it does the work for you.
### Analytics
View and manage your custom metrics. See [Analytics](/analytics/kpis/introduction).
### Conversations
Browse the full history of your conversations, with [PolyScores](/analytics/polyscore), search and filtering. See [Conversations](/analytics/conversations/introduction).
### Custom Dashboards
Build, manage and view custom dashboards. See [Dashboards](/analytics/dashboards/introduction).
### Knowledge
Manage your agent's knowledge base, including [FAQs](/knowledge/faqs/introduction), [external sources](/knowledge/sources/introduction) and [variants](/knowledge/variants/introduction). See [Knowledge](/knowledge/faqs/introduction).
### Flows
Create and manage flows for structured, step-by-step user journeys (e.g. ID\&V). See [Flows](/flows/introduction).
### Tools
Create and manage the tools your agent can use. See [Tools](/tools/introduction).
### Testing
Create, manage and run simulation tests across all channels. See [Testing](/testing/simulation-tests).
### Voice
Manage all your voice settings. See [Voice](/voice-channel/agent).
### Messaging
Manage all your messaging channel settings (e.g. webchat, SMS, email etc.). See [Messaging](/messaging-channel/introduction).
### Integrations
Set up and manage your integrations across app integrations, APIs and MCP connections. See [Integrations](/integrations/introduction).
### Deployments
Manage deployments, view deployment history and run A/B tests. See [Deployments](/environments-and-versions/introduction).
### Widgets
Create, manage and deploy your widgets across channels and surfaces. See [Widgets](/widgets/introduction).
### Account
Manage your account settings. See [Account](/settings/introduction).
## Appearance
Agent Studio supports **light and dark themes**. Set your preference under **Appearance**, reachable from the workspace sidebar: choose light, dark, or match your operating system. Your choice is saved to your profile and follows you across every environment. Dark mode is available to all users.
## Next steps
Build your first agent in minutes
Structured training for Agent Studio
# Quickstart
Source: https://docs.poly.ai/get-started/quickstart
Create an account, build your first agent, and deploy it in minutes.
Create an account, build an agent, add knowledge, test it, and deploy. Five steps.
**In a hurry?** [Wren](/wren/introduction) is the fastest way to scaffold an agent — describe what you want and it generates flows, topics, entities, and settings on a branch you can review. The manual steps below still work if you'd rather build it by hand.
## Prerequisites
* A use case in mind (e.g., customer support, reservations, FAQ)
Go to the [sign-up page](https://studio.us.poly.ai/) and create your PolyAI account.
1. Click **"Sign up with Google"**
2. Select your Google account or enter your credentials
3. Click **"Continue"** to authorize PolyAI
1. Enter your **first name**, **last name**, and **email address**
2. Create a password (at least 12 characters, with 3 of 4 character types: lowercase, uppercase, numbers, special characters)
3. Click **"Create account"** and verify your email
Once signed in, you'll land on the Agent Studio home page.
From the home page, click **+ Agent** to start the agent creation wizard. You can create a blank agent or import an existing configuration.
Configure the basics:
* **Agent name**, internal identifier for your project
* **Response language**, primary language for responses (see [multilingual support](/behavior/language/multilingual) for additional languages)
* **Voice**, select from available [text-to-speech (TTS)](https://en.wikipedia.org/wiki/Speech_synthesis) voices
* **Welcome greeting**, first message users receive (can be customized later in [agent settings](/behavior/general/agent))
Click **Next** to enter Agent Studio.
You can also duplicate an existing agent by clicking the three-dot menu next to any agent on the home page.
Navigate to **Knowledge > FAQs** in the sidebar.
Click **Add topic** and provide:
* **Topic name**, what this topic covers (e.g., "Store hours")
* **Sample questions**, up to 20 ways users might ask (e.g., "When are you open?")
* **Answer**, the response your agent should give
Click **Save** to create the topic.
Changes are saved as **Drafts**. Publish to **Sandbox** to test them. Learn more about [environments and versions](/environments-and-versions/introduction).
**Optional:** Add more topics to expand your agent's capabilities. You can also:
* Upload PDFs or URLs to auto-generate topics
* Connect external knowledge sources like [Zendesk](/integrations/zendesk) or [Google Sheets](/integrations/google-sheets) using the [Connected tab](/knowledge/sources/introduction) in Knowledge
* Add [actions](/knowledge/faqs/actions/introduction) to trigger handoffs, SMS, or other behaviors
See the full [FAQs guide](/knowledge/faqs/introduction) for details on how [RAG](/knowledge/faqs/RAG/introduction) (retrieval-augmented generation) powers topic matching.
The fastest way to test, uses your device's microphone directly.
1. Click the **phone icon** in the top-right corner
2. Select **Sandbox** from the environment dropdown
3. Begin speaking to your agent
Test with text-based chat, directly from Agent Studio.
1. Go to the agent main page
2. Click the **webchat icon** to open a text-based conversation
The chat window shows tool calls and topic citations alongside the conversation. Click the Settings icon to toggle these.
See [webchat setup](/messaging-channel/introduction) for more details.
Test in a production-like environment by calling a connected number.
1. Go to **Voice > Numbers** in the sidebar
2. Click **"Add number"** to purchase a number or connect an existing one
3. Call the number to speak with your agent
See [Numbers](/voice-channel/numbers/introduction) for setup instructions.
**Testing tips:**
* Use specific keywords to trigger your agent's topics
* Test with different accents and speaking styles
* For [multilingual agents](/behavior/language/multilingual), switch languages mid-conversation to test detection
* Review conversations in the [Conversations dashboard](/analytics/conversations/introduction) after testing
Promote your agent through the deployment pipeline:
1. Go to **Deployments** in the sidebar
2. Click **Promote to Pre-release** for user acceptance testing (if available in your project)
3. Click **Promote to Live** to make your agent production-ready
Each environment can have its own phone number and configuration.
Some projects use a simplified pipeline that promotes directly from Sandbox to Live, skipping Pre-release. You can roll back to any previous version if issues arise, see the [deployment pipeline guide](/environments-and-versions/introduction) for details.
## Next steps
Connect APIs and add dynamic behavior with Python functions
Multi-step workflows for bookings, forms, and structured tasks
TTS, voice selection, and audio settings
Dashboards, conversation review, and metrics
## How your agent works
Each conversation turn follows a pipeline: **ASR** (speech to text) → **LLM** (knowledge retrieval and response generation) → **TTS** (text to speech). See [architecture](/glossary/architecture) for a detailed breakdown, or [processing order](/essentials/order) for the step-by-step flow.
# Meet the new Agent Studio
Source: https://docs.poly.ai/get-started/whats-new
Agent Studio got a new layout on 24 June 2026. Watch the tour, see what moved, and find your favorite features in their new homes.
On **24 June 2026** Agent Studio got a new layout. It's on by default for everyone — you can **switch back to the old navigation** using the **New layout** toggle at the bottom of the sidebar while you get used to things.
## Watch the tour
A quick walkthrough of the new layout, the top toolbar, and where things have moved.
[Watch the tour on Loom →](https://www.loom.com/share/d72a63904281443c97e2037ba9b0e3be) (3-minute walkthrough of the new layout)
## What's different
Related settings are now grouped together, so you spend less time jumping between sections. The sidebar goes from 29 pages down to 16, and the old **Build**, **Channels**, and **Configure** groups have been replaced with sections that match the task at hand.
## The new top-level sections
| Section | What it contains |
| --------------------- | ---------------------------------------------------------------------------------------------------------------- |
| **Home** | Wren, our natural-language builder. Describe what you want to do in plain language and it does the work for you. |
| **Analytics** | View and manage your custom metrics. |
| **Conversations** | Browse the full history of your conversations, with PolyScores, search and filtering. |
| **Custom Dashboards** | Build, manage and view custom dashboards. |
| **Knowledge** | Manage your agent's knowledge base, including FAQs, external sources and variants. |
| **Flows** | Create and manage flows for structured, step-by-step user journeys (e.g. ID\&V). |
| **Tools** | Create and manage the tools your agent can use. |
| **Testing** | Create, manage and run simulation tests across all channels. |
| **Voice** | Manage all your voice settings. |
| **Messaging** | Manage all your messaging channel settings (e.g. webchat, SMS, email etc.). |
| **Integrations** | Set up and manage your integrations across app integrations, APIs and MCP connections. |
| **Deployments** | Manage deployments, view deployment history and run A/B tests. |
| **Widgets** | Create, manage and deploy your widgets across channels and surfaces. |
| **Account** | Manage your account settings. |
The previous **Build / Channels / Configure** top-level groups are gone — every page that lived under them now lives in one of the sections above.
## Where common features moved
If you have muscle memory for the old sidebar, this is where to look now:
**Was:** Build > Agent
**Now:** [Behavior > General](/behavior/general/agent)
**Was:** Build > Knowledge > FAQs
**Now:** [Knowledge > FAQs](/knowledge/faqs/introduction). External sources are under [Knowledge > Sources](/knowledge/sources/introduction) (formerly "Connected Knowledge").
**Was:** Build > Variants
**Now:** [Knowledge > Variants](/knowledge/variants/introduction)
**Was:** Channels > Voice > Voice configuration, Audio management, Response control, Speech recognition
**Now:** All under [Voice](/voice-channel/introduction). Common settings are at the top; less-used options are under [Voice > Advanced](/voice-channel/advanced/call-settings).
**Was:** Build > Call handoffs
**Now:** [Voice > Handoffs](/voice-channel/handoffs)
**Was:** Configure > Numbers
**Now:** [Voice > Numbers](/voice-channel/numbers/introduction)
**Was:** Channels > Chat configuration
**Now:** [Messaging](/messaging-channel/introduction). The chat widget moves out to [Widgets](/widgets/introduction).
**Was:** Build > SMS
**Now:** [Voice > Message templates](/voice-channel/message-templates). SMS templates sit under Voice because they're usually triggered from voice handoffs and follow-ups.
**Was:** Build > Test suite
**Now:** [Testing](/testing/simulation-tests) — a dedicated top-level section with **Tests** (the test list) and **Test runs** (run history).
**Was:** Various places under Configure > General and Channels > Voice configuration
**Now:** Behind the **Advanced settings** button in [Behavior](/behavior/introduction). Includes safety filters, phrase filters, and runtime configuration.
**Was:** Build > Real-time configuration / Configuration builder
**Now:** [Real-time config](/real-time-config/introduction). The schema and data tabs are unchanged.
**Was:** Analytics > Smart Analyst (standalone top-level page)
**Now:** Part of [Wren](/wren/introduction). Same questions, same answers — just open Wren and ask. See [Analyze conversations](/wren/analyze).
**Was:** Configure > Metrics, Configure > Post-call reporting
**Now:** Custom metrics via the **Metrics** button on the [Analytics page](/analytics/kpis/introduction), and post-call extraction with [`prompt_llm`](/tools/classes/conv-utils#prompt_llm)
**Was:** Configure > Dashboards
**Now:** [Analytics > Dashboards](/analytics/dashboards/introduction)
**Was:** Configure > APIs
**Now:** [Integrations > API integrations](/integrations/api/introduction)
**Was:** Configure > General, User management, API keys
**Now:** All under [Account](/settings/introduction)
**Was:** Configure > Call data
**Now:** [Settings > Call data](/call-data/introduction)
## Removed
* **Agent Analysis** — per-call diagnosis is now in [Conversations > Diagnosis](/analytics/conversations/diagnosis); aggregate trends are in [Dashboards](/analytics/dashboards/introduction) and by [asking Wren](/wren/analyze).
* **Project history** — version history and diffs are now on the [Deployments](/environments-and-versions/introduction) page. See [Compare versions](/environments-and-versions/diffs).
* **Luzmo dashboards** — replaced by [Standard](/analytics/dashboards/introduction), [Safety](/analytics/dashboards/introduction), and [Custom dashboards](/analytics/dashboards/custom).
* **Add/view numbers** — removed from the toolbar. Manage numbers under [Voice > Numbers](/voice-channel/numbers/introduction).
## Old URLs still work
Every page in the old sidebar redirects to its new location, so existing bookmarks, links in old emails, and links in your help-desk macros continue to work. You don't need to update anything urgently.
## Reverting (temporary)
If you need the old sidebar while you migrate internal docs or run training, use the **New layout** toggle at the bottom of the sidebar to switch back.
The revert toggle will stay available for a short period after launch, then be removed.
## Questions?
* Open [Wren](/wren/introduction) and ask "where do I find ...?" — it'll point you to the right page.
* Or reach out to your account manager or the customer Slack channel.
# Architecture overview
Source: https://docs.poly.ai/glossary/architecture
Understand how PolyAI agents process conversations from start to finish.
How PolyAI agents process conversations from start to finish.
## How conversations flow
When a user connects to your PolyAI agent, the conversation passes through several key stages. The exact path depends on the channel–voice (telephony), webchat, or SMS.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
flowchart LR
A[User] --> B{Channel}
B -->|Voice| C[Telephony]
B -->|Chat| D[HTTP/WebSocket]
C --> E[Speech Recognition]
D --> F[Agent Service]
E --> F
F --> G[Language Understanding]
G --> H[Decision Engine]
H --> I[Response Generation]
I -->|Voice| J[Text-to-Speech]
I -->|Chat| K[Text Response]
J --> C
K --> D
```
### 1. Connection layer
The connection layer handles how users reach your agent. For voice, this is telephony; for webchat, it's HTTP/WebSocket connections. PolyAI's infrastructure supports automatic failover across all channels.
#### Voice (Telephony)
PolyAI's voice infrastructure is designed for enterprise reliability with automatic failover and redundancy. Calls are distributed across multiple media servers for continuous availability. If the PolyAI service goes down, calls automatically transfer back to your contact center – designed to minimize dropped calls.
**Supported telephony providers:**
* Twilio
* Amazon Connect
* Genesys
* SIP-based systems
* Custom telephony integrations
- **Load balancing**: Incoming calls are distributed across multiple media servers
- **Redundant media processing**: Multiple media servers provide continuous availability
- **Automatic failover**: If a primary service experiences issues, calls are automatically routed to secondary services
- **Contact center transfer**: Automatic transfer back to your contact center if the PolyAI service goes down
- **Enterprise reliability**: Built for high-volume voice applications
#### Webchat
For webchat interactions, users connect via HTTP/WebSocket:
* **Instant connection**: No telephony latency–conversations begin immediately
* **Persistent sessions**: Maintains conversation state across page reloads
* **Customizable widget**: Embed directly in your website or application
See also: [Webchat integration](/messaging-channel/introduction)
#### SMS
SMS interactions are handled through integrated messaging providers, allowing agents to send and receive text messages.
See also: [SMS integration](/voice-channel/message-templates), [Voice integrations](/integrations/voice/introduction)
### 2. Input processing
How user input reaches the agent depends on the channel:
* **Voice**: Speech is converted to text using automatic speech recognition (ASR)
* **Webchat/SMS**: Text input is received directly–no ASR needed
#### Speech recognition (ASR) – Voice only
For voice interactions, the user's speech is converted to text using automatic speech recognition (ASR). PolyAI's platform integrates with multiple ASR providers for accuracy and coverage across use cases and languages.
**Supported ASR providers:**
* [Google Cloud Speech-to-Text](https://cloud.google.com/speech-to-text/docs)
* [Google Gemini](https://ai.google.dev/gemini-api/docs)
* [Amazon Transcribe](https://docs.aws.amazon.com/transcribe/)
* [Deepgram](https://developers.deepgram.com/docs/introduction)
* [OpenAI](https://platform.openai.com/docs/guides/speech-to-text)
* [Mistral](https://docs.mistral.ai/)
* [NVIDIA Riva](https://docs.nvidia.com/deeplearning/riva/user-guide/docs/index.html)
* [NVIDIA NeMo](https://docs.nvidia.com/nemo-framework/user-guide/latest/index.html)
* Custom ASR integrations
The platform routes requests to the best-fit provider based on language, domain, and availability, with automatic fallback if a provider is unavailable.
**Key capabilities:**
* Multiple languages and accents
* Industry-specific vocabulary
* Real-time transcription with low latency
* ASR biasing and keyphrase boosting for domain-specific terms
* Automatic provider failover for high availability
See also: [ASR](/glossary/introduction#asr-automatic-speech-recognition), [ASR biasing](/glossary/introduction#asr-biasing), [Global ASR configuration](/learn/guides/advanced/global-asr)
### 3. Agent service
The agent service is the core of the system, powered by PolyAI's LLM-native architecture. It receives the transcribed user input and coordinates:
* **Language understanding**: Uses large language models (LLMs) to interpret what the user said, their intent, and extract entities in a conversational, context-aware manner
* **Decision making (Policy engine)**: Determines the appropriate response based on your configured [FAQs](/knowledge/faqs/introduction), [flows](/flows/introduction), and [rules](/behavior/general/rules) by executing nodes in priority order
* **Knowledge retrieval**: Uses RAG (Retrieval-Augmented Generation) to pull relevant information from both tabs of the [Knowledge](/knowledge/faqs/introduction) area – [FAQs](/knowledge/faqs/introduction) and [Connected](/knowledge/sources/introduction)
* **Action execution**: Triggers any necessary [tool calls](/tools/introduction) or API integrations
* **Context management**: Maintains dialogue context and turn history throughout the conversation
Unlike intent-based NLU, the LLM processes the full conversation history each turn rather than classifying into fixed intents.
See also: [LLM](/glossary/introduction#llm-large-language-model), [Policy engine](/glossary/introduction#policy-engine), [Node](/glossary/introduction#node), [RAG](/glossary/introduction#rag-retrieval-augmented-generation)
### 4. Response generation
Based on the decision engine's output, the system generates an appropriate response using your agent's configured voice, tone, and knowledge. This may involve:
* Retrieving relevant information using RAG (Retrieval-Augmented Generation)
* Applying global rules and response control filters
* Generating contextually appropriate responses via the LLM
See also: [RAG](/glossary/introduction#rag-retrieval-augmented-generation), [LLM](/glossary/introduction#llm-large-language-model), [Response control](/glossary/introduction#response-control)
### 5. Response delivery
How responses reach the user depends on the channel:
* **Voice**: Text is converted to speech using TTS and streamed to the user
* **Webchat/SMS**: Text responses are delivered directly
#### Text-to-speech (TTS) – Voice only
For voice interactions, the generated response is converted to natural-sounding speech and played back to the user. PolyAI integrates with multiple TTS providers to deliver high-quality, natural-sounding voices across languages and use cases.
**Supported TTS providers:**
* [ElevenLabs](https://elevenlabs.io/docs)
* [Amazon Polly](https://docs.aws.amazon.com/polly/)
* [Azure Speech](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/)
* [Cartesia](https://docs.cartesia.ai/)
* [Google Cloud Text-to-Speech](https://cloud.google.com/text-to-speech/docs)
* [Hume](https://dev.hume.ai/docs)
* [MiniMax](https://www.minimax.io/)
* [Neuphonic](https://docs.neuphonic.com/)
* [OpenAI](https://platform.openai.com/docs/guides/text-to-speech)
* [PlayHT](https://docs.play.ht/)
* [Rime](https://rime.ai)
* Custom TTS integrations
**Audio management and caching:**
PolyAI's audio management system optimizes user experience and reduces latency through intelligent caching:
* **Audio cache**: Frequently used phrases (greetings, confirmations, transfer messages) are cached for instant playback — the same audio plays every time, with no TTS latency
* **Cache requirements**: Audio is cached when the same utterance is generated at least twice within a 24-hour window
* **Regeneration control**: Edit cached audio directly in Agent Studio to adjust stability, clarity, and pronunciation
* **UX optimization**: Fine-tune voice quality for critical phrases without regenerating audio on every call
**Additional capabilities:**
* [SSML](https://www.w3.org/TR/speech-synthesis/) markup for fine-grained control over pronunciation, pauses, and emphasis
* Custom pronunciations using [IPA](https://www.internationalphoneticassociation.org/content/ipa-chart) notation
* Multiple voice options and custom voice cloning
* Real-time audio streaming for low-latency responses
See also: [TTS](/glossary/introduction#tts-text-to-speech), [SSML](/glossary/introduction#ssml-speech-synthesis-markup-language), [Pronunciations](/glossary/introduction#pronunciations), [Audio Management](/learn/guides/advanced/audio-management)
## Data storage and synchronization
During and after a conversation, PolyAI captures, stores, and synchronizes several types of data to support analytics, compliance, and operational workflows.
### Data types and retention
| Data type | Purpose | Nature | Retention |
| --------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------- | ---------------- |
| Dialogue context | Tracks the full dialogue history, state variables, and turn data for the current call | Real-time, in-memory during call | Duration of call |
| Turn data | Stores individual exchanges (user input, agent response, intents, entities) for analytics and review | Structured conversation logs | Configurable |
| Conversation metadata | Records conversation-level information (duration, variant, environment, handoff state) | Structured metadata | Configurable |
| Audio recordings | Full call recordings for quality assurance and compliance | Audio files (WAV/MP3) | Configurable |
| Transcripts | Complete text transcripts of conversations | Structured text | Configurable |
| Metrics and events | Records events for reporting and dashboards | Time-series data | Configurable |
### Data synchronization and access
PolyAI provides multiple methods to access and synchronize conversation data with your systems:
* **[Studio transcripts](/call-data/studio-transcripts)**: Review transcripts and recordings directly in the PolyAI platform
* **[Conversations API](/call-data/conversations-api/list-conversations)**: Programmatically retrieve conversation metadata, transcripts, and recordings
* **[AWS S3 integration](/call-data/s3-to-s3)**: Automatically sync call data to your AWS S3 bucket for long-term storage and compliance
* **[Handoff metadata](/api-reference/handoff/introduction)**: Share real-time conversation state with live agents during transfers
All data handling follows enterprise security standards and can be configured to meet compliance requirements such as HIPAA, GDPR, and PCI-DSS.
See also: [Dialogue context](/glossary/introduction#dialogue-context), [Turn](/glossary/introduction#turn), [Conversation metadata](/glossary/introduction#conversation-metadata), [Call data documentation](/call-data/introduction)
## Key components you configure
As a builder in Agent Studio, you control how the agent behaves through:
* **[Knowledge](/knowledge/faqs/introduction)**: The Knowledge area under Build contains two tabs:
* **[FAQs](/knowledge/faqs/introduction)**: Curated knowledge with fine-grained control over utterances and actions. Use for structured, stable information that requires precise agent behavior. Can trigger functions, flows, and other agentic actions.
* **[Connected](/knowledge/sources/introduction)**: Fast integration of external knowledge sources (URLs, files, Zendesk, Gladly, ServiceNow). Ideal for FAQ-style content and large volumes of continuously updated information. Cannot trigger actions or flows.
* **[Flows](/flows/introduction)**: Structured conversation paths for complex tasks
* **[Tools](/tools/introduction)**: Custom logic and external integrations
* **[Behavior](/behavior/general/rules)**: Global behavior constraints
* **[Voice settings](/voice-channel/introduction)**: How the agent sounds
### FAQs tab vs. Connected tab
Both tabs within the Knowledge area expose information to your agent, but serve different purposes:
| Capability | FAQs tab | Connected tab |
| --------------------------------------- | -------- | ------------- |
| Trigger actions, functions, flows, SMS | Yes | No |
| Precise control over agent responses | Yes | No |
| Auto-sync from external sources | No | Yes |
| Best for stable, structured info | Yes | -- |
| Best for frequently updated FAQ content | -- | Yes |
| Fine-grained behavior control | Yes | No |
**Supported Sources integrations:**
* Zendesk
* Gladly
* ServiceNow
* Additional integrations are in development – contact your PolyAI representative for the latest availability
If both tabs contain conflicting information, **FAQs always takes priority**.
See also: [FAQs overview](/knowledge/faqs/introduction), [Sources overview](/knowledge/sources/introduction)
## Processing a single turn
Each turn in a conversation follows this sequence:
The system receives user input–speech is transcribed using ASR (voice), or text is received directly (webchat/SMS).
The LLM analyzes what the user wants and extracts entities in a context-aware manner.
Relevant information is fetched from both tabs of the Knowledge area (FAQs and Connected) using RAG (Retrieval-Augmented Generation).
The policy engine evaluates nodes and any active flows or functions are executed.
The LLM composes a response based on all available context, applying global rules and response control filters.
The response is delivered to the user–synthesized to speech via TTS (voice) or sent as text (webchat/SMS).
See also: [Turn](/glossary/introduction#turn), [Policy engine](/glossary/introduction#policy-engine)
## Related resources
Definitions of key terms used throughout the platform.
Build your first agent step by step.
Explore available platform integrations.
Learn about data storage and synchronization options.
# Glossary
Source: https://docs.poly.ai/glossary/introduction
Definitions of key terms used in the PolyAI Agent Studio.
Quick reference for terms, acronyms, and concepts used across the platform.
## A2P 10DLC
Application-to-Person 10-Digit Long Code. A US regulatory framework requiring registration for businesses sending SMS. Required for all US-based Twilio numbers to prevent message blocking.
## Agent
An AI-driven voice agent built on PolyAI that interacts with customers in real time across voice (telephony), chat (webchat), and SMS. Each agent is a self-contained project with its own knowledge, flows, tools, voice configuration, and deployment environments. You set global behavior — such as greeting, personality, and role — in the Agent settings page, and the agent uses these alongside its knowledge to generate contextual, grounded responses. See [Agent settings](/behavior/introduction), [Quickstart](/get-started/quickstart).
## Agent Studio
The primary interface for designing, configuring, and deploying AI agents. Agent Studio is organized into sections: **Home** (Wren, the natural-language builder), **Analytics** (custom metrics), **Conversations** (full conversation history with PolyScores), **Custom Dashboards**, **Knowledge** (FAQs, Sources, Variants), **Flows**, **Tools**, **Testing** (simulation tests), **Voice**, **Messaging**, **Integrations**, **Deployments**, **Widgets**, and **Account**. See [Getting started](/get-started/quickstart), [Build essentials](/essentials/order).
## Abandonment
A conversation that ends before the user's request is resolved and without a handoff to a human — for example, the caller hangs up mid-conversation.
* **Chat:** tracked internally via the `CHAT_ABANDONED` metric on conversations that disengage without resolution or handoff.
* **Voice:** no dedicated "abandonment rate" widget is surfaced by default; abandoned calls can be identified in [Conversation Review](/analytics/conversations/review) (short duration, no handoff, no task completion) or via the [Conversations API](/api-reference/conversations/introduction) using `total_duration`, handoff fields, and [PolyScore](/analytics/polyscore).
For a business-specific definition, track it as a [custom metric](/analytics/kpis/introduction).
## AHT (Average Handle Time)
The mean time spent handling a conversation end-to-end, computed as total conversation duration ÷ total conversations. Surfaced as the **Average handle time** widget on the [Self-serve dashboards](/analytics/dashboards/introduction) and home page insights. Also accessible per-conversation via the [Conversations API](/api-reference/conversations/introduction) `total_duration` field (with `polyai_duration` for the agent-handled portion). For business-specific variants (e.g. excluding silence or hold), define a [custom metric](/analytics/kpis/introduction).
## Annotation
A manual tag applied during [Conversation Review](/analytics/conversations/review) to highlight issues like incorrect transcriptions or missing topics.
## ASR (Automatic Speech Recognition)
Converts spoken language into text. PolyAI uses ASR models optimized for conversational accuracy with support for multiple languages, accents, and industry-specific vocabulary. See [Advanced voice settings](/voice-channel/advanced/call-settings#keyphrases).
## ASR biasing
A technique to improve speech recognition by instructing the ASR model to prioritize specific words or phrases. Available at three levels: **global** (also called [keyphrase boosting](#keyphrase-boosting), configured in Advanced voice settings), **per-step** (configured on individual flow steps), and **dynamic** (set at runtime via `conv.set_asr_biasing()`). See [ASR biasing in flows](/flows/asr-biasing) and [Advanced voice settings](/voice-channel/advanced/call-settings#keyphrases).
## Barge-in
When a user interrupts the agent mid-sentence. The system detects this and stops playback to process the user's input immediately.
## Branch
A parallel working copy of your agent's draft, enabling multiple team members to make changes simultaneously without conflicts. Branches can be merged back into the main draft with visual conflict resolution. See [Environments](/environments-and-versions/introduction).
## BYOM (Bring Your Own Model)
Integrate your own LLM by exposing an API endpoint that follows the OpenAI `chat/completions` schema. See [BYOM](/behavior/models/model-use#bring-your-own-model-byom).
## Channel
The medium through which users interact with the agent. In custom functions, the channel is exposed as `conv.channel_type` with values like `"sip.polyai"` (voice), `"webchat.polyai"` (webchat), `"chat.polyai"` (agent chat), and `"sms.twilio"` (SMS). In the Conversations API, the `channel` field uses analytics-level labels such as `VOICE-SIP` and `WEBCHAT`. See [Telephony](/voice-channel/numbers/introduction), [Webchat](/messaging-channel/introduction), [SMS](/voice-channel/message-templates).
## Chunking
Splitting large bodies of text into smaller pieces for retrieval. Sources sources are scraped, chunked, and matched against user input via RAG. See [Sources](/knowledge/sources/introduction), [RAG](/knowledge/faqs/RAG/introduction).
## CSAT (Customer Satisfaction Score)
A metric collected via post-conversation voice surveys. Callers rate their experience on a 1–5 scale; responses are tracked in dashboards. Triggered using `conv.goto_csat_flow()` in the end function. See [Surveys (CSAT)](/analytics/csat/introduction).
## Sources
External knowledge sources (websites, PDFs, Zendesk) connected to your agent via **Knowledge > Sources**. Read-only, synced from external sources, cannot trigger actions. See [Sources](/knowledge/sources/introduction).
## Containment rate
Percentage of conversations fully handled by the agent without human handoff. A key metric for agent effectiveness.
Scope and caveats:
* **Per-conversation**, not per-caller. Each conversation is counted independently, so a caller who hangs up and calls back is treated as two separate conversations. Recontact is **not** factored into the containment metric.
* **Transfers are not contained.** Any conversation that ends in a handoff to a human (via flows, knowledge actions, or tools) is excluded from the contained count.
* For caller-level views, use the [Conversations API](/api-reference/conversations/introduction) to group by caller identifier and apply your own recontact logic, or define a [custom metric](/analytics/kpis/introduction).
See [Standard dashboards](/analytics/dashboards/introduction).
## Conversation metadata
Structured data about a conversation: duration, start time, associated variant. See [Conversations API](/api-reference/conversations/introduction), [Call data](/call-data/introduction).
## Conversation diagnosis
A debugging tool within Conversation Review showing which flows, functions, and topics were activated during a conversation. See [Conversation diagnosis](/analytics/conversations/diagnosis).
## Dialogue context
The conversation state maintained throughout a call, including turn history, state variables, and all context needed for processing. See [Conv object](/tools/classes/conv-object), [Variables](/tools/variables).
## Draft
The state between the latest published version and ongoing changes. Drafts become versions upon publishing. See [Environments](/environments-and-versions/introduction).
## DTMF (Dual-Tone Multi-Frequency)
Touch-tone input from phone keypads. Allows users to enter PINs, account numbers, or menu selections by pressing keys. See [DTMF](/flows/dtmf).
## Embedding
A numerical vector representation of text used for semantic search and retrieval. PolyAI computes embeddings for Knowledge topics to enable RAG-based matching. See [RAG](/knowledge/faqs/RAG/introduction).
## End function
A function that finalizes an interaction: closing conversations, sending confirmations, triggering logging, or performing clean-up. See [End function](/tools/end-tool).
## Environment
The deployment stage of a project: `sandbox`, `pre-release`, or `live`. See [Environments](/environments-and-versions/introduction).
## Entity
Typed extracted data from user input (phone numbers, dates, names, addresses). Helps structure and validate information collected during conversations. See [Flows](/flows/introduction), [Variables](/tools/variables).
## Event-sourced
A data architecture pattern where all changes are stored as a sequence of events. The Conversations API v3 uses this for reliable, scalable data ingestion. See [Conversations API v3](/api-reference/conversations/v3/endpoint/get-conversations).
## Flows
A conversation logic system that guides users through structured, multi-step interactions. Use flows when your agent needs to follow step-by-step instructions for complex tasks like reservations, data collection, or authentication. Each flow contains steps, transition functions, and conditions (such as "all entities collected") that determine when the agent moves to the next step. Flows can be built with a visual no-code editor or with code-driven transition functions. See [Flows](/flows/introduction), [Examples](/flows/example), [No-code flows](/flows/no-code/introduction).
## Function
A reusable backend operation (also called a "tool") that gives your agent the ability to interact with the outside world during a conversation. Functions can retrieve external data, modify conversation state, perform calculations, send SMS messages, or log structured data. Each function has an LLM description that helps the model understand when to call it, along with named parameters and optional delay control with filler responses for high-latency operations. Special function types include [start functions](/tools/start-tool) (triggered at conversation start) and [end functions](/tools/end-tool) (triggered when a conversation ends). See [Functions](/tools/introduction), [Function classes](/tools/classes).
## Behavior (global rules)
Behavior constraints applied across all agent interactions for consistency, compliance, and tone control. Enforced by the LLM during response generation. See [Behavior](/behavior/general/rules).
## Handoff
Transferring a user from the AI agent to a human agent or external system. Handoffs can be triggered from knowledge actions, flows, or tools. Each handoff includes a destination (where to route the call), a reason (why the transfer is happening), and optionally an utterance (what the agent says before transferring). The [Handoff API](/api-reference/handoff/introduction) lets downstream platforms retrieve conversation context at the moment of transfer. See [Call handoff](/voice-channel/handoffs).
## HITL (Human in the loop)
A system design where a human can review, intervene in, or take over an AI-driven process. In Agent Studio, HITL is implemented through several distinct mechanisms rather than a single feature:
* **Runtime escalation** – the agent transfers the conversation to a live human agent when it detects a scenario that requires human judgement (billing disputes, complaints, policy violations, low confidence, or explicit caller request). See [Call handoff](/voice-channel/handoffs) and [Handoff context handover](/voice-channel/handoffs#handoff-context-handover).
* **Post-call quality review** – humans inspect transcripts in [Conversation Review](/analytics/conversations/review), score them with [PolyScore](/analytics/polyscore), and tag issues with [annotations](/analytics/conversations/annotations) to drive agent improvements.
* **Build-time approvals** – every change to a deployed agent passes through Sandbox → Pre-release → Live with [version](/environments-and-versions/introduction) gating, so a human reviews and promotes each version before it reaches production traffic.
When prospects ask about HITL, they usually mean *runtime escalation*; the related signals are containment rate, handoff rate, and handoff reason. See [Containment rate](#containment-rate), [Handoff](#handoff), [Conversation review](/analytics/conversations/review).
## Integration
A pre-built connection between PolyAI and a third-party platform. Integrations are grouped by category: Telephony (Twilio, Amazon Connect, Genesys, Five9, Dialpad), CRM (Salesforce), Hospitality (OpenTable, TripleSeat), Healthcare (Epic), Knowledge (Gladly), and MCP tool servers. Browse available integrations under **Integrations** in Agent Studio. See [Integrations](/integrations/introduction).
## IPA (International Phonetic Alphabet)
A standardized pronunciation system used in the Pronunciations feature to define how the agent pronounces specific terms. See [Voice configuration](/voice-channel/introduction).
## Keyphrase boosting
The global level of [ASR biasing](#asr-biasing), configured in Advanced voice settings. Biases the ASR model toward recognizing specific words and phrases on every turn of the conversation for better domain-specific transcription accuracy. See [Keyphrases](/voice-channel/advanced/call-settings#keyphrases).
## LLM (Large Language Model)
The AI model powering the agent's understanding and response generation. PolyAI's proprietary [Raven](/behavior/models/raven) model family is recommended for most deployments – designed for conversational AI with strong grounding and natural speech. Third-party models (GPT, Claude) are also supported. See [Model](/behavior/models/model-use).
## Live
The production environment where the agent handles real customer traffic. See [Environments](/environments-and-versions/introduction).
## MCP (Model Context Protocol)
An open standard for connecting AI agents to external tool servers. In Agent Studio, you can add MCP servers under **Integrations > MCP** to extend your agent with third-party tools and resources. Agent Studio auto-discovers available tools and lets you toggle them individually. See [MCP integrations](/mcp/agent-studio-integrations).
## FAQs
Version-controlled, editable topics in **Knowledge > FAQs**. Each topic has a name, content that determines relevance to user input, sample questions, and actions the agent can execute when the topic is matched. Topics can trigger tool calls, handoffs, and SMS messages. See [FAQs](/knowledge/faqs/introduction), [RAG](/knowledge/faqs/RAG/introduction).
## Multi-site configuration
Customizing agent responses based on location using Variants. See [Variants](/knowledge/variants/introduction), [CSV imports](/knowledge/variants/csv-imports).
## NLU (Natural Language Understanding)
The component responsible for interpreting user input, detecting intents, and extracting entities. See [Flows](/flows/introduction), [Variables](/tools/variables).
## Node
A building block within a conversation flow defining what the agent says, listens for, and where the conversation goes next. See [Flows](/flows/introduction), [No-code flows](/flows/no-code/introduction).
## Out-of-domain (OOD)
User input that falls outside the agent's configured knowledge. Tracked in analytics; may trigger fallback behaviors. See [Conversation review](/analytics/conversations/review).
## Policy engine
The decision-making component that controls conversation flow by executing nodes in priority order: global nodes, current node, then fallback nodes. See [Flows](/flows/introduction).
## PolyScore
An AI-generated quality score (1–5) assigned to each eligible conversation across voice, messaging, and email. Composed of two sub-scores: **Agent Quality** (how competently the agent handled the exchange) and **Task Success** (whether the conversation delivered on its objective). Scores are interpreted as: None (user not engaged, or fewer than three user turns), Low (1–2, conversation likely failed to resolve the issue), Medium (3–4, operating as designed but with areas for improvement), and High (5, resolved cleanly with strong performance). Displayed as a color-coded badge in Conversation Review and accessible via the Conversations API. See [PolyScore](/analytics/polyscore).
## Pre-release
A staging environment for UAT before promoting to production. See [Environments](/environments-and-versions/introduction).
## Processing
The sequence of operations on each conversation turn: ASR, language understanding, knowledge retrieval, decision making, response generation, and delivery. See [Processing order](/essentials/order), [Architecture](/glossary/architecture).
## Project
A complete agent configuration including knowledge, flows, tools, settings, and voice. Can have multiple variants. See [Variants](/knowledge/variants/introduction).
## Pronunciations
Custom rules for adjusting how the agent pronounces specific terms using IPA and regex-based substitution. See [Voice configuration](/voice-channel/introduction).
## Performance metrics
Key indicators for agent effectiveness and quality. See [Standard dashboards](/analytics/dashboards/introduction), [Self-serve dashboards](/analytics/dashboards/introduction).
## RAG (Retrieval-Augmented Generation)
A technique where the system retrieves relevant knowledge before generating a response, so the model can ground its answer in specific content. See [RAG](/knowledge/faqs/RAG/introduction).
## Ranking and retrieval
The system that computes and manages embeddings for Knowledge topics, performing vector operations and runtime matching against user input. See [RAG](/knowledge/faqs/RAG/introduction).
## Raven
PolyAI's proprietary model family for conversational AI, specialized for customer service across voice and chat. **Raven 3.5** is the recommended model for all deployments, supporting both voice and chat. See [Raven](/behavior/models/raven).
## Response control
Modifies how agents respond: rate-limiting, interruption handling, compliance filtering. See [Advanced voice settings](/voice-channel/advanced/call-settings).
## Safety dashboard
Monitors flagged content, risky user utterances, and safety filter performance. See [Self-serve dashboards](/analytics/dashboards/introduction).
## Sandbox
The development environment for creating, modifying, and testing agent versions before promotion. See [Environments](/environments-and-versions/introduction).
## Wren
The AI assistant built into Agent Studio. Ask it in natural language to build or change your agent, or to answer questions about recent conversation data — it samples up to 500 conversations per query and surfaces insights on containment, escalation reasons, and caller sentiment. See [Analyze conversations](/wren/analyze).
## SBC (Session Border Controller)
A network element managing and securing SIP calls, handling routing, security, and interoperability between VoIP networks. See [Telephony](/voice-channel/numbers/introduction).
## Secrets
Sensitive credentials or tokens stored securely and accessed by the agent for integrations. See [Secrets](/secrets/introduction).
## SIP (Session Initiation Protocol)
A signaling protocol for initiating, maintaining, and terminating voice calls over IP networks. See [Telephony](/voice-channel/numbers/introduction), [Call handoff](/voice-channel/handoffs).
## SIP header
Metadata fields in SIP messages for routing instructions or custom information. Custom headers typically start with `X-`. See [Call handoff](/voice-channel/handoffs).
## SSML (Speech Synthesis Markup Language)
XML-based markup for controlling TTS output: pauses, pitch, emphasis. See [Voice configuration](/voice-channel/introduction).
## Step
A self-contained conversation state within a flow, consisting of text prompts and optional functions. See [Flows](/flows/introduction), [No-code flows](/flows/no-code/introduction).
## Stop keyword
A regex pattern that halts or logs agent responses containing specific phrases. See [Stop keywords](/voice-channel/advanced/call-settings#stop-keywords).
## Start function
A function triggered at the beginning of an interaction for state initialization, authentication, or conditional logic. See [Start function](/tools/start-tool).
## Transcript correction
Editing AI-generated conversation transcripts to improve ASR accuracy and training data quality. See [Speech recognition](/voice-channel/advanced/call-settings#transcript-corrections).
## TTS (Text-to-Speech)
Converts written text into spoken language. PolyAI supports multiple providers and custom voice configurations. See [Voice configuration](/voice-channel/introduction), [Choosing a voice](/voice-channel/choosing-a-good-voice).
## Turn
A single exchange in a conversation, defined as the execution between two consecutive user inputs. See [Conversation review](/analytics/conversations/review).
## UAT (User Acceptance Testing)
Testing in pre-release to validate agent behavior before production. See [Environments](/environments-and-versions/introduction).
## Utterance
A unit of meaning that can be expressed in multiple ways (e.g. "Hello", "Hi", "Bonjour"). See [Multilingual agents](/behavior/language/multilingual).
## Validation
Checking whether user-provided information is complete, accurate, or matches expected criteria within a flow. See [Flows](/flows/introduction).
## Variable
Values stored and retrieved during a conversation via `conv.state`. Persist for the duration of a single conversation. See [Variables](/tools/variables).
## Value extractor
An extractive model that identifies and extracts specific information (dates, times, names, phone numbers) from user input.
## Variant
A configuration allowing different behaviors within a single agent project. Supports multi-site deployments, A/B testing, and per-region customization. See [Variants](/knowledge/variants/introduction).
## Variant ID
Fields used in API filters and analytics to identify and segment conversations by specific agent variant.
## Translation
A localized version of agent content (greetings, confirmations, prompts) managed through the Translations page under **Behavior > Language**. Translations can be auto-generated or manually overridden. Reference them in functions via `conv.translations.your_key`. See [Translations](/behavior/language/translations).
## VoIP (Voice over Internet Protocol)
Delivering voice communications over IP networks rather than traditional telephone lines.
## Webhook
An HTTP callback that PolyAI sends to your endpoint when specific events occur (for example, a conversation ends or a handoff is triggered). Configure webhooks via the [Webhooks API](/api-reference/webhooks/introduction) to integrate with external systems in real time.
# PolyAI documentation
Source: https://docs.poly.ai/home
Build agents ready for real customer conversations.
PolyAI Platform · Documentation
From first draft to production.
PolyAI is the Agentic Dialog Platform for voice and chat agents that complete
real customer tasks — bookings, payments, claims, escalations — across 24+
languages, at sub-300ms latency. Built for resolution, not deflection.
## How a PolyAI agent works
Every agent handles a conversation the same way: it listens, decides what to do from your configuration and knowledge, then responds — in under 300ms, in 24+ languages. [Raven](/behavior/models/raven), our voice-native model, drives each turn.
You shape that behavior across four areas:
* **Behavior** — the agent's personality, rules, and [guardrails](/behavior/guardrails/introduction).
* **Knowledge** — grounded [FAQs and sources](/knowledge/faqs/introduction) so answers stay accurate, not hallucinated.
* **Flows** — [multi-step logic](/flows/introduction) for tasks like booking, payment, or verification.
* **Voice** — natural [text-to-speech](/voice-channel/introduction) tuned for real phone conversations.
Every change lands in a [shareable test environment](/environments-and-versions/introduction) before it reaches production — so you can try the agent on a real call before your customers do.
## Choose how you build
The same agent is available through three surfaces. Start in whichever fits how you work — and mix them at any time.
**No-code & low-code.** Configure behavior, knowledge, voice, and flows visually in the browser.
**Code & Git.** Pull your agent into local YAML and Python, edit in your IDE, and ship from the CLI.
**Programmatic.** Build, run, and observe agents from your own systems — no SDK required.
**New — Wren.** Describe your agent in natural language and let it draft the first version for you. [Get started](/wren/introduction).
## What you can build
PolyAI agents run in production for banks, hotels, healthcare providers, and retailers — handling the calls and chats that used to need a person:
* **Bookings & reservations** — check availability, take a reservation, modify or cancel.
* **Payments** — take secure payments and process refunds mid-conversation.
* **Claims & servicing** — file a claim, check a balance, update an account.
* **Escalations** — resolve what it can, then [hand off](/voice-channel/handoffs) to a human with full context.
## Explore the docs
Behavior, knowledge, voice, flows, and messaging channels.
Telephony, CRM, payments, and custom or MCP connectors.
Environments, dashboards, and conversation review.
Maintenance, troubleshooting, and release notes.
# API integrations
Source: https://docs.poly.ai/integrations/api/introduction
Define and call external APIs directly from Agent Studio without writing custom HTTP request code.
You can define custom HTTP APIs in Agent Studio so your agent can fetch data, create records, and trigger downstream systems without hardcoding request logic in [tools](/tools/introduction). Configuration is centralized, environment-aware, and exposes a consistent runtime interface.
This page covers custom API integrations configured in the **APIs** tab in Agent Studio — that is, **outbound** HTTP calls your agent makes at runtime. For PolyAI's public APIs (Chat, Conversations, [Agents](/api-reference/agents/introduction), Handoff, etc.), see the [API Reference](/api-reference/introduction). For pre-built third-party integrations (e.g., OpenTable, Salesforce, Zendesk), contact your PolyAI account manager to enable them via the **Integrations** tab.
The **APIs** tab (in the **Integrations** section of the Agent Studio sidebar) provides:
* A shared, inspectable API definition
* [Environment-specific](/environments-and-versions/introduction) base URLs
* A consistent calling interface inside [`conv.api`](/tools/classes/conv-api)
## What API integrations are for
Use the APIs tab when you want your agent to:
* Fetch or send data to an external system
* Call internal services (CRM, ticketing, booking, payments)
* Avoid maintaining potentially weighty custom HTTP logic in tools
Typical use cases:
* Look up a ticket, booking, or account
* Create or update a record
* Trigger downstream systems from a [flow](/flows/introduction)
## How API definitions work
An API definition consists of:
1. A named API
2. Environment-specific base URLs
3. One or more operations
## API name
The API name becomes the namespace under `conv.api`.
Use `snake_case` for API names and operation names (e.g., `my_service`, `get_contact`).
Example:
* API name: `salesforce`
* Runtime access: `conv.api.salesforce`
## Base URL and environments
Each API supports separate configuration for:
* Draft and Sandbox
* Pre-release
* Live
You can:
* Test against staging services
* Promote safely without changing code
* Keep flows identical across environments
At runtime, the agent automatically uses the base URL for the active environment.
## Operations
Each operation represents a single HTTP endpoint.
You define:
* Method (`GET`, `POST`, `PATCH`, `PUT`, or `DELETE`)
* Operation name
* Resource path
Example resource path:
`/tickets/{ticket_id}`
Path variables are automatically exposed as arguments when calling the operation.
## Referencing APIs while you are building an agent
All defined APIs are available inside functions under the `conv.api` object.
The structure is:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.api..(...)
```
Example:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.salesforce.get_contact("123")
```
## What gets returned from an API call
Calling an operation returns a `requests.Response`-like object (a standard Python HTTP response object).
This means you can:
* Check `response.status_code`
* Access `response.text`
* Call `response.json()` to parse JSON responses
Every API call is automatically logged (method, URL, status code, and elapsed time) and the response appears in [Conversation Review](/analytics/conversations/review). You can add additional context with `conv.log.info()`, `conv.log.warning()`, or `conv.log.error()`.
### Debugging with `conv.log`
Use `conv.log.info()`, `conv.log.warning()`, or `conv.log.error()` to add custom log entries that appear in Conversation Review alongside the automatic request log. This is useful when troubleshooting failed or unexpected API calls.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.salesforce.get_contact("123")
if response.status_code != 200:
conv.log.error(f"Salesforce lookup failed: {response.status_code} {response.text}")
```
### Example: reading JSON from a response
Assume your API returns the following JSON:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"id": "123",
"status": "active",
"email": "customer@example.com"
}
```
You would handle it like this inside a function:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.salesforce.get_contact("123")
data = response.json()
status = data["status"]
email = data["email"]
```
You can then use those values to decide what to return to the agent.
### Error handling
Always check the response status before processing data. Non-200 responses may indicate the API call failed or the resource was not found.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.salesforce.get_contact("123")
if response.status_code == 200:
data = response.json()
return {"content": f"The account status is {data['status']}."}
else:
conv.log.error(f"Salesforce lookup failed: {response.status_code}")
return {"content": "I wasn't able to retrieve the account details. Please try again later."}
```
You can also use `response.raise_for_status()` to raise an exception on non-2xx responses:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.salesforce.get_contact("123")
try:
response.raise_for_status()
data = response.json()
return {"content": f"The account status is {data['status']}."}
except Exception as e:
conv.log.error(f"Salesforce lookup failed: {e}")
return {"content": "Something went wrong while fetching your account details."}
```
## Returning values from your function
After calling an API and processing the response, your function must return a dictionary.
There are two common patterns:
### 1. Return natural language content
Use this when you want the LLM to continue the conversation naturally:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {"content": f"The account is currently {status}."}
```
### 2. Return a programmatic utterance
Use this when you want to return a fully controlled response:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {"utterance": "Your account is active and ready to use."}
```
In most cases:
* `"content"` lets the model incorporate your result into a broader response.
* `"utterance"` returns a fixed, deterministic reply.
## Path variables
You can pass path variables as positional or keyword arguments.
Examples:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.salesforce.get_contact("123")
```
Or:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.salesforce.get_contact(contact_id="123")
```
## Query parameters, body, and headers
Operations accept arbitrary keyword arguments at call time.
You can pass:
* Query parameters
* JSON bodies
* Custom headers
Example:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.stripe.create_payment(
amount=100,
params={"expand": "customer"},
json={"amount": 100, "currency": "usd"},
headers={"X-Custom-Header": "value"}
)
```
This behaves similarly to a thin wrapper around a standard HTTP client.
## Authentication
Authentication is configured per environment (Draft and Sandbox, Pre-release, Live) on each API. This lets you point each environment at the appropriate credentials without changing code.
Authentication configuration is **not branch-specific**. Auth settings on an API apply across all branches for a given environment, so editing auth on any branch also affects the main branch. Take care when modifying auth configuration.
Supported authentication types:
* **No auth** – No authentication required.
* **Basic auth** – Username and password encoded in the `Authorization` header.
* **API key** – A key sent in a header or query parameter.
* **OAuth 2.0** – Token-based authentication using the client credentials grant, with automatic token refresh. You configure the access token URL, client ID, client secret, scope, whether credentials are sent as a header or in the body, and optional auth header name and token prefix.
Auth configuration covers three aspects:
1. **Type** – The authentication method (`No auth`, `Basic auth`, `API key`, or `OAuth 2.0`).
2. **Location** – For API keys, where the credential is sent (`Header` or `Query`).
3. **Secret value** – The credential itself, managed securely by Agent Studio.
Auth credentials are handled by Agent Studio and are not embedded in flows or functions.
## Summary
The APIs tab provides:
* Centralized API configuration
* Environment-aware routing
* A simple runtime interface with `conv.api`
* A `requests.Response`-like return object that you process with `response.json()`
* Automatic request logging, plus custom log entries via `conv.log.info()`, `conv.log.warning()`, and `conv.log.error()`
## Related pages
Call defined APIs from your agent tools.
Reference APIs in flow steps and actions.
Debug API calls using conversation logs.
# Amazon Connect chat handoff
Source: https://docs.poly.ai/integrations/chat/amazon-connect
Hand off a webchat or SMS conversation from PolyAI to an Amazon Connect chat agent.
Hand off a live webchat or SMS conversation from your PolyAI agent to an Amazon Connect chat agent. PolyAI calls the Connect [`StartChatContact`](https://docs.aws.amazon.com/connect/latest/APIReference/API_StartChatContact.html) API and then proxies messages between the end user and the Connect agent for the rest of the session.
This integration is available from **Integrations > Amazon Connect** in Agent Studio. For voice routing into the same Connect instance, see the [Amazon Connect voice integration](/integrations/voice/amazon-connect/amazon-connect).
## Prerequisites
* An Amazon Connect instance with chat enabled.
* A chat contact flow in Connect that routes the conversation to the queue you want PolyAI to escalate into.
* An IAM role PolyAI can assume that grants `connect:StartChatContact` and the matching session policies. The role's trust policy must allow `sts:AssumeRole` **and** `sts:TagSession` from the PolyAI worker account (PolyAI uses IRSA, which adds session tags on the assume-role call).
## 1. Configure Amazon Connect
1. In the AWS console, open Amazon Connect and note the **Instance ID** for the instance you want PolyAI to hand off into.
2. Open or create the chat contact flow that should receive PolyAI's handoff and note the **Contact flow ID**. Make sure the flow routes to a queue that has live agents assigned.
3. Create or reuse an IAM role for PolyAI (for example `PolyAIAccessConnect`) and note the **Role ARN**.
## 2. Connect from Studio
In Agent Studio, open **Integrations** and click **Connect** on the **Amazon Connect** tile. Paste:
| Field | Required | Description |
| ----------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `instance_id` | Yes | The Amazon Connect instance ID. |
| `contact_flow_id` | Yes | The contact flow that routes the chat to a queue. |
| `role_arn` | Yes | IAM role PolyAI assumes to call `StartChatContact`. |
| `aws_region` | No | Region of the Connect instance (for example `eu-west-2`). Set this if your Connect instance is not in the PolyAI worker's default region. |
If your worker defaults to `us-east-1` but your Connect instance lives in `eu-west-2`, set `aws_region` explicitly. Without it the handoff fails with `ResourceNotFoundException`.
## 3. Write the handoff function
The handoff payload can include the Connect configuration inline. This is useful for testing or when different topics hand off to different Connect instances:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def transfer_to_connect(conv: Conversation):
return {
"utterance": "No problem, I'll transfer you to one of our agents now.",
"handoff": {
"text_chat": {
"amazon_connect_integration": {
"instance_id": "12345678-aaaa-bbbb-cccc-1234567890ab",
"contact_flow_id": "abcdef12-3456-7890-abcd-ef1234567890",
"role_arn": "arn:aws:iam::123456789012:role/PolyAIAccessConnect",
}
}
}
}
```
You can also pass an optional `user_display_name` to set the name the live agent sees for the user.
## 4. Assign the function to a topic
Open the Knowledge topic that should escalate, add an **Action**, and select `transfer_to_connect`. The same function can be called from any flow step or directly via `conv.call_handoff()` for dynamic routing.
## 5. Verify
1. Start a session in your webchat widget and steer the conversation into the topic you wired the handoff to.
2. Confirm a new chat contact appears in the Amazon Connect agent workspace (Contact Control Panel) for the queue the contact flow routes to.
3. Reply from Connect and confirm the message is delivered back to the user inside the same widget.
4. End the contact from Connect and confirm the widget shows the conversation as closed.
If you see `ResourceNotFoundException`, the region is almost certainly wrong – set `aws_region` on the integration. If you see assume-role errors, confirm the trust policy allows both `sts:AssumeRole` and `sts:TagSession` from the PolyAI worker account.
## Related pages
Voice routing, IAM, and full Connect setup.
All chat handoff integrations and the shared payload shape.
Wire a function into a Knowledge topic.
# Five9 chat handoff
Source: https://docs.poly.ai/integrations/chat/five9
Hand off a webchat or SMS conversation from PolyAI to a Five9 agent.
Hand off a live webchat or SMS conversation from your PolyAI agent to a [Five9](https://www.five9.com/) agent. PolyAI continues to proxy messages between the end user and the Five9 agent for the rest of the session, so the customer stays in the same conversation while a human takes over.
## Prerequisites
* A Five9 tenant with administrator access, and an **Open Messaging** license enabled on the domain. Open Messaging is consumption-billed by Five9, confirm availability with your Five9 representative.
* An inbound Five9 campaign in the **Running** state, with a chat profile and at least one skill assigned. At least one agent with that skill should be logged in and set to **Ready** with the Chat channel enabled.
* A PolyAI project with at least one Knowledge topic that should escalate to a human.
## 1. Open the Five9 setup wizard in Studio
PolyAI connects to Five9 through Five9's **Open Messaging** channel. At handoff time, PolyAI opens a chat in Five9 and proxies messages between the user and the Five9 agent for the rest of the session.
In Agent Studio, open **Integrations** and click **Connect** on the **Five9** tile to start the setup wizard. The wizard generates a webhook URL, username, and password you'll paste into Five9. Keep the wizard open, you'll come back to it in step 3.
The webhook password is shown once and cannot be retrieved later. Copy it before leaving the page.
## 2. Configure Five9
Using the values from the wizard, your Five9 administrator sets up the pieces Five9 needs to receive and route the chat. Your PolyAI contact will walk through this with you if it's your first Five9 integration. The main steps are:
* Create an API credential (OAuth client) that PolyAI uses to open chats in Five9.
* Create an authorization profile and a delivery profile using the webhook values from the wizard, so Five9 can send agent events back to PolyAI.
* Point the delivery profile at your inbound campaign, and confirm the campaign, chat profile, skills, and agent availability are all in place.
## 3. Finish setup in Studio
Return to the wizard and enter your Five9 details: region, domain ID, delivery profile ID, campaign name, and the API consumer key and secret. Save to complete the connection.
## 4. Write the handoff function
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def transfer_to_five9(conv: Conversation):
return {
"utterance": "No problem, I'll transfer you to one of our agents now.",
"handoff": {
"text_chat": {
"five9_integration": {}
}
}
}
```
## 5. Assign the function to a topic
Open the Knowledge topic that should escalate, add an **Action**, and select `transfer_to_five9`. The same function can be called from any flow step or directly via `conv.call_handoff()` for dynamic routing.
## 6. Verify
1. Start a session in your webchat widget and steer the conversation into the topic you wired the handoff to.
2. Confirm a new chat appears in the Five9 agent desktop for the skill you configured.
3. Reply from Five9 and confirm the message is delivered back to the user inside the same widget.
4. End the session from Five9 and confirm the widget shows the conversation as closed.
If the conversation never reaches Five9, check that the inbound campaign is **Running**, the skill is assigned to both the campaign and the agent user, and the agent is **Ready** with the Chat channel enabled.
## Related pages
All chat handoff integrations and the shared payload shape.
Wire a function into a Knowledge topic.
Scope a custom CCaaS integration if Five9 isn't enough.
# Chat handoff integrations
Source: https://docs.poly.ai/integrations/chat/introduction
Hand off webchat and SMS conversations from your PolyAI agent to a live agent in your CCaaS or CRM.
Use a chat handoff integration to escalate a webchat or SMS conversation from your PolyAI agent to a live agent in your existing CCaaS or CRM. PolyAI continues to proxy messages between the end user and the live agent, so the conversation stays in the same widget the user started in.
All chat handoff integrations are configured from **Integrations** in Agent Studio. The handoff is triggered from a Python tool returning a `handoff` payload, then assigned to the Knowledge topic (or flow step) that should escalate.
## Available integrations
Hand off to Salesforce Service Cloud Messaging using an Embedded Service deployment.
Hand off to Zendesk Messaging through a Conversations Integration and switchboard.
Hand off to NICE CXone Digital using Brand ID and Channel ID routing.
Hand off to an Amazon Connect chat contact flow via the `StartChatContact` API.
Hand off through Genesys Cloud Open Messaging to a queue with live agents.
Hand off to Webex Contact Center through Bring Your Own Channel.
Hand off to Five9 through Open Messaging to an inbound campaign and skill.
## How it works
1. **Set up the integration on the CCaaS or CRM side.** Each platform expects a webhook, channel, or messaging app to be created so PolyAI has a place to deliver the conversation.
2. **Connect from Studio.** Go to **Integrations**, click **Connect** on the relevant tile, and paste the IDs and secrets generated in step 1.
3. **Write a handoff function.** Return a structured `handoff` payload from a Python tool – the payload tells PolyAI which integration to hand off to.
4. **Assign the function to a topic.** Add the tool to the Knowledge topic (or flow step) that should trigger the handoff. When that topic fires, PolyAI announces the transfer, sends the conversation to the live agent, and continues to proxy turns.
## The handoff payload
All chat handoff integrations share the same payload shape. Replace `` with the integration key (e.g. `salesforce_integration`, `nice_integration`, `amazon_connect_integration`, `genesys_integration`, `zendesk_integration`, `webex_integration`, `five9_integration`):
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def transfer_to_agent(conv: Conversation):
return {
"utterance": "No problem, I'll transfer you to a live agent now.",
"handoff": {
"text_chat": {
"_integration": {}
}
}
}
```
The `utterance` is the message the agent sends to the user immediately before the handoff. The empty object `{}` is where integration-specific parameters go – most integrations accept routing hints such as a queue ID, skill, or department.
The same conversation continues in the same widget after handoff – PolyAI relays each side's messages through the same WebSocket connection. The end user never has to switch tools or re-authenticate.
## Choosing an integration
| If your live agents work in… | Use |
| ---------------------------------- | ---------------------------------------------------------------------------------------------------- |
| Salesforce Service Cloud Messaging | [Salesforce](/integrations/chat/salesforce) |
| Zendesk Messaging | [Zendesk](/integrations/chat/zendesk) |
| NICE CXone Digital | [NICE CXone](/integrations/chat/nice-cxone) |
| Amazon Connect chat | [Amazon Connect](/integrations/chat/amazon-connect) |
| Genesys Cloud (Open Messaging) | [Genesys Cloud](/integrations/messaging/genesys) |
| Webex Contact Center | [Webex by Cisco](/integrations/chat/webex) |
| Five9 (Open Messaging) | [Five9](/integrations/chat/five9) |
| Anything else | [Contact your PolyAI account manager](/integrations/managed-services) to scope a custom integration. |
Talkdesk and other CCaaS providers can be scoped as custom integrations, contact your PolyAI account manager.
## Related pages
Voice (SIP-based) handoff configuration and the handoff context model.
Retrieve handoff context programmatically for screen-pop or CRM enrichment.
Integrate PolyAI into a custom chat surface and observe the `handoff` object.
# NICE CXone chat handoff
Source: https://docs.poly.ai/integrations/chat/nice-cxone
Hand off a webchat or SMS conversation from PolyAI to a NICE CXone Digital agent.
Hand off a live webchat or SMS conversation from your PolyAI agent to a NICE CXone agent through a Digital messaging channel. PolyAI continues to proxy messages between the end user and the CXone agent.
This integration is available from **Integrations > NICE CXone Messaging** in Agent Studio.
## Prerequisites
* A NICE CXone tenant with administrator access to **Digital > Points of Contact Digital**.
* A messaging channel created (or available) in CXone.
* A PolyAI project with at least one Knowledge topic that should escalate to a human.
## 1. Create the CXone Digital channel
PolyAI integrates with NICE CXone through a Digital First Omnichannel (DFO) messaging channel. You will need to create or identify a messaging channel in CXone and collect the IDs PolyAI needs to connect. Your PolyAI contact will confirm the exact fields required.
Configure routing on the CXone side (queues, skills, agent assignments) the way you'd handle any other Digital channel.
## 2. Connect from Studio
In Agent Studio, open **Integrations** and click **Connect** on the **NICE CXone Messaging** tile. Paste the values from the CXone channel configuration.
## 3. Write the handoff function
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def transfer_to_nice(conv: Conversation):
return {
"utterance": "No problem, I'll transfer you to one of our agents now.",
"handoff": {
"text_chat": {
"nice_integration": {}
}
}
}
```
## 4. Assign the function to a topic
Open the Knowledge topic that should escalate, add an **Action**, and select `transfer_to_nice`. When that topic fires, PolyAI delivers the `utterance`, opens a CXone Digital conversation, and proxies subsequent turns between the two sides.
The same function can be called from any flow step or directly via `conv.call_handoff()` for dynamic routing.
## 5. Verify
1. Start a session in your webchat widget and steer the conversation into the topic you wired the handoff to.
2. Confirm a new chat appears in the CXone agent inbox for the queue or skill you configured.
3. Reply from CXone and confirm the message is delivered back to the user inside the same widget.
4. End the session from CXone and confirm the widget shows the conversation as closed.
If the conversation never reaches CXone, double-check **Integrations > NICE CXone Messaging** for connection errors, and verify the Brand ID, Channel ID, and Environment match the channel you opened in step 1.
## Related pages
Voice routing into CXone over SIP.
All chat handoff integrations and the shared payload shape.
Wire a function into a Knowledge topic.
# Salesforce chat handoff
Source: https://docs.poly.ai/integrations/chat/salesforce
Hand off a webchat or SMS conversation from PolyAI to a Salesforce Service Cloud Messaging agent.
Hand off a live webchat or SMS conversation from your PolyAI agent to a Salesforce Service Cloud Messaging agent. PolyAI continues to proxy messages between the end user and the Salesforce agent for the rest of the session, so the user stays in the same widget.
This integration is available from **Integrations > Salesforce Messaging** in Agent Studio.
## Prerequisites
* A Salesforce org with Service Cloud and Messaging for In-App and Web enabled.
* Salesforce administrator access to create a Messaging channel and a Connected App.
* A PolyAI project with at least one Knowledge topic that should escalate to a human.
## 1. Set up Salesforce
PolyAI connects to Salesforce through the [Bring Your Own Channel](https://developer.salesforce.com/docs/service/messaging-partner/guide/create-your-own-integration.html) integration. You will need:
1. A **Messaging channel** in Salesforce Setup (Messaging Settings) for PolyAI to hand conversations into.
2. A **Connected App** with OAuth enabled so PolyAI can authenticate against Salesforce's Messaging API.
3. **Omni-Channel routing** configured so that messages on this channel land in a queue or skill that human agents are subscribed to.
Your PolyAI contact will walk you through the exact fields and OAuth scopes required for your Salesforce edition.
For broader Salesforce CRM access (case lookup, account data), see the [Salesforce CRM integration](/integrations/salesforce) – the two integrations can coexist on the same project.
## 2. Connect from Studio
In Agent Studio, open **Integrations** and click **Connect** on the **Salesforce Messaging** tile. Paste the credentials and IDs from your Connected App and Messaging channel setup.
## 3. Write the handoff function
Add a Python tool that returns the chat handoff payload. The empty object lets you pass routing hints later (queue or skill) if needed.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def transfer_to_salesforce(conv: Conversation):
return {
"utterance": "No problem, I'll transfer you to one of our agents now.",
"handoff": {
"text_chat": {
"salesforce_integration": {}
}
}
}
```
## 4. Assign the function to a topic
Open the Knowledge topic that should escalate to a live agent, add an **Action**, and select the `transfer_to_salesforce` function. When that topic fires, PolyAI delivers the `utterance` to the user, opens a Salesforce Messaging conversation, and proxies subsequent turns between the two sides.
You can call the same function from a flow step or directly via `conv.call_handoff()` if you need more dynamic routing.
## 5. Verify
1. Start a session in your webchat widget (or send an SMS, if SMS is wired up) and steer the conversation into the topic you wired the handoff to.
2. Confirm that the `utterance` is sent and that a new conversation appears in the Salesforce Omni-Channel inbox for the routing queue you configured.
3. Reply from Salesforce and confirm the message is delivered back to the user inside the original widget.
4. End the session from the Salesforce side and confirm the widget shows the conversation as closed.
If nothing arrives in Salesforce, check **Integrations > Salesforce Messaging** for connection errors, and confirm the Connected App is approved for the user PolyAI is authenticating as.
## Related pages
Customer record lookup and case management.
All chat handoff integrations and the shared payload shape.
# Webex by Cisco chat handoff
Source: https://docs.poly.ai/integrations/chat/webex
Hand off a webchat or SMS conversation from PolyAI to a Webex Contact Center agent.
Hand off a live webchat or SMS conversation from your PolyAI agent to a [Webex Contact Center](https://www.webex.com/contact-center.html) agent. PolyAI continues to proxy messages between the end user and the Webex agent for the rest of the session.
## Prerequisites
* A Webex Contact Center tenant with administrator access to **Bring Your Own Channel**.
* A messaging channel created in Webex Contact Center routed to a queue with live agents subscribed.
* A PolyAI project with at least one Knowledge topic that should escalate to a human.
## 1. Configure Webex Contact Center
PolyAI connects to Webex Contact Center through the [Bring Your Own Channel](https://help.webexconnect.io/docs/create-conversation-1) capability. At handoff time, PolyAI creates a conversation in Webex on demand and proxies messages between the user and the Webex agent.
1. Work with your PolyAI contact to configure the integration in the Webex Control Hub under **Contact Center**.
2. Configure routing and skills the way you'd handle any other Webex digital channel. The simplest setup routes the new conversation to a queue that has live agents subscribed.
3. Record the values Webex generates – you'll paste them into Studio. Your PolyAI contact will confirm the exact fields required.
## 2. Connect from Studio
In Agent Studio, open **Integrations** and click **Connect** on the **Webex Messaging** tile. Paste the values you recorded in step 1.
## 3. Write the handoff function
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def transfer_to_webex(conv: Conversation):
return {
"utterance": "No problem, I'll transfer you to one of our agents now.",
"handoff": {
"text_chat": {
"webex_integration": {}
}
}
}
```
## 4. Assign the function to a topic
Open the Knowledge topic that should escalate, add an **Action**, and select `transfer_to_webex`. The same function can be called from any flow step or directly via `conv.call_handoff()` for dynamic routing.
## 5. Verify
1. Start a session in your webchat widget and steer the conversation into the topic you wired the handoff to.
2. Confirm a new conversation appears in the Webex Contact Center agent desktop for the queue you configured.
3. Reply from Webex and confirm the message is delivered back to the user inside the same widget.
4. End the session from Webex and confirm the widget shows the conversation as closed.
If the conversation never reaches Webex, check the Bring Your Own Channel configuration in Control Hub and confirm the channel is active.
## Related pages
All chat handoff integrations and the shared payload shape.
Wire a function into a Knowledge topic.
Scope a custom CCaaS integration if Webex isn't enough.
# Zendesk chat handoff
Source: https://docs.poly.ai/integrations/chat/zendesk
Hand off a webchat or SMS conversation from PolyAI to a Zendesk Messaging agent.
Hand off a live webchat or SMS conversation from your PolyAI agent to a Zendesk Messaging agent. PolyAI continues to proxy messages between the end user and the Zendesk agent for the rest of the session.
This integration is available from **Integrations > Zendesk Messaging** in Agent Studio. For ticketing-only flows (no live messaging), see [Zendesk Ticketing](/integrations/zendesk-ticketing-solutions).
## Prerequisites
* A Zendesk account with Admin Center access.
* Your PolyAI project ID (for example `PROJECT-f07a975e`).
* Your PolyAI messaging handoff base URLs – ask your PolyAI contact for the right values for your environment (dev or prod).
## 1. Create the Conversations Integration in Zendesk
The Conversations Integration gives PolyAI a messaging webhook endpoint, so Zendesk can forward new messages and typing events to Agent Studio.
1. In **Zendesk Admin Center**, go to **Apps and integrations > Integrations > Conversations integrations**.
2. Click **Create integration** and name it (e.g. `PolyAI Handoff`).
3. On the **Details** tab, record the auto-generated values – you'll paste them into Studio:
| Field | Purpose |
| ------------------ | ----------------------------------------------------------- |
| **App ID** | Identifies your Zendesk account. |
| **Integration ID** | Identifies this specific integration. |
| **Webhook ID** | Identifies the auto-created webhook. |
| **Shared secret** | PolyAI uses this to verify requests originate from Zendesk. |
4. Set the **Webhook endpoint** to your PolyAI messaging handoff URL. Your PolyAI contact will provide the base domain for your environment. The URL follows this pattern, replacing `` and ``:
```
https:///handoff/webhooks/zendesk/PLATFORM//live/events
```
5. Set **Webhook version** to **v2**.
## 2. Create the Ticket Status Webhook
This standalone webhook lets PolyAI react to ticket status changes (e.g. when a Zendesk agent solves the ticket).
1. In **Admin Center**, go to **Apps and integrations > Webhooks > Webhooks** and click **Create webhook > Zendesk events**.
2. Configure:
| Setting | Value |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | `On Ticket Status Changed` |
| **Endpoint** | `https:///handoff/webhooks/zendesk/PLATFORM//live/status` (your PolyAI contact will provide the base domain) |
| **Request method** | `POST` |
| **Request format** | `JSON` |
3. Under **Authentication**, select **API key** and use the secret key shared with PolyAI (or generate one and pass it back to your PolyAI contact).
4. On the **Event subscription** tab, add **Status changed**.
5. Make sure the webhook shows **Active**.
## 3. Connect from Studio
In Agent Studio, open **Integrations** and click **Connect** on the **Zendesk Messaging** tile. Paste:
* **App ID**, **Integration ID**, **Webhook ID**, **Shared secret** (from the Conversations Integration).
* **Webhook secret key** (from the Ticket Status Webhook).
* **Zendesk subdomain** (the part before `.zendesk.com`, e.g. `d3v-polyai`).
## 4. Write the handoff function
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def transfer_to_zendesk(conv: Conversation):
return {
"utterance": "No problem, I'll transfer you to one of our agents now.",
"handoff": {
"text_chat": {
"zendesk_integration": {}
}
}
}
```
## 5. Assign the function to a topic
Open the Knowledge topic that should escalate, add an **Action**, and select `transfer_to_zendesk`. The same function can be reused from any flow step.
## 6. Verify
* **Conversations Integration:** Trigger the topic from your widget. Confirm a new conversation appears for the configured Zendesk routing rule. Then reply from Zendesk and confirm the message lands back in the widget.
* **Logs:** In Zendesk, go to **Apps and integrations > Integrations > Logs** to see events delivered to the PolyAI endpoint.
* **Ticket Status Webhook:** Change a ticket's status in Zendesk and check the webhook's **Activity** tab for a successful `200` response.
## Related pages
REST API integration for creating and updating tickets.
Voice handoff to Zendesk agents over SIP.
All chat handoff integrations and the shared payload shape.
# DeepL
Source: https://docs.poly.ai/integrations/deepl
Enable real-time translation during calls with DeepL integration.
Connect your PolyAI agent to [DeepL](https://www.deepl.com/) for real-time translation during conversations. Support callers in multiple languages with accurate, contextual translations.
This is a managed integration. Contact your PolyAI account manager to enable DeepL for your project.
## Capabilities
* **Real-time translation**: Translate text between supported languages during calls
* **Content localization**: Provide information in the caller's preferred language
* **Multi-language support**: Access DeepL's extensive language coverage
## Supported languages
DeepL supports translation between numerous languages including:
* English (UK/US)
* German
* French
* Spanish
* Italian
* Dutch
* Polish
* Portuguese
* Russian
* Japanese
* Chinese
* And many more
See the [DeepL Supported Languages](https://www.deepl.com/docs-api/translate-text) for the complete list.
## Getting started
### Prerequisites
* A DeepL API account (Free or Pro)
* DeepL API authentication key
* PolyAI project access
### Step 1: Create a DeepL API account
1. Go to [DeepL API](https://www.deepl.com/pro-api)
2. Sign up for a DeepL API Free or Pro plan
3. Complete account verification
### Step 2: Obtain your API key
1. Log in to your [DeepL Account](https://www.deepl.com/account)
2. Navigate to the **API Keys** section
3. Copy your authentication key
### Step 3: Provide credentials to PolyAI
Share your DeepL API key with your PolyAI account manager. It will be stored securely.
## Use cases
### Dynamic content translation
Translate Knowledge responses or system messages into the caller's language on-the-fly.
### Multi-market support
Support callers in multiple languages using a single set of Knowledge topics with translated responses.
### Cross-language handoffs
Prepare translated notes for human agents when transferring non-English speaking callers.
## Limitations
* **API quotas**: Free tier has character limits; monitor usage for high-volume deployments
* **Voice synthesis**: Translated text requires compatible TTS voices for natural delivery
* **Context preservation**: Complex domain-specific terminology may require custom glossaries
## Best practices
1. **Pre-translate common phrases**: For frequently used responses, pre-translate to reduce latency
2. **Use glossaries**: Define translations for brand names, product terms, and industry jargon
3. **Test thoroughly**: Verify translations in all target languages before deployment
## Support
For integration assistance, contact your PolyAI account manager.
For DeepL-specific questions, see the [DeepL API Documentation](https://www.deepl.com/docs-api).
## Related pages
Browse all available integrations.
Other managed integrations requiring account manager setup.
# DesignMyNight
Source: https://docs.poly.ai/integrations/design-my-night
Connect your PolyAI agent to DesignMyNight for restaurant and venue booking management.
Connect PolyAI to [DesignMyNight](https://www.designmynight.com/) (DMN), a restaurant and venue booking platform used across the UK and internationally. Your agent can check availability, make bookings, and manage existing reservations during conversations.
This is a managed integration. Prepare the credentials below, then contact your PolyAI account manager to complete setup.
## Capabilities
* **Availability checks**: Query available booking slots for specific dates, times, party sizes, and booking types
* **Make bookings**: Create new reservations with guest details, including name, contact information, and special requests
* **Modify bookings**: Update existing reservations including date, time, or party size changes
* **Lookup bookings**: Retrieve booking details using confirmation numbers or guest information
* **Venue information**: Access venue details including opening hours, booking types, and offers
## Getting started
### Prerequisites
* A DesignMyNight account with API access
* Your DMN venue ID(s)
* PolyAI project access
### Step 1: Obtain API credentials
1. Contact DesignMyNight support or your account manager to request API access
2. You will receive:
* **API Base URL**: Typically `https://api.designmynight.com`
* **Collins URL**: For Collins-specific endpoints
* **API headers/tokens**: Authentication credentials for API requests
### Step 2: Identify your venue IDs
1. Log in to your DesignMyNight dashboard
2. Navigate to your venue settings
3. Locate the **Venue ID** for each property you want to integrate
### Step 3: Provide credentials to PolyAI
Share the following with your PolyAI account manager:
* API Base URL
* Collins URL and headers (if applicable)
* Venue ID(s)
* Any specific booking types or restrictions
PolyAI will configure the integration and confirm when it's ready for testing.
## Booking types
DesignMyNight supports multiple booking types per venue. Common examples include:
* Standard dining reservations
* Private dining/events
* Brunch bookings
* Special experiences (tasting menus, chef's table)
Work with your PolyAI representative to configure which booking types the voice agent should handle.
## Limitations
* **Deposit payments**: Bookings requiring deposits must be completed through the DMN website
* **Complex experiences**: Multi-course experiences with detailed selections may require human assistance
* **Real-time sync**: Changes made directly in DMN are reflected in real-time, but large-scale updates may have slight delays
## Support
For integration assistance, contact your PolyAI account manager or PolyAI Support.
## Related pages
Alternative restaurant reservation integration.
Other managed integrations requiring account manager setup.
# Epic
Source: https://docs.poly.ai/integrations/epic
Connect your PolyAI agent to Epic EHR for patient data access through SMART on FHIR.
PolyAI integrates with Epic EHR through [SMART on FHIR](https://docs.smarthealthit.org/), giving virtual agents read access to patient and operational data (subject to permissions). Integration requires approval through Epic's trusted application process.
This integration uses Epic's [SMART on FHIR framework](https://fhir.epic.com/) and the [FHIR R4 specification](https://hl7.org/fhir/R4/) standards.
## How it works
You submit the integration request from **both sides** — in Epic (naming the PolyAI Epic App as a trusted application) and in PolyAI Agent Studio (linking your Epic submission to your project). PolyAI then reviews and approves it.
## PolyAI Epic App details
Use these values when configuring the trusted application in Epic:
| Field | Value |
| ---------------------------- | -------------------------------------- |
| **Application name** | PolyAI |
| **Client ID** | `c6cc31b3-800f-4297-8d5c-08f56ed57fa3` |
| **Non-Production Client ID** | `a0d62d80-de30-4261-a963-34790dbf0473` |
| **SMART Scope** | SMART V1 |
| **FHIR Version** | R4 |
Configure scopes and permissions according to your organization's requirements and Epic policies. Grant only what the integration needs.
## Integration steps
1. Log in to your **Epic** administrative account.
2. Navigate to **Build App**.
3. Under **My Apps**, select the app to integrate with PolyAI.
In your Epic app, invite the **PolyAI Epic App** as a trusted application using the [PolyAI Epic App details](#polyai-epic-app-details) above.
Submit the integration request in Epic once every required field is filled in.
1. Log in to **PolyAI Agent Studio**.
2. Open your project.
3. Go to **Integrations**.
4. Find the **Epic** tile and click **Submit Request**.
This links your Epic submission with your PolyAI project.
Your request stays in a **pending** state while PolyAI reviews it. PolyAI will reach out if additional information or configuration is required. Once approved, the Epic integration is active in your project.
## Useful links
Official SMART App Launch framework.
Epic's FHIR API reference.
HL7 FHIR R4 standard.
## Support
If you hit issues during the Epic integration process or have questions about required permissions or scopes, contact your PolyAI representative or PolyAI Support.
## Related pages
Browse all available integrations.
Other managed integrations requiring account manager setup.
# Gladly
Source: https://docs.poly.ai/integrations/gladly
Connect your PolyAI agent to Gladly for real-time knowledge access.
Connect PolyAI to your **Gladly Knowledge Base** so your agent uses the same content as your human agents. Updates in Gladly automatically sync to PolyAI conversations.
This integration is available from **Integrations** in Agent Studio under **Knowledge**.
## What you'll need
To connect, gather three values from your Gladly instance:
| Credential | What it is |
| -------------------- | ------------------------------------------------------------------------------ |
| **Organization URL** | Your Gladly tenant URL, e.g. `https://acme.gladly.com` |
| **API username** | A Gladly username with API access — a dedicated service account is recommended |
| **API token** | An API token generated on that user (this replaces the login password) |
## Prerequisites
* Access to the **Gladly Admin Console**
* Permission to manage users and API access
* Familiarity with the [Gladly permissions guide](https://developer.gladly.com/rest/#section/Getting-Started/Permissions)
## Set up credentials
Log in to your Gladly instance and copy the base URL from the address bar:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
https://.gladly.com
```
For example, `https://acme.gladly.com` — the organization name is `acme`.
A dedicated user makes access auditable and rotatable.
1. In the **Gladly Admin Console**, go to **Settings → Users**.
2. Click **Add User**.
3. Use a descriptive name like `polyai-integration`.
4. Save the user.
Prefer creating a service user over reusing a human's account — you can rotate the token without disrupting a real employee's login.
Open the user profile and assign a role with the API permissions your integration needs. See the [Gladly permissions guide](https://developer.gladly.com/rest/#section/Getting-Started/Permissions) for the full matrix.
Grant only the permissions your integration actually needs.
Gladly uses **Basic Authentication** with:
* **Username** — the Gladly username
* **Password** — an API token (not the user's login password)
In the user profile, open the **API Token** or **API Access** section, generate a new token, and copy it.
The API token is shown **only once**. Store it securely — it cannot be retrieved later.
Share the organization URL, API username, and API token with your PolyAI representative through a secure channel. PolyAI stores these as encrypted secrets and configures the integration in your project.
## Support
If you hit issues generating credentials or assigning permissions, contact **PolyAI Support** or your PolyAI account manager.
## Related pages
Browse all available integrations.
Other managed integrations requiring account manager setup.
# Google Sheets
Source: https://docs.poly.ai/integrations/google-sheets
Connect your PolyAI agent to Google Sheets for lookups, logging, and dynamic content.
Read from and write to Google Sheets from your agent for lookup tables, call logging, or dynamic content updates during conversations.
This is a managed integration. Prepare the credentials below, then contact your PolyAI account manager to complete setup.
## Capabilities
* **Read data**: Query spreadsheet cells, rows, or ranges
* **Write data**: Append new rows or update existing cells
* **Search**: Find specific values in a spreadsheet
## Getting started
### Prerequisites
* A Google account with access to Google Sheets
* The spreadsheet(s) you want to connect
* PolyAI project access
### Step 1: Prepare your spreadsheet
1. Create or open the Google Sheet you want to use
2. Ensure the first row contains clear column headers
3. Note the **Spreadsheet ID** from the URL:
```
https://docs.google.com/spreadsheets/d/{SPREADSHEET_ID}/edit
```
### Step 2: Create a service account
1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select an existing one
3. Enable the **Google Sheets API**
4. Navigate to **APIs & Services → Credentials**
5. Click **Create Credentials → Service Account**
6. Fill in the service account details and create
7. Click on the service account, then **Keys → Add Key → Create new key**
8. Select **JSON** and download the key file
### Step 3: Share the spreadsheet
1. Open your Google Sheet
2. Click **Share**
3. Add the service account email (found in your JSON key file) as an editor
4. Click **Send** or **Share**
### Step 4: Provide credentials to PolyAI
Share the following with your PolyAI representative:
* Service account JSON key file (securely transferred)
* Spreadsheet ID(s)
* Sheet names and structure description
PolyAI will store your credentials securely and configure the integration.
## Use cases
### Lookup tables
Store reference data that your agent can query:
| Product Code | Name | Price | Availability |
| ------------ | -------- | ------- | ------------ |
| SKU-001 | Widget A | \$19.99 | In Stock |
| SKU-002 | Widget B | \$29.99 | Limited |
### Call logging
Append call data for tracking:
| Timestamp | Caller Number | Intent | Resolution |
| ---------------- | ------------- | --------------- | ---------- |
| 2024-01-15 10:30 | +1234567890 | Booking inquiry | Completed |
### Dynamic content
Store content that changes frequently:
| Topic | Response |
| ------------- | ------------------------------ |
| Holiday hours | We're closed Dec 25-26 |
| Special offer | 20% off all bookings this week |
## Limitations
* **Rate limits**: Google Sheets API has usage quotas; high-volume operations may require optimization
* **Data size**: Best suited for small to medium datasets; large datasets should use a proper database
* **Concurrent access**: Multiple simultaneous writes may cause conflicts; design your sheet structure accordingly
* **Latency**: API calls add slight delay compared to cached data
## Best practices
1. **Keep data clean**: Use consistent formatting and avoid merged cells
2. **Use named ranges**: Makes queries more reliable than cell references
3. **Limit sheet size**: Archive old data to maintain performance
4. **Test thoroughly**: Verify read/write operations before going live
## Support
For setup assistance, contact your PolyAI account manager.
## Related pages
Browse all available integrations.
Other managed integrations requiring account manager setup.
# HotSOS
Source: https://docs.poly.ai/integrations/hotSOS
Automate room service and maintenance requests with HotSOS and PolyAI.
[Amadeus HotSOS](https://www.amadeus-hospitality.com/service-optimization-software/hotsos/) (**Hot**el **S**ervice **O**ptimization **S**olution) is a hospitality platform used to manage guest service requests, housekeeping, and maintenance workflows — a centralized task management system for hotels.
PolyAI's integration logs guest service requests directly into HotSOS with predefined issue codes, routing each request to the correct team automatically.
This integration is available from **Integrations** in Agent Studio under **Hospitality**.
## Prerequisites
* Your property uses **HotSOS** for guest services and property management.
* You have HotSOS **API credentials** from Amadeus:
| Credential | Example |
| ------------------------- | --------------------------------------------------- |
| **REST or SOAP endpoint** | `https://ifc.emea.hot-sos.net/api/service.svc/rest` |
| **API key / auth token** | Provided by Amadeus |
## Capabilities
Requests are logged into HotSOS with a predefined issue code — no staff data entry.
Requests are categorized (housekeeping, maintenance, supplies) and routed to the right team.
Up to 4 units of a single item, or up to 3 distinct items per request (e.g. *"Two towels, a toothbrush, and shampoo"*).
Automatically detected from SIP data for in-room calls, with keypad fallback.
Requests flow into HotSOS dashboards for completion rates and trend analysis.
Tailor issue codes, item limits, and workflows to your property.
## Setup and activation
The integration currently only supports service orders from the [standardized list](#standardized-service-orders). See [limitations](#limitations) for details.
Open the [HotSOS service order list](https://help.amadeus-hospitality.com/operations/service-optimization/content/service-orders.html). It includes predefined tasks like delivering towels or reporting maintenance issues. Customize the list for your property.
* Define **item limits**, e.g. up to 4 towels per request.
* Enable **multi-item handling** — up to 3 distinct items in one request, like *"Towel, toothbrush, and shampoo"*.
Confirm whether your phone system supports automatic room number detection from in-room calls. If not, enable manual entry via the phone keypad as a fallback.
Contact PolyAI to deploy and configure the integration. PolyAI will connect your HotSOS instance to the virtual agent and help with setup.
Confirm:
* Requests are logged with the correct issue codes and quantities in HotSOS.
* Room number detection (or the manual fallback) works as expected.
Contact your PolyAI account manager for deployment or troubleshooting help.
## Limitations
Cancellations, amendments, and confirmations of existing orders are not supported.
Duplicate requests cannot be created for unresolved issues already logged in HotSOS.
Tasks outside the enabled service order list are redirected to the property app or transferred to staff.
Only English is currently supported for this integration.
## Standardized service orders
HotSOS ships with a predefined list of service orders compatible with the PolyAI integration. Customize this list to fit your property's needs.
Need a service order that isn't listed? Contact PolyAI to discuss customization options.
| Service order | Issue code |
| ----------------------------- | ---------- |
| Complimentary water - request | 9032 |
| Carpet - dirty | 1076 |
| Dental kit - request | 9057 |
| Kettle - request | 8089 |
| Sofa bed setup - request | 9210 |
| Bath towels - request | 8110 |
| Bathrobe - request | 8105 |
| Shower gel - request | 8106 |
| Adapter - request | 9005 |
| Blanket (queen) - request | 9030 |
| Shampoo - request | 9212 |
| Coffee (regular) - request | 9046 |
| Iron & board - request | 9109 |
| Coffee mug - request | 9044 |
| Toilet paper - request | 8057 |
| Shaving kit - request | 9213 |
| Bottled water - request | 9032 |
| Tea (regular) - request | 9235 |
| Duvet (queen) - request | 8064 |
| Wine glasses - request | 9085 |
| Foam pillow - request | 9080 |
| Feather pillow - request | 9076 |
| Laundry pickup - request | 9259 |
| Slippers - request | 8100 |
| Blanket (king) - request | 9029 |
| Sewing kit - request | 9211 |
| Clean balcony | 4034 |
| Conditioner - request | 9051 |
| Lotion - request | 9122 |
| Toothpaste - request | 8081 |
| Toothbrush - request | 8080 |
For more information, see the [HotSOS documentation](https://help.amadeus-hospitality.com/operations/service-optimization/content/service-orders.html) or contact PolyAI support.
## Related pages
Browse all available integrations.
Manage event and large party bookings.
# Ideal Postcode
Source: https://docs.poly.ai/integrations/ideal-postcode
Enable UK address lookup and validation during calls with Ideal Postcode.
Connect your PolyAI agent to [Ideal Postcode](https://ideal-postcodes.co.uk/) for UK address lookup and validation. Help callers find and confirm their address using just a postcode during conversations.
This is a managed integration. Prepare the credentials below, then contact your PolyAI account manager to complete setup.
## Capabilities
* **Postcode lookup**: Retrieve all addresses associated with a UK postcode
* **Address selection**: Help callers identify their specific address from results
* **Address validation**: Confirm address details during booking or registration flows
## Getting started
### Prerequisites
* An Ideal Postcode account with API credits
* Your API key
* PolyAI project access
### Step 1: Obtain API key
1. Log in to your [Ideal Postcode Dashboard](https://ideal-postcodes.co.uk/users/sign_in)
2. Navigate to **API Keys**
3. Copy your API key (or create a new one for the PolyAI integration)
### Step 2: Configure usage limits (optional)
You can set usage limits and IP restrictions on your API key for additional security:
1. Go to **API Keys → Edit**
2. Set daily/monthly lookup limits
3. Add IP allowlists if required
### Step 3: Provide credentials to PolyAI
Share your API key with your PolyAI account manager. It will be stored securely.
## How it works
Typical conversation flow:
1. **Agent**: "Can I take your postcode?"
2. **Caller**: "SW1A 1AA"
3. **Agent**: "I found several addresses at that postcode. Is it number 10 Downing Street?"
4. **Caller**: "Yes, that's correct."
5. **Agent**: "Perfect, I've confirmed your address as 10 Downing Street, London, SW1A 1AA."
## Use cases
* **Booking confirmations**: Verify delivery or service addresses
* **Account registration**: Capture accurate address details for new customers
* **Address updates**: Help existing customers update their address on file
## Limitations
* **UK only**: Ideal Postcode covers UK addresses only; use alternative providers for international addresses
* **Credit-based**: Each lookup consumes API credits; monitor usage to avoid service interruption
* **Multiple addresses**: Some postcodes have many addresses; the agent may need to narrow down results
## Support
For integration help, contact your PolyAI account manager.
For API questions, see the [Ideal Postcode Documentation](https://ideal-postcodes.co.uk/documentation).
## Related pages
Browse all available integrations.
Other managed integrations requiring account manager setup.
# Integrations
Source: https://docs.poly.ai/integrations/introduction
Connect PolyAI to your existing platforms: telephony, CRM, payments, and knowledge systems.
Connect PolyAI to telephony, CRM, payments, and knowledge platforms. Your agent can hand off to live agents, look up reservations, create tickets, and retrieve knowledge articles during calls.
All integrations listed here are available from **Integrations** in Agent Studio.
## Telephony
Call routing, transfers, and live agent handoffs.
Multi-tenant SIP trunk for call routing and management
Integrate using the Signal API
Flex contact center integration with handoffs
Voice agent integration with Amazon Connect
Connect via Genesys Cloud BYOC
Route calls and hand off to agents via SIP
Voice automation with Dialpad
## Chat
Text-based messaging channels and CCaaS handoff.
Send and receive text messages
Hand off webchat and SMS to Salesforce, Zendesk, NICE CXone, Amazon Connect, Genesys, Webex, or Five9
WhatsApp is also available in the Studio UI.
## CRM
Caller context, ticket management, and customer data lookup.
Access customer records and manage cases
Zendesk CRM, HubSpot, and Microsoft Dynamics 365 are also available in the Studio UI.
## Hospitality
Look up, create, and modify reservations during calls.
Restaurant reservation management
Capture event and large party leads automatically
Automate room service and maintenance requests
Cendyn is also available in the Studio UI.
## Healthcare
Electronic health record (EHR) systems for scheduling and patient data.
Connect to Epic EHR for patient data access
ModMed, athenaOne, Cerner (Oracle Health), and Raintree are also available in the Studio UI.
## Knowledge
External knowledge sources for article and answer retrieval during calls.
Pull articles from your Gladly knowledge base
Zendesk Knowledge Management, Salesforce Knowledge Management, and Azure AI Search are also available in the Studio UI.
## MCP
Any external tool that exposes an MCP server. Agent Studio discovers available functions automatically.
Add MCP servers and manage tool access
## Don't see your platform?
Build a custom integration using the [APIs tab](/integrations/api/introduction). Define HTTP endpoints, configure per-environment base URLs and authentication, and call them from Python functions using `conv.api`.
For integrations not available in the Studio UI (Custom SIP, PCI Pal, Stripe, and others), see the [Managed services](/integrations/managed-services) section.
## Related pages
Define custom HTTP APIs for any platform
Connect MCP servers for automatic tool discovery
Integrations managed by the PolyAI team
# LiveRes (Zonal)
Source: https://docs.poly.ai/integrations/liveres
Connect your PolyAI agent to LiveRes for restaurant reservation management.
Connect PolyAI to [LiveRes](https://www.zonal.co.uk/products/liveres/), a restaurant reservation system owned by Zonal and widely used across the UK hospitality industry. Your agent can interact directly with LiveRes, letting callers make, modify, and cancel reservations through natural conversation.
This is a managed integration. Prepare the credentials below, then contact your PolyAI account manager to complete setup.LiveRes is being phased out by Zonal and replaced by Zonal Events. If you are setting up a new integration, check with your Zonal account manager whether LiveRes or Events is the active booking system for your property.
## Capabilities
* **Check availability**: Query available time slots for specific dates, party sizes, and dining areas
* **Make reservations**: Create bookings with guest details, special requests, and occasion information
* **Modify bookings**: Update reservation details including time, date, party size, or special requirements
* **Cancel bookings**: Process cancellation requests with appropriate policies
* **Menu availability**: Check and communicate menu availability for specific booking times
* **Deposit requirements**: Retrieve deposit and card guarantee requirements for bookings
## Getting started
### Prerequisites
* An active LiveRes account with API access enabled
* Zonal API credentials
* Your outlet/restaurant ID(s)
### Step 1: Request API access
1. Contact your Zonal account manager to enable API access for your LiveRes account
2. You will receive:
* **API Base URL**: Your LiveRes API endpoint
* **Username and password**: For Basic Authentication
* **Outlet list**: IDs for your restaurant locations
### Step 2: Configure authentication
LiveRes uses Basic Authentication. Provide the following to PolyAI:
* API endpoint URL
* Username
* Password (stored securely as a secret)
* Outlet IDs for each location
### Step 3: Define booking parameters
Work with your PolyAI representative to configure:
* Available booking areas (main dining, private rooms, bar, etc.)
* Party size limits
* Booking lead times and cutoffs
* Deposit requirements and policies
* Menu options and special experiences
## Dining areas
LiveRes supports multiple dining areas per venue. The voice agent can:
* Offer specific areas based on availability
* Handle area preferences from callers
* Fall back to alternative areas when first choice is unavailable
## Special requirements
### Occasions
LiveRes supports occasion tags for bookings. Common examples:
* Birthday
* Anniversary
* Business meeting
* Special celebration
### Deposit handling
For bookings requiring deposits or card guarantees:
* The voice agent will inform callers of deposit requirements
* Card details must be collected through secure channels (transferred to staff or online completion)
* Deposit policies are communicated based on your LiveRes configuration
## Limitations
* **Payment processing**: Card payments for deposits cannot be processed over voice; callers are directed to complete payment online or with staff
* **Complex modifications**: Significant booking changes may require human assistance
* **Menu pre-orders**: Detailed menu selections are typically handled post-booking
## Error handling
Common scenarios the integration handles:
| Scenario | Response |
| ------------------ | ------------------------------------------------- |
| No availability | Offers alternative times/dates |
| Invalid party size | Requests correction or suggests alternatives |
| Booking not found | Asks for additional details to locate reservation |
| System timeout | Gracefully informs caller and offers callback |
## Support
For integration questions, contact your PolyAI account manager or reach out to PolyAI Support.
## Related pages
Alternative restaurant and venue booking platform.
Other managed integrations requiring account manager setup.
# Managed services
Source: https://docs.poly.ai/integrations/managed-services
Integrations that require coordination with your PolyAI account manager.
These integrations are not available as self-service in the Studio UI. To get started:
Reach out to your PolyAI account manager or representative.
Provide the required credentials and configuration details (see individual pages).
PolyAI configures the integration in your sandbox environment for testing.
After successful testing, deploy to production.
Some integrations may require additional agreements or licensing with the third-party provider.
## Voice & telephony
Advanced telephony routing and contact center connections.
Dynamic routing and personalization with SIP headers
Connect your telephony system using a pool of DNIs
## Reservations
Restaurant and venue booking integrations.
Restaurant and venue bookings
Restaurant reservation management
## CRM & ticketing
Customer support and ticket management.
Create and manage support tickets through the API
## Payments
Secure payment processing during calls.
Secure, PCI-compliant payment processing
Process payments with Stripe
## Data & utilities
Data connectors, translation, and address services.
Read and write to Google Sheets from your agent
UK address lookup and validation
Real-time translation during conversations
# Genesys Cloud
Source: https://docs.poly.ai/integrations/messaging/genesys
Connect a Genesys Cloud org to PolyAI Agent Studio via Open Messaging, with handoff routed to an ACD queue.
Connect a Genesys Cloud org to PolyAI Agent Studio over [Open Messaging](https://help.mypurecloud.com/articles/open-messaging-overview/). Genesys forwards inbound conversation events to PolyAI, PolyAI replies through the same channel, and an [Architect inbound message flow](https://help.mypurecloud.com/articles/about-message-flows/) routes any agent handoff to an ACD queue.
This integration is available from **Integrations > Genesys Messaging** in Agent Studio. For voice routing into the same Genesys tenant, see the [Genesys Cloud voice integration](/integrations/voice/sip/genesys).
## Prerequisites
* **Genesys Cloud Admin access** to create roles, OAuth clients, messaging integrations, queues, and Architect flows.
* Your **PolyAI account ID and project ID** (e.g. `ACCOUNT-27a33660` / `PROJECT-c4876e30`).
* Your **PolyAI messaging webhook URL** and **webhook secret** — both are generated in the Agent Studio Genesys setup wizard (see [Supply credentials to Agent Studio](#supply-credentials-to-agent-studio)).
* A place to store the webhook secret token securely. PolyAI stores it in AWS Secrets Manager.
## 1. Create the PolyAI role and assign permissions
Create a dedicated role so the OAuth client used by PolyAI has exactly the permissions it needs and no more.
1. Go to **Admin > People & Permissions > Roles / Permissions**.
2. Click **Add Role** and give it a name (e.g. `PolyAI Integration`).
3. Assign the permissions in the table below, then save.
| Domain | Permission | Purpose |
| ------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------- |
| Agent UI → Conversation Details | All Permissions / View | View the Agent UI Conversation Details panel |
| Analytics → Conversation Detail | All Permissions / View | Query for conversation details |
| Architect → Datatable | Add / Edit / View | Add, edit, and view data tables |
| Conversation → Communication | Disconnect | Disconnect a communication |
| Conversation → Message | All Permissions, Accept, Assign, Create, Monitor, Park, Pull, Receive, View | Full inbound/outbound message handling |
| Messaging → Integration | All Permissions, Add, Delete, Edit, View | Create and manage the messaging provider integration |
| Messaging → Setting | All Permissions, Add, Delete, Edit, View | Create and manage settings associated with an integration |
| Routing → Message | All Permissions, Manage | Manage messaging workflow configurations |
| Routing → Queue | Add, Delete, Edit, Join, View | Create and manage the ACD queue and its membership |
## 2. Create the OAuth client
PolyAI authenticates to the Genesys Platform API using a **Client Credentials** OAuth grant scoped to the role created above.
1. Go to **Admin > Integrations > OAuth**.
2. Click **Add Client**.
3. Enter a **Name** and **Description** (e.g. `PolyAI Open Messaging`).
4. Set **Grant Type** to **Client Credentials**.
5. Under **Roles**, assign the role created in [step 1](#1-create-the-polyai-role-and-assign-permissions).
6. Leave **Token Duration** at the default.
7. Save, then record the **Client ID** and **Client Secret** — you will supply these to Agent Studio in [step 8](#supply-credentials-to-agent-studio).
## 3. Create the messaging Platform Configuration
The platform configuration controls messaging behaviour such as typing indicators.
1. Go to **Admin > Message > Platform Configurations** (under Digital & Telephony).
2. Create a new configuration with **typing indicators** enabled.
3. Save. You will select this configuration when creating the Open Messaging integration in the next step.
## 4. Create the Open Messaging platform integration
This integration is the channel over which Genesys forwards inbound messages to PolyAI and PolyAI sends replies back.
1. Go to **Admin > Message > Platform Integrations** (under Digital & Telephony).
2. Create a new **Open Messaging** integration and give it a name.
3. Set the **Outbound Notification Webhook URL** to your PolyAI messaging webhook URL. The pattern is:
```
https://messaging..poly.ai/handoff/webhooks/genesys////events
```
Replace ``, ``, ``, and `` (e.g. `live`) with your values, and adjust the base domain for your environment (`dev` vs `us-1` / `uk-1` / `euw-1`). This URL is shown in the Agent Studio Genesys setup wizard.
4. Enter the **Outbound Notification Webhook Signature Secret Token** provided by Agent Studio. Store this secret securely — PolyAI keeps it in AWS Secrets Manager.
5. Click **Save**.
6. Select the **Platform Configuration** created in [step 3](#3-create-the-messaging-platform-configuration).
7. Select a **Content Profile** (use the default, or configure one if your deployment needs specific content types).
8. Click **Save**, then record the **Integration ID** from the URL — you will supply it to Agent Studio.
## 5. Create the ACD queue
Conversations that hand off from the PolyAI agent to a human are routed to an ACD queue.
1. Go to **Admin > Contact Center > Queues**.
2. Click **New Queue** (e.g. `PolyAI Test Queue`).
3. Set the **Division** (e.g. `Home`).
4. Create the queue and add members — the agents who will receive handed-off conversations.
## 6. Build the Architect inbound message flow
An Architect inbound message flow receives the conversation and transfers it to the ACD queue.
1. Go to **Admin > Architect** and create a new **Inbound Message** flow (e.g. `PolyAI Handoff Flow`).
2. Edit the flow so that **Flow Start → Transfer to ACD**.
3. In the **Transfer to ACD** action, set the **Queue** to the queue created in [step 5](#5-create-the-acd-queue).
4. **Save** and **Publish** the flow.
If the message flow does not end in a Transfer to ACD with a queue that has live agents subscribed, PolyAI's handoff will land in Genesys with nowhere to go.
## 7. Attach the flow via message routing
Routing binds the published flow to the Open Messaging integration address so inbound messages enter the flow.
1. Go to **Admin > Routing > Message Routing** (Orchestration).
2. Click **Attach new addresses**.
3. Select the published flow from [step 6](#6-build-the-architect-inbound-message-flow) and the Open Messaging integration from [step 4](#4-create-the-open-messaging-platform-integration) as the address.
4. **Attach** the address and **Save**.
## Supply credentials to Agent Studio
In the Agent Studio Genesys setup wizard, credentials are entered across two screens.
### Genesys Cloud credentials
Open Messaging credentials never leave PolyAI infrastructure.
| Field | Source |
| -------------- | ------------------------------------------------------------------------------------------------- |
| Region | From the URL bar when signed into Genesys |
| Integration ID | From the Open Messaging integration ([step 4](#4-create-the-open-messaging-platform-integration)) |
| Client ID | From the OAuth client ([step 2](#2-create-the-oauth-client)) |
| Client Secret | From the OAuth client ([step 2](#2-create-the-oauth-client)) |
### Genesys Cloud setup info (generated by Agent Studio)
Agent Studio generates the values you paste back into Genesys in [step 4](#4-create-the-open-messaging-platform-integration):
| Field | Use |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Webhook Secret | Enter as the **Outbound Notification webhook signature secret token** in the Open Messaging integration |
| Webhook URL | Set as the **Outbound Notification Webhook URL** in the Open Messaging integration: `https://messaging..poly.ai/handoff/webhooks/genesys////events` |
## Verify
* **Open Messaging integration**: Send a test message through the messaging channel and confirm Genesys forwards events to the PolyAI webhook URL (check the integration / OAuth client activity).
* **Handoff routing**: Trigger a handoff and confirm the conversation lands in the queue created in [step 5](#5-create-the-acd-queue) via the published Architect flow.
* **Permissions**: If calls into the Platform API fail, re-check that the OAuth client's role includes the required permissions listed in [step 1](#1-create-the-polyai-role-and-assign-permissions).
## Genesys Admin paths
Quick reference for the Genesys Admin paths used in this guide.
| Resource | Genesys Admin path |
| -------------------------- | -------------------------------------------------- |
| Role & permissions | Admin → People & Permissions → Roles / Permissions |
| OAuth client | Admin → Integrations → OAuth |
| Platform Configuration | Admin → Message → Platform Configurations |
| Open Messaging integration | Admin → Message → Platform Integrations |
| Queue | Admin → Contact Center → Queues |
| Inbound message flow | Admin → Architect → Inbound Message |
| Message routing | Admin → Routing → Message Routing |
## Related pages
BYOC voice setup and SIP routing.
Configure and embed the PolyAI webchat widget.
All Agent Studio integrations.
# OpenTable
Source: https://docs.poly.ai/integrations/opentable
Connect your PolyAI agent to OpenTable for automated restaurant reservation management.
Connect PolyAI to OpenTable for [Core](https://www.opentable.com/restaurant-solutions/plans/core/) and [Pro](https://www.opentable.com/restaurant-solutions/plans/pro/) customers using a unique identifier from PolyAI.
Before getting started, you should have both an OpenTable and PolyAI account.
## Getting started
1. Log in to the [OpenTable for Groups dashboard](https://guestcenter.opentable.com/login).
2. Use the OpenTable marketplace search to find the **PolyAI tile**.
3. Click **"Integrate with PolyAI"**.
4. You will be redirected to a page in OpenTable and asked for a unique identifier.
* If you are not yet a PolyAI customer, click **"Contact PolyAI"** and trigger a sales form.
5. If you have your unique identifier (provided by PolyAI during integration setup), enter it into the **"Unique identifier"** text field. Contact your PolyAI account manager if you need this value.
6. PolyAI and OpenTable will enable your integration and contact you once it is ready to use.
See the [OpenTable article on PolyAI](https://support.opentable.com/s/article/polyai?language=en_US) for more details. You must contact PolyAI directly if you want to **deactivate** your integration.
## Authorization
OpenTable uses [OAuth 2.0](https://oauth.net/2/) for secure access to its API. To get started:
1. Request your `client_id` and `client_secret` from OpenTable.
2. Exchange your credentials for an **access token**.
OpenTable provides two endpoints:
* **Production:** `https://oauth.opentable.com`
* **QA:** `https://oauth-pp.opentable.com`
You will POST to the following URI (Production example):
`POST` [https://oauth.opentable.com/api/v2/oauth/token?grant\_type=client\_credentials](https://oauth.opentable.com/api/v2/oauth/token?grant_type=client_credentials)
### Submitting client credentials
Credentials are passed in the `Authorization` header as specified in the [OAuth spec](https://datatracker.ietf.org/doc/html/rfc6749). Use the following steps to submit your client credentials:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import base64
import requests
client_id = "your_client_id"
client_secret = "your_client_secret"
# Encode the client credentials
auth_string = f"{client_id}:{client_secret}"
encoded_auth = base64.b64encode(auth_string.encode()).decode()
# Set up the request
url = "https://oauth.opentable.com/api/v2/oauth/token?grant_type=client_credentials"
headers = {
"Authorization": f"Basic {encoded_auth}",
"Content-Type": "application/x-www-form-urlencoded"
}
# Send the request
response = requests.post(url, headers=headers)
print(response.json())
```
## Making a booking
Below is a simplified Python example referencing the `make_booking` logic:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import requests
def get_access_token(client_id, client_secret):
auth_url = "https://oauth.opentable.com/api/v2/oauth/token?grant_type=client_credentials"
auth_string = f"{client_id}:{client_secret}"
encoded_auth = requests.utils.quote(auth_string)
headers = {
"Authorization": f"Basic {encoded_auth}",
"Content-Type": "application/x-www-form-urlencoded"
}
response = requests.post(auth_url, headers=headers)
if response.status_code == 200:
return response.json()["access_token"]
else:
raise Exception(f"Failed to obtain token: {response.text}")
def make_booking(
access_token, rid, first_name, last_name, phone_number, country_code="US", special_request=""
):
booking_url = f"https://platform.opentable.com/inhouse/v1/booking/{rid}/reservations"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
payload = {
"first_name": first_name,
"last_name": last_name,
"phone": {
"number": phone_number,
"country_code": country_code
},
"restaurant_id": rid,
"reservation_token": "YOUR_RESERVATION_TOKEN_HERE",
"sms_notifications_opt_in": True,
"special_request": special_request
}
response = requests.post(booking_url, json=payload, headers=headers)
if response.status_code == 200:
res_data = response.json()
return f"Successfully booked table for {res_data['party_size']} on {res_data['date_time']}"
else:
return f"Booking failed: {response.text}"
```
## Next steps
1. **Error handling:** In production, handle normal HTTP status codes like `401 Unauthorized`, `403 Forbidden`, and `400 Bad Request`.
2. **Data validation:** Add logic to ensure users provide all required fields (e.g., names, valid phone numbers, reservation token).
3. **Token renewal:** Monitor token expiration and re-run the OAuth flow to obtain fresh tokens.
For more advanced usage–such as seat preferences, custom error flows, or credit card holds–expand on these snippets with your own business logic.
## Related pages
Browse all available integrations.
Alternative integration for event and large party bookings.
# PCI Pal
Source: https://docs.poly.ai/integrations/pci-pal
Integrate PCI Pal for secure, compliant payment processing during calls.
Use [PCI Pal](https://www.pcipal.com/) to take secure phone payments while staying PCI DSS compliant. Callers are transferred to PCI Pal's secure environment to enter card details, then returned to your agent with a payment confirmation.
This is a managed integration. Contact your PolyAI account manager to enable PCI Pal for your project.
## How it works
When a caller needs to pay, the PolyAI agent initiates a PCI Pal session.
The call is transferred to PCI Pal's secure environment where card details are captured.
Card numbers entered by keypad are masked and never exposed to the call recording.
PCI Pal processes the payment through your configured payment gateway.
After completion, the caller is transferred back to the PolyAI agent for confirmation.
## Capabilities
PCI DSS Level 1 compliant payment collection.
Callers enter card details on the phone keypad.
Immediate payment confirmation.
Success or failure is passed back to the agent so it can continue the conversation.
## Getting started
### Prerequisites
* A PCI Pal account with API access
* Your payment gateway credentials configured in PCI Pal
* PolyAI project access
### Set up
Contact PCI Pal to obtain the following values:
| Credential | Description |
| -------------------- | --------------------------------------- |
| **Tenant name** | Your PCI Pal tenant identifier |
| **Username** | API username |
| **Client ID** | OAuth client identifier |
| **Client Secret** | OAuth client secret |
| **Auth endpoint** | Authentication URL |
| **Session endpoint** | Payment session URL |
| **Flow ID** | Your configured payment flow identifier |
Work with PCI Pal to configure:
* Payment amounts and currencies
* Card types accepted
* Retry logic for failed payments
* Confirmation messaging
Securely share the PCI Pal credentials with your PolyAI representative. PolyAI stores them as encrypted secrets.
1. PolyAI configures the integration in your sandbox environment.
2. Run test payments using PCI Pal's test card numbers.
3. Verify successful processing and the return-to-agent flow.
4. Deploy to production once testing passes.
## Caller experience
A typical payment sounds like:
*"I'll now transfer you to our secure payment line."*
*"Please enter your 16-digit card number using your keypad."*
Digits are masked in the recording and never seen by the agent.
*"Payment successful. Transferring you back."*
*"Thank you, your payment of \$50 has been processed."*
## Security
* **PCI DSS compliance** — PCI Pal is certified Level 1 PCI DSS compliant.
* **No card data storage** — PolyAI never stores or has access to card details.
* **Encrypted transmission** — All payment data is encrypted in transit.
* **Recording pause** — Card entry is automatically excluded from call recordings.
## Limitations
* **Voice entry** — Card numbers must be entered by keypad, not spoken.
* **Transfer required** — Caller experiences a brief transfer to the payment system.
* **Single payment** — Each session handles one payment transaction.
## Support
* Contact your PolyAI account manager for integration assistance.
* Contact PCI Pal support for payment gateway issues.
## Related pages
Alternative payment processing integration.
Other managed integrations requiring account manager setup.
# Salesforce
Source: https://docs.poly.ai/integrations/salesforce
Connect your PolyAI agent to Salesforce for customer data lookup and case management.
Connect PolyAI to Salesforce to retrieve customer records, create cases, and manage data during calls. Your agent can access account information, update records, and send follow-up messages.
This integration is available from **Integrations** in Agent Studio under **CRM**.
## Prerequisites
* A Salesforce account with **administrator** access
* Access credentials for the **PolyAI Portal**
## What you'll provide to PolyAI
Once you finish the Salesforce setup below, share the following with your PolyAI representative through a secure channel:
| Credential | Description |
| -------------------- | ------------------------------------------------------------------------------------------- |
| **Client ID** | Consumer Key from the Connected App |
| **Client Secret** | Consumer Secret from the Connected App |
| **Username** | Salesforce username for the integration user |
| **Password** | Salesforce password appended with the user's security token |
| **Access Token URL** | `https://login.salesforce.com/services/oauth2/token` (or `test.salesforce.com` for sandbox) |
| **Base URL** | Your Salesforce instance URL, e.g. `https://your_instance.salesforce.com` |
## Set up Salesforce for API access
1. Log in to Salesforce with admin privileges.
2. Go to **Setup → Users → Profiles**.
3. Edit the profile for the user that will connect to PolyAI.
4. Ensure **API Enabled** is checked.
1. Go to **Setup → App Manager**.
2. Click **New Connected App**.
3. Fill in the following:
| Field | Value |
| ------------------------- | --------------------------------------------------------------------------------------------------------- |
| **Connected App Name** | `PolyAI Integration` |
| **API Name** | `PolyAIIntegration` |
| **Contact Email** | Your email |
| **Enable OAuth Settings** | Checked |
| **Callback URL** | Any HTTPS URL you control, e.g. `https://yourcompany.com/oauth/callback` — not actively used but required |
| **Selected OAuth Scopes** | `Full Access (full)` and `Perform requests on your behalf at any time (refresh_token, offline_access)` |
4. Save the Connected App and copy the **Consumer Key** and **Consumer Secret** — these are your Client ID and Client Secret.
Send the values from the table above to your PolyAI representative. PolyAI uses them to generate the OAuth access token and stores them as encrypted secrets.
## Code example: create a Salesforce case
Once the integration is live, your agent can create cases through the Salesforce REST API. This is a simplified example of the underlying call.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import requests
def create_salesforce_case(base_url, access_token, case_data):
"""
Create a case in Salesforce.
:param base_url: Salesforce base URL (e.g., https://your_instance.salesforce.com)
:param access_token: OAuth access token
:param case_data: Dictionary containing case details
"""
url = f"{base_url}/services/data/v55.0/sobjects/Case"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
response = requests.post(url, headers=headers, json=case_data)
if response.status_code == 201:
print("Case created successfully!", response.json())
else:
print("Failed to create case.", response.status_code, response.text)
if __name__ == "__main__":
base_url = "https://your_instance.salesforce.com"
access_token = "your_access_token"
case_data = {
"Subject": "Support Request",
"Description": "Details about the issue.",
"Origin": "Web",
"Status": "New",
}
create_salesforce_case(base_url, access_token, case_data)
```
## Next steps
Once PolyAI has your credentials, the integration is configured on our side and you'll be contacted when it's ready. For custom functionality, contact your PolyAI account manager.
## Related pages
Browse all available integrations.
Configure agent handoff routing.
## Additional resources
* [Salesforce REST API documentation](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/)
* [OAuth 2.0 in Salesforce](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_web_server_flow.htm)
# Stripe
Source: https://docs.poly.ai/integrations/stripe
Connect your PolyAI agent to Stripe for payment status, refunds, and coupon management.
Use the [Stripe](https://stripe.com/) integration to let your agent check payment status, process refunds, create coupons, and manage subscriptions during conversations.
This is a managed integration. Prepare the credentials below, then contact your PolyAI account manager to complete setup.
Direct card collection over voice requires PCI DSS compliance. Pair Stripe with [PCI Pal](/integrations/pci-pal) to capture card details securely, then process the payment through Stripe as your gateway.
## Capabilities
Look up the status of existing payments or charges.
Initiate refunds for eligible transactions.
Generate promotional discounts on demand.
Query subscription status and details.
## Getting started
### Prerequisites
* A Stripe account (Test or Live mode)
* Stripe API keys with the permissions your integration needs
* PolyAI project access
### Set up
1. Log in to your [Stripe Dashboard](https://dashboard.stripe.com/).
2. Go to **Developers → API keys**.
3. Copy the values you need:
| Credential | Used for |
| ------------------- | ---------------------------------------------- |
| **Publishable key** | Client-side operations (only if needed) |
| **Secret key** | Server-side API calls — required |
| **Webhook secret** | Only if you set up a webhook endpoint (step 2) |
Use test-mode keys during development, and never expose secret keys publicly.
If your integration needs real-time payment notifications:
1. Go to **Developers → Webhooks**.
2. Click **Add endpoint**.
3. Enter the webhook URL provided by PolyAI.
4. Select the events to receive, for example `payment_intent.succeeded` and `charge.refunded`.
Securely share with your PolyAI representative:
* API secret key
* Webhook secret (if using webhooks)
* Any specific configuration requirements
PolyAI stores these as encrypted secrets and completes the integration.
## Use cases
**Caller:** "Did my payment go through?"
**Agent:** Looks up recent charges by customer email or phone and confirms the payment status.
**Caller:** "I'd like a refund for my order."
**Agent:** Verifies the transaction and initiates the refund through Stripe.
**Caller:** "I was promised a discount."
**Agent:** Creates a one-time coupon in Stripe and reads the code back to the caller.
## Limitations
* **Card collection** — Direct card number capture requires PCI compliance; use [PCI Pal](/integrations/pci-pal) for secure entry.
* **Dispute handling** — Complex dispute resolution needs human intervention.
* **Sensitive account changes** — Verify with additional authentication before modifying account details.
## Security
* API keys are stored as encrypted secrets.
* All API calls use HTTPS.
* Webhook signatures are verified to prevent spoofing.
## Support
* For integration assistance, contact your PolyAI account manager.
* For Stripe-specific questions, see the [Stripe documentation](https://stripe.com/docs).
## Related pages
Secure payment processing for card collection.
Other managed integrations requiring account manager setup.
# Tripleseat
Source: https://docs.poly.ai/integrations/tripleseat
Capture event and large party leads automatically with PolyAI and Tripleseat.
Connect PolyAI to [Tripleseat](https://tripleseat.com/) to capture event and private dining leads from voice and webchat conversations. PolyAI collects guest details and creates leads directly in Tripleseat.
**Before getting started, you need both a Tripleseat account and a PolyAI account.**
## How it works
PolyAI acts as a conversational front end for your Tripleseat-powered locations. When a caller or chat user asks about private dining, events, or large party bookings, the PolyAI agent:
1. Collects event details from the guest (name, date, party size, etc.)
2. Creates a lead in Tripleseat through the Leads API
3. Your events team picks up the lead in Tripleseat to follow up, send proposals, and confirm bookings
This works across multiple channels:
* **Voice** - Callers speak their event details and PolyAI captures them automatically
* **Webchat** - Chat users provide details through a guided conversation
* **SMS** - For collecting information like email addresses that are difficult to capture over voice
## Prerequisites
To set up the Tripleseat integration, you need:
* A **Tripleseat account** with API access
* A **PolyAI account** with access to Agent Studio
## Setup
There are two ways to connect Tripleseat to PolyAI, depending on your project type.
### Connect through Agent Studio
Projects using the PLG restaurant template can connect Tripleseat directly from Agent Studio using OAuth 2.0.
In Agent Studio, go to the **Integrations** page and find **Tripleseat**.
Click **Connect** and sign in with your Tripleseat credentials. This authorizes PolyAI to create leads on your behalf.
Navigate to your site settings and find the **Large party handling** section. Set the **max party number** – when a caller requests a party larger than this, your agent will capture their details for Tripleseat instead of making a standard reservation.
Select **Send details to Tripleseat** as the handoff method, then choose the **Tripleseat location** from the dropdown.
Choose a fallback phone number in case the Tripleseat connection fails. You can use your default transfer number or set a custom large party transfer number.
### Connect with a public API key
For projects not using the self-service flow, your PolyAI representative can set up the integration using a public API key.
Reach out to your PolyAI account manager to start the integration process. You will need to decide which sites, locations, and lead sources to use.
In your Tripleseat dashboard, go to **Settings > API & Webhook details** and copy your **Public API Key**. Share this with your PolyAI representative.
Share the following with PolyAI:
* **Site ID** and **location ID** for each location
* The **lead source** you want used for PolyAI-created leads (e.g., "Phone" or a custom source)
If you have multiple locations under one site, each location requires its own location ID mapping.
PolyAI configures the integration for your locations. Once complete, your agent begins capturing leads automatically.
## Tripleseat hierarchy
Tripleseat organizes data in a hierarchy that maps to how PolyAI routes leads:
* **Customer** - Your organization in Tripleseat
* **Site** - A grouping such as a brand or business unit that contains one or more locations
* **Location** - An individual restaurant or event space where leads are recorded. Each location has its own **location ID** and belongs to a parent **site** (identified by a **site ID**).
* **Lead** - A booking inquiry created by PolyAI
If you are setting up Tripleseat from scratch, we recommend one site with one location per restaurant. Only use multiple sites if you need completely separate dashboards or permissions per brand.
## Data collected
PolyAI collects the following event details during a conversation and submits them as a Tripleseat lead.
### Standard fields
| Field | Description |
| ---------------- | ----------------------------------------- |
| Full name | Guest's first and last name |
| Phone number | Guest's contact number |
| Event date | Requested date for the event |
| Start time | Requested start time |
| Party size | Number of guests expected |
| Event type | Type of event (e.g., birthday, corporate) |
| Additional notes | Any extra details provided by the guest |
### Enterprise fields
These additional fields are available for enterprise-tier projects:
| Field | Description |
| ------------------ | ------------------------------------------------------- |
| Email address | Collected through SMS (not over the phone) |
| Company name | Guest's company or organization |
| Contact preference | Phone, email, or text |
| Event style | On-premise, full-service catering, pick-up, or drop-off |
| End time | Requested end time |
| Marketing opt-in | Whether the guest opts in to marketing emails |
Email addresses are collected through SMS rather than voice because alphanumeric characters are unreliable over speech recognition.
## API reference
PolyAI creates leads using the Tripleseat Leads API.
**Endpoint:**
```
POST https://api.tripleseat.com/v1/leads/create.json?public_key={Public_Key}
```
**Request body:**
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"lead": {
"first_name": "Jane",
"last_name": "Smith",
"phone_number": "+15551234567",
"email_address": "jane@example.com",
"guest_count": 25,
"event_date": "06-15-2026",
"start_time": "6:00 PM",
"event_description": "Birthday dinner",
"additional_information": "Prefer private room",
"lead_sources": [
{
"id": 18,
"name": "Phone"
}
],
"location": {
"id": 12345,
"name": "Downtown",
"site_id": 6789
}
}
}
```
If a site has more than one location, the `location.id` field is required.
For full API documentation, see the [Tripleseat API overview](https://support.tripleseat.com/hc/en-us/articles/205162108-API-Overview) and [Leads endpoint reference](https://support.tripleseat.com/hc/en-us/articles/212528787-API-Leads-Endpoint).
## TripleseatDirect
[TripleseatDirect](https://tripleseat.com/tripleseatdirect/) is a separate, guest-facing self-serve booking layer. Instead of creating a lead through the API, PolyAI sends the guest an SMS link to the TripleseatDirect booking page where they can complete their reservation directly.
**TripleseatDirect is handled as an SMS action rather than an API integration.** Contact your PolyAI representative to set this up.
## Disconnecting
If you disconnect Tripleseat from the integrations page, large party handling will fall back to your default transfer number. Callers requesting large parties will be transferred to the restaurant directly instead of having their details sent to Tripleseat.
## Next steps
* Connect Tripleseat from the **Integrations** page in Agent Studio, or contact your PolyAI account manager
* Configure **large party handling** in your site settings to set the party size threshold and Tripleseat location
* Review the [Tripleseat API documentation](https://support.tripleseat.com/hc/en-us/articles/19394408627479-API-Authentication) for authentication details
## Related pages
Connect to OpenTable for restaurant reservations.
Manage guest service requests in hospitality settings.
# Amazon Connect
Source: https://docs.poly.ai/integrations/voice/amazon-connect/amazon-connect
Connect your PolyAI agent to Amazon Connect for voice automation and AWS integration.
Use the [Amazon Connect integration](https://aws.amazon.com/marketplace/pp/prodview-rwoh2vu3mruba?sr=0-1\&ref_=beagle\&applicationId=AWSMPContessa) to connect PolyAI voice agents to your AWS ecosystem. Use [Amazon Connect contact flows](https://docs.aws.amazon.com/connect/latest/adminguide/contact-initiation-methods.html) for dynamic call routing and [DynamoDB](https://aws.amazon.com/dynamodb/) and [Lambda](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html) for real-time data processing.
## Capabilities
### Call handling and analytics
PolyAI voice agents manage inbound calls routed using [Amazon Connect](https://aws.amazon.com/connect/). Agents handle inbound queries and can resolve them or hand off to live agents. Call performance, customer satisfaction, and agent efficiency are tracked using Amazon Connect analytics, integrated with PolyAI's conversation data.
### Live agent handoff
Calls requiring human assistance are routed to Amazon Connect agents with full contextual data for continuity and personalized support.
### Secure data access
PolyAI securely retrieves and updates customer data using [AWS STS](https://docs.aws.amazon.com/STS/latest/APIReference/welcome.html) and [Amazon DynamoDB](https://aws.amazon.com/dynamodb/).
## How the integration works
1. Inbound call handling: Amazon Connect selects a number from a pool of PolyAI-provisioned phone numbers for incoming calls.
2. Routing to PolyAI: Calls are transferred to the PolyAI conversational agent, which retrieves contact attributes from DynamoDB using an AWS IAM role with appropriate permissions.
3. Guided conversational flows: The PolyAI agent greets the caller and executes customized conversational flows, such as identity verification or information collection.
4. Handoff or resolution: If the query is resolved, the call is terminated. Unresolved queries are routed back to Amazon Connect for queue-based agent handling.
5. Data management: Data from the interaction (e.g., transcripts, query status) is sent to Amazon Connect and stored in S3 buckets using the same AWS IAM role.
## Setup guide
### Prerequisites
* **Amazon Connect instance**: Ensure an active Amazon Connect instance with administrative access.
* **PolyAI project**: Set up a PolyAI agent aligned with your Amazon Connect workflows.
* **AWS services**:
* DynamoDB: For securely storing call attributes.
* Lambda: For processing event data.
* STS: For secure role-based data access.
* **Integration credentials**:
* IAM role: Create an IAM role with policies for DynamoDB and Lambda access, allowing PolyAI to interact with AWS resources.
### 1: Configure Amazon Connect
1. Log in to the AWS Management Console and open Amazon Connect.
2. Use the Flow Designer to:
* Create or edit a contact flow.
* Add:
* A PolyAI handoff node to route calls to the voice agent.
* A Lambda function node to retrieve and process call data.
3. Save and publish the contact flow.
### 2: Set up DynamoDB for call attributes
1. Create a DynamoDB table:
* Define a primary key, such as CallID or CustomerID.
* Add attributes like `customer_name` or `reservation_number`.
2. Link the table to the Lambda function for real-time updates.
**Correlating Amazon Connect `ContactId` with PolyAI conversation `id`**
Amazon Connect's `ContactId` (from `event['Details']['ContactData']['ContactId']`) and PolyAI's conversation `id` (returned by the [Conversations API](/api-reference/conversations/introduction)) are **separate identifiers** issued by each platform. They are not automatically mapped.
To correlate them, write the `ContactId` to a PolyAI variable at call start (for example via a start function or SIP header) so it appears alongside the PolyAI `id` in the Conversations API response. You can then join Amazon Connect CTRs and PolyAI conversation data on this shared field.
### 3: Configure AWS Lambda function
1. Create a Lambda function in the AWS Management Console:
* Use a provided or custom script to process call events.
2. Assign an IAM role with:
* Permissions for DynamoDB and STS.
3. Deploy the function and link it to the Amazon Connect contact flow.
Example Lambda function template:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import boto3
def lambda_handler(event, context):
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('YourDynamoDBTable')
# Extract call attributes
call_id = event['Details']['ContactData']['ContactId']
caller_number = event['Details']['ContactData']['CustomerEndpoint']['Address']
# Update or retrieve data
response = table.update_item(
Key={'CallID': call_id},
UpdateExpression="SET caller_number =:val",
ExpressionAttributeValues={':val': caller_number}
)
return {
'statusCode': 200,
'body': 'Call attributes updated successfully'
}
```
### 4: Integrate PolyAI agent
1. Provide PolyAI with:
* Your [12-digit AWS Account ID](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-identifiers.html).
* Your [Amazon Connect region](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/).
2. Set up the integration using [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html):
* PolyAI will share an AWS CloudFormation template using [Amazon S3](https://aws.amazon.com/s3/).
* Create a service role in your AWS account with the following policy, with `` replaced by your actual 12-digit AWS Account ID:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"cloudformation:*"
],
"Resource": "*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": "s3:*",
"Resource": "arn:aws:s3:::amazon-connect-integration-sample-code//*"
},
{
"Sid": "VisualEditor2",
"Effect": "Allow",
"Action": "lambda:*",
"Resource": "arn:aws:lambda:*::function:PolyAI*"
},
{
"Sid": "VisualEditor3",
"Effect": "Allow",
"Action": "iam:*",
"Resource": [
"arn:aws:iam:::policy/PolyAI*",
"arn:aws:iam:::role/PolyAI*"
]
},
{
"Sid": "VisualEditor4",
"Effect": "Allow",
"Action": "dynamodb:*",
"Resource": "arn:aws:dynamodb:*::table/PolyAI*"
}
]
}
```
3. Deploy the PolyAI CloudFormation stack, which will automatically create:
* A DynamoDB table.
* A Lambda function.
* The required IAM roles.
4. Update your Amazon Connect flow to route calls to PolyAI voice agents.
### 5: Test and deploy
1. Test integration:
* Verify everything works, like:
* Try routing a call to various PolyAI agents.
* Make dummy data retrieval and status updates to test DynamoDB.
* Simulate a live-agent handoff processes to make sure there are no problems.
2. Monitor analytics:
* Use the Amazon Connect analytics dashboard to track call volume, routing efficiency, and agent handoffs.
## Chat and messaging handoff
For webchat and SMS conversations, PolyAI escalates to a live agent through the Amazon Connect [Chat API](https://docs.aws.amazon.com/connect/latest/APIReference/API_StartChatContact.html) instead of SIP. Configure this under **Integrations > Amazon Connect** in Agent Studio.
| Field | Required | Description |
| ----------------- | -------- | -------------------------------------------------------------------------------------------------------- |
| `instance_id` | Yes | The Amazon Connect instance ID. |
| `contact_flow_id` | Yes | The contact flow that routes the chat to a queue. |
| `role_arn` | Yes | IAM role PolyAI assumes to call `StartChatContact`. |
| `aws_region` | No | Region of the Connect instance (for example, `eu-west-2`). Overrides the PolyAI worker's default region. |
| `api_url` | No | Override for the Connect API endpoint. |
| `headers` | No | Static headers to include on API calls. |
### When to set `aws_region`
PolyAI's messaging handoff worker uses a default AWS region. If your Amazon Connect instance is in a different region — for example, your worker defaults to `us-east-1` but your Connect instance lives in `eu-west-2` — set `aws_region` on the integration so `StartChatContact` targets the right region. Without this override, the handoff fails with a `ResourceNotFoundException`.
Use the [region code](https://docs.aws.amazon.com/general/latest/gr/connect_region.html) shown in the Amazon Connect console URL (for example, `eu-west-2`, `us-west-2`, `ap-southeast-2`).
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"instance_id": "12345678-aaaa-bbbb-cccc-1234567890ab",
"contact_flow_id": "abcdef12-3456-7890-abcd-ef1234567890",
"role_arn": "arn:aws:iam::123456789012:role/PolyAIAccessConnect",
"aws_region": "eu-west-2"
}
```
### IAM trust policy requirements
PolyAI's handoff worker runs on EKS with [IRSA](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html), which adds session tags to its credentials when chain-assuming your IAM role. Your `PolyAIAccessConnect` role's trust policy must therefore allow both `sts:AssumeRole` **and** `sts:TagSession` from the PolyAI worker account. If `sts:TagSession` is missing, the assume-role step fails before any Connect API call is made.
## Useful links
* [Amazon Connect Documentation](https://aws.amazon.com/connect/)
* [AWS Lambda Documentation](https://docs.aws.amazon.com/lambda/latest/dg/welcome.html)
* [Amazon DynamoDB Documentation](https://docs.aws.amazon.com/dynamodb/latest/developerguide/Introduction.html)
* [PolyAI API Reference](/api-reference)
# Dialpad
Source: https://docs.poly.ai/integrations/voice/dialpad
Connect your PolyAI agent to Dialpad for voice automation and handoffs.
Connect PolyAI to Dialpad to automate voice interactions through Dialpad's cloud communications platform. Handle inbound calls and route complex issues to live agents.
Dialpad integration is available through the quick setup flow in Agent Studio. Go to **Integrations** and click **Dialpad** to get started.
## Quick setup
Agent Studio provides click-based integration with Dialpad:
1. Go to **Integrations** in the sidebar.
2. Click the **Dialpad** card.
3. Follow the guided setup flow to connect your Dialpad account.
4. After setup, you're routed to the **Handoffs** page to configure call routing.
The quick setup handles authentication and basic configuration. For advanced settings, continue with the manual configuration below.
## Prerequisites
Before integrating:
* Active Dialpad account with admin access
* PolyAI project with a configured agent
* Phone numbers provisioned in Dialpad
## How it works
When integrated, PolyAI handles incoming calls routed from Dialpad:
1. Caller dials a Dialpad number
2. Dialpad routes the call to PolyAI
3. PolyAI agent handles the conversation
4. If needed, the agent transfers back to Dialpad for human handoff
## Configuring handoffs
After integration, configure how calls transfer back to human agents:
1. Go to **Voice > Handoffs**.
2. Create handoff destinations for different scenarios.
3. Map destinations to Dialpad queues or agents.
See [Call handoffs](/voice-channel/handoffs) for detailed configuration options.
## Testing the integration
1. Make a test call to your Dialpad number.
2. Verify the call routes to your PolyAI agent.
3. Test a handoff scenario to confirm transfers work.
4. Review the call in **Conversations**, filtered to Voice.
## Troubleshooting
| Issue | Solution |
| ------------------------- | -------------------------------------------------- |
| Calls not reaching PolyAI | Verify Dialpad routing configuration |
| Handoffs failing | Check handoff destination mapping in Call Handoffs |
| Audio quality issues | Review network connectivity and codec settings |
## Related pages
* [Integrations overview](/integrations/introduction) – All available integrations
* [Call handoffs](/voice-channel/handoffs) – Configure call transfers
* [Voice integrations](/integrations/voice/introduction) – Other voice platform integrations
# DNIs Pool
Source: https://docs.poly.ai/integrations/voice/dnis-pool
Integrate your telephony system with PolyAI using a pool of Dynamic Number Insertion (DNI) numbers.
A DNIs Pool is a rotating pool of Dynamic Number Insertion (DNI) numbers that lets you connect to your Virtual Agent hosted on PolyAI's infrastructure. This integration passes metadata through PSTN numbers when SIP headers are unavailable.
## Overview
DNI pooling lets you:
* Share structured JSON metadata with your agent at the start of a call
* Retrieve call state information at the end of the conversation
* Avoid custom SIP header requirements
This is useful when:
* You're using PSTN, which does not support sharing metadata
* You're using SIP installations that do not support custom SIP headers
A typical use case is sending a shared ID (or other metadata) at call start, and using it later to retrieve handoff details, such as whether the call completed successfully or needs escalation to a human agent.
## Requirements
To set up the DNIs Pool integration, you'll need the following from your PolyAI contact:
* API token for the `Reserve DNI` endpoint
* API token for the `Get Handoff` endpoint
* Your `account_id` and `project_id`
* Agree with your PolyAI contact on the attributes the Agent will need to extract
## Integration Runtime
The integration involves two main API endpoints:
* [`Reserve DNI`](/api-reference/dni/endpoint/dni-reservation)
* [`Get Handoff`](/api-reference/handoff/endpoint/get-handoff)
### Call flow
The integration flow works as follows:
1. Client sends project-specific attributes to the Reserve DNI endpoint. An example payload would look like this:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"attributes": {
"shared_id": "1234567",
"ani": "+44123456789"
}
}
```
2. PolyAI stores the attributes and returns a reserved DNI (e.g., `+12345`).
3. Client initiates a call to the DNI.
4. PolyAI looks up the attributes for that DNI.
5. Attributes are forwarded to the Virtual Agent based on your API Key.
6. The Virtual Agent handles the conversation and stores any agreed data (e.g., handoff status).
7. The call ends, and PolyAI sends a SIP BYE message.
8. Client queries the Get Handoff endpoint to retrieve the call outcome.
An example response will look like this - note that the `shared_id` is the same ID you would have sent above:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"data": {
"handoff_to": "default_queue",
"handoff_reason": "test_reason"
},
"id": "POLYAI_CALL_SID",
"shared_id": "1234567"
}
```
9. If a handoff state is present, the Client can transfer the call to a human agent; otherwise, the call is considered complete.
## Agent setup
For the integration to work, your agent needs to:
* Store the attributes you are sending through the [`Reserve DNI`](/api-reference/dni/endpoint/dni-reservation) API
* Save the handoff info into state to have the [`Get Handoff`](/api-reference/handoff/endpoint/get-handoff) API expose them when invoked
### Store attributes
Attributes received from the API can **only** be accessed in the `start_function`.
Not doing it here would mean losing them for all the conversations, making it impossible for the integration to work.
In your `start_function`, attributes are stored in the `integration_attributes` field as a dictionary.
Retrieve them like this (assuming the attributes sent are the ones in the example above):
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
#### anywhere in start_function ####
# Retrieve DNIs Pooling Attributes
shared_id = conv.integration_attributes.get("shared_id")
ani = conv.integration_attributes.get("ani")
if not (shared_id or ani):
# In case no attributes are received, make sure to handoff to a default queue
# on your side if you can't handle the call without the data
conv.state.handoff_to = "DEFAULT_QUEUE"
print(f"shared_id: {shared_id}, ani: {ani}") # you can optionally print the attributes
# Store them in the state to use them later in the conversation
conv.state.shared_id = shared_id
conv.state.ani = ani
```
You can then use those attributes in other functions, in the FAQs or elsewhere
in your Agent. This will be useful to customize your Agent behavior, or use those attributes
in other API calls to your infrastructure for example.
### Save handoff info
The conversation will always end with this integration. Handoff information must be prepared to be exposed by the Handoff API.
The Handoff API can be configured to expose all the `state` variables you desire. The most useful variables are often:
* `conv.state.handoff_reason` - the reason for handoff
* `conv.state.handoff_to` - the destination for the call
As long as you store your desired values in `state` variables, the Handoff API will expose them (if configured to do so).
Configuration is handled by your PolyAI contact. Send them all the variables you want to expose, and they will set up the Handoff API accordingly.
## Pool size
It's important to make sure that every customer hitting your solution gets to PolyAI.
In a DNIs Pooling integration, the pool size is fundamental, as it will dictate how many
concurrent calls you will be able to handle.
As a rule of thumb, the pool size should be set to **at least** the maximum number of
concurrent calls you expect to handle at peak times to avoid disruptions to your service.
The pool size is configured by your PolyAI contact. Share the maximum number of concurrent calls you expect at peak times when requesting a change.
## Fallback handling
If no DNI is available for your call, the Reserve DNI API returns a 404 status code. Set up your telephony system to handle this case by routing calls to a fallback queue.
PolyAI monitors pool utilization per project and will be notified automatically, enabling prompt action to extend the pool size.
# Voice integrations
Source: https://docs.poly.ai/integrations/voice/introduction
Connect PolyAI to voice platforms for call routing and SIP-based handoffs.
PolyAI integrates with voice platforms for call routing, SIP data exchange, and live-agent handoffs. Most telephony integrations can be connected from **Integrations** in Agent Studio.
## Available integrations
| Platform | Connection | Key features |
| ----------------------------------------------------------------------- | --------------------------- | ------------------------------------------------------------------------ |
| **[Five9](/integrations/voice/sip/five9)** | Studio UI | SIP trunking, auth tokens, metadata exchange |
| **[NICE CXone](/integrations/voice/sip/NICECXone)** | Studio UI | Signal API handoffs, pseudo-number routing |
| **[Twilio](/integrations/voice/twilio)** | Studio UI | Marketplace connector, TwiML/Studio routing, Flex handoff |
| **[Amazon Connect](/integrations/voice/amazon-connect/amazon-connect)** | Studio UI | DynamoDB contact flows, Connect analytics |
| **[Genesys](/integrations/voice/sip/genesys)** | Studio UI | BYOC trunks, sequential failover, custom SIP headers |
| **[Dialpad](/integrations/voice/dialpad)** | Studio UI | Cloud contact center, call management |
| **[Zoom](/integrations/voice/zoom)** | Zoom App Marketplace (beta) | Zoom Phone & Contact Center, App Marketplace install, custom SIP headers |
| **[Custom SIP](/integrations/voice/sip/custom-sip)** | Managed | SIP header access, SIP INVITE/REFER transfers |
| **[DNI Pooling](/integrations/voice/dnis-pool)** | Managed | Rotating number pool for metadata passing |
Custom SIP and DNI Pooling require coordination with your PolyAI account manager. Zoom is connected through the Zoom App Marketplace rather than Agent Studio. All other telephony integrations can be connected directly from Agent Studio.
# NICE CXone
Source: https://docs.poly.ai/integrations/voice/sip/NICECXone
Connect your PolyAI agent to NICE CXone using the Signal API.
The PolyAI and NICE CXone integration uses NICE's [Signal API](https://developer.niceincontact.com/API/AdminAPI#/Contacts/Signal%20a%20Contact) to manage handoff and voice interactions. This guide covers setup, configuration, and usage.
## Overview
### Shared NICE integration
PolyAI integrates with NICE CXone using a shared [connector service](https://help.nice-incontact.com/content/studio/advanced/dbconnector/dbconnector.htm). Each
client is assigned unique [pseudo numbers](https://en.wikipedia.org/wiki/Pseudorandom_number_generator) by NICE, which are used alongside an [authentication token](https://developer.niceincontact.com/API/AuthenticationAPI#/Token/getToken) to route calls to the correct project.
## Handoff process
### Signal API
PolyAI uses the Signal API for call handoffs. This API supports up to **9** additional parameters, making it useful for passing larger
data sets to clients.
#### Requirements for the Signal API
* A Client-provided `access_key_id` and access\_key\_secret.
* The `Client_id` and `client_secret` from NICE: To obtain these, fill out the [NICE API application form](https://forms.microsoft.com/pages/responsepage.aspx?id=vdojcYcOqU2cubfsggEarfHlkRVlgSlMjqsp52ASGttUMEJaSkQ0Rk5LVkIwOFZNWUtCUkFTWUVHUS4u\&route=shorturl). Include:
* Your CxOne [business unit](https://help.nice-incontact.com/content/acd/businessunits/businessunit.htm) number.
* Your contact details, arbitrary application name, and description.
* Answer tenant-related questions: Select **Single** and **Global**.
* Select `secret_basic` for the authentication method and `AdminAPI` for the API type.
* Select **Back-End** for the application type.
Processing typically takes several business days. Once complete, you will receive a `client_id` and `client_secret` from NICE.
### Using the Signal API
1. Configure the Signal API in the PolyAI project:
* PolyAI configures the Signal API connection for your project.
* The `contact_id` is passed in an X-Header in the initial SIP INVITE.
2. Pass additional data:
* The Signal API supports up to 9 additional parameters if more detailed data is needed beyond what the SIP X-Header can accommodate.
## Setup steps
### 1: Obtain required credentials
* **From NICE**: Retrieve `client_id` and `client_secret` after form submission.
* **From the client**: `Access_key_id` and `access_key_secret`, and the CxOne business unit number.
### 2: Configure the NICE connector in PolyAI
1. Add a NICE connector to your PolyAI project.
2. Save the provided authentication token for routing calls.
3. If applicable, enable the Handoff API in your project and generate an API key for screen-pop or related functionality.
# Custom SIP
Source: https://docs.poly.ai/integrations/voice/sip/custom-sip
Access SIP headers for dynamic routing, personalization, and custom handoffs.
Access SIP headers and attributes to extract caller metadata, route calls dynamically, and implement custom SIP-based handoffs.
## Key attributes
The `conv` object provides access to SIP-related attributes that you can use in your agent logic:
#### **sip\_headers**
* **Description**: A dictionary containing SIP headers and their corresponding values.
* **Use case**: Extract caller-specific metadata or route calls dynamically.
* **Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
caller_id = conv.sip_headers.get("From", "Unknown Caller")
print(f"Caller ID: {caller_id}")
```
#### **caller\_number**
* **Description**: The phone number of the caller.
* **Use case**: Personalize responses based on the caller's number.
* **Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if conv.caller_number == "+15551234567":
conv.goto_flow("vip_support_flow")
else:
conv.goto_flow("general_support_flow")
```
#### **callee\_number**
* **Description**: The phone number the caller dialed.
* **Use case**: Determine routing based on the number dialed.
* **Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
dialed_number = conv.callee_number
if dialed_number == "+15557654321":
conv.state.branch = "North Branch"
else:
conv.state.branch = "South Branch"
```
### How to Use Custom SIP
1. **Access SIP headers**:
* Use `conv.sip_headers` to retrieve metadata from the SIP protocol.
* This is particularly useful for identifying caller attributes or custom routing.
2. **Dynamic routing**:
* Use `caller_number` and `callee_number` to configure routing flows based on incoming and dialed numbers.
3. **Store data for contextual responses**:
* Save SIP-related information into `conv.state` to persist data across the conversation.
* Example:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.state.metadata = {
"caller_id": conv.sip_headers.get("From", "Unknown"),
"dialed_number": conv.callee_number,
}
```
### Use cases
* **VIP routing**: Automatically direct high-priority callers to specific support flows.
* **Branch-specific handling**: Route calls to the appropriate branch or location based on the number dialed.
* **Dynamic personalization**: Use SIP headers to provide tailored greetings or responses.
For more details on the `Conversation` object, visit the **[`conv` object](/tools/classes/conv-object)** page.
## Initiating SIP transfers programmatically
In addition to configuring call handoffs in the UI, you can initiate SIP transfers directly from a function using `return` values. This is useful when routing logic depends on in-conversation data or SIP header values.
You can use two methods: **INVITE** and **REFER**. These are mutually exclusive with UI-based `Call Handoffs` and require manual setup.
### When to use INVITE vs REFER
**Use SIP INVITE when:**
* You want PolyAI to remain in the call as a bridge between the caller and the target
* You need to monitor or record the transferred call
* You want to inject custom SIP headers for the outbound leg
* You're transferring to a PSTN number or external SIP URI
**Use SIP REFER when:**
* You want the Session Border Controller (SBC) to handle the transfer directly
* You want to remove PolyAI from the call path after transfer (reduces latency and infrastructure load)
* Your SBC supports REFER and has "Take Back and Transfer" enabled
* You're performing an attended or blind transfer in the same SIP domain
### SIP INVITE
This creates a new call and bridges the user with the target. Use this when you want PolyAI to act as a bridge.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"handoff": {
"invite": {
"phone_number": "2222222", // SIP URI or PSTN number
"outbound_caller_id": conv.caller_number,
"outbound_endpoint": "YOUR_OUTBOUND_ENDPOINT_NAME", // Provided by PolyAI during setup
"sip_headers": {
"X-Customer-ID": conv.state.customer_id,
"X-Call-Type": "Support"
}
}
}
}
```
The `outbound_endpoint` value is provided by PolyAI during your SIP integration setup. Contact your PolyAI account manager or representative if you need this value.
### SIP REFER
This instructs the Session Border Controller to take over and transfer the call.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"handoff": {
"refer": {
"phone_number": conv.caller_number, // Number PolyAI used
"refer_to": "sip:support@example.com", // SIP URI or number
"sip_headers": {
"X-Caller-ID": conv.caller_number
}
}
}
}
```
To avoid 405 errors, make sure the "Take Back and Transfer" setting is enabled when using SIP REFER on any platform with that setting.
These code patterns are especially useful when dynamic call routing or SIP metadata injection is needed mid-conversation.
# Five9
Source: https://docs.poly.ai/integrations/voice/sip/five9
Connect your PolyAI agent to Five9 through a SIP trunk for call routing and handoffs.
Connect PolyAI to Five9 through a shared SIP trunk for call routing and live-agent handoff. This guide covers setup, routing methods, and transfer configuration.
## 1: Integrate with the shared Five9 and PolyAI SIP trunk
Five9 uses a multi-tenant SIP trunk to connect with PolyAI. Contact your [Five9 account manager](https://www.five9.com/) to configure your environment to connect to this SIP trunk and route calls to PolyAI.
## 2: Five9 to PolyAI routing methods
PolyAI provides an authentication token included in the SIP INVITE message under the X-header (`X-PolyAi-Auth-Token`). This token identifies and distinguishes traffic for accurate agent routing.
Five9 and PolyAI agree on unique extension numbers for routing calls. This method offers flexibility and scalability, especially for clients managing multiple PolyAI agents.
## 3: PolyAI to Five9 transfer methods
When PolyAI is unable to contain a call, it sends a SIP BYE message with custom headers to indicate the status of the call. This method is simple and suitable for scenarios with limited metadata requirements.
Key headers include:
* `X-contained`: Indicates whether the call was contained by PolyAI. (`true` or `false`)
* `X-destination`: Specifies where to transfer the call if it was not contained. Values are determined during deployment.
Five9 supports up to 10 custom headers. Additional requirements should be discussed with PolyAI during deployment.
For cases requiring detailed metadata, PolyAI provides a [Handoff API](/api-reference/handoff/introduction) to return larger data payloads. This method involves:
1. PolyAI sends a SIP BYE message to Five9 to indicate the agent call leg is complete.
2. Five9 uses scripting to call the PolyAI Handoff API, using the Five9 session ID as the `shared_id` parameter to retrieve data.
3. The API response includes:
* `contained`: Indicates whether the call was contained. (`true` or `false`)
* `destination`: Specifies the transfer destination if the call was not contained.
**Benefits**:
* No size limit on the API response, enabling detailed metadata transfer.
* Flexible for advanced routing decisions.
**Considerations**:
* Ensure error handling in the Five9 scripting to manage API response issues.
### Example API integration
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import requests
def call_handoff_api(api_url, api_key, shared_id):
"""
Calls the PolyAI Handoff API to retrieve call metadata.
:param api_url: Handoff API URL
:param api_key: PolyAI API key
:param shared_id: Five9 session ID used for the call
"""
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
payload = {"shared_id": shared_id}
response = requests.post(api_url, headers=headers, json=payload)
response_data = response.json()
if response.status_code == 200:
return response_data
else:
raise Exception(f"API Error: {response.status_code} - {response.text}")
# Example usage
api_url = "https://api.{region}.platform.polyai.app/handoff" # Replace {region} with us-1, uk-1, or euw-1
api_key = "your_polyai_api_key"
shared_id = "five9_session_id"
data = call_handoff_api(api_url, api_key, shared_id)
print("Call metadata:", data)
```
# Genesys
Source: https://docs.poly.ai/integrations/voice/sip/genesys
Connect your PolyAI agent to Genesys Cloud using BYOC service.
Connect PolyAI to Genesys Cloud's [Bring Your Own Carrier (BYOC) service](https://help.mypurecloud.com/articles/about-byoc-bring-your-own-carrier/) to provide call handling, routing, and handoff capabilities. This guide explains how to integrate your Genesys Cloud instance directly with PolyAI over BYOC.
BYOC setups use shared SIP Trunks, so PolyAI connects using a preconfigured multi-tenant trunk with shared IPs and settings.
## PolyAI configuration
1. **Get connection tokens**:
* PolyAI will provide you with two tokens:
* A **UAT Token** to use for testing.
* A **Production Token** to use for live deployment.
* Use these tokens to authenticate your SIP trunks with PolyAI.
2. **Enable Handoff API**:
* If [screen pop](https://all.docs.genesys.com/PEC-GPA/Current/Administrator/GplusScreenPop90) is required, make sure you have configured your project to meet the standards of the [Handoff API](/api-reference/handoff/endpoint/get-handoff).
* Request an API key from PolyAI for this functionality.
3. **Prepare for SIP INVITE data**:
* Your Genesys SIP INVITE will include:
* `x-inin-cnv`: the Genesys [Conversation ID](https://developer.genesys.cloud/commdigital/digital/openmessaging/gettingConversationId).
* `X-User-to-User`: Custom data in the format `X-User-to-User:{data};encoding=ascii`.
4. **Send data during SIP REFER**:
* To return data to Genesys, use this format in the SIP REFER message:
`Refer-To: `
* Replace `{pd}` with the protocol discriminator (two hex digits).
* Replace `{data}` with the hex-encoded information.
### Genesys trunk setup
#### Prerequisites
* **PolyAI tokens**: You should have both UAT and Production tokens provided by PolyAI.
* **Admin access**: Ensure your Genesys account has [permissions to manage external SIP trunks](https://help.mypurecloud.com/articles/external-trunk-settings/).
#### Configure the primary trunk
To configure a SIP trunk with Genesys Cloud, follow these steps.
**Configuration steps for the US region:**
1. Go to `Telephony > Trunks > External Trunks` in Genesys Cloud.
2. Create a new SIP trunk:
* Type: **BYOC Carrier**
* Subtype: **Generic BYOC Carrier**
* Protocol: **TLS**
3. SIP server: `kam.us-1.polyai.app`, Port: `5061`
4. Allow IP: `3.221.248.55`
5. Media settings: Codec `audio/PCMU`
6. UUI passthrough:
* Protocol: `X-User-to-User`
* Encoding: `Ascii`
7. Custom SIP headers:
* Header: `X-PolyAi-Auth-Token`
* Value: Use the token provided by PolyAI.
**Configuration steps for the UK region:**
1. Go to `Telephony > Trunks > External Trunks` in Genesys Cloud.
2. Create a new SIP trunk:
* Type: **BYOC Carrier**
* Subtype: **Generic BYOC Carrier**
* Protocol: **TLS**
3. SIP server: `kam1.uk-1.polyai.app`, Port: `5061`
4. Allow IP: `3.10.92.139`
5. Media settings: Codec `audio/PCMU`
6. UUI passthrough:
* Protocol: `X-User-to-User`
* Encoding: `Ascii`
7. Custom SIP headers:
* Header: `X-PolyAi-Auth-Token`
* Value: Use the token provided by PolyAI.
**Configuration steps for the EU region:**
1. Go to `Telephony > Trunks > External Trunks` in Genesys Cloud.
2. Create a new SIP trunk:
* Type: **BYOC Carrier**
* Subtype: **Generic BYOC Carrier**
* Protocol: **TLS**
3. SIP server: `kam.euw-1.polyai.app`, Port: `5061`
4. Allow IP: `54.77.217.78`
5. Media settings: Codec `audio/PCMU`
6. UUI passthrough:
* Protocol: `X-User-to-User`
* Encoding: `Ascii`
7. Custom SIP headers:
* Header: `X-PolyAi-Auth-Token`
* Value: Use the token provided by PolyAI.
**Configuration steps for the APAC region:**
1. Navigate to `Telephony > Trunks > External Trunks` in Genesys Cloud.
2. Create a new SIP trunk:
* Type: **BYOC Carrier**
* Subtype: **Generic BYOC Carrier**
* Protocol: **TLS**
3. SIP server: `kam1.sg-1.polyai.app`, Port: `5061`
4. Allow IP: `18.140.207.42`
5. Media settings: Codec `audio/PCMU`
6. UUI passthrough:
* Protocol: `X-User-to-User`
* Encoding: `Ascii`
7. Custom SIP headers:
* Header: `X-PolyAi-Auth-Token`
* Value: Use the token provided by PolyAI.
#### Create the secondary trunk
Repeat the above steps with the secondary PolyAI server and IP for your region:
* US: `kam2.us-1.polyai.app`, `35.170.209.49`.
* UK: `kam2.uk-1.polyai.app`, `18.168.178.6`.
* EU: `kam2.euw-1.polyai.app`, `34.255.224.245`.
* APAC: `kam2.sg-1.polyai.app`, `13.250.218.51`.
#### Set failover routing
1. Go to **Site > Edit Site > Outbound Routes** in Genesys Cloud.
2. Set the distribution pattern to **Sequential** to enable failover from the primary trunk to the secondary trunk.
# Twilio
Source: https://docs.poly.ai/integrations/voice/twilio
Connect your PolyAI agent to Twilio for voice automation and handoffs.
For basic Twilio number and SMS setup, see [Voice > Numbers > Twilio](/voice-channel/numbers/twilio/introduction).
Connect Twilio to your PolyAI project from Agent Studio. PolyAI uses your Twilio API credentials to automatically provision the SIP trunk(s) needed to route calls to your agent — you just bring your API keys and your own phone numbers.
## Quick setup
In Agent Studio, go to **Integrations** and click **Connect** on the Twilio card.
In the **Step 1: Twilio account credentials** panel, paste:
* **API Key**
* **API Secret**
* **Account SID**
PolyAI uses these to provision the SIP trunk(s) on your Twilio account. Click **Continue**.
Add the Twilio numbers you want to route to PolyAI for **Sandbox**, **Pre-release**, and **Live**. Numbers must be in E.164 format with a country code (e.g. `+441234567890`).
PolyAI provisions the SIP trunk on your Twilio account and wires up routing per environment. You're then taken to **Voice > Handoffs** to configure transfers back to a human agent.
That's it — no manual TwiML, no PolyAI ticket, no shared SIP trunk to negotiate.
## Where to find your Twilio credentials
In the [Twilio Console](https://console.twilio.com/):
* **Account SID** — top of the **Account Info** panel on the dashboard.
* **API Key + API Secret** — **Account > API keys & tokens > Create API key**. The secret is shown once at creation; copy it before closing the dialog.
Use a Standard API key with permissions to manage SIP trunks on the account.
## Editing or removing the integration
* **Update routing** — re-open the Twilio card in **Integrations** to add or remove numbers per environment.
* **Disconnect** — use the **Disconnect** action on the integration. PolyAI removes the routing on its side and tears down the SIP trunk it provisioned.
## Configuring handoffs
After the integration is connected, configure how calls transfer back to human agents in **Voice > Handoffs**. See [Call handoffs](/voice-channel/handoffs) for the full reference.
## Related pages
* [Voice > Numbers > Twilio](/voice-channel/numbers/twilio/introduction) — basic Twilio number and SMS setup
* [Call handoffs](/voice-channel/handoffs) — configure transfer destinations
* [Voice integrations](/integrations/voice/introduction) — other voice platforms
# Zoom
Source: https://docs.poly.ai/integrations/voice/zoom
Connect your PolyAI agent to Zoom Phone and Zoom Contact Center as a native third-party voice agent.
This integration is in beta. Contact your PolyAI account manager to check availability for your org.
Connect PolyAI to **Zoom Phone** or **Zoom Contact Center (ZCC)** as a native third-party Voice Agent through the **PolyAI App** in the Zoom App Marketplace. The app is built on Zoom's Voice Framework, so there's no shared SIP trunk to negotiate.
## Prerequisites
* Admin access to the Zoom App Marketplace
* Admin access to Zoom Phone System Management or Zoom Contact Center Management, depending on which product you're connecting
* Confirmation from your PolyAI account manager that the integration is enabled for your org
## Quick setup
Installing the app is shared across both Zoom Phone and Zoom Contact Center.
Go to **Advanced > App Marketplace** and install the **PolyAI App**.
Under the installed app, click the number shown under **Connections** (defaults to 1). You typically want **2 connections** — one for **live**, one for **pre-release** — named so it's obvious which is which.
A single connection can serve both Zoom Phone and Zoom Contact Center; each gets its own "To Number".
Recommendation: **one connection per AI Agent, per environment**. You don't need one connection per phone number, and you'll rarely need more than 2 connections total.
## Setup
### Virtual Agent setup
1. Go to **Phone System Management > Agents**.
2. Open the agent to view the number that will be passed to PolyAI.
3. Provide the numbers to PolyAI and specify which is **live** and which is **pre-release**.
The **Callback URL** field is only needed if PolyAI passes data directly back to Zoom Phone. You can skip it if you're using the PolyAI Screen Pop app.
### Add the Virtual Agent to an Auto Receptionist
Under the appropriate key press:
1. Select **Virtual Agent**.
2. Select **Third-party Agent**.
3. Choose the agent named after the connection you created above.
### Data Zoom Phone passes to PolyAI
Zoom Phone sends an `X-ZOOM-Call-SID` header (also sent by ZCC, so you can use it to distinguish the two call sources).
On Zoom Phone, the `From` number does not carry the original ANI. The original calling number arrives in the `X-ZOOM-ORIGINAL-CALLING-NUMBER` header instead.
| Purpose | Header |
| ----------------------------- | -------------------------------- |
| Call session ID | `X-ZOOM-Call-SID` |
| Original calling number (ANI) | `X-ZOOM-ORIGINAL-CALLING-NUMBER` |
| Forwarding number | `X-ZOOM-FORWARDING-NUMBER` |
| Forwarding extension | `X-ZOOM-FORWARDING-EXTENSION` |
| Transfer number | `X-ZOOM-TRANSFER-NUMBER` |
### Returning calls from PolyAI to Zoom Phone
PolyAI hands calls back via SIP REFER/transfer to the appropriate Zoom extension, then drops off the call. The refer target format is `@:5061`.
### Screen pop / handoff data
Two options for presenting handoff data to the receiving Zoom Phone user:
* **PolyAI Zoom Handoff App** — a Zoom app that acts as a screen-pop facility, securely surfacing PolyAI handoff data to the agent.
* **Zoom Webhook** — Zoom Phone can pass data back via webhook. Viewing this data may require a **Power Pack license**, and the payload format is pre defined.
### Connectors
1. Go to **Contact Center Management > Integrations > Connectors**.
2. Click **(...)** next to the connector and select **View connector settings**.
3. Select the correct region for your connector.
4. Copy the **Static URL** — this lets PolyAI pass a rich data payload back to ZCC.
### Add the AI Agent to a Flow
1. Go to **Contact Center Management > Flows**.
2. Open the flow you want the AI Agent to serve.
3. From the **Widgets** menu, under **Advanced**, drag a **Bot** widget onto the canvas.
4. Set the **Phone Number** on the widget, and tell PolyAI whether it's the **live** or **pre-release** environment.
Wire the **Timeout** branch to a fallback destination in case the call can't reach PolyAI.
### Passing data to and from PolyAI
Within the Bot configuration, data can be passed via **SIP header mapping** and/or the bot's **Outbound to Bot** / **Inbound from Bot** options.
**Outbound to Bot** (Zoom → PolyAI)
* ZCC already passes `X-ZOOM-Call-Sid` and `From` by default.
* Use SIP header mapping to pass through anything else you need (e.g. the originally dialed number).
**Inbound from Bot** (PolyAI → Zoom)
* Recommended: use the provided **Webhook** to pass data via API. Passing values back via SIP header mapping is also possible.
### Handoff from PolyAI to ZCC
Pass metadata back via custom SIP headers or the **Webhook** (recommended, paired with **Inbound from Bot** above).
The ZCC refer target format is always `HumanAgent@:5061`.
When the call lands with the receiving agent, the handoff data passed via the Webhook appears as a screen pop in the ZCC agent console:
## Testing the integration
1. Make a test call to your Zoom Phone or Zoom Contact Center number.
2. Verify the call routes to your PolyAI agent.
3. Test a handoff scenario to confirm transfers back to a human agent work.
4. Review the call in **Conversations**, filtered to Voice.
## Troubleshooting
| Issue | Solution |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------- |
| Calls not reaching PolyAI | Verify the connection's number and environment (live/pre-release) mapping in the Zoom App |
| Missing original caller ANI (Zoom Phone) | Read `X-ZOOM-ORIGINAL-CALLING-NUMBER` instead of `From` |
| Handoff data not appearing | Confirm Screen Pop app install (Zoom Phone) or Webhook + Inbound from Bot mapping (ZCC) |
| Handoff data not visible in Zoom Phone webhook | Check whether a Power Pack license is required for your org |
## Related pages
General SIP trunk integration reference.
Configure call transfers back to a human agent.
Other voice platform integrations.
# Zendesk Talk
Source: https://docs.poly.ai/integrations/zendesk
Connect to Zendesk Talk to route inbound calls and hand off calls to live agents through SIP.
Route calls through Zendesk Talk and hand off to Zendesk agents through SIP. This integration handles inbound calls with agent escalation.
Zendesk Talk is available from **Integrations** in Agent Studio under **Telephony**. For Zendesk CRM integration, see the CRM section in Agent Studio.
## Call flow
1. A caller dials a number linked to a [Zendesk account](https://support.zendesk.com/hc/en-us/articles/4408884056346-Introduction-Getting-started-with-Zendesk-Support).
2. Zendesk uses the [Overflow](https://support.zendesk.com/hc/en-us/articles/4408832017690-Managing-overflow-calls-and-after-hours-routing-with-Talk) feature to redirect the call to PolyAI. This requires a phone number from a [SIP provider](https://www.techtarget.com/searchunifiedcommunications/definition/Session-Initiation-Protocol) such as [Twilio](https://www.twilio.com/sip-trunking) or [Gamma](https://gammagroup.co/products/sip-trunking-call-management/).
3. Twilio converts the [PSTN](https://www.techtarget.com/searchnetworking/definition/PSTN) call to [SIP](https://www.ietf.org/rfc/rfc3261.txt) and sends it to PolyAI.
4. PolyAI initiates a [SIP INVITE](https://datatracker.ietf.org/doc/html/rfc3261#section-13) to an outbound integration, routing the call to a SIP URI provided by Zendesk.
5. Zendesk agents receive the call. If a ticket exists, they see its ID with the `X-Zendesk-Ticket-Id` SIP header.
## Setup
### Configure PSTN forwarding
Zendesk numbers route calls internally by default. To send calls to PolyAI, enable Overflow.
1. Create an empty agent group in Zendesk Talk.
2. Set the inbound number to route only to this group.
3. Configure Overflow to forward calls:
* If no agents are available (the group is empty, so this is always the case).
* Outside business hours.
4. Disable voicemail for this number.
### Enable SIP in Zendesk
Zendesk requires SIP to send calls to external systems.
1. In Zendesk, go to **Admin Center**.
2. Select **Channels → Talk → Lines**.
3. Click **Add Line → Add SIP Line**.
4. Register a SIP address. Name it to match the client or project.
5. Add PolyAI's IP address range to your allowlist to permit SIP traffic.
### Create the outbound integration
Once PSTN forwarding and SIP are configured, contact your PolyAI account manager to set up the outbound integration. PolyAI will configure the SIP routing to connect your Zendesk environment.
## Handoff using the outbound integration
Once the SIP address is set up and verified, PolyAI can hand off calls to Zendesk agents.
For detailed handoff configuration, see [Call handoffs](/voice-channel/handoffs). Your PolyAI representative can help with Zendesk-specific routing requirements.
## Related pages
Configure call routing and agent escalation.
Browse other telephony integrations.
# Zendesk Ticketing Solutions
Source: https://docs.poly.ai/integrations/zendesk-ticketing-solutions
Connect your PolyAI agent to Zendesk for ticket lookup, creation, and updates using API token authentication.
PolyAI integrates with Zendesk Ticketing Solutions so that virtual agents can retrieve ticket information, create tickets, and update records in real time. Authentication uses API tokens with permission-based access.
This is a managed integration. Prepare the credentials below, then contact your PolyAI account manager to complete setup.
## How to Integrate Zendesk with PolyAI
To complete the integration, you will generate a Zendesk API token, identify the required authentication details, and provide them to PolyAI to finalise the connection.
***
### Step 1: Log in to Your Zendesk Account
1. Log in to your **Zendesk Admin Center**
2. Ensure you have permissions to manage API access (Admin role required)
***
### Step 2: Enable API Token Access
If API token access is not already enabled:
1. In **Zendesk Admin Center**, navigate to:\
**Apps and integrations → APIs → Zendesk API**
2. Enable **Token access**
3. Save your changes
For detailed instructions, refer to [Zendesk's guide on managing API token access](https://support.zendesk.com/hc/en-us/articles/4408889192858-Managing-API-token-access-to-the-Zendesk-API).
***
### Step 3: Generate an API Token
1. While still in **Zendesk API settings**, click **Add API token**
2. Enter a descriptive name (for example, `polyai-integration`)
3. Click **Create**
4. Copy the generated API token securely
The API token is displayed **only once**. Store it securely, as it cannot be retrieved later.
***
### Step 4: Obtain the Required Authentication Details
Zendesk API authentication using an API token requires the following three pieces of information:
#### 1. Organisation URL
Your Zendesk organization URL follows this format:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
https://.zendesk.com
```
**Example:**
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
https://acme.zendesk.com
```
#### 2. User Email Address
Use the email address of the Zendesk user who generated the API token.
**Example:**
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
polyai-integration@acme.com
```
#### 3. API Token
The API token generated in Step 3.
Zendesk uses **Basic Authentication**, where:
* **Username:** `/token`
* **Password:** ``
Reference: [Zendesk API authentication](https://developer.zendesk.com/api-reference/introduction/security-and-auth/#api-token)
***
### Step 5: Provide Credentials to PolyAI
Once you have collected all required information, provide the following details to PolyAI through integration page under your project:
* **Zendesk Organisation URL**
* **Zendesk User Email Address**
* **Zendesk API Token**
These credentials allow PolyAI to securely connect to your Zendesk instance and complete the integration.
***
## Support
If you encounter issues generating the API token or identifying the required details, please contact your Zendesk administrator or your PolyAI representative.
## Related pages
Route calls through Zendesk Talk.
Other managed integrations requiring account manager setup.
# RAG
Source: https://docs.poly.ai/knowledge/faqs/RAG/introduction
How retrieval-augmented generation powers topic matching.
This page explains how your agent finds the right topic to answer a caller's question. You do not need to configure RAG directly – it works automatically based on how you structure your [FAQs](/knowledge/faqs/introduction) and [Sources](/knowledge/sources/introduction).
## What is RAG?
Retrieval-Augmented Generation (RAG) is a technique where the system first searches a knowledge source for relevant content, then feeds that content to a language model to generate an accurate response.
## Components of RAG
1. **Retrieval component**: Searches the knowledge for relevant information based on the input query.
2. **Augmentation**: Uses retrieved information to enhance the original query with additional context.
3. **Generation component**: Generates responses using a language model, integrating both the query and retrieved information.
## How RAG works in Agent Studio
PolyAI uses RAG to match user queries to Knowledge topics and generate contextual responses. Here is how it works in Agent Studio:
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
flowchart LR
A[User Query] --> B[Retriever]
B --> C[Knowledge Topics]
C --> D[Top Matches]
D --> E[LLM]
E --> F[Response]
E --> G[Action]
```
1. **Query processing**: When a caller provides a query, the RAG framework is initiated.
2. **Retrieval**: The retriever component searches the structured knowledge to find matching topics. The knowledge is organized to optimize retrieval performance and ensure precise matches.
3. **Generation**: The LLM uses the retrieved information to select and generate the response.
Write clear, specific topic names and realistic sample questions for best results. These are key signals the retriever uses to find the right match. You can add up to **20** sample questions per topic – more questions help the retriever find the right match. For best retrieval and generation quality, use PolyAI's [Raven](/behavior/models/raven) model – it is robust to irrelevant retrieved content and will say "I don't know" rather than hallucinate when information isn't available.
## Managed Topic structure for RAG
Each Managed Topic is structured for effective retrieval. A topic includes:
* **Topic name**: The FAQ name or category of the information.
* **Sample Questions**: Example queries that callers might use. These help RAG understand user intent and improve matching accuracy.
* **Content**: The information you want the agent to provide to users.
* **Action**: Specific actions triggered by the query, such as calling a function, initiating a workflow, or handing off to a human agent.
## Disabling topics at runtime
For deterministic control over which topics are available during a conversation, you can disable specific FAQs from a Python function using `conv.disable_kb_topics()`. Disabled topics are excluded from retrieval until the conversation ends or you re-enable them. See [Disable KB topics](/tools/classes/disable-kb-topics).
## Why RAG?
You do not need to retrain a model when you update your Knowledge. RAG retrieves from the current Knowledge at query time, so updates are available as soon as they are [promoted to the target environment](/environments-and-versions/introduction).
Behavior may vary depending on your agent's configuration. For example, agents using the real-time (speech-to-speech) model may trigger retrieval differently than standard voice agents. For multilingual agents, see [multilingual configuration](/behavior/language/multilingual) for guidance on setting up Knowledge topics across languages.
# Handoff
Source: https://docs.poly.ai/knowledge/faqs/actions/handoff
Trigger call handoffs from Managed Topic actions
Add a handoff action to a Managed Topic so your agent can transfer callers to a live agent or specific department when the topic is matched. For voice, this routes the call via SIP; for webchat, this can connect users to live chat agents.
Before adding a handoff action, you need at least one handoff destination configured. See [Call Handoffs](/voice-channel/handoffs) for setup instructions.
## How handoff actions work
When a topic with a handoff action is matched, the agent follows your prompt to decide whether and when to transfer the call. You can configure the handoff to happen immediately (direct handoff) or after asking the caller if they want to be transferred (offer handoff).
## Adding a handoff trigger
Add a handoff to a topic's **Actions** field using any of these methods:
1. Type `/` in the Actions field and select **Call handoff**
2. Right-click in the Actions field and choose **Call handoff** from the menu
3. Click the **+** icon on the right side of the field and select **Call handoff**
## Selecting the destination
1. After selecting **Call handoff**, choose an existing handoff destination from the dropdown.
2. To create a new destination, click **+ Add handoff**. You will be redirected to the [Call Handoffs](/voice-channel/handoffs) page to configure the SIP method, route, and headers.
## Writing the action prompt
The prompt controls *when* the handoff triggers. Common patterns:
**Offer handoff (recommended for most cases):**
```plaintext theme={"theme":{"light":"github-light","dark":"github-dark"}}
After the caller says they would like to be handed off to an agent, call {{handoff_destination}} to transfer the call to an agent.
```
**Direct handoff (no agent reply):**
```plaintext theme={"theme":{"light":"github-light","dark":"github-dark"}}
Immediately call {{billing_support}} to transfer the call. Do not respond to the caller first.
```
**Conditional handoff:**
```plaintext theme={"theme":{"light":"github-light","dark":"github-dark"}}
If the caller's issue is about billing, call {{billing_support}}. If it's about technical support, call {{tech_support}}.
```
## Related pages
Trigger SMS messages from Managed Topic actions.
Call custom functions from topic actions.
Configure destinations and SIP routing.
# Actions
Source: https://docs.poly.ai/knowledge/faqs/actions/introduction
Guide to setting up actions for SMS, functions, and handoffs
Use actions when your agent needs to do something beyond answering a question – send an SMS, look up data, or transfer to a live agent. Without actions, your agent can only provide static answers.
**Actions** tell your agent what to do when a topic is matched: send [SMS](./send-sms), run [functions](./tool-call), or [transfer to a live agent](./handoff).
Actions are optional. If a topic only needs to provide a text answer, leave the Actions field empty.
## Adding actions to your topics
Actions are configured in the **Actions field** of a managed topic. Type `/`, right-click, or click the **+** icon to insert an action. See each action type below for detailed steps.
## Common actions and use cases
### Sending SMS
Use SMS actions to share essential details directly with users.
**Example**: A user asks for refund instructions. The agent sends an SMS with a link to the refund portal.
**Configuration**:
* **Action**: *Send SMS*
* **Trigger**: *User queries refund process*
* **SMS Template**: *RefundPolicyTemplate*
[Learn more: How to send SMS](/knowledge/faqs/actions/send-sms)
### Invoking functions
Functions allow the agent to perform advanced tasks, such as retrieving external data or performing calculations.
**Example**: A user asks for the weather forecast. The agent runs a function to fetch the current weather details.
**Configuration**:
* **Action**: *Invoke Function*
* **Function**: *getWeatherForecast*
* **Trigger**: *User queries weather details*
[Learn more: How to invoke functions](/knowledge/faqs/actions/tool-call)
### Triggering handoffs
Use handoff actions to transfer users to live agents when they need human support.
**Example**: A user mentions a billing dispute. The agent connects them to the billing department.
**Configuration**:
* **Action**: *Trigger Handoff*
* **Destination**: *Billing Queue*
* **Trigger**: *User mentions billing*
[Learn more: How to trigger handoffs](/knowledge/faqs/actions/handoff)
## Best practices for actions
1. **Use precise triggers**
* Clearly define conditions for actions to avoid false triggers.
* Example: Use specific phrases like *User mentions refund* instead of broad keywords.
2. **Write concise SMS templates**
* Keep messages short, professional, and easy to understand.
* Example: "Your refund request is being processed. Visit \[this link] for details."
3. **Test custom functions thoroughly**
* Validate function performance across different user scenarios.
* Add fallback actions to handle potential errors.
4. **Plan handoff workflows**
* Use polite, clear messages to inform users before transferring them.
* Example: "Let me connect you to a billing specialist who can help further."
5. **Combine actions for complex workflows**
* Chain multiple actions for advanced tasks.
* Example: Fetch booking details using a function and send the information with SMS.
## FAQs
**Can I add multiple actions to one topic?**
Yes. You can send an SMS and invoke a function in the same topic.
**What happens if an action fails?**
Set up fallback actions like redirecting to a global topic.
**Do I need coding experience?**
No. Most actions use dropdown menus and templates.
See also: [Send SMS](./send-sms) | [Functions](./tool-call) | [Handoffs](./handoff)
# Send SMS
Source: https://docs.poly.ai/knowledge/faqs/actions/send-sms
Trigger an SMS directly from Managed Topic actions
Add an SMS action to a Managed Topic so your agent can send text messages to callers during a conversation – for example, sending a link, confirmation, or reference number.
You must complete the [SMS setup](/voice-channel/message-templates) (connect Twilio, create templates) before you can add SMS actions to topics.
## How SMS actions work
When a topic with an SMS action is matched, the agent follows your prompt to determine *when* to send the message. Typically, you instruct the agent to ask for consent before sending – the SMS is not sent automatically just because the topic matches.
## Adding an SMS trigger
Add an SMS trigger to a topic's **Actions** field using any of these methods:
1. Type `/` in the Actions field to open the insert menu and choose your template from the **SMS** group (e.g. `booking_confirmation`, `cancellation_confirmation`)
2. Right-click in the Actions field and choose a template from the **SMS** group
3. Click the **+** icon on the right side of the field and select a template from the **SMS** group
## Selecting the SMS template
1. In the **SMS** group of the insert menu, choose an existing template.
2. To create a new template, click **+ Add SMS template**. This opens the **Add SMS template** modal (also reachable from **Knowledge**), where you define the message content and phone number. Once saved, the template can be used in your knowledge base or rules and is managed under [Voice > Message templates](/voice-channel/message-templates).
## Writing the action prompt
The prompt tells the agent *when* and *how* to trigger the SMS. Always include a consent step so the agent asks before sending.
**Example prompt:**
```plaintext theme={"theme":{"light":"github-light","dark":"github-dark"}}
After the caller confirms that they would like to receive an SMS message with further details, call {{SMS_template}} to send the SMS out.
```
**Example with a fallback:**
```plaintext theme={"theme":{"light":"github-light","dark":"github-dark"}}
Offer to send the caller a text message with booking details. If they agree, call {{booking_confirmation_sms}}. If they decline, continue the conversation normally.
```
Always require explicit caller consent before sending SMS. This is both a best practice and a regulatory requirement in many regions.
## Related pages
Transfer callers to live agents from FAQs.
Call custom functions from topic actions.
# Invoke tool
Source: https://docs.poly.ai/knowledge/faqs/actions/tool-call
Invoke a tool (Python function) from a managed-topic action to extend your agent with custom logic.
Add tools to actions to extend your agent's capabilities with custom logic – look up data, call APIs, or perform calculations during a conversation.
Tools are Python `function`s – "tool" and "function" refer to the same feature, and the `{"{{fn:…}}"}` reference syntax below keeps the `fn:` prefix for backward compatibility.
## Actions vs Content
Tools only run when referenced from the **Actions** field. Content is what the agent says; Actions are what the agent does next. If you come from an intent-based background, the mental model is:
| Field | Equivalent | Visible to retriever |
| ------- | --------------------- | -------------------- |
| Content | `say` function | Yes |
| Actions | `transition` function | No |
Putting `{"{{fn:...}}"}` in Content will not call the tool and produces no error – the reference is treated as plain text.
## Adding a tool
Add a tool to the **Actions** field of a managed topic in three ways:
1. Type `/` in the Actions field
2. Right-click in the Actions field
3. Click the **+** icon on the right side of the field
Select a tool from the menu, or create a new one to populate later. You can reuse the same tool across multiple topics and actions.
Tool references like `{"{{fn:order_lookup}}"}` are **only valid in the Actions field**. Placing them in the Content field will not trigger the tool, and no error is shown. Always add tool references in Actions.
Iterative testing helps get your agent to call tools as expected.
## `{"{{fn:...}}"}` vs `{"{{ft:...}}"}` syntax
The `{"{{fn:...}}"}` syntax references **global tools** (also called global functions), which can be used across topics, flows, and rules. This is different from **[transition functions](/flows/transition-functions)** (`{"{{ft:...}}"}` syntax), which are scoped to a single flow.
## Known limitation: tools in flows
When a tool is referenced inside a flow action, it may not be included in the LLM's tool definitions. This means the LLM cannot see or call the tool. If you encounter this, add the tool reference to the [Behavior](/behavior/general/rules) section so the LLM is aware of it.
If your topic detours through a flow and needs to return to the original topic afterwards, see [Returning to the topic after a flow](/knowledge/faqs/introduction#returning-to-the-topic-after-a-flow) for the state-preservation pattern (storing the originating topic in `conv.state` and using the flow's exit function to direct the LLM back).
## Example prompt
Use this pattern in the **Actions** field to ensure the agent calls a tool before responding:
```plaintext theme={"theme":{"light":"github-light","dark":"github-dark"}}
When the user asks "where is my order," do not respond until you have called {{fn:order_lookup}} with an order number. If you don't have the order number, ask for it first, then call {{fn:order_lookup}}. Use the tool's response to answer the caller.
```
The agent will always invoke the tool before responding, and ground its answer in the tool's output.
The reason this pattern is reliable: it splits the speaking turn from the tool-calling turn. Telling the agent to both say something and call a tool in the same turn is a known [anti-pattern](/knowledge/faqs/introduction#common-anti-patterns) – the agent typically does one or the other, but not both consistently.
## Controlling agent behavior after a tool call
Tools can return values that control what the agent says or does next – for example, returning an exact `utterance` for the agent to speak, triggering a `handoff`, or ending the call with `hangup`.
See [Return values](/tools/return-values) for the full reference.
## Testing
Save your agent and click **Play** in the header to open the test chat panel.
Ask a test question like "Where is my order?" and observe how the agent handles the interaction.
Enable the **Tool calls** toggle in the test panel settings to inspect which tools were called and what parameters were passed. Use this to confirm the tool was actually triggered and that the correct arguments were sent – especially when the agent is not behaving as expected.
## Related pages
Trigger SMS messages from Managed Topic actions.
Transfer callers to live agents from topics.
Set up a new tool with parameters and Python code.
Control agent behavior with tool return values.
# FAQs
Source: https://docs.poly.ai/knowledge/faqs/introduction
Create curated FAQ topics with sample questions, answers, and actions to handle common caller requests.
FAQs define what your agent knows and how it responds. Each topic has sample questions, an answer, and optional actions (handoffs, SMS, tool calls). Topics cover the questions callers ask most: return policies, store hours, appointment availability, account lookups.
FAQs are found under **Knowledge > FAQs tab**.
**Use FAQs when** you need precise control over what the agent says, or when the answer should trigger an action (SMS, handoff, tool call). **Use [Sources](/knowledge/sources/introduction) instead** when you want to expose large volumes of external content (help articles, PDFs, FAQs) without curating individual topics.
**Use [Wren](/wren/introduction) to maintain topics in natural language.** Ask it to *"add a topic for store hours"*, *"tighten the refund answer"*, or *"create topics from `https://example.com/faq`"* — it edits FAQs on a branch you review before merging. You can also edit topics directly in this tab.
If you need to add custom logic to your topics, such as API calls, data lookups, or conditional behavior, see the [Tools](/tools/introduction) section under **Build** in the sidebar.
## The FAQs interface
Searchable, filterable list of all topics. Click any topic to edit its content, sample questions, or actions. Supports bulk CSV import, activation toggling, and deletion.
## How retrieval works
The agent **does not** see all MTs at once. Instead, it uses [retrieval-augmented generation (RAG)](/knowledge/faqs/RAG/introduction) to find the best match for the user's message.
1. The retriever compares the message to the topic's **name**, **sample questions**, and **content**, with higher weighting given to the name and sample questions.
2. It returns the top matching topics to the LLM.
3. The [LLM](https://en.wikipedia.org/wiki/Large_language_model) selects the best match and generates a reply (and may trigger an **[action](/knowledge/faqs/actions/introduction)**).
[Raven](/behavior/models/raven) is the recommended model. It grounds answers in retrieved topics, says "I don't know" instead of hallucinating, and converts topic content into natural responses without example utterances.
## Types of Managed Topic entries
### Simple FAQ
Used when the agent just needs to answer a question with no follow-up or action required.
#### Single turn
One message in, one message out.
* Add a short, helpful reply in `content`.
* Leave `actions` empty.
#### Multi turn
Ask a clarifying question before giving the answer.
* Use branching inside `content` to structure the reply.
* Leave `actions` empty.
### Handoff
Used when the agent should offer to connect the user to a human agent.
#### Offer
Agent replies, then offers to transfer.
* `content`: Include the answer and the offer.
* `actions`: Trigger `transfer_call` (or another [handoff function](/voice-channel/handoffs)) only if the user accepts.
#### Direct
Immediate transfer with no agent reply.
* Leave `content` empty.
* Always run `transfer_call` in `actions`.
#### Conditional
Transfer based on user clarification, for example, group size or request type.
* `content`: Ask a disambiguating question.
* `actions`: Map each answer to the correct destination.
### Outbound messaging
Used when you want to send a follow-up message via SMS or WhatsApp.
#### Offer
Offer to send a link via message.
* `content`: Include a short message and ask for consent.
* `actions`: If accepted, call `start_sms_flow`. See [SMS setup](/voice-channel/message-templates) for details.
#### Conditional
Let the user choose which message to receive.
* `content`: Ask a branching question.
* `actions`: Map answers to different `sms_id`s.
### Info only
Used for static reference material (like opening hours or prices). No interaction or action required.
* Fill `content`.
* Leave `actions` empty.
## Entity extraction and structured data collection
FAQs do not support entity extraction directly. If you need to collect structured data (names, dates, phone numbers, etc.) when a topic is matched, trigger a flow from the topic's action and configure [entity extraction](/flows/no-code/entities) in that flow's steps.
There are two ways to trigger a flow from a topic:
### Using the /Flow shortcut (no code)
In the topic's **Actions** field, type `/Flow` to insert a flow action. Select or create the target flow. When the topic matches, the agent enters the flow and can begin collecting entities.
### Using a tool call (code)
Alternatively, add a tool call action to the topic that calls `conv.goto_flow()`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start_booking_flow(conv: Conversation):
conv.goto_flow("Collect booking details")
return
```
This approach is useful when you need additional logic before entering the flow, for example, storing the originating topic name in `conv.state` so the agent can return to it after the flow completes.
### Returning to the topic after a flow
If the agent is answering an FAQ and takes a detour through a flow (e.g. to collect a date or verify identity), the agent can pick up where it left off after exiting the flow. To do this:
1. Before entering the flow, store the topic name in state (e.g. `conv.state.original_topic = "how_to_make_claim"`).
2. In the flow's exit function, check the stored topic and return a prompt directing the LLM back to the relevant topic content.
This avoids a generic "Is there anything else I can help with?" when the caller's original question has not yet been answered.
For the full triggering-flows reference, see [Triggering flows](/flows/triggering-flows).
## Best practices
### Writing good sample questions
Include at least **3 sample questions** per topic (maximum 20). This baseline helps the AI generalize intent across typical phrasings. For broad topics, add more to ensure better coverage.
* Focus on variety in **language and structure**, don't just make minor rewrites of the same sentence
* Think about how real users might describe the same issue, confused, specific, vague, or using non-standard terminology
* Additional examples should reflect **distinct phrasings**, not slight rewordings
* Vary sentence lengths alongside phrasing variety
* Consider including short queries ("billing help") alongside full questions ("Why was I charged twice this month?")
If migrating from an intent-based project, you might have more than 10 sample questions. Focus on keeping the most diverse items that preserve the full semantic scope of the original list.
### Topic naming
Because the topic **Name** is shown to the retriever, it should be semantically close to the topic content. For instance, a topic about payment disputes named "general\_behavior-payment" will likely trigger on payment queries in general rather than only on disputes.
* Make topic names **semantically descriptive**, the name should clearly reflect what the topic is about
* Avoid generic names like `Misc`, `Help`, `Info`, or `General`
* Use natural language: `Payment dispute resolution` is better than `payment_dispute_v2`
### Writing topic content
* Keep replies **brief, helpful, and consistent** with your brand style
* Give the agent **one task per turn**, do not mix text responses and tool calls in the same round
* Break down multi-step processes into **multiple turns or topics** rather than cramming everything into a single entry
* For multi-turn topics, structure the conversation so each turn has a clear purpose (ask a question, provide information, or trigger an action)
* After answering a simple FAQ, ensure the agent asks if the user needs more help, either in the topic content or in the [Behavior](/behavior/general/rules) section
### Using actions effectively
* Use `actions` **only when necessary**, if the agent just needs to answer a question, leave actions empty
* Limit to **one action per turn** for reliability
* When an action requires a handoff, include the handoff utterance as a parameter to `transfer_call` rather than mixing a `content` response with the action
* For conditional actions (transfer to different destinations based on caller response), use the disambiguating question in `content` and map each answer to its destination in `actions`
### General guidelines
**Do this:**
* Use specific topic names.
* Add **3–20** realistic sample phrasings per topic.
* Keep replies short, helpful, and on-brand.
* Use `actions` only when necessary.
* Split multi-part flows into separate turns or topics.
* Give the agent one task per turn, either speak or act, not both.
**Avoid this:**
* Bundling multiple intents into one topic.
* Running more than one action in a single turn.
* Using vague topic names like `Misc`, `Help`, or `Info`.
* Mixing text and tool calls in the same turn.
### Common anti-patterns
**Mixing text and tool calls in one turn:** For best results, split utterances and tool calls into separate turns, give the agent one task per turn. When a topic requires both a spoken response and an action, structure it so the response happens in one turn and the action in the next.
Turn 1: Agent answers the question and asks if the user wants to be transferred.
Turn 2: If the user says yes, call `transfer_call` with a handoff utterance.
Single turn: Answer the question, then immediately call `transfer_call` with utterance, the agent may skip one of these tasks.
**Overloading a topic with multiple intents:** If a topic covers "billing questions" but includes return policies, refund processes, and payment methods, it becomes too broad. Split each into its own topic with specific sample questions.
**Missing follow-up prompt:** Without a follow-up question like "Is there anything else I can help with?", the agent may hallucinate a next step. Add this to each simple FAQ's content or configure it in the [Behavior](/behavior/general/rules) section.
## Live collaboration
Multiple users can edit FAQs at the same time. Changes are synced in real time, so you can collaborate on topic content without overwriting each other's work.
## Activating and deactivating topics
Not every topic needs to be available all the time. You can temporarily **deactivate** a topic to keep it out of retrieval without deleting it or removing its content.
* Inactive topics are **ignored by the [LLM](https://en.wikipedia.org/wiki/Large_language_model)** and marked clearly in the UI.
* Activation status is **environment-specific**, so you can test changes safely in Sandbox before going live. Learn more about [environments and versions](/environments-and-versions/introduction).
* CSV import/export includes an `Active` column (`Y`/`N`) for bulk updates.
This is useful for seasonal articles, phased rollouts, A/B tests, or hiding content that is not ready for production.
## Automate with the Agents API
If you already have a knowledge base somewhere else (a help center, CMS, or intents list), you can create and sync topics programmatically instead of clicking through the UI.
The [Agents API](/api-reference/agents/introduction) has full CRUD for knowledge base topics. This is especially handy for the initial migration or for scheduled syncs from an external source.
Topics can't be written to `main` directly — create a branch, add them there, then merge it into `main` (which also publishes to Sandbox). Example queries go in `exampleQueries.queries`.
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Create a branch, add a topic on it, then merge to main
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/branches \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "branchName": "seed-kb" }' # response includes { "branchId": "BRANCH-…" }
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/branches/BRANCH_ID/knowledge-base/topics \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Password reset",
"content": "To reset your password, visit the account portal...",
"exampleQueries": { "queries": ["How do I reset my password?", "I forgot my password"] }
}'
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/branches/BRANCH_ID/merge \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "deploymentMessage": "Seed knowledge base" }'
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os, requests
BASE = "https://api.us.poly.ai"
HEADERS = {"x-api-key": os.environ["POLYAI_API_KEY"]}
# Sync all FAQ entries from your CMS into PolyAI topics.
# main is read-only, so seed the topics on a branch and merge once.
branch_id = requests.post(
f"{BASE}/v1/agents/{AGENT_ID}/branches",
headers=HEADERS,
json={"branchName": "cms-sync"},
).json()["branchId"]
for faq in cms.list_faqs():
requests.post(
f"{BASE}/v1/agents/{AGENT_ID}/branches/{branch_id}/knowledge-base/topics",
headers=HEADERS,
json={
"name": faq["title"],
"content": faq["answer"],
"exampleQueries": {"queries": faq["variants"]},
},
)
requests.post(
f"{BASE}/v1/agents/{AGENT_ID}/branches/{branch_id}/merge",
headers=HEADERS,
json={"deploymentMessage": "Sync FAQ topics from CMS"},
)
```
See [Knowledge base endpoints](/api-reference/agents/endpoint/knowledge-base/list-knowledge-base-topics) for the full list.
## Resources
Add tool calls, handoffs, and SMS triggers to topics.
Common issues with topics, rules, and actions.
Keep topics accurate and up to date over time.
Create and sync topics via the Agents API.
# Sources
Source: https://docs.poly.ai/knowledge/sources/introduction
Manage multiple external knowledge sources (sources) to inform your agent's responses.
Import existing content – help articles, PDFs, internal docs – so your agent can reference it without rewriting everything as individual topics. Connected Knowledge aggregates sources and re-syncs automatically.
The **Connected** tab is found under **Knowledge > Sources** in Agent Studio. [Raven](/behavior/models/model-use) is the recommended model — it paraphrases unstructured content more naturally than other models.
**Use Connected Knowledge when** you want to expose large volumes of external content quickly without curating individual topics. **Use [FAQs](/knowledge/faqs/introduction) instead** when you need actions, flows, or precise control over what the agent says and does. Both use [RAG (retrieval-augmented generation)](https://en.wikipedia.org/wiki/Retrieval-augmented_generation) to match user queries.
## Supported sources
* Websites
* Documents (PDF, CSV, JSON)
* Help desk systems (Zendesk, Gladly)
Sources sync automatically and can be reused across projects.
## How Sources differs from FAQs
Both tabs expose information to your agent. Key differences:
| Capability | Connected tab | FAQs tab |
| --------------------------------------- | --------------------------------- | ------------------------------------------------ |
| Trigger actions, functions, flows, SMS | No | Yes |
| Precise control over agent responses | No | Yes |
| Auto-sync from external sources | Yes | No |
| Best for frequently updated FAQ content | Yes | -- |
| Best for stable, structured info | -- | Yes |
| Fine-grained behavior control | No | Yes |
| Setup complexity | Low – no prompting skill required | Higher – requires more expertise and maintenance |
**Connected** = fast import of external content. **FAQs** = precise control with actions and flows.
If both tabs contain conflicting information, **FAQs always takes priority**.
## Add a new source
1. Go to **Knowledge > Sources tab**
2. Select **New source**
3. Choose one of:
* **Upload files**
* **Add URL**
* **Zendesk**
* **Gladly**
* Additional integrations are in development – contact your PolyAI representative for the latest availability
4. Complete the required details and click **Add**
Your agent will begin **Syncing** the content. Once ready, the source appears in the list.
## Supported source types
| Source Type | Details |
| ----------------------------------------- | ------------------------------------------------------------------------------------ |
| **Upload files – Text & structured data** | `.txt`, `.csv`, `.json`, `.xml`, `.md`, `.html`, `.rtf` |
| **Upload files – PDF** | `.pdf` |
| **Upload files – Microsoft Office** | `.docx`, `.doc`, `.docm`, `.xlsx`, `.xls`, `.xlsm`, `.pptx`, `.ppt`, `.pptm`, `.msg` |
| **Upload files – OpenDocument** | `.odt`, `.ods`, `.odp` |
| **Upload files – Email files** | `.eml` |
| **Upload files – E-books** | `.epub` |
| **URL scraping** | Public documentation pages and help center articles |
| **Zendesk** *(beta)* | Help Center content with API sync |
| **Gladly** *(beta)* | Knowledge source sync |
| Additional integrations | In development – contact your PolyAI representative for the latest availability |
## What exactly gets scraped when I upload a URL?
URL scraping traverses linked pages from the provided URL, with the following limits:
1. **Depth** → Only one level below the initial [URL](https://en.wikipedia.org/wiki/URL).
2. **Breadth** → A maximum of 10 embedded pages.
If your page contains more than 10 links, not all will be scraped. In that case, upload additional URLs individually or use integrations like [Zendesk](/integrations/zendesk)/[Gladly](/integrations/gladly) for complete coverage.
Where possible, connect applications such as Zendesk rather than relying on website scraping.
## Keeping content fresh
After external content changes:
* click **Update** to re-scrape files or URLs
* or use the **Sync** icon per source
If a URL requires login or credentials change, syncing may fail. Update access and retry.
## Group and manage sources
Group sources by product line, team, region, or document type. Sort by **newest**, **oldest**, **type**, or **name**. Each source offers:
* **Sync**
* **Rename**
* **Move to group**
* **Remove**
## Why isn't my agent using the sources I connected?
Several factors affect retrieval:
### Data structure
Sources splits content into 2000-character chunks with 500-character overlap. Very large documents or widely separated related sections may struggle more with relevance.
**What to do:**
* Restructure documents into smaller, tighter pieces.
* Repeat key headings or terms.
* Or curate the material as a managed topic for guaranteed usage.
### Update state
Two updates must be current:
* **Source Update** → keeps the data in each source fresh
* **Agent Update** → applies knowledge connection changes to the agent
Both can be triggered manually. Agent updates also run automatically every few minutes.
### Environments, variants, saved changes
Each source must be enabled in the correct **environment** and **variant**. Any edits must be **saved** before leaving the page.
## Conflicting information?
If the [FAQs](/knowledge/faqs/introduction) and Sources contain conflicting data, **the FAQs tab wins**. Content from the FAQs tab is always prioritized.
## Viewing Connected Knowledge in Conversation Review
When your agent retrieves content from Connected Knowledge during a conversation, you can see exactly which sources were used in [Conversation Review](/analytics/conversations/review).
1. Open a conversation in **Analytics > Conversations > Voice**.
2. In the **Diagnosis** dropdown, toggle **Sources** on.
3. Each turn where Connected Knowledge was retrieved shows a **Sources** tag beneath the agent's response, alongside any matched FAQs.
4. Click a source name to open an inline preview panel showing the exact text chunks the agent used.
5. Use **Open in Knowledge** in the panel to navigate directly to the source in the Knowledge area.
This is useful for:
* Verifying the agent retrieved the correct content for a given question
* Debugging cases where the agent's response seems inaccurate or incomplete
* Confirming that newly added or updated sources are being picked up
Combine the **Sources** and **Topic citations** diagnosis layers to see both Connected Knowledge and FAQs side by side for each turn.
## Behavior and configuration notes
* **Use PolyAI's Raven LLM** for best results – it paraphrases structured and unstructured content more naturally.
* Sources results are given ranking priority to ensure they surface alongside FAQs.
* Sources and FAQs data are merged at runtime.
* Any [system-prompt style](/flows/introduction) guidance applies to both.
## Related pages
Create curated topics alongside connected sources. FAQs always take priority.
Understand how retrieval-augmented generation works across your knowledge.
Verify which knowledge sources were retrieved on each turn.
# CSV imports
Source: https://docs.poly.ai/knowledge/variants/csv-imports
Bulk import and update variants using CSV files
CSV imports let you create or update multiple variants at once, whether you're managing multi-site deployments or making bulk changes to variant attributes.
## Export your current variants
Before importing, export your existing variants to understand the file format:
1. Navigate to **Knowledge > Variants** in Agent Studio
2. Click **Export CSV**
3. Save the file to your local machine
The exported CSV contains all your variants with their attributes and configurations.
Always use the exported file as your starting template. The exported format may use a different delimiter than a standard comma-separated file. If you edit the CSV in a spreadsheet application, re-export it in the same format to avoid import errors.
## CSV file format
The CSV file must include these columns:
| Column | Required | Description |
| ------------- | -------- | ------------------------------------------------------------------------------------------------ |
| `id` | No | Leave empty for new variants. Include existing IDs to update variants. |
| `name` | Yes | Unique name for the variant |
| `is_default` | Yes | `true` or `false`. Only one variant can be default. See [note below](#default-variant-behavior). |
| `attribute_*` | No | Custom attributes (e.g., `attribute_phone_number`, `attribute_address`) |
### Default variant behavior
The `is_default` field is included in the CSV format but does not control which variant is used as the default. The platform assigns the default variant based on creation order. To control which variant is active for each conversation, use `conv.set_variant()` in your [start function](/tools/start-tool).
### Example CSV
```csv theme={"theme":{"light":"github-light","dark":"github-dark"}}
id,name,is_default,attribute_phone_number,attribute_opening_hours
,London Bridge,false,+440000000001,9am-5pm Mon-Fri
,Manchester,false,+440000000002,9am-6pm Mon-Sat
,Birmingham,true,+440000000003,24/7
```
## Import process
* Keep column headers exactly as exported
* Leave `id` empty for new variants
* Include `id` values for variants you want to update
* Include `is_default` values as required by the format
Navigate to **Knowledge > Variants**, click **Import CSV**, and select your file.
The platform shows a diff of what will change. New variants are highlighted and updated fields are shown side-by-side.
Review the diff carefully and click **Confirm import**. Changes take effect immediately in the current environment.
If you are working in a sandbox environment, changes only apply to sandbox. Promote your sandbox to live when you are ready. See the [variants overview](/knowledge/variants/introduction) for more on testing workflows.
## Best practices
* **Test in sandbox first** - Import to your sandbox environment before production
* **Keep a backup** - Export before importing to preserve your current state
* **Validate data** - Check phone numbers, URLs, and other data before importing
* **Use consistent naming** - Follow a naming convention that scales (e.g., `City-Location`)
* **One default only** - Include exactly one variant with `is_default=true` in your CSV
## Common use cases
### Adding new locations
Export your CSV, add new rows with empty `id` fields, and import to create new variants.
### Updating phone numbers
Export, update the `attribute_phone_number` column, keep the `id` values, and import.
### Changing default variant
To control which variant is active, use `conv.set_variant()` in your [start function](/tools/start-tool). See [default variant behavior](#default-variant-behavior).
## Troubleshooting
Variant names must be unique. Check for duplicate entries in your CSV.
Refresh the page. If changes still don't appear, check the import confirmation screen for errors.
The `is_default` CSV field does not control the default variant at runtime. The platform assigns the default based on creation order. Use `conv.set_variant()` in your [start function](/tools/start-tool) to control which variant is active for each conversation.
Attribute columns must start with `attribute_`. Check your column headers.
This can occur when the CSV contains invalid data. Check for:
* Empty variant name fields (every row must have a `name` value)
* Duplicate variant names
* Malformed attribute values
If the error persists, try importing fewer rows at a time to isolate the problematic entry.
This usually means a row has an empty required field, such as `name`. Ensure every row in your CSV has a value in all required columns. Remove any blank rows at the end of the file.
## Limits
* Maximum 1000 variants per project
* Maximum 50 custom attributes per variant
* CSV file size limit: 10MB
For larger imports or complex migrations, contact your PolyAI representative for assistance.
## Automate with the Agents API
CSV is the right tool for human-curated edits. If the source of truth lives elsewhere — a CRM, a locations database, a provisioning system — syncing via the API is usually a better fit.
The [Agents API](/api-reference/agents/introduction) has CRUD for [attributes](/api-reference/agents/endpoint/variants/list-attributes) and [variants](/api-reference/agents/endpoint/variants/list-variants), which usually fits the job better than CSV round-tripping once the source of truth lives in another system.
## Related pages
Learn about multi-site configurations and variant attributes.
Access variant data programmatically in your agent logic.
CRUD for variants and attributes in the Agents API.
# Variants
Source: https://docs.poly.ai/knowledge/variants/introduction
Manage multi-site configurations for a single agent.
**Some sections on this page require Python.** Default variant handling, testing with functions, and the advanced `conv.variant` examples need Python familiarity. The UI-based setup (creating variants, adding attributes, CSV imports) does not require code.
Variants is for agents that serve multiple locations, brands, or configurations. Each variant stores attributes like phone numbers, addresses, and hours, so one agent can give the right answer for each site. Without variants, you would need a separate agent per location – multiplying maintenance and increasing the risk of inconsistencies.
Variants is found under **Knowledge > Variants** in Agent Studio.
**Use Variants when** your agent needs location- or site-specific responses (hours, addresses, phone numbers). If your agent serves a single location, you do not need variants – use [FAQs](/knowledge/faqs/introduction) and [functions](/tools/introduction) directly.
## Prerequisites
1. Ensure you have admin access to **Knowledge > Variants** in your PolyAI agent.
2. Set up [FAQs](/knowledge/faqs/introduction) aligned with your multi-site configuration goals.
3. Understand how to use [functions](/tools/introduction) in your agent.
To bulk update or create variants, use the [CSV import guide](/knowledge/variants/csv-imports).
## Key capabilities
### Multi-site configurations
Use **Variants** to manage multiple locations in the same agent. Attributes such as phone numbers and opening hours are stored per variant, so the agent gives location-specific answers.
### Knowledge integration
Attributes defined in Variants are accessible in your FAQs rules, templates, and actions using the `${variant_}` syntax. The `variant_` prefix is required – an attribute named `opening_hours` is referenced as `${variant_opening_hours}`, not `${opening_hours}`. In Python functions, the same attribute is accessed without the prefix as `conv.variant.opening_hours`.
### Flexible routing
Set up routing in the [start function](/tools/start-tool) to direct users to the appropriate variant. For voice, route based on phone numbers or SIP headers; for webchat, use URL parameters or session data. Variants can also tailor SMS messages dynamically.
### Advanced functions
Use the [`conv.variant`](/tools/classes/conv-object#set-variant) object to retrieve variant attributes during conversations or make decisions based on variant data.
### Testing and troubleshooting
You can select a specific variant when making in-app test calls. The variant selector appears in the **Call configuration** section when variants exist for your project, letting you test variant-specific behavior directly in Agent Studio without manual function workarounds.
The first variant created is used as the default for the agent. If this variant is deleted, the next variant in the list automatically becomes the new default. To control which variant is active, use `conv.set_variant()` in your [start function](/tools/start-tool).
## Real-life use case
A hotel chain with multiple branches worldwide uses Variant Management to manage its agent. Each branch (e.g., "London" and "New York") has a variant configured with attributes like phone numbers, addresses, and check-in hours. When a guest contacts the agent, the branch is identified based on the user's context–phone number for voice, URL parameters for webchat–and the response is tailored accordingly.
## Variants and attributes
Think of the Variants table like a spreadsheet:
* **Add variant** (with the **Add variant** button) adds a new **row**. Each variant represents a distinct location or site, such as "London" or "Tokyo." It is the *who* – a new instance of the configuration.
* **Add attribute** (with the **+** plus sign) adds a new **column**. Each attribute is a data field that exists across *all* variants, such as phone number, address, or operating hours. It is the *what* – a new piece of information that every variant can hold a value for.
### Setting up a new variant
To configure variants:
1. Open **Knowledge > Variants** in the sidebar.
2. Add a new variant and provide a name, such as "London" or "Tokyo."
3. Save your configuration.
### Setting up a new attribute
Define attributes for the variant, such as:
* **Phone numbers**
* **Address**
* **Operating hours**
* **Menu**
* **Accessibility**
**Attribute values are stored as strings.** Whatever you type or paste into the Variants table — including JSON, dict literals, comma-separated lists, or numbers — is persisted verbatim as a string and returned as a string from `conv.variant.`. To use structured data:
* Store it as **JSON** (use `null`, not `None`; lists, not tuples) and parse with `json.loads()` in your function, or
* **Split** structured values into multiple attributes (for example `tuesday_open`, `tuesday_close`) so each cell holds a single scalar value.
Reading an attribute that does not exist on the active variant returns `None`, not an `AttributeError`.
### Default variant handling
By default, the first variant in the list is used unless otherwise specified. If a variant needs to be changed programmatically, use
the `conv.set_variant()` method in your start function.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if not conv.variant:
conv.set_variant("default_variant_name")
```
## Using variants in SMS templates
Main article: [SMS](/voice-channel/message-templates)
To include variants dynamically in SMS messages, use the syntax `${variant_}` (the `variant_` prefix is required). For example:
* `${variant_phone_number}` dynamically includes the phone number associated with the active variant.
The same `variant_`-prefixed syntax works in FAQs rules, templates, and actions. Inside Python functions, the prefix is dropped: `conv.variant.phone_number`.
## Testing variants
### In-app calling
When making test calls from Agent Studio, you can select a specific variant from the **Call configuration** section. The variant selector only appears when variants exist for your project. Call settings are grouped in a collapsible panel for easier navigation.
### In chat
For webchat testing, you can set variants manually in the start function:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if not conv.callee_number:
conv.set_variant("London")
```
Alternatively, create functions such as `set_variant1`, `set_variant2` to switch variants during testing.
## Advanced: Accessing variants in functions
This example shows how to assign variants dynamically based on user context. The `conv.variant` object lets you retrieve and set the appropriate variant so responses match the user's location or context.
### Voice example (phone number-based)
Match the dialled number against a variant attribute to set the active variant. The attribute name (e.g., `phone_number`, `callee`) must match a column you defined in **Knowledge > Variants**.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start_function(conv: Conversation):
phone_numbers = {
variant.phone_number: variant_name
for variant_name, variant in conv.variants.items()
}
if conv.callee_number and conv.callee_number in phone_numbers:
conv.set_variant(phone_numbers[conv.callee_number])
```
If your variant table uses a different column name for the phone number (for example, `callee`), replace `variant.phone_number` with `variant.callee`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start_function(conv: Conversation):
callee_map = {
variant.callee: variant_name
for variant_name, variant in conv.variants.items()
}
if conv.callee_number and conv.callee_number in callee_map:
conv.set_variant(callee_map[conv.callee_number])
```
### Webchat example (URL parameter-based)
For webchat interactions, you can use URL parameters or session data to determine the variant:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start_function(conv: Conversation):
# Get variant from webchat metadata (e.g., URL parameter)
location = conv.metadata.get("location")
if location and location in conv.variants:
conv.set_variant(location)
```
## Flow activation per variant
Variants control which [flows](/flows/introduction) are available during a conversation. Each variant supports two fields:
* `active_flows` – a list of flow names that are enabled for this variant
* `inactive_flows` – a list of flow names that are disabled for this variant
When a variant is active, only its `active_flows` are available to the agent. Any flows listed in `inactive_flows` are skipped during processing — this lets you enable or disable specific conversation paths per location or configuration without duplicating flow logic.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Check which flows are active for the current variant
if conv.variant:
active = conv.variant.active_flows
log.info(f"Active flows: {active}")
```
## Automate with the Agents API
If your variants live in a source of truth outside Agent Studio — a locations database, a CRM, a spreadsheet — you can sync them directly rather than editing by hand.
The [Agents API](/api-reference/agents/introduction) exposes full CRUD for both [attributes](/api-reference/agents/endpoint/variants/list-attributes) (the dimensions) and [variants](/api-reference/agents/endpoint/variants/list-variants) (the per-site combinations).
Attributes and variants can't be written to `main` directly — create a branch, add them there, then merge it into `main` (which also publishes to Sandbox).
Each attribute you create returns an **attribute ID** (e.g. `ATTRIBUTE-B8060842`). A variant maps those IDs to values under `attributeValues.values` — the keys are attribute **IDs**, not names, so capture each ID as you create it.
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Create a branch to hold the changes
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/branches \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "branchName": "add-sites" }' # response includes { "branchId": "BRANCH-…" }
# Create an attribute (a variant dimension) — response: { "id": "ATTRIBUTE-…", "name": "location_id" }
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/branches/BRANCH_ID/attributes \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "location_id" }'
# Create a variant, mapping each attribute ID to its value for this site
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/branches/BRANCH_ID/variants \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "London Flagship",
"attributeValues": {
"values": {
"ATTRIBUTE-B8060842": "LON-01"
}
}
}'
# Merge to main once every site is added
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/branches/BRANCH_ID/merge \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "deploymentMessage": "Add site variants" }'
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os, requests
BASE = "https://api.us.poly.ai"
HEADERS = {"x-api-key": os.environ["POLYAI_API_KEY"]}
# Sync many sites in one pass.
# main is read-only, so build them on a branch and merge once.
branch_id = requests.post(
f"{BASE}/v1/agents/{AGENT_ID}/branches",
headers=HEADERS,
json={"branchName": "crm-sites"},
).json()["branchId"]
# Create each dimension once and keep its attribute ID — variant values are keyed by ID.
attribute_ids = {}
for dimension in ("location_id", "address", "hours"):
resp = requests.post(
f"{BASE}/v1/agents/{AGENT_ID}/branches/{branch_id}/attributes",
headers=HEADERS,
json={"name": dimension},
)
attribute_ids[dimension] = resp.json()["id"]
for site in load_locations_from_crm():
requests.post(
f"{BASE}/v1/agents/{AGENT_ID}/branches/{branch_id}/variants",
headers=HEADERS,
json={
"name": site["name"],
"attributeValues": {
"values": {
attribute_ids["location_id"]: site["id"],
attribute_ids["address"]: site["address"],
attribute_ids["hours"]: site["hours"],
}
},
},
)
requests.post(
f"{BASE}/v1/agents/{AGENT_ID}/branches/{branch_id}/merge",
headers=HEADERS,
json={"deploymentMessage": "Sync site variants from CRM"},
)
```
## Related pages
Bulk create or update variants using CSV files.
Use variant attributes in topic responses with \$ syntax.
Access variant data programmatically with conv.variant.
Route callers to the correct variant based on phone number or metadata.
CRUD for variants and attributes in the Agents API.
# Design principles
Source: https://docs.poly.ai/learn/guides/design-principles
Core priorities and practical guidelines for building agents that work well.
Read this before the hands-on lessons. It covers **why** agent design works the way it does.
## Design priorities
Every design decision should be evaluated against these priorities, **in order**:
The user must be able to complete their goal. Right integrations, edge-case fallbacks, no dead ends. If the task is impossible, nothing else matters.
Once the task is possible, remove friction. Each step should be obvious, unnecessary steps should be cut, and the path to completion should be short.
Once it works and it's easy, polish it. Natural pacing, warm tone, good turn-taking.
**The order matters.** If there's a conflict, the earlier priority wins. A polished agent that can't complete the task is a failure.
### When priorities conflict
Collecting a long reference number by voice has a high transcription error rate. Asking for DTMF (keypad) input is less conversational – but if the alternative is three failed attempts and a handoff, completing the task wins. Use DTMF.
## Why sound human
PolyAI agents sound human by design. Not to deceive – because **humans know how to talk to other humans**. Open questions, natural turn-taking, and a human-like voice remove the need for users to learn a new interaction model. Even when users know it's automated, they're more comfortable and more successful when the agent meets them on familiar territory.
## Design guidelines
Use these during design, before build, and when reviewing live conversations. Later lessons reference these by number – they're the shared vocabulary for evaluating agent quality.
### 1. Guide the user
The user should always know what to say next. Two ways to do this:
* **Implicit guidance** – sound conversational, and users respond conversationally. If the agent says "What can I help you with?", most people describe their problem naturally. No instructions needed.
* **Explicit guidance** – tell the user the expected format. "Your order number should be six digits starting with a letter" prevents three rounds of "sorry, I didn't catch that."
Where this matters most: collecting structured data (reference numbers, dates, addresses). If the agent asks "What's your booking reference?" without hinting at format, users guess – and guess wrong.
> **Good:** "Could you read me your booking reference? It's six characters – starts with two letters, then four numbers."
>
> **Bad:** "What's your booking reference?"
### 2. Listen robustly
Real users don't answer one question at a time. They say "tomorrow at 6 for three people" when you asked for the date. They answer "both" when you gave two options. They say "actually, never mind that – can you check my balance instead?" mid-flow.
A well-designed agent handles all of this:
* **Information in any order** – if the user gives name, date, and party size in one sentence, capture all three.
* **Or-questions** – "yes", "no", "both", "neither", "the first one", "whichever's cheaper."
* **Topic switches** – the user can abandon one request and start another without the agent getting confused.
* **Predictable out-of-scope requests** – if users often ask about parking during a restaurant booking, handle it gracefully even if it's not part of the flow.
> A user says "I want to book for tomorrow at 6, there'll be three of us, and my kid has a nut allergy." Your flow collects date, time, party size, and dietary needs separately. The agent should accept all four values from that single utterance and skip ahead.
### 3. Give feedback
Users need to know the agent heard them correctly and that something is happening. Two types:
* **Implicit confirmation** – weave the user's input into the next question. "To look up the booking under 07700 900123, I'll just need your surname." This confirms the phone number without asking "Did you say 07700 900123?"
* **Process feedback** – when something takes time, say so. "Let me pull that up" is better than silence. Silence on a voice call feels like a dropped connection.
Implicit confirmation is almost always better than explicit. Asking "Did you say X?" on every turn doubles the call length and makes the agent sound like a bad phone tree.
> **Implicit:** "Great, so that's a table for three tomorrow at 6. And you mentioned a nut allergy – I'll add that to the booking."
>
> **Explicit (avoid unless critical):** "You said three people. Is that correct?" / "You said tomorrow. Is that correct?" / "You said 6pm. Is that correct?"
Reserve explicit confirmation for high-stakes values – payment amounts, medical details, irreversible actions.
### 4. Support correction
Users make mistakes. They also change their minds. The agent should handle both without restarting the entire flow.
This means:
* **Correct a value** – "Actually, it's the 15th, not the 14th" should update the date without re-collecting everything else.
* **Switch workflows** – if a user starts booking a table and then says "wait, I actually want to cancel a reservation", the agent should pivot cleanly.
* **Undo an action** – if possible, let the user reverse what just happened. If not possible (e.g., an API call already fired), say so clearly.
A common anti-pattern: the agent says "I'm sorry, let's start over" and drops all collected information. This is a design failure – the agent should update only the corrected value and retain everything else.
### 5. Prevent errors
Two parts: confirm before irreversible actions, and plan for things going wrong.
**Before irreversible actions:**
* Booking submissions, payments, cancellations, account changes – always read back the details and get a "yes" before executing.
* This adds one turn to the call, but the cost of undoing an incorrect booking is far higher.
**Plan for failure:**
* APIs time out. Build a fallback ("I wasn't able to process that – let me connect you with someone who can").
* Speech recognition fails. Design retry logic that doesn't sound robotic ("Sorry, I didn't quite catch that. Could you say it one more time?").
* The user gives an answer you didn't expect. Don't dead-end. Route to a sensible default.
> **Good:** "Just to confirm: a table for three on Thursday the 15th at 6pm, with a note about nut allergies. Should I go ahead and book that?"
>
> **Bad:** The agent silently submits the booking after collecting the last field.
### 6. Act efficiently
Every unnecessary turn costs time, patience, and containment rate. Remove unnecessary steps wherever possible:
* If the user already gave information, don't ask for it again.
* If only one option makes sense, don't present it as a choice – proceed directly.
* If an explanation isn't needed for the user to make a decision, skip it.
* Shorten utterances. "What's your phone number?" not "Could you please provide me with the phone number associated with your account?"
The most common efficiency failure: the agent explains *why* before acting. "In order to look up your booking, I'll need your reference number. Could you please provide that?" Ask directly: "What's your booking reference?"
This is revisited in detail in [Level 3: Writing agent speech](/learn/guides/expert/utterance-design).
### 7. Speak clearly and naturally
Err on the side of informality. Voice agents that sound like legal documents or corporate emails create an unnatural conversational experience and increase user disengagement.
Practical rules:
* Use contractions: "I'll", "we're", "that's"
* Use short sentences. One idea per sentence.
* Avoid filler preambles: "In order to assist you with your request" → cut.
* Avoid hedging: "I believe", "It seems like", "I think" → state the fact or say you don't know.
* Match how real humans speak, not how they write.
| Avoid | Prefer |
| ------------------------------------------------------- | ----------------------------- |
| "Could you please provide me with your account number?" | "What's your account number?" |
| "I apologize for the inconvenience." | "Sorry about that." |
| "I'm going to go ahead and process that for you." | "Done." or "All set." |
This is the guideline that matters most for voice. Long, formal sentences create awkward pacing and make users more likely to interrupt or disengage.
This is revisited in detail in [Level 3: Writing agent speech](/learn/guides/expert/utterance-design).
### 8. Behave consistently
Users build expectations fast. If the agent is warm and casual in the greeting, it should stay that way throughout the call. If it uses "we" to refer to the company, it should always use "we."
Consistency applies to:
* **Voice** – same voice model, same speed, same warmth throughout the call.
* **Phrasing style** – if you use contractions, always use them. If you don't, never use them.
* **Response length** – if most answers are 1-2 sentences, a sudden 5-sentence answer feels wrong.
* **Turn-taking rhythm** – if the agent usually waits for a pause before speaking, one instance of cutting the user off is jarring.
The one exception: **deliberate mode shifts**. Reading a legal disclaimer in a different tone signals "this part is important and different." That's intentional inconsistency with a purpose.
### 9. Be flexible
Not every user follows the expected path. Design for edge cases:
* **Can't receive SMS** – offer an alternative (email, verbal read-back, transfer to a human).
* **Can't spell their name** – accept phonetic spelling, offer letter-by-letter confirmation.
* **Doesn't have the expected information** – "I don't have my booking reference" should not dead-end the conversation. Offer lookup by name, date, or phone number.
* **Accessibility** – some users need more time, repeat information, or have speech patterns that challenge ASR. The agent should be patient.
The test: can *every* user who has a legitimate reason to call actually complete their task? If any common user profile is locked out, the design fails guideline 1 (complete the task) as well.
### 10. Adapt to the user
Use what you know. If the system has context about the user – their account, their recent activity, their location – use it to skip steps and personalize the conversation.
Examples:
* Caller has a cancelled flight → "I can see your flight was cancelled. Are you calling about rebooking?"
* Caller authenticated via IVR → don't ask for their account number again.
* Caller is on a mobile number you recognize → "Is this about the account ending in 4821?"
* Return caller in the last 24 hours → "Are you calling back about the same issue?"
This saves time and signals competence. However, avoid surfacing information in a way that feels intrusive – if you reference data the user didn't expect you to have, briefly explain the source ("I can see from your account that...").
The risk of *not* adapting: the agent asks for information it already has, which wastes time and makes the user feel like they're talking to a system, not a service.
***
Ready to build? Start with [Level 1: Get started](/learn/guides/get-started).
# Get started
Source: https://docs.poly.ai/learn/guides/get-started
Build your first working agent – from blank project to testable voice assistant in about 30 minutes.
**Level 1: Build your first agent.** By the end of these 6 short lessons, you'll have a real agent that answers questions, speaks with a voice you've chosen, and is safely versioned. No code required – estimated time: 30 minutes.
## What you'll build
You'll go from an empty project to a working agent that:
* Answers FAQ-style questions in both Chat and Call
* Speaks with a voice and personality you've configured
* Lives safely in versioned environments (Sandbox → Pre-release → Live)
## The 6 lessons
Pick a name, set a language, and get your workspace ready
Define who the agent is, how it speaks, and what it should never do
Add a simple FAQ topic so your agent can answer a real question
Pick how your agent sounds and tune it for clarity
Understand how to test safely before anything goes live
Look at real conversations and confirm your agent did the right thing
## Lessons
Set up your first agent with a clear name and configuration
Define personality, rules, and what the agent can and can't do
Create a one-turn FAQ topic that works in Chat and Call
Choose how your agent sounds and adjust clarity
Test and promote changes safely: Sandbox → Pre-release → Live
Review real transcripts and check what your agent actually did
**You've completed Level 1 when:**
* Your project is created and configured
* Your agent has a personality and rules
* A simple FAQ topic works in both Chat and Call
* You've published and promoted through environments
* You can find and review a conversation
***
[Level 2](/learn/guides/advanced/add-complex-kb-topic) covers multi-turn topics, functions, response controls, audio tuning, and advanced diagnostics.
# Home
Source: https://docs.poly.ai/learn/guides/introduction
Learn to build, configure, and maintain production-ready voice agents through hands-on guides across three skill levels.
**PolyAcademy** is a step-by-step path from a blank project to a production agent, organized by skill level.
PolyAcademy is structured for builders who are new to PolyAI and for teams refining an existing agent. If you already have a live agent, go directly to [Maintain](/learn/maintain/introduction).
## Before you start
Core priorities, practical guidelines, and the reasoning behind agent design decisions.
## Choose your level
Go from a blank project to a working agent you can talk to – no code required.
**For:** First-time users · \~30 min
Add functions, multi-turn topics, response controls, and audio tuning.
**For:** Users who completed Level 1
Organize code, build flows, write natural speech, and polish voice quality.
**For:** Developers and senior designers
## How each lesson works
Every lesson follows the same pattern so you always know what to expect:
A short explanation with examples – just enough to get going
Quick questions to check you understood the key idea
A hands-on challenge you complete in Agent Studio
Mark lessons complete as you go and pick up where you left off
## What you'll build at each level
By the end, you'll have a real agent that answers questions, speaks with a configured voice, and is safely versioned across environments.
* Create and configure a project
* Build simple FAQ topics
* Test in Chat and Call
* Manage environments and versions
* Review conversations to see what happened
No coding needed. Level 1 is designed for anyone – even if you've never used Agent Studio before.
Extend your agent with multi-turn conversations, automated actions, and fine-tuned speech.
* Complex multi-turn topics with actions
* Functions and return values
* Response controls and pronunciations
* Audio management and voice tuning
* Global ASR biasing
* Variants for multi-location agents
You'll learn the techniques that make agents sound professional and scale reliably.
* Code organization with imports and shared utilities
* Flows for structured multi-turn interactions
* Flow patterns: collection, validation, handoff
* Writing natural agent speech
* Voice polish: filler, turn-taking, personalization
Each PolyAcademy lesson links to the relevant **reference page** in the Help Center. Look for the "Go deeper" links at the end of each section.
# PolyAcademy: Learn to build voice agents
Source: https://docs.poly.ai/learn/guides/polyacademy
Free, hands-on tutorials for building voice agents in Agent Studio. Go from beginner to expert at your own pace – no coding required to start.
PolyAcademy is hands-on training for Agent Studio. You'll build a real voice agent from scratch, test it, and progressively add more advanced capabilities – all through guided, structured lessons.
Whether you're new to Agent Studio or sharpening your skills, PolyAcademy gives you a clear path from beginner to expert.
**No coding required to start.** Level 1 is designed for anyone. Levels 2 and 3 introduce more advanced concepts, including Python functions and code-driven flows.
## What is PolyAcademy?
PolyAcademy is a free, self-paced learning program built into the PolyAI documentation. It's structured into three levels that progressively build on each other:
Go from a blank project to a working voice agent in about 30 minutes. You'll configure a personality, teach it to answer questions, choose a voice, and learn to test safely.
**No code required.**
Add multi-turn topics, connect functions to APIs, fine-tune how your agent speaks, and use diagnostics to debug calls.
Structure code for maintainability, build reliable multi-step flows, write natural-sounding speech, and polish the voice experience for real users.
## Who is it for?
PolyAcademy is designed for anyone who works with Agent Studio:
* **New users** looking for a guided introduction
* **Agent designers** who want to deepen their skills
* **Developers** learning to extend agents with code
* **Project managers** who want to understand how agents are built
You don't need prior experience with PolyAI or voice agents. Level 1 starts from the very beginning.
## How lessons work
Every PolyAcademy lesson follows the same structure, so you always know what to expect:
A short explanation with examples – just enough context to understand the feature
Quick questions to check you understood the key idea before moving on
A hands-on challenge you complete in Agent Studio using what you just learned
Links to the full reference documentation if you want more detail on any topic
## What you'll build
By the time you complete all three levels, you'll have built an agent that:
* Answers real questions with configured knowledge topics
* Speaks with a voice and personality you've chosen
* Handles multi-turn conversations with actions like SMS and handoffs
* Runs Python functions that connect to external systems
* Uses flows for structured, multi-step interactions
* Is safely versioned and promoted across environments
## Start learning
Build your first working agent – the best place to begin
See the full PolyAcademy curriculum and pick your starting point
Already have a live agent and want maintenance tips instead? Check out the [Maintain](/learn/maintain/introduction) section for best practices on keeping agents healthy.
# Improve your agent after a real call
Source: https://docs.poly.ai/learn/iterate-open-platform
Read the transcript, find the failure, ask Wren to fix it. The loop that gets the agent from working to actually good.
This page covers the iterate-after-a-call loop with [Wren](/wren/introduction): read the transcript, ask Wren to fix it, share the link. Wren is available to everyone with project access; applying the changes it proposes needs edit access to the areas involved.
Real callers say things your prompts did not anticipate, mispronounce reference numbers, and go off-script in ways no demo covers. The fastest way to improve an agent is to read the transcripts, identify the failure, ask Wren to fix it, and share the resulting build for review.
## Quick reference
| Issue | Where to look | What to do |
| --------------------------------------------- | ----------------------------- | --------------------------------------------------------------------------------------- |
| Agent gave a wrong answer | Conversations → transcript | Tighten the topic that matched. Add the right sample questions to the correct topic. |
| Agent missed a topic that should have matched | Conversations → Diagnosis tab | Open the topic, add 3-5 phrasings closer to how the caller actually asked. |
| Agent went off-topic | Conversations → transcript | Add a behavior rule in **Behavior > General > Behavior**, or tighten the topic content. |
| Agent collected info but didn't act on it | Conversations → Diagnosis tab | Wire up the action on the topic, or add a handoff step. |
| Welcome message is wrong | Wren | *"Rewrite the welcome message to be warmer and mention reservations specifically."* |
| Voice sounds wrong | **Voice > Settings** | Pick another voice. Test on the next call. |
## The loop
### 1. Find the call
Open **Conversations**. Every call lands here, including your test calls. Sort by date, filter by environment, or scan the PolyScore column for the lowest ratings.
Click into one. The right panel opens: transcript on the left side, debugging tabs (Transcription, Scores, Details, Custom metrics) on the right.
### 2. Read what happened
Look for:
* Where the caller pushed back, repeated themselves, or asked the same thing twice. That's usually a wrong-answer or missed-topic moment.
* Where the agent went quiet, said *"I'm sorry, I didn't understand"*, or asked for the same piece of info more than once. Likely a flow validation issue.
* Where the agent answered something it shouldn't have. Guardrail or topic-scoping problem.
* What the caller wanted that you didn't expect. Sometimes the failure is missing intent coverage, not broken logic.
The Diagnosis tab tells you why. It shows which topics matched, which flows ran, which functions were called, and the LLM input on each turn.
### 3. Ask Wren to fix it
Be specific. The plan you get back is only as good as what you said:
* *"The agent told the caller we offer same-day delivery. We don't. Update the shipping topic to say next-day at best, and add a confirmation step before quoting any delivery time."*
* *"On the booking flow, the agent kept asking for the date even after the caller said 'tomorrow'. Fix the entity extraction for relative dates."*
* *"Add a new topic for 'where is my refund' that says we process refunds in 5-10 business days back to the original payment method."*
* *"The welcome message is too formal. Make it warmer, like a friendly receptionist."*
Wren proposes a plan. Read it. Approve when it looks right, or send feedback if it doesn't.
### 4. Test the fix
Open the agent in the browser (**Test** in the top-right) and replay the scenario that broke. If it works, promote the new version from Sandbox to Live. If not, send Wren another revision; the branch is still open.
### 5. Send the link to someone new
You know what you fixed, so you're biased. Send the [share link](/widgets/share) to someone who isn't in the loop and ask them to try the thing you broke. They'll find the next thing.
## One change at a time
Resist the urge to batch fixes. One change per request is easier to plan, easier to review, easier to undo. The fair-use allowance favors small focused requests too.
## Related
Where every call goes after it happens.
Patterns for getting good fixes from short prompts.
The knowledge layer most fixes touch.
Three routes for getting it in front of testers.
# Common issues
Source: https://docs.poly.ai/learn/maintain/common-issues
Diagnose and fix the most frequent agent issues with step-by-step troubleshooting.
Most issues fall into a handful of patterns. Before you open a support ticket, run through the checks below.
**Faster route for many of these:** open the failing conversation, then ask [Wren](/wren/introduction) to fix it — *"the agent told the caller we offer same-day delivery; we don't, update the shipping topic"*. See [Iterate after a call](/learn/iterate-open-platform) for the full loop.
## Quick reference
| Symptom | Likely cause | Quick fix | Where to look |
| ---------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| **My edit isn't live** | You saved in *Sandbox* but didn't promote. | Promote the new version in **Deployments**. | [Environments & versions ↗](/environments-and-versions/introduction) |
| **The agent still uses the old answer** | Cached variant or conflicting topic. | 1) Clear your browser cache / soft-reload. 2) Check if another topic has similar sample questions and higher priority. | [FAQs ↗](./knowledge-base) |
| **Calls aren't transferring** | Wrong number format, SIP error, or firewall. | Confirm **Route** starts with `+` and country code. If SIP, double-check URI and headers. Test in *Sandbox* first. | [Routing and handoffs ↗](./routing-handoffs) |
| **Analytics show zero calls** | Dashboard filtered to the wrong environment or date. | Switch environment to **Live** and widen the date range. | [Dashboards overview ↗](/analytics/dashboards/introduction) |
| **CSV import failed** | Header names or file encoding off. | Re-export a fresh CSV, copy-paste rows, save as UTF-8, re-import. | [CSV imports ↗](/knowledge/variants/csv-imports) |
| **I can't edit anything** | Your permission level for that area is **Read** or **None**. | Ask an account Admin to set the area to **Edit** for this project on the **Users** page. | [Role permissions ↗](/user-management/access-control-scope) |
| **Audio is silent or distorted** | Voice config mismatch or bad network. | Check **Voice > Advanced settings** for the correct TTS voice. Test your network or try another device. | [Voice config ↗](/voice-channel/advanced/call-settings) |
| **Agent answers a totally different question** | Mis-matched topic or ASR error. | Review the conversation, tag **Wrong transcription** or **Missing topic**, and fix accordingly. | [Conversation review ↗](/analytics/conversations/review) |
## Knowledge base issues
**Diagnosis steps:**
1. Open the conversation in **Conversations** and enable **Topic citations** in the **Diagnosis** toggle group on the Transcription tab.
2. Check which topic was retrieved – is it the right one?
3. If the wrong topic was retrieved, the sample questions or topic names may be too similar between topics. Make them more distinct.
4. If the right topic was retrieved but the answer is wrong, update the **Content** field.
**Common causes:**
* Two topics with overlapping sample questions – the retriever picks the wrong one
* Topic name is too vague (e.g., `info` or `general`) and matches too broadly
* Content is outdated or incomplete
**Diagnosis steps:**
1. Check that the topic has been **published** and **promoted** to the environment you are testing in.
2. Verify the topic has sample questions – without them, retrieval accuracy drops significantly.
3. Test different phrasings. If only very specific wording triggers the topic, add more varied sample questions.
**Common causes:**
* Topic exists in Sandbox but hasn't been promoted to Live
* Sample questions are too few or too narrow
* Topic name is not semantically related to the user's query
When a topic instructs the agent to both say something and call a function in the same turn, the agent may do one but not the other. This is a common anti-pattern.
**Fix:** Structure actions so the agent only has one task per turn. If you need to say something and then call a function, split the instruction across turns – let the agent deliver the answer first, then handle the action in a follow-up.
## Voice and audio issues
The change may be pulling from cached audio. Go to **Voice > Audio library**, find the specific audio clip, update the speed setting, and regenerate the audio.
Use the **Pronunciation** section in **Voice > Advanced settings > Speech**. You can provide IPA notation, SSML overrides, or regex-based corrections. See [Audio Management](/learn/guides/advanced/audio-management) for details.
The disclaimer uses a separate voice configuration. Check **Voice > Settings** – the **Disclaimer** section has its own voice selection and tuning, independent of the main agent voice.
## Deployment and version issues
You need to **publish** your changes and then **promote** them through your environments (Sandbox → Pre-release → Live). Changes in Draft or Sandbox are not automatically applied to Live.
See [Environments](/environments-and-versions/introduction) for the full promotion flow.
1. Wait a few minutes – promotion can take a short time to propagate.
2. Check that you promoted the **correct version** by comparing version numbers in the Environments page.
3. If using a phone number to test, confirm the number is assigned to the correct environment.
If there are no knowledge topics published to the Live environment, the agent has nothing to say after the disclaimer and will end the call. Ensure at least one topic is published and promoted before going live.
## Function and flow issues
1. Check that the function is referenced in a topic's **Actions** field or a step's function list using `@function_name`.
2. Verify the function description is clear – the LLM uses it to decide when to call the function.
3. Check for typos in the function name reference.
4. Use `conv.log` statements to confirm whether the function is being reached at all.
Check the `goto_step` calls in your flow functions. A common bug is using multiple `if` statements instead of `if/elif/else`, which causes later transitions to overwrite earlier ones. See [Flow fundamentals](/learn/guides/expert/flow-fundamentals) for the correct pattern.
1. Add `conv.log` statements to trace execution.
2. Check the function's parameters match what the LLM is passing.
3. If calling an external API, verify secrets are configured for the correct environment.
4. Review the conversation in **Conversations** with the **Tool calls** diagnosis toggle enabled.
See [Function maintenance](/learn/maintain/tool-maintenance) for detailed debugging steps.
## Telephony and call routing issues
1. Verify the forwarding number is correct and includes the country code.
2. Check that call forwarding is enabled with your telephone provider.
3. Ensure the transfer number is not part of the same ring group as the mainline number, which can create a loop.
1. Check that the transfer number includes the country code (starts with `+`).
2. For SIP transfers, verify the URI and any custom headers.
3. Test in Sandbox first before promoting to Live.
4. If using Twilio, confirm the fallback number is configured correctly.
See [Routing and handoffs](/learn/maintain/routing-handoffs) for step-by-step guidance.
Some telephone providers mask the caller number during forwarding, replacing it with the restaurant or business number. Contact your telephone provider and ask them to pass the caller's real number through instead of overwriting it.
## Additional troubleshooting resources
For more specific issues, see:
* [Performance monitoring](/learn/maintain/performance-monitoring) – latency, ASR, and quality issues
* [Function maintenance](/learn/maintain/tool-maintenance) – debugging function errors
* [Voice and audio updates](/learn/maintain/voice-audio-updates) – audio quality and pronunciation
* [Version management](/learn/maintain/version-management) – deployment and rollback issues
* [Analyze conversations](/wren/analyze) – ask Wren to find patterns across hundreds of conversations and identify root causes
## When to escalate
Contact [PolyAI support](/troubleshoot/faq) when:
* You see repeated **4XX/5XX** errors in call logs that persist after retrying
* SIP handoffs fail consistently and you've validated the number, URI, and headers
* Dashboards or transcripts aren't loading at all
* Call volume drops more than **30%** unexpectedly
* The agent's error rate exceeds **5%** across conversations
* You need changes beyond what's available in the UI
Maintain a brief changelog of changes – it makes debugging faster when issues arise later.
# Health checks
Source: https://docs.poly.ai/learn/maintain/health-checks
Daily, weekly, and monthly monitoring routines to keep your live agent running smoothly and reliably.
Follow daily, weekly, and monthly health check routines to catch issues early. Use dashboards for high-level metrics and Wren for deep pattern analysis across hundreds of conversations.
[Wren](/wren/analyze) can accelerate every routine below. Its **deep sampling** analyzes up to 500 conversations per query – use it to surface patterns, identify failure reasons, and prioritize fixes without manually reviewing calls.
## Schedule overview
| Routine | Frequency | Time required | What to check |
| --------------- | --------------------- | ------------- | ---------------------------------------------------------------- |
| Daily check | Every day | 10-15 min | Dashboards, recent errors, handoff rate |
| Weekly review | Every week | 30-60 min | Trends, unhandled queries, sample calls, test sets |
| Monthly review | Every month | 2-4 hours | Month-over-month metrics, knowledge audit, function optimization |
| Pre-deployment | Before each promotion | 20-30 min | Test sets, manual testing, integration checks |
| Post-deployment | After each promotion | 30 min | Live calls, key metrics, function logs |
## Daily check
Spend 10-15 minutes each morning:
1. **Review the [Self-serve dashboards](/analytics/dashboards/introduction)** – check call volume, handoff rate, and latency against your baseline
2. **Scan recent errors** in **Analytics > Conversations > Voice** filtered to last 24 hours
3. **Spot-check handoff reasons** for new patterns – or ask [Wren](/wren/analyze): *"What are the top handoff reasons from the last 24 hours?"*
4. **Verify integrations** – look for API errors in function logs
### Red flags
Stop and investigate if you see:
* Handoff rate > 50% (or 20% above baseline)
* Average latency > 3 seconds
* Error rate > 5%
* Call volume drop > 30%
## Weekly review
Spend 30-60 minutes each week:
1. Compare this week's metrics to last week (call volume, containment, duration, latency)
2. Review **unhandled queries** in dashboards – prioritize knowledge gaps
3. **Ask [Wren](/wren/analyze) for a deep sampling analysis** to surface trends across hundreds of conversations at once – for example: *"What are the top 5 reasons calls are handed off this week?"* or *"What knowledge gaps are causing containment failures?"*
4. Listen to 5-10 calls flagged by Wren (mix of successful and unsuccessful)
5. Check [test set](/testing/simulation-tests) results for regressions
6. Plan improvements for the following week based on Wren's insights and test results
## Monthly review
Spend 2-4 hours at month end:
1. Compare all key metrics month-over-month
2. **Use [Wren's deep sampling](/wren/analyze)** for a full analysis – sample up to 500 conversations to break down containment by handoff reason, identify recurring failure patterns, and surface sentiment trends. Try: *"Analyze the top transfer reasons and containment blockers over the last 30 days with percentage breakdowns."*
3. Audit all [FAQs](/knowledge/faqs/introduction) for outdated content – ask Wren to identify knowledge gaps: *"What questions are we not handling well?"*
4. Review function performance – optimize or refactor slow functions
5. Maintain [test sets](/testing/simulation-tests) – add new scenarios, remove obsolete ones
6. Review [version history](/environments-and-versions/project-history) – document major changes
## Pre-deployment check
Before promoting any version to Pre-release or Live:
1. Run all test sets – investigate any failures before promoting
2. Manually test critical user journeys and edge cases
3. Test all external API integrations
4. Check voice quality and pronunciations
5. Compare to the current Live version using [diffs](/environments-and-versions/diffs)
6. Have a rollback plan ready (identify last known good version)
**Promote if:** All tests pass, no critical bugs, performance is acceptable.
**Do not promote if:** Test failures exist, performance degraded, or integrations are failing.
## Post-deployment monitor
After promoting to Pre-release or Live:
**First 30 minutes:**
* Watch [Conversation Review](/analytics/conversations/review) in real time
* Monitor latency, error rate, and handoff rate
* Verify function logs show no errors
* Be ready to rollback
**First 24 hours:**
* Check metrics every 2-4 hours against baseline
* Review handoff reasons for new patterns
**First week:**
* Analyze full week of data vs. pre-deployment baseline
* Document lessons learned
### Rollback triggers
Rollback immediately if:
* Error rate > 10%
* Handoff rate doubles
* Critical function failures
* Customer complaints spike
## Tips
* **Start small** – if you can't do everything, prioritize daily checks and pre-deployment checks (highest ROI)
* **Let Wren do the heavy lifting** – instead of manually reviewing calls, use [Wren's deep sampling](/wren/analyze) to analyze hundreds of conversations in minutes. It's especially effective for weekly and monthly reviews where you need to spot patterns across large volumes of data.
* **Automate** – use [test sets](/testing/simulation-tests) for regression testing and the [Alerts API](/api-reference/alerts/introduction) for anomaly notifications
* **Adjust frequency** – high-volume or mission-critical agents need tighter monitoring; stable agents can relax the schedule
# Already have an agent?
Source: https://docs.poly.ai/learn/maintain/introduction
Quick-reference maintenance guides for keeping your live agent running smoothly.
Use this section when you have a live agent and need to make a targeted change — updating a topic, changing a voice, fixing a function, or adjusting a behavior. Each guide is scoped to a single task.
**Drive most maintenance through [Wren](/wren/introduction).** Ask Wren in natural language to update topics, tweak flows, rewrite welcome messages, or fix a behavior that surfaced on a real call. Every change lands on a branch you review before merging — see the [prompting guide](/wren/prompting) for patterns that work well. Wren is open to everyone with project access; building and applying changes needs edit access to the areas involved — see [Role permissions](/user-management/access-control-scope).
## Quick decision guide
| I need to... | Go here | Time | Skill level |
| --------------------------------------- | ---------------------------------------------------------------- | ---------- | --------------- |
| Describe a change in natural language | [Wren](/wren/introduction) | 1-15 min | No code |
| Update FAQs or business info | [FAQs](/learn/maintain/knowledge-base) | 10-30 min | No code |
| Sync external sources (websites, files) | [Sources](/learn/maintain/knowledge-connected-knowledge) | 5-15 min | No code |
| Change voice or fix pronunciation | [Voice and audio](/learn/maintain/voice-audio-updates) | 5-20 min | No code |
| Update or debug a function | [Function maintenance](/learn/maintain/tool-maintenance) | 15-60 min | Requires Python |
| Maintain multilingual agents | [Multi-language updates](/learn/maintain/multi-language-updates) | 30-90 min | No code |
| Check performance or fix issues | [Performance monitoring](/learn/maintain/performance-monitoring) | 30-120 min | No code |
| Analyze conversations at scale | [Ask Wren](/wren/analyze) – deep sampling up to 500 calls | 5-10 min | No code |
| Publish or rollback changes | [Version management](/learn/maintain/version-management) | 5-15 min | No code |
| Daily/weekly health checks | [Health checks](/learn/maintain/health-checks) | 5-60 min | No code |
## Core maintenance tasks
Describe the change you want in plain language — Wren edits topics, flows, entities, and settings on a branch you review.
Edit existing answers or add new topics to guide your agent's responses.
Sync and manage external sources like websites, files, and integrations.
Update voice settings, fix pronunciations, and optimize audio quality.
Update code, debug errors, and manage API integrations. Requires Python familiarity.
Maintain and optimize multilingual agent configurations.
Identify issues, track metrics, and improve agent performance.
Publish, promote, and rollback changes safely.
Daily, weekly, and monthly maintenance checklists.
Ask Wren about your conversation data – deep sampling analyzes up to 500 calls per query to surface patterns and insights.
## Additional resources
Update phone numbers, SIP routing, or call transfer logic.
Listen to calls, assess quality, and review performance data.
Troubleshoot frequent problems and find quick solutions.
Set up automated alerts for latency, errors, and call volume.
## Typical workflows
### Daily (5-10 minutes)
1. Check overnight metrics
2. Review recent calls
3. Verify critical functions
4. Address urgent issues
**See:** [Health checks](/learn/maintain/health-checks)
### Weekly (30-60 minutes)
1. **Ask [Wren](/wren/analyze) to run a deep sampling analysis** to surface the week's trends and failure patterns across hundreds of conversations
2. Update FAQs content based on Wren's insights
3. Sync Sources sources
4. Fix identified issues
5. Publish and promote changes
**See:** [Analyze conversations](/wren/analyze), [FAQs](/learn/maintain/knowledge-base), [Performance monitoring](/learn/maintain/performance-monitoring)
### Monthly (2-4 hours)
1. **Ask [Wren](/wren/analyze) for a deep sampling review** – analyze containment blockers, handoff reasons, and sentiment trends across the full month
2. Optimize functions and performance
3. Update voice and audio settings
4. Address knowledge gaps identified by Wren
5. Test and validate improvements
**See:** [Analyze conversations](/wren/analyze), [Voice and audio](/learn/maintain/voice-audio-updates)
When in doubt, use your [environments](/environments-and-versions/introduction) carefully. Make any changes in the **Sandbox**, test thoroughly, and only then promote up to **Live**.
# Maintaining FAQs
Source: https://docs.poly.ai/learn/maintain/knowledge-base
Quickly edit, add, or bulk-update FAQs – the agent's FAQ and knowledge.
Edit, add, or bulk-update FAQs to keep your agent's answers current. Use quick single-topic edits for small changes, CSV imports for seasonal refreshes, or work directly in the interface for new topics.
**Prefer natural language?** Ask [Wren](/wren/introduction) — *"tighten the refund answer to mention the 30-day window"*, *"add a topic for store hours: 9-5 Mon-Fri"*, *"create topics from `https://example.com/faq`"*. Changes land on a branch you review before merging.
## Quick reference
| I need to... | Action | Time estimate |
| -------------------------- | -------------------------------------------------- | ------------- |
| Edit a single answer | Click topic → Edit answer → Save | 2 min |
| Add a new topic | + Topic → Add questions & answer → Save | 5 min |
| Update 10-20 topics | Export CSV → Edit → Import CSV | 15 min |
| Update 50+ topics | Export CSV → Edit → Import CSV | 30 min |
| Fix wrong topic triggering | Add sample questions to correct topic | 3 min |
| Merge duplicate topics | Combine content → Delete duplicate | 5 min |
| Test changes | Agent Chat in Sandbox | 5 min |
| Promote to Live | Publish → Promote to Pre-release → Promote to Live | 5 min |
Always test in *Sandbox* first and only promote once the change is validated. Details are in [Environments & versions](/environments-and-versions/introduction).
## Edit an existing topic
1. Click the topic you want to change.
2. Update text directly in the *Answer* panel.
3. Click **Save**.
4. Use **Publish** and run tests in sandbox before publishing.
Common fixes:
| Issue | What to do |
| -------------------------------- | --------------------------------------------------------------- |
| The agent gives a partial answer | Add clarifying details to the *Answer* field. |
| It triggers the wrong topic | Add a few of the user's actual phrasings as *Sample questions*. |
| It overlaps another topic | Merge the content or re-title one topic to be more specific. |
## Add a new topic
1. Click **+ Topic**.
2. Enter a clear title.
3. Add at least three *sample questions*–think of real user wording.
4. Write the *Answer* in plain language.
5. Save and preview.
## Bulk edits
Large updates are faster in a spreadsheet.
1. Click **Export CSV** to download a CSV file containing all of your FAQs.
2. Edit or add rows in your spreadsheet app.
* Keep column headers exactly as exported.
* Leave IDs intact for topics you're editing; new rows will create new topics.
3. Save as CSV.
4. Click **Import CSV** and upload the file.
5. Review the diff, then **Confirm import**.
Keep column headers exactly as exported. New rows (without an ID) create new topics; rows with an existing ID update that topic.
## Promote your changes
1. Open **Deployments**.
2. Select the new *Sandbox* version.
3. Click **Promote to pre-release**.
4. Confirm.
5. Click **Promote to Live**
6. Confirm.
## Common workflows
### Weekly content update
1. Review analytics for knowledge gaps (5 min)
2. Add or update 3-5 topics (15 min)
3. Test in Agent Chat (5 min)
4. Publish to Sandbox (2 min)
5. Promote to Pre-release for UAT (3 min)
6. Promote to Live (3 min)
**Total time:** \~30 minutes
### Seasonal content refresh
1. Export current topics to CSV (2 min)
2. Update seasonal information (hours, policies, etc.) (20 min)
3. Import updated CSV (5 min)
4. Test seasonal queries in Agent Chat (10 min)
5. Publish and promote (5 min)
**Total time:** \~45 minutes
### Fixing a knowledge gap
1. Identify the gap from call logs or analytics (5 min)
2. Create new topic or update existing (5 min)
3. Add sample questions matching user phrasing (3 min)
4. Test with actual user queries (5 min)
5. Publish and promote (5 min)
**Total time:** \~25 minutes
## Automate with the Agents API
CSV round-trips work well for occasional refreshes. When the source of truth is another system — a help center, CMS, or internal spreadsheet — syncing via the API keeps topics current without manual exports.
The [Agents API](/api-reference/agents/introduction) has full CRUD for [knowledge base topics](/api-reference/agents/endpoint/knowledge-base/list-knowledge-base-topics), so you can run the sync on a schedule rather than exporting and importing CSVs by hand.
## Related pages
* [Sources](/learn/maintain/knowledge-connected-knowledge) - Sync external sources
* [Performance monitoring](/learn/maintain/performance-monitoring) - Identify knowledge gaps
* [Version management](/learn/maintain/version-management) - Publish and promote changes
* [FAQs overview](/knowledge/faqs/introduction) - Complete feature documentation
* [Knowledge base endpoints](/api-reference/agents/endpoint/knowledge-base/list-knowledge-base-topics) - Create and sync topics from code
## When to ask for help
Contact PolyAI support if you need help with:
* Migrating content between separate projects
* Undoing a large import that has gone wrong
You can add Python functions yourself – see [Level 2: Using tools](/learn/guides/advanced/using-tools). For multi-language setup, see [Multi-language updates](/learn/maintain/multi-language-updates).
# Maintaining Sources
Source: https://docs.poly.ai/learn/maintain/knowledge-connected-knowledge
Sync and manage external knowledge sources – websites, files, and integrations – to keep content current.
The **Connected** tab lets your agent pull information from external sources like websites, files, and integrations. Keep sources fresh by syncing regularly, managing them per environment, and removing outdated or deprecated sources.
## Quick reference
| I need to... | Action | Time estimate |
| --------------------------------- | -------------------------------- | ------------- |
| Refresh a single URL source | Click sync icon next to source | 2-5 min |
| Update a file source | Upload new file version | 3 min |
| Add a new source | Add source → Configure → Enable | 5-10 min |
| Disable a source temporarily | Toggle source off in environment | 1 min |
| Remove outdated source | Delete source from list | 1 min |
| Check when source was last synced | View "Last synced" timestamp | 30 sec |
## Understanding source updates
Sources sources can become outdated when:
* **Website content changes** - Product pages, FAQs, or documentation are updated
* **Files are modified** - PDFs, CSVs, or documents are revised
* **Integration data changes** - External systems update their information
* **URLs become inaccessible** - Pages move, are deleted, or require new authentication
Unlike the FAQs tab (which you edit directly), Sources sources must be **synced** to pull in the latest content.
## How to sync sources
### Manual sync
To refresh a single source:
1. Go to **Knowledge > Sources**
2. Find the source you want to update
3. Click the **sync icon** next to the source name
4. Wait for the sync to complete (typically 1-5 minutes depending on source size)
The "Last synced" timestamp will update when complete.
### When to sync
Sync your sources when:
* You know the external content has changed
* You're preparing to publish a new version
* You're troubleshooting incorrect or outdated responses
* It's been more than a week since the last sync (for frequently changing content)
* Before promoting to Pre-release or Live environments
Set a regular schedule for syncing high-traffic sources (e.g., weekly for product catalogs, daily for pricing pages).
## Managing sources across environments
Sources sources can be enabled or disabled per environment (Sandbox, Pre-release, Live).
### Enable/disable sources by environment
1. Go to **Knowledge > Sources tab**
2. Find the source in your list
3. Use the environment toggles to control where the source is active
**Common scenarios:**
* **Testing new sources** - Enable only in Sandbox until validated
* **Seasonal content** - Disable holiday hours sources after the season ends
* **Deprecated information** - Disable in Live while keeping in Sandbox for reference
* **Gradual rollout** - Enable in Sandbox → Pre-release → Live as you gain confidence
### Source scope and variants
If you're using [Variants](/knowledge/variants/introduction) to handle multiple locations or configurations:
* Sources can be scoped to specific variants
* This lets different locations reference different knowledge sources
* Example: Each restaurant location can have its own menu PDF
To scope a source to a variant:
1. Add or edit a Sources source
2. Select which variants should have access to this source
3. Save and sync
## Troubleshooting sync failures
### Common issues and solutions
| Issue | Likely cause | Solution |
| ---------------------------------- | ---------------------------------------------- | -------------------------------------------------------- |
| "Failed to sync" error | URL is inaccessible or requires authentication | Verify URL is publicly accessible or update credentials |
| Source syncs but content seems old | Cached version being served | Wait 5 minutes and sync again; check source URL directly |
| "File too large" error | File exceeds size limits | Split into smaller files or reduce file size |
| Integration source fails | API credentials expired or changed | Update integration credentials in settings |
| Partial content synced | Source has restricted sections | Ensure entire document is publicly accessible |
### Checking sync status
After syncing, verify the update worked:
1. Check the "Last synced" timestamp
2. Test the agent in Agent Chat with a question that should use the updated content
3. Review **Conversation Review → Diagnosis** to see which sources were retrieved
4. Compare the agent's response to the actual source content
## Best practices
### File-based sources
* **Keep files under 10MB** for faster syncing
* **Use clear, structured formatting** (headings, bullet points) for better retrieval
* **Name files descriptively** for easy identification in the source list
* **Version your files** (e.g., `menu-2025-02.pdf`) to track changes over time
### URL-based sources
* **Use stable URLs** that won't change or redirect
* **Avoid dynamic content** that requires JavaScript to load
* **Test URLs in incognito mode** to ensure they're publicly accessible
* **Prefer direct content URLs** over landing pages with navigation
### Integration sources
* **Monitor API rate limits** to avoid sync failures
* **Keep credentials up to date** and rotate them securely
* **Test integrations in Sandbox** before enabling in Live
* **Document integration dependencies** for your team
### General maintenance
* **Audit sources regularly** - Remove outdated or unused sources
* **Document source purposes** - Add notes about what each source covers
* **Sync before major releases** - Ensure all sources are current before promoting to Live
* **Monitor retrieval patterns** - Use analytics to see which sources are actually being used
## Update vs. sync: what's the difference?
* **Sync** = Refresh a Sources source to pull in the latest external content
* **Update** = Publish a new version of your agent configuration (including which sources are enabled)
You need to do both:
1. **Sync** the source to get fresh content
2. **Publish** a new version to make the synced content available
3. **Promote** the version through environments to reach Live
## Monitoring source freshness
To track when sources were last updated:
1. Go to **Knowledge > Sources tab**
2. Review the "Last synced" column for each source
3. Sort by sync date to identify stale sources
Sources that haven't been synced in 30+ days may contain outdated information. Review and sync regularly.
## When to use the Connected tab vs. the FAQs tab
| Use the Connected tab when... | Use the FAQs tab when... |
| -------------------------------------- | ------------------------------------------- |
| Content lives in external systems | You control the content directly |
| Information changes frequently | Information is relatively stable |
| Multiple sources need to be aggregated | You have a single source of truth |
| Content is maintained by other teams | Your team owns the content |
| You want automatic updates from URLs | You prefer manual control over every change |
## Common workflows
### Weekly maintenance routine
1. Review analytics to identify knowledge gaps
2. Sync high-traffic sources (pricing, hours, FAQs)
3. Test agent with recent customer questions
4. Update or add sources as needed
5. Publish and promote if changes were made
### Responding to content changes
When you learn external content has changed:
1. Identify which Sources source contains that content
2. Sync the source immediately
3. Test in Sandbox to verify the update
4. If urgent, publish and promote to Live
5. If not urgent, batch with other updates
### Adding seasonal content
1. Add new source with seasonal information
2. Enable only in Sandbox initially
3. Test thoroughly with seasonal queries
4. Promote to Pre-release for UAT
5. Promote to Live when season begins
6. Disable source when season ends (don't delete - you'll need it next year)
## Related pages
* [Sources overview](/knowledge/sources/introduction) - Learn about Sources capabilities
* [Maintaining FAQs](/learn/maintain/knowledge-base) - How to update FAQs
* [Environments](/environments-and-versions/introduction) - Understanding the deployment pipeline
# Multi-language updates
Source: https://docs.poly.ai/learn/maintain/multi-language-updates
Add languages, update language-specific knowledge and voices, and manage pronunciation rules per language.
Maintain multilingual capabilities by adding languages, updating language-specific knowledge and voices, and managing pronunciation rules. Keep translations and behavior rules up-to-date as your business expands to new markets.
## Quick reference
| I need to... | Action |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Add a new language | **Behavior** → Additional languages |
| Update language-specific knowledge | Edit topics with language variants or use `` tags |
| Change voice for a language | **Voice > Settings** → select language card |
| Fix a translation issue | **Behavior > Language > [Translations](/behavior/language/translations)** → edit the card |
| Add language-specific pronunciations | ****Voice > Advanced settings > Speech** > [Pronunciation](/voice-channel/advanced/call-settings#pronunciation)** → add rules per language |
| Test language switching | Agent Chat → select language from dropdown |
## Adding or removing languages
### Adding a new language
1. Go to **Behavior** and add the language under **Additional languages**
2. Configure a voice for the new language in **Voice > Settings**
3. Add language-specific knowledge (see below)
4. Add any necessary [translation overrides](/behavior/language/translations)
5. Update [behavior rules](/behavior/general/rules) for the new language – use `` tags to scope rules to specific languages
6. Add [pronunciation rules](/voice-channel/advanced/call-settings#pronunciation) for the new language
7. Test in Agent Chat using the language dropdown, then publish
Use a multilingual voice model (such as ElevenLabs multilingual or Cartesia sonic) for proper pronunciation across languages. See [Voice](/tools/classes/voice) for available providers including ElevenLabs, Cartesia, Hume, Rime, Minimax, PlayHT, and Google TTS.
### Removing a language
Remove the language from **Behavior** under **Additional languages**, then update any language-specific knowledge, translations, pronunciation rules, and functions. The agent will fall back to the main language for callers speaking the removed language.
## Language-specific knowledge
### FAQs with language variants
1. Go to **Knowledge > FAQs**
2. Create or edit a topic
3. Add **language variants** for each supported language with translated or culturally-appropriate content
4. Translate both sample questions and content for each variant
5. Save and test
Sample questions must be in the same language as caller inputs – they are compared with user inputs during the retrieval process.
### Conditional content with `` tags
For content where you don't need full language variants, use `` tags to serve language-specific content in a single prompt:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Please hold while I check your account.
Por favor espere mientras reviso su cuenta.
```
This works in [behavior rules](/behavior/general/rules), [FAQs](/knowledge/faqs/introduction) content, [flow steps](/flows/introduction), and [function](/tools/introduction) descriptions.
### Sources per language
Add separate sources per language:
* **URL sources** – add the language-specific version of your website
* **File sources** – upload language-specific documents
* **Integration sources** – configure integrations (such as [Zendesk](/integrations/zendesk) or [Gladly](/integrations/gladly)) to return language-specific content
### Shared vs. language-specific knowledge
| Use shared knowledge when... | Use language-specific knowledge when... |
| --------------------------------------------------- | --------------------------------------- |
| Information is universal (phone numbers, addresses) | Content needs translation |
| Data is language-agnostic (product IDs, prices) | Cultural context matters |
| Maintaining multiple versions is impractical | Local regulations differ by region |
## Updating voices per language
* **Use native voices** – don't use an English voice for Spanish
* **Match regional accents** – Mexican Spanish for Mexico, Castilian for Spain
* **Test pronunciation** of language-specific characters
* Multilingual TTS models are convenient but may have slightly lower quality than language-specific models
## Language detection and switching
By default, the agent detects the caller's language automatically through ASR and responds in that language.
If detection is too aggressive or not sensitive enough, adjust in [behavior rules](/behavior/general/rules) – for example, instruct the agent to only switch after the caller has spoken consistently in a different language for multiple turns.
For explicit selection, create a Managed Topic that lets callers choose their language and use a function to set it:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def set_language(language_code):
conv.set_language(language_code)
return {"utterance": f"Switching to {language_code}."}
```
## Language-specific functions
### Accessing the current language
Access the current language in functions with `conv.language`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def dynamic_response():
current_language = conv.language
if current_language == "es":
return {"utterance": "Respuesta en español"}
else:
return {"utterance": "Response in English"}
```
### Using translation cards in functions
For hard-coded utterances that need language-specific versions, use the `conv.translations` object instead of if/else branching. Create a [translation card](/behavior/language/translations), then reference it by key:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.translations.tn_greeting
```
For translation keys with special characters:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
getattr(conv.translations, "key with special chars")
```
## What to translate
Not all project content needs translation. See the full reference table in [Multi-language setup](/behavior/language/multilingual#what-to-translate). Key rules:
* Keep **instructions** in English (e.g., "Ask for the user's phone number")
* Translate **example utterances** and scripted responses
* If content is directed at the **agent**, keep it in English. If it will be spoken aloud to the customer, translate it.
* Topic names, actions, function names, and Python code should stay in English.
## Fixing translation issues
| Issue | Solution |
| --------------------- | ------------------------------------------------------------------------------------------------------------- |
| Awkward phrasing | Add a manual override on the [Translations](/behavior/language/translations) page |
| Cultural mismatches | Use culturally-appropriate equivalents for idioms |
| Incorrect terminology | Use domain-specific terms with a glossary |
| Formatting issues | Localize date/time/number formats per language |
| Pronunciation issues | Add language-specific rules in [Advanced voice settings](/voice-channel/advanced/call-settings#pronunciation) |
For content where auto-translation isn't sufficient, use the [Translations](/behavior/language/translations) page to create manual overrides. For bulk updates to FAQs, export as CSV, translate the content, then re-import.
## Testing
1. Open Agent Chat and select a language
2. Verify the agent responds correctly
3. Switch languages mid-conversation and confirm detection works
4. Test with native speakers for naturalness and cultural appropriateness
## Maintenance checklist
* **Monthly:** Audit content parity across languages
* **Quarterly:** Review voices for naturalness
* **Ongoing:** Update all languages together – don't let translations lag behind
## Related pages
* [Multi-language setup](/behavior/language/multilingual) – configure language support and see the full "what to translate" reference
* [Translations](/behavior/language/translations) – manage manual translation overrides
* [Advanced voice settings](/voice-channel/advanced/call-settings#pronunciation) – pronunciation and speech configuration
* [Voice Library](/voice-channel/voice-library) – browse and select voices per language
* [FAQs](/knowledge/faqs/introduction) – add language variants
* [Voice](/tools/classes/voice) – programmatic voice configuration with provider classes
# Performance monitoring
Source: https://docs.poly.ai/learn/maintain/performance-monitoring
Track latency, containment, and ASR accuracy – use dashboards and Wren for deep insights.
Monitor key metrics like response latency, containment rate, and ASR accuracy. Use dashboards for overview data and Wren for deep sampling across 500+ conversations to identify root causes of performance issues.
Ask [Wren](/wren/analyze) to investigate performance issues at scale. Its **deep sampling** analyzes up to 500 conversations per query, helping you quickly identify root causes behind metrics like high handoff rates, low containment, or latency spikes – without manually reviewing individual calls.
## Quick reference
| I need to... | Where to go |
| ------------------------- | ----------------------------------------------------------------------- |
| Check overall performance | **Analytics > Dashboards** |
| Find slow responses | **Analytics > Conversations > Voice** → filter by latency |
| Debug ASR accuracy | **Analytics > Conversations > Voice** → Diagnosis → check transcription |
| Find knowledge gaps | **Analytics > Dashboards** → unhandled queries |
| Check function errors | **Analytics > Conversations > Voice** → filter by errors |
| Track version performance | Compare versions in **Deployments** |
## Key metrics
### Response latency
Time from when the user stops speaking to when the agent starts responding. Target: under 2 seconds.
**Common causes of high latency:** slow function execution, external API delays, complex knowledge retrieval, overly complex prompts.
### Containment rate
Percentage of calls handled without [human handoff](/voice-channel/handoffs). Target varies by use case (typically 60-90%). The inverse — handoff rate — is the primary metric for the [human-in-the-loop](/glossary/introduction#hitl-human-in-the-loop) portion of the agent's traffic.
**Common causes of low containment:** knowledge gaps, complex queries, caller preference for humans, technical errors.
### ASR accuracy
How accurately the agent transcribes what the caller says. Target: above 95% word accuracy.
**Common causes of low accuracy:** background noise, strong accents, uncommon words or jargon, poor phone connection.
## Monitoring tools
### Dashboards
Go to **Analytics > Dashboards** for high-level metrics: call volume, average duration, handoff rate, top intents, and performance trends. Filter by date range, environment, variant, or version.
### Conversation Review
Go to **Analytics > Conversations > Voice** to drill into individual calls. Search, filter, listen to recordings, review transcriptions, and toggle layers in the **Diagnosis** toggle group on the Transcription tab for technical details (function logs, knowledge retrieval, LLM prompts, timing breakdown).
### Wren
Ask [Wren](/wren/analyze) for deeper investigation. Its **deep sampling** capability analyzes up to 500 conversations per query, surfacing patterns across your data that would take hours to find manually. Launch it directly from dashboard charts using the **Generate insights** button, or open Wren and ask questions like *"Why are calls failing containment this week?"* or *"What do low-PolyScore calls have in common?"*
### Simulation tests
Use [test sets](/testing/simulation-tests) for automated regression testing and version comparison. Run them before promoting versions.
## Diagnosing common issues
### High latency
1. Filter **Analytics > Conversations > Voice** by high latency
2. Open **Diagnosis** → check the timing breakdown
3. Identify the bottleneck: function execution, knowledge retrieval, LLM generation, or TTS
**Fixes:** optimize slow functions, cache common audio phrases, switch to a faster TTS provider, simplify knowledge sources, add [delay controls](/tools/delay-control).
### Low ASR accuracy
1. Review transcriptions in Conversation Review
2. Compare to audio recordings
3. Look for patterns (specific words, accents, noise)
**Fixes:** add custom vocabulary, adjust ASR sensitivity, add clarification prompts for ambiguous input.
### Knowledge gaps
1. Check **Analytics > Dashboards** → unhandled queries
2. Review common questions without answers in Conversation Review
3. Use [Wren's deep sampling](/wren/analyze) to identify gaps at scale – try: *"What questions are we not handling well?"* or *"Where does the agent give incorrect or incomplete answers?"*
**Fixes:** add missing topics to [FAQs](/knowledge/faqs/introduction), add [Sources](/knowledge/sources/introduction) sources, improve topic descriptions for better retrieval.
### Function errors
1. Filter **Analytics > Conversations > Voice** by errors
2. Review **Diagnosis** → function logs
**Fixes:** fix code, update API credentials, add error handling and retries, add logging with `conv.log` for better debugging.
### High handoff rate
1. Check **Analytics > Dashboards** → handoff metrics
2. Review handoff reasons for patterns
3. Ask [Wren](/wren/analyze): *"What are the top 5 reasons conversations are transferred to a human agent?"* – deep sampling gives you a percentage breakdown across hundreds of calls
**Fixes:** add knowledge for common handoff reasons, adjust handoff rules, add self-service options before handoff.
## Optimization quick wins
| Area | Quick wins |
| --------------- | ----------------------------------------------------------------------------------------------- |
| **Latency** | Cache common audio, switch to Cartesia TTS, use Turbo interaction mode, optimize slow functions |
| **ASR** | Add custom vocabulary, use clarification prompts, enable noise cancellation |
| **Containment** | Add missing knowledge, improve handoff rules, clarify agent capabilities upfront |
| **Quality** | Fix pronunciations, improve response clarity, test with real users |
## Debugging toolkit
All debugging tools available in Agent Studio:
| Tool | Purpose | Where to find it |
| ------------------------------------------------------------------------ | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| [Diagnosis layers](/analytics/conversations/diagnosis) | Inspect tool calls, knowledge retrieval, LLM prompts, and latency per turn | **Analytics > Conversations > Voice** → select a call → Transcription tab → Diagnosis |
| [`conv.log`](/tools/classes/conv-log) | Add structured logging (info, warning, error) from Python functions | Function code → appears in Diagnosis |
| [`conv.log_api_response()`](/tools/classes/conv-object#log_api_response) | Log full HTTP responses from API integrations for debugging | Function code → appears in Diagnosis |
| [Simulation tests](/testing/simulation-tests) | Automated regression testing across versions | **Testing** |
| [Alerts API](/api-reference/alerts/introduction) | Automated alerts for latency, errors, and call volume anomalies | API configuration |
| [Wren](/wren/analyze) | AI-powered analysis across up to 500 conversations | **Wren** |
| [Dashboards](/analytics/dashboards/introduction) | High-level metrics: call volume, latency, handoff rates, containment | **Analytics > Dashboards** |
## Related pages
* [Analyze conversations](/wren/analyze) – AI-powered conversation analysis with deep sampling
* [Health checks](/learn/maintain/health-checks) – proactive monitoring routines
* [QA and analytics](/learn/maintain/qa-analytics) – daily dashboard and conversation review workflows
* [Function maintenance](/learn/maintain/tool-maintenance) – debugging and optimizing functions
* [Alerts API](/api-reference/alerts/introduction) – automated alerts for latency, errors, and call volume
# QA and analytics
Source: https://docs.poly.ai/learn/maintain/qa-analytics
Review conversations, analyze dashboards, and ask Wren for insights across hundreds of calls.
Use Dashboards for high-level metrics, Conversation Review for transcript inspection, and Wren for AI-powered analysis across hundreds of conversations without manual review.
**Skip manual call reviews — ask Wren.** Instead of listening to individual calls, use [Wren's deep sampling](/wren/analyze) to analyze up to 500 conversations per query. Ask questions like *"What are the top handoff reasons this week?"* and get structured insights in minutes. It's the fastest way to identify patterns and prioritize fixes.
## Quick reference
| I need to... | Action | Time estimate |
| --------------------------------------- | ------------------------------------------------------ | ------------- |
| Check daily performance | Analytics > Dashboards → Standard → Review key metrics | 5 min |
| Listen to recent calls | Analytics > Conversations > Voice → Filter → Listen | 10-20 min |
| Find knowledge gaps | Analytics > Dashboards → Unhandled queries | 10 min |
| Analyze trends across hundreds of calls | [Wren](/wren/analyze) → Ask a question | 5 min |
| Review a specific topic | Analytics > Conversations > Voice → Filter by topic | 5 min |
| Export call data | Analytics > Conversations > Voice → Export CSV/JSON | 5 min |
| Tag problematic calls | Open call → Add category/annotation | 2 min |
| Check containment rate | Analytics > Dashboards → Standard → Containment metric | 2 min |
| Review safety issues | Safety dashboard → Check flags | 10 min |
## Pick the right dashboard
| Use case | Go here |
| -------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Daily health check (calls, containment, AHT) | [Self-serve dashboards](/analytics/dashboards/introduction) |
| Risk or policy compliance | [Self-serve dashboards](/analytics/dashboards/introduction) |
| Anything bespoke (e.g. bookings, revenue) | [Custom dashboards](/analytics/dashboards/custom) – contact support if you need one set up |
Dashboards default to **Live** and **last 7 days**. Change the environment or date range at the top-right.
## Example workflow: did my FAQ edit improve containment?
1. Open the **Standard dashboard**.
2. Set the date range:
* **From** = the day you promoted the change
* **To** = today
3. Check **Containment rate**.
* If it went up → good sign
* If it dropped or stayed flat, drill down to conversations (next section)
## Review conversations
1. Go to **Analytics > Conversations > Voice**.
2. Use filters:
* **Date range** – same window you used in the dashboard
* **Topic** – select the FAQ you edited
3. Click a call to open the transcript.
4. Check:
* Did the agent serve the new answer?
* Did the caller stay in self-service or ask for a human?
5. If something looks off, tag it.
### Tagging and annotations
At the bottom-right of a transcript:
* **Category** – pick a label like *Needs KB fix* or *Escalated unnecessarily*
* **Annotation** – flag *Wrong transcription* or *Missing topic*
These tags surface in QA reports and help you spot patterns over time. They are also the primary [feedback loop from human reviewers back into the agent](/voice-channel/handoffs#feedback-loops-from-the-human-side) — escalations tagged *Escalated unnecessarily* or *Wrong topic* are where to look first when tightening handoff rules.
## Export transcripts (optional)
Need raw data for deeper analysis?
1. In **Conversations**, select your filters.
2. Click **Export** → *CSV* or *JSON*.
3. You'll get a download link with call metadata and the full transcripts.
See [Call data → Transcripts](/call-data/studio-transcripts) for field definitions.
## Common workflows
### Daily performance check
1. Open Standard dashboard
2. Review key metrics vs. baseline:
* Call volume
* Containment rate
* Average handle time
* Handoff rate
3. Identify any anomalies
4. Drill into Conversation Review if needed
**Time:** 5-10 minutes
### Weekly quality review
1. **Start by [asking Wren](/wren/analyze)** – run a deep sampling query to surface the week's key patterns: *"What are the top issues and failure reasons from the last 7 days?"*
2. Review Wren's findings and identify areas that need manual review
3. Listen to 5-10 sample calls flagged by Wren or filtered in Conversation Review
4. Tag issues for follow-up
5. Update knowledge or settings based on insights
**Time:** 20-40 minutes (Wren reduces manual review time)
### Investigating a spike in handoffs
1. Go to **Analytics > Dashboards** → Standard dashboard
2. Identify when handoff rate increased
3. **Ask [Wren](/wren/analyze):** *"What are the top reasons for handoffs in the last 3 days? Give me percentage breakdowns."* – deep sampling analyzes up to 500 conversations to surface root causes
4. Review specific conversations in **Analytics > Conversations > Voice** to validate Wren's findings
5. Add missing knowledge or adjust handoff rules based on the patterns identified
**Time:** 15-20 minutes (Wren replaces manual filtering and pattern identification)
## Related pages
* [Analyze conversations](/wren/analyze) - AI-powered conversation analysis with deep sampling
* [Performance monitoring](/learn/maintain/performance-monitoring) - Detailed performance analysis
* [Health checks](/learn/maintain/health-checks) - Proactive monitoring routines
* [Self-serve dashboards](/analytics/dashboards/introduction) - Key metrics overview
* [Conversation Review](/analytics/conversations/review) - Transcript inspection
* [Call data](/call-data/introduction) - Accessing raw call data
* [Alerts API](/api-reference/alerts/introduction) - Set up automated alerts for operational metrics
## When to escalate
| Situation | Who to contact |
| ----------------------------------- | -------------------------------------- |
| Metrics flat after multiple tweaks | PolyAI support – may need flow changes |
| Consistent ASR errors | PolyAI support – include call IDs |
| Dashboard missing a metric you need | PolyAI support |
| Safety dashboard shows spikes | PolyAI support – escalate immediately |
## Next steps
* Check the [self-serve dashboards](/analytics/dashboards/introduction) now to make sure your latest edit had the intended effect.
* Ask [Wren](/wren/analyze) to analyze hundreds of conversations at once – its deep sampling capability surfaces patterns, failure reasons, and sentiment trends far faster than manual review.
* Set up [health checks](/learn/maintain/health-checks) for proactive monitoring.
* Configure [automated alerts](/api-reference/alerts/introduction) for latency, errors, and call volume.
# Routing and handoffs
Source: https://docs.poly.ai/learn/maintain/routing-handoffs
Update handoff destinations, manage SIP routing, and test transfers without disrupting live traffic."
Update handoff destinations, add new transfer points, and manage SIP routing – all without taking the agent offline. Test changes safely in Sandbox before promoting to Live.
## Quick reference
| I need to... | Action | Time estimate |
| ----------------------------- | -------------------------------------------- | ------------- |
| Update a phone number | Edit destination → Change number → Save | 2 min |
| Add a new handoff destination | Add handoff → Fill details → Save | 5 min |
| Add SIP headers | Edit destination → Add SIP header → Save | 3 min |
| Test a handoff | Call Sandbox → Trigger transfer → Verify | 5 min |
| Connect Twilio number | Voice > Numbers > Twilio → Enter credentials | 10 min |
| Fix failed transfer | Check number format, firewall, SIP headers | 10 min |
This page covers the **UI-based Call Handoff** feature. Ask if your project uses the code-level `transfer_call` [function](/tools/introduction).
## Edit an existing destination
1. Go to **Voice > Handoffs**.
2. Hover over the destination you want and click **Edit**.
3. Change the **Number / SIP URI** or **Description**.
4. **Save**.
5. Make a quick test call in *Sandbox* and confirm the transfer works, then [promote your version](/environments-and-versions/introduction).
Common use cases:
| Reason | What to update |
| ----------------------- | ---------------------------------------------------------------------------- |
| Front-desk line changed | Replace the number in **Route** |
| Routing after-hours | Add a note in **Description** so team mates know when to switch destinations |
## Add a new handoff destination
1. Click **Add handoff**.
2. Fill in:
* **Name** – e.g. "VIP host desk".
* **Method** – leave **SIP REFER** unless your telephony team says otherwise.
* **Route / Number** – `+1XXXXXXXXXX` or a SIP URI.
* *(Optional)* **SIP headers** if your PBX needs extra context (see next section).
3. **Add** → test → promote.
## Add SIP headers (optional)
SIP headers let you pass metadata – account ID, language, VIP flag, etc.
1. While creating or editing a destination, click **Add SIP header**.
2. Enter a header name (custom headers start with `X-`).
3. Enter a value, or use a variable such as `$caller_id`.
4. Save.
## Using your own Twilio number
If you bring your own Twilio DID:
1. **Connect Twilio** under **Voice > Numbers > Twilio** (enter Account SID + Auth Token).
2. In Twilio, point the number's **Voice webhook** at your agent URL.
3. Back in **Voice > Handoffs**, use that number as the **Route** field.
US numbers need [A2P 10DLC registration](https://www.twilio.com/docs/messaging/compliance/a2p-10dlc) for SMS.
## Test before you ship
1. Call the *Sandbox* number.
2. Trigger the scenario that should transfer.
3. Confirm the call operates how you are expecting.
4. Promote the new version to *Live*.
If the transfer fails, double-check:
* Number format (**+** and country code)
* Firewalls or other rules on the destination side
* SIP header spelling
## Common workflows
### Updating after-hours routing
1. Identify which handoff destination handles after-hours
2. Update the phone number or SIP URI
3. Test in Sandbox during and after business hours
4. Publish and promote to Live
**Time:** 10-15 minutes
### Adding a new department handoff
1. Get the phone number or SIP URI from the department
2. Add new handoff destination with clear name
3. Update FAQs to reference the new handoff
4. Test the full flow in Sandbox
5. Publish and promote
**Time:** 20-30 minutes
## Related pages
* [Call handoff overview](/voice-channel/handoffs) - Complete handoff documentation
* [Twilio integration](/voice-channel/numbers/twilio/introduction) - Twilio setup guide
* [Function-based transfers](/tools/introduction) - Code-level transfer control
* [Version management](/learn/maintain/version-management) - Safe deployment practices
## When to escalate
* You need more conditional logic than the UI can express
* The destination uses a non-standard SIP method
* You are seeing [4XX/5XX SIP errors](https://datatracker.ietf.org/doc/html/rfc3261#section-13.2.2.3)
# Temporary closures
Source: https://docs.poly.ai/learn/maintain/temporary-closures
Close your agent early or play a closure message for same-day schedule changes.
Handle same-day schedule changes like closing early, holidays, or emergency closures. Choose the approach that matches your agent's current setup.
## Quick reference
| I need to... | Best approach | Time estimate |
| ------------------------------------------ | -------------------------------------------------------- | ------------- |
| Play a closure message (no routing change) | [Managed Topic](#option-1-managed-topic) | 5 min |
| Toggle after-hours mode on/off | [Configuration Builder](#option-2-configuration-builder) | 2 min |
| Redirect calls to a different number | [Call handoff](#option-3-update-call-handoff-routing) | 10 min |
## Option 1: Managed Topic
**Best for:** Playing a closure message without changing call routing. No developer setup required.
Go to **Knowledge > FAQs**. Create a new topic or find an existing one related to hours or closures.
* **Name:** `Temporary closure`
* **Sample questions:** "Are you open?", "What are your hours?", "Can I speak to someone?"
* **Content:** "We're currently closed and will reopen at our normal time tomorrow. Please call back then."
* **Actions:** Leave empty (info only) or add a handoff if you want to transfer the caller.
Make sure the topic is active in the **Live** environment. If you have an existing opening hours topic, consider deactivating it temporarily so the closure topic takes priority.
Use **Agent Chat** in Sandbox to confirm the agent responds with your closure message.
Publish your changes, then promote to **Pre-release** and **Live**.
Deactivate the closure topic and reactivate your normal hours topic. Publish and promote again.
This approach changes what the agent **says**, but does not change call routing. The agent will still answer calls and have a conversation. If you need to hang up or transfer after the message, use one of the other options below.
## Option 2: Configuration Builder
**Best for:** Toggling after-hours mode on and off instantly. Requires a developer to set up the schema first (one-time setup).
If your agent already has after-hours fields in the Configuration Builder, skip to [step 2](#flip-the-toggle).
### One-time setup (developer required)
A developer needs to add an after-hours toggle to the Configuration Builder schema and wire it into the agent's code. See [Configuration Builder](/real-time-config/introduction) for the full guide.
Example schema fields:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"after_hours_enabled": {
"type": "boolean",
"title": "After hours mode",
"description": "Enable to play the after-hours message and skip normal conversation"
},
"after_hours_message": {
"type": "string",
"title": "After hours message",
"description": "Message to play when after-hours mode is on"
}
}
```
Example function code:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
config = conv.real_time_config
if config.get("after_hours_enabled"):
message = config.get("after_hours_message", "We're currently closed.")
conv.call_handoff(
destination="after_hours",
utterance=message
)
return
```
### Flip the toggle
Go to **Real-time config > Data** tab.
Switch to the **Live** tab. Changes here take effect immediately — no publish required.
Toggle **After hours mode** on and enter your closure message (e.g., "We're closing early today. Please call back tomorrow.").
Click **Save**. The change is live immediately.
Toggle **After hours mode** off and save. Normal behavior resumes instantly.
Configuration Builder changes in Live affect all active calls instantly. Double-check your values before saving.
## Option 3: Update call handoff routing
**Best for:** Redirecting all calls to a different number or voicemail during the closure.
Go to **Voice > Handoffs**.
Find the handoff destination used for after-hours or the default handoff. Update the **Route** to point to the closure number or voicemail.
Call the Sandbox number and trigger a transfer to verify it routes correctly.
Publish and promote to Live.
Change the route back to the original number. Publish and promote again.
For detailed steps, see [Routing and handoffs](/learn/maintain/routing-handoffs).
## Which option should I use?
| Situation | Recommended option |
| ---------------------------------------------- | --------------------------------------------------------------------------------------------- |
| Quick same-day closure, no developer available | [Managed Topic](#option-1-managed-topic) |
| Recurring closures (holidays, early Fridays) | [Configuration Builder](#option-2-configuration-builder) |
| Need to redirect calls to another team | [Call handoff](#option-3-update-call-handoff-routing) |
| Need the agent to hang up after the message | [Configuration Builder](#option-2-configuration-builder) (with `conv.call_handoff()` in code) |
## Related pages
* [FAQs](/knowledge/faqs/introduction) - Create and manage agent knowledge
* [Configuration Builder](/real-time-config/introduction) - Real-time configuration without publishing
* [Routing and handoffs](/learn/maintain/routing-handoffs) - Update handoff destinations
* [Environments](/environments-and-versions/introduction) - Publish and promote changes
# Tool maintenance
Source: https://docs.poly.ai/learn/maintain/tool-maintenance
Update tool code (Python functions), debug errors using logs, manage credentials, and fix integration issues.
Update tool code, debug errors using `conv.log`, rotate API credentials, and fix integration issues without taking the agent offline.
## Quick reference
| I need to... | Action |
| ----------------------- | -------------------------------------------------------------- |
| Update tool code | Edit in Function Editor → Save → Test in Agent Chat |
| Debug a tool error | **Analytics > Conversations > Voice** → Diagnosis → check logs |
| Update API credentials | Workspace homepage > Secrets tab → Edit → Save |
| Add logging | Use `conv.log.info()` in code |
| Fix tool not triggering | Review KB action or rules → clarify description |
| Update API integration | **Integrations > API** → edit endpoint |
## Updating tool code
1. Go to **Tools** and select the tool
2. Edit the Python function in the Function Editor
3. **Save**, then test in Agent Chat
4. Review logs in **Conversation Review → Diagnosis**
5. Publish when satisfied
Always test tool changes in Sandbox before promoting to Live.
### Example: updating an API endpoint
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def book_reservation(date, time, party_size, special_requests=None):
conv.log.info(f"Booking for {party_size} guests on {date} at {time}")
response = requests.post(
"https://api.example.com/v2/bookings",
json={"date": date, "time": time, "guests": party_size,
"special_requests": special_requests or ""},
headers={"Authorization": f"Bearer {conv.utils.get_secret('booking_api_key')}"}
)
if response.status_code == 200:
conv.log.info("Booking successful")
return {"utterance": "Your reservation is confirmed."}
else:
conv.log.error(f"Booking failed: {response.text}")
return {"utterance": "I'm having trouble completing your reservation. Let me transfer you to someone who can help."}
```
## Debugging
### Using conv.log
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.log.info("Starting payment processing") # general flow
conv.log.warning("Customer account has low balance") # potential issues
conv.log.error(f"Payment API returned: {error}") # failures
conv.log.info("Processing order", pii=True) # sensitive data
```
Logs appear in **Conversation Review → Diagnosis**, **Agent Chat** (during testing), and the [Conversations API](/api-reference/conversations/introduction).
### Common debugging steps
1. Review **Diagnosis** logs for the failing tool
2. Reproduce the issue in Agent Chat
3. Check tool inputs – are parameters being passed correctly?
4. Validate external APIs directly (Postman, curl)
5. Review the tool description – is it clear when the tool should trigger?
### Common errors
| Error | Likely cause | Fix |
| ------------------- | -------------------------------- | --------------------------------------------------------- |
| Tool not triggering | Unclear description or KB action | Simplify description; clarify when to call it |
| Wrong parameters | LLM misunderstanding | Improve parameter names and descriptions |
| Timeout | Slow API or complex logic | Add [delay controls](/tools/delay-control); optimize code |
| Auth failures | Expired credentials | Update [secrets](/secrets/introduction) |
| Import errors | Missing library | Check [available libraries](/tools/import-library) |
## Managing secrets
When API keys or credentials change:
1. Go to the **Secrets** tab on the workspace homepage
2. Find and edit the secret
3. Save, then test all tools using it
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
api_key = conv.utils.get_secret("my_api_key")
headers = {"Authorization": f"Bearer {api_key}"}
```
* Rotate credentials every 90 days
* Use descriptive names (`stripe_live_api_key`, not `key1`)
* Test all dependent tools after rotation
## Managing API integrations
Configure per-environment endpoints in **Integrations > API**:
* **Sandbox** – test/staging endpoints
* **Pre-release** – UAT endpoints
* **Live** – production endpoints
You won't call production APIs during testing.
## Optimizing performance
If tools are slow:
1. Add [delay controls](/tools/delay-control) with filler phrases ("Let me check that for you.")
2. Cache frequently-accessed data
3. Reduce unnecessary API calls
4. Simplify logic and remove unnecessary processing
You can reference state variables in delay responses using `$`. For example: `Still checking availability at $branch_name...`
## Improving tool triggering
If the agent isn't calling your tool when expected:
1. **Simplify the description** – make it clear when to call the tool
2. **Update KB actions** – ensure topics reference the tool correctly
3. **Check for conflicts** – ensure other tools aren't being called instead
**Bad description:** "Handles reservations"
**Good description:** "Call this tool when the user wants to book a table. Required: date, time, party\_size. Only call after confirming all three with the user."
## Related pages
* [Tools overview](/tools/introduction) – tool capabilities
* [Secrets](/secrets/introduction) – credential management
* [conv.log reference](/tools/classes/conv-log) – structured logging
* [Delay controls](/tools/delay-control) – managing latency
* [Test sets](/testing/simulation-tests) – automated regression testing to verify tool changes
* [Alerts API](/api-reference/alerts/introduction) – automated alerts for tool errors, latency, and API failures
# Version management
Source: https://docs.poly.ai/learn/maintain/version-management
Publish, promote through environments, compare versions, and rollback safely when needed.
Use versions to track changes, test safely through Sandbox → Pre-release → Live, and rollback instantly if problems occur.
## Quick reference
| I need to... | Action |
| ---------------------- | -------------------------------------------------- |
| Publish a new version | Click **Publish** → add description |
| Promote to Pre-release | **Deployments** → Options → Promote to Pre-release |
| Promote to Live | **Deployments** → Options → Promote to Live |
| Rollback | Find the last good version → Promote to Live |
| Compare versions | Select two versions → **Compare** |
| View history | **Deployments** |
## Publishing
Publish a version when you've made changes in Sandbox and want to create a checkpoint, test in a clean environment, or prepare for promotion.
1. Make and test your changes in Sandbox
2. Click **Publish** in the top right
3. Add a clear version description
4. Click **Publish**
A published version includes: FAQs, Sources configs, functions, agent settings, response controls, routing rules, API integrations, and test sets.
Use descriptive version descriptions. Examples: "Added Spanish language support with new voice", "Fixed booking function timeout", "Updated holiday hours".Sources content is synced separately. Sync sources before publishing to ensure the latest content is available.
## Promoting
### To Pre-release
1. Go to **Deployments**
2. Click the **Options Menu** next to the version
3. Select **Promote to Pre-release**
Use Pre-release for UAT, validation with select users, and final checks before production.
### To Live
1. Go to the **Pre-release** tab in **Deployments**
2. Click Options → **Promote to Live**
3. Confirm by checking the box and clicking **Promote**
Promoting to Live affects real customers immediately. Always test in Pre-release first.
You can promote directly from Sandbox to Live, but this is only recommended for emergency hotfixes.
## Comparing versions
1. Go to **Deployments**
2. Select two versions and click **Compare**
3. Review the diff:
* Green – additions
* Red – deletions
* Blue – edits
You can compare FAQs, function code, agent settings, response controls, and routing rules.
## Rolling back
Rollback when a new version has critical bugs, performance degrades, or customer complaints spike. Rollback is just promoting an older version:
1. Go to **Deployments**
2. Find the last known good version
3. Promote it to Live
**Time to rollback:** 2-5 minutes.
After rolling back, investigate the issue, fix it in Sandbox, and re-promote when ready.
## Deployment workflows
### Standard deployment
1. Develop and test in Sandbox
2. Publish with descriptive notes
3. Promote to Pre-release → validate
4. Promote to Live → monitor closely
### Hotfix
1. Reproduce the issue in Sandbox
2. Fix, test, publish
3. Promote through Pre-release briefly, then to Live
4. Monitor closely
### Seasonal updates
1. Apply seasonal changes in Sandbox (hours, policies, etc.)
2. Publish, promote, and schedule for the right date
3. Revert after the season ends
## Best practices
* **Publish regularly** – don't accumulate too many changes in a single version
* **Run [test sets](/testing/simulation-tests) before promoting** – catch regressions early
* **Monitor after promotion** – watch the first 30 minutes closely, check metrics for 24 hours
* **Be ready to rollback** – know which version to revert to before promoting
* **Use consistent descriptions** – consider tags like `[HOTFIX]`, `[FEATURE]`, `[SEASONAL]`
## Automate with the Agents API
If agent promotions belong inside a wider release pipeline, the same publish / promote / rollback actions are available over HTTP.
The [Agents API](/api-reference/agents/introduction) exposes [publish](/api-reference/agents/endpoint/deployments/publish-the-current-draft-to-an-environment), [promote](/api-reference/agents/endpoint/deployments/promote-a-deployment-to-the-next-environment), and [rollback](/api-reference/agents/endpoint/deployments/rollback-to-a-previous-deployment) so the same steps can run from CI alongside backend services or infrastructure.
A typical CI job publishes to Sandbox, runs your [simulation testing](/testing/simulation-tests), promotes to Pre-release on success, and blocks promotion to Live behind a manual approval gate.
## Related pages
* [Environments](/environments-and-versions/introduction) – technical reference
* [Version diffs](/environments-and-versions/diffs) – comparing changes
* [Project history](/environments-and-versions/project-history) – viewing past versions
* [Simulation testing](/testing/simulation-tests) – automated validation
* [Deployments endpoints](/api-reference/agents/endpoint/deployments/publish-the-current-draft-to-an-environment) – publish, promote, and rollback from code
# Voice and audio updates
Source: https://docs.poly.ai/learn/maintain/voice-audio-updates
Change voice, fix pronunciations, adjust latency, and manage cached audio for optimal voice quality.
Update voice settings, fix mispronunciations, manage cached audio, and tune call behavior to maintain high-quality voice experiences for your callers.
## Quick reference
| I need to... | Action |
| ------------------------ | ------------------------------------------------------ |
| Change the agent's voice | **Voice > Settings** → Change |
| Adjust voice parameters | **Voice > Settings** → settings gear |
| Fix a mispronunciation | **Voice > Advanced settings > Speech** → Pronunciation |
| Update cached audio | **Voice > Audio library** → Edit → Sync |
| Enable/disable barge-in | **Voice > Advanced settings > Call** → toggle |
| Upload custom audio | **Voice > Audio library** → Upload |
## Changing your agent's voice
Consider updating when your brand refreshes, customers report clarity issues, you're expanding to new languages, or newer voice models become available.
1. Go to **Voice > Settings**
2. Click **Change** to open the [Voice Library](/voice-channel/voice-library)
3. Filter by **Language**, **Region**, and **Gender**
4. Preview voices with custom text
5. Click **Select** to apply
6. Test in Agent Chat before publishing
For non-English projects, use a `multilingual_v2` model to ensure proper language support.
For programmatic voice configuration, see [voice classes](/tools/classes/voice) and [Add a voice](/voice-channel/add-a-new-voice).
## Barge-in
Toggle in **Voice > Advanced settings > Call**. Lets callers interrupt the agent mid-sentence.
**Enable when:** callers frequently interrupt, or you want more natural conversations.
**Disable when:** delivering complete information (legal disclaimers), background noise causes false interruptions.
## Managing audio quality
### Cached audio
The Audio library tab lets you cache and optimize frequently-used audio for reduced latency and consistent quality.
* Open **Voice > Audio library**
* Click **Edit** to adjust stability/clarity settings or add IPA pronunciation corrections
* Click the **sync** icon to regenerate, then preview
Audio is only cached after the same TTS is generated at least twice in 24 hours. For critical phrases (greetings, transfers), generate them repeatedly or upload manually.
### Custom audio uploads
Upload pre-recorded audio (WAV or MP3) for maximum control over greetings, legal disclaimers, or brand-specific moments.
## Fixing pronunciations
When the agent mispronounces words:
1. Go to **Voice > Advanced settings > Speech** → **Pronunciation** section
2. Add a pronunciation rule
3. Enter the regex pattern for the word as it appears in text
4. Provide the IPA replacement (e.g., "PolyAI" → `/ˈpɒli eɪ aɪ/`)
5. Test in Agent Chat
You can also use SSML for advanced control:
```xml theme={"theme":{"light":"github-light","dark":"github-dark"}}
Speak this slowly
```
## Troubleshooting
| Issue | Likely cause | Fix |
| ------------------------------ | -------------------------- | ---------------------------------------------------- |
| Voice sounds robotic | Low-quality TTS | Switch to Cartesia or ElevenLabs |
| Agent speaks too fast | Rate set too high | Adjust with the settings gear in Voice Settings |
| Agent interrupts frequently | Barge-in too sensitive | Disable barge-in in Advanced settings > Call |
| Mispronunciations | TTS doesn't recognize word | Add pronunciation rule in Advanced settings > Speech |
| High latency | Slow TTS provider | Switch to Cartesia or use cached audio |
| Background noise interruptions | Barge-in too sensitive | Disable barge-in or increase speech end delay |
## Maintenance routine
* **Monthly:** Listen to recent calls and identify voice quality issues
* **As needed:** Add pronunciations for new terms
* **After voice changes:** Regenerate cached audio
## Related pages
* [Audio library](/voice-channel/audio-library) – audio caching and optimization
* [Advanced voice settings](/voice-channel/advanced/call-settings) – model, barge-in, speech recognition, pronunciation
* [Voice library](/voice-channel/voice-library) – browse and select voices
* [Voice settings](/voice-channel/agent) – voice configuration options
# Recipes
Source: https://docs.poly.ai/learn/recipes/introduction
Copy-paste patterns for common voice agent workflows — tested, annotated, and ready to adapt.
Recipes are focused, working examples for the patterns you'll build most often. Each recipe shows the complete code, explains the key decisions, and flags common mistakes.
Recipes assume you're comfortable with [functions](/learn/guides/advanced/using-tools) and basic [return values](/learn/guides/advanced/tool-return-values). Most Level 2+ learners can use them directly; Level 1 learners should finish the core lessons first.
## Available recipes
Send an SMS after collecting consent. Handles the full consent → send → confirm loop.
Deterministic retry counter that escalates to a live agent after N failures.
Collect and verify a caller's identity before proceeding to sensitive information.
Route calls to different destinations based on what the caller says they need.
## How to use recipes
Each recipe is a starting point, not a final implementation. Adapt the function names, prompts, and logic to match your agent's context. The comments in each snippet explain *why* each decision was made — read them before editing.
If you find yourself writing the same pattern more than once, it's a good candidate for a recipe. The [ADK](/extend/adk) makes it easy to pull patterns into reusable utility files.
# Compliance
Source: https://docs.poly.ai/legal/compliance
Our global standards and certifications for data security and privacy.
PolyAI meets international standards for data security and privacy. Our voice agents comply with governmental and industry frameworks.
Below is an overview of the certifications and standards we adhere to and how they support our clients' compliance programs.
## Certifications and standards
### ISO27001
We are certified for **ISO/IEC 27001**, the international standard for information security management systems (ISMS).
* Learn more about [ISO27001](https://www.iso.org/standard/54534.html).
### SOC 2 Type II
PolyAI has achieved **SOC 2 Type II** compliance, covering data security, availability, processing integrity, confidentiality, and privacy.
* Learn more about [SOC 2](https://www.aicpa-cima.com/topic/audit-assurance/audit-and-assurance-greater-than-soc-2).
### HIPAA
Where relevant, our systems are designed to meet **HIPAA (Health Insurance Portability and Accountability Act)** requirements. Protected health information (PHI) is handled securely.
* Learn more about [HIPAA](https://www.hhs.gov/hipaa/).
* Learn about [AWS S3](/call-data/s3-to-s3) for long-term storage.
### PCI-DSS
Where relevant, PolyAI is committed to complying with the **PCI-DSS (Payment Card Industry Data Security Standard)** for payment card data.
* Learn more about [PCI-DSS](https://www.pcisecuritystandards.org/).
### Cyber Essentials & Cyber Essentials Plus
We are certified under the [UK NCSC (National Cyber Security Center)](https://www.ncsc.gov.uk/) **Cyber Essentials** and **Cyber Essentials Plus** frameworks, which protect against a wide variety of cyber threats.
* Learn more about [Cyber Essentials](https://www.ncsc.gov.uk/cyberessentials/overview).
### GDPR
PolyAI complies with the **General Data Protection Regulation (GDPR)** to protect personal data and the privacy of individuals in the European Union. This includes:
* Transparent data processing practices.
* Secure handling of personal and sensitive information.
* Measures to prevent data breaches.
* Providing individuals with control over their personal data, including access and deletion requests.
* Learn more about [GDPR](https://ec.europa.eu/info/law/law-topic/data-protection_en).
# Training data
Source: https://docs.poly.ai/legal/training-data
Information on the datasets used for our proprietary LLM, PolyAI Raven
This page provides information about the training data used in [Raven](/behavior/models/model-use), in line with PolyAI's commitment to transparency, responsible AI development, and applicable regulatory expectations.
The details below describe the provenance, composition, processing, and intended use of the datasets used to develop Raven v3 and v3.5.
## System overview
| Field | Details |
| --------------- | ------- |
| **System Name** | Raven |
| **Developer** | PolyAI |
| Version | Release date |
| ------- | ----------------- |
| v3 | 16 September 2025 |
| v3.5 | 10 March 2026 |
## Dataset summary
| Category | v3 | v3.5 |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Source or Owner** | Data is sourced from PolyAI customers to the extent contractually authorized by customers and permitted by applicable law, or otherwise generated by PolyAI. | Data is sourced from PolyAI customers to the extent contractually authorized by customers and permitted by applicable law, or otherwise generated by PolyAI. |
| **Purchased or Licensed** | Licensed or otherwise owned by PolyAI. | Licensed or otherwise owned by PolyAI. |
| **Time Period of Data Collection** | November 2024 – August 2025 | November 2024 – February 2026 |
| **Date of First Use in Development** | December 2024 | January 2026 |
| **Scale of Dataset** | Hundreds of thousands of conversational turns across tens of thousands of conversations. | Hundreds of thousands of conversational turns across tens of thousands of conversations. |
| **Entirely Public Domain** | No | No |
## Intellectual property considerations
| Category | Description |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| **Copyright, Trademark, or Patent Protection** | The dataset may include information protected by copyright or trademark law belonging to PolyAI customers or PolyAI. |
| **Ownership and Rights** | All data used is licensed to or owned by PolyAI in accordance with contractual agreements and applicable law. |
## Personal and consumer data
| Category | Description |
| ------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| **Contains Personal Information** | PolyAI takes all reasonable steps to redact personal information from the dataset prior to use. |
| **Contains Aggregate Consumer Information** | No |
## Synthetic data usage
| Category | Description |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| **Use of Synthetic Data** | Yes. PolyAI augments real-world data with synthetic data where necessary to broaden coverage or improve specific system capabilities. |
## Data processing and preparation
The datasets used for Raven v3 and v3.5 have undergone multiple processing steps for quality, safety, and suitability for training customer service agents.
| Processing Step | Description |
| --------------- | --------------------------------------------------------------------------------------- |
| **Redaction** | Removal of personal information. |
| **Translation** | Support for multilingual customer service use cases. |
| **Filtering** | Selection of desired data distributions to improve specific system capabilities. |
| **Labeling** | Annotation to provide efficient learning signals during system training and evaluation. |
## Types of data used
| Category | Description |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| **Data Format** | Conversational logs. |
| **Labeling Methodology** | Conversations are labeled as positive and/or preferred customer service interactions and/or assigned graded preference scores. |
## Purpose and intended use
| Category | Description |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Purpose in Relation to the System** | The dataset supports Raven's intended purpose of powering agentic customer service conversations by providing real-world and synthetic examples of high-quality customer service interactions. |
## Ongoing governance
PolyAI regularly reviews its data practices against current legal, regulatory, and ethical standards. Dataset composition and processing methods may be updated over time to reflect improvements in safety, coverage, and system performance.
# Agent Studio MCP integrations
Source: https://docs.poly.ai/mcp/agent-studio-integrations
Connect your agent to external MCP servers so it can call their tools during conversations.
Connect your agent to external [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) servers so it can use the tools they expose during conversations — look up data, trigger actions, or interact with third-party systems without writing custom code.
This page covers connecting Agent Studio **out** to third-party MCP servers as a client. To connect a client **in** and build agents from your IDE, see [Builder MCP](/mcp/builder/introduction).This feature is available from **Integrations** in Agent Studio under the **MCP** tab.
## How it works
When you add an MCP server, Agent Studio connects to the server endpoint and discovers the tools it exposes. Each tool has a name, description, and input schema defined by the server — you don't need to configure these manually.
During a conversation, your agent can call any enabled MCP tool. The flow is:
1. **Discovery** — Agent Studio sends a discovery request to the MCP server URL and receives a list of available tools with their schemas.
2. **Configuration** — You review the discovered tools and toggle on the ones your agent should use.
3. **Execution** — When the agent decides to use a tool during a conversation, Agent Studio sends a tool call to the MCP server with the appropriate authentication and waits for a response (up to the configured timeout).
4. **Response** — The MCP server runs the tool and returns the result. The agent uses the response to continue the conversation.
The MCP server controls *what* tools are available, and you control *which* of those tools the agent can use. Toggling tools off keeps them out of the agent's context window, keeping prompts focused and efficient.
## When to use MCP
MCP integrations let your agent act as an MCP **client** that connects to external MCP servers. Instead of writing custom [functions](/tools/introduction) and [API configurations](/integrations/api/introduction), you point Agent Studio at an MCP server URL and the platform discovers the available tools automatically.
Common use cases include:
* Connecting to internal business systems that expose MCP endpoints
* Adding third-party tool capabilities to your agent
* Reusing tools across multiple agents without duplicating code
## Prerequisites
* You must have the **Admin** role in Agent Studio. If you don't have admin access, ask an existing admin to [update your role](/user-management/manage-users#edit-a-user).
* The MCP server must be accessible over HTTPS
* If the server requires authentication, you need the credentials and a [secret](/secrets/introduction) stored in the Secrets Vault with access granted to your project
## Add an MCP integration
Go to **Integrations** and select the **MCP** tab.
Click **Add MCP integration**. In the modal, configure:
| Field | Description |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------- |
| **MCP server URL** | The HTTPS endpoint of the MCP server (e.g., `https://mcp.example.com`) |
| **Timeout** | How long to wait for the server to respond, in seconds (1–30, default 10) |
| **Authentication type** | Optional. Choose **Header**, **Query parameter**, or **OAuth** depending on how the server authenticates requests |
Select an authentication type and fill in the required fields:
Send a secret value in an HTTP header.
| Field | Description |
| --------------- | ------------------------------------------------------------------------------------- |
| **Header name** | The header the server expects (e.g., `X-API-Key`) |
| **Secret name** | A [secret](/secrets/how-to-setup) from the Secrets Vault that contains the credential |
Append a secret value as a URL query parameter.
| Field | Description |
| ------------------------ | ------------------------------------------------------------------------------------- |
| **Query parameter name** | The parameter the server expects (e.g., `api_key`) |
| **Secret name** | A [secret](/secrets/how-to-setup) from the Secrets Vault that contains the credential |
Authenticate using OAuth 2.0 client credentials.
| Field | Description |
| ---------------------- | ---------------------------------------------------------------------------------------- |
| **Client ID** | The OAuth client identifier |
| **Client secret name** | A [secret](/secrets/how-to-setup) from the Secrets Vault that contains the client secret |
| **Token URL** | The OAuth token endpoint (e.g., `https://auth.example.com/oauth/token`) |
| **Audience** | Optional. The intended audience for the token |
| **Scope** | Optional. Select **Read**, **Write**, or both |
Add secrets in the **Secrets** page before configuring MCP authentication. Each secret must have access granted to your project. See [Secrets](/secrets/introduction) for setup instructions.
Click **Connect**. Agent Studio discovers the tools available on the MCP server and displays them in the configuration panel.
Toggle individual tools on or off in the configuration panel. Only enabled tools are available to your agent during conversations.
## Use MCP tools in prompts
Once an MCP server is connected, discovered tools are available to the LLM in step prompts. There are two approaches.
### Basic usage
Name the tool and give a brief description of what it does. The LLM reads the tool's input schema directly from the MCP server, so you don't need to specify parameters.
```
### Available tools
1. join_session
- Initiates a screen sharing session using a six-digit code provided by the user.
```
This is the simplest approach — minimal prompt maintenance, and the LLM figures out the inputs from the schema.
### Advanced usage
For complex tools where the LLM may struggle with correct inputs, you can specify the expected parameters explicitly in the prompt:
```
CALL: highlight_element_and_wait({
cbid: "uXX-123",
session_id: "...",
user_message: "Click the Billing tab"
})
```
You can also wrap MCP tool calls with [platform functions](/tools/introduction) for additional logic like metric logging or state updates. For example, call a function after an MCP tool succeeds to log the result or trigger a step transition.
Explicit input specs add maintenance overhead — schema changes on the MCP server force prompt updates. Use only when the basic approach is unreliable.
### Whitelisting tools by URL
You can restrict which tools are exposed by the MCP server using the `tools` query parameter in the server URL:
```
https://mcp.example.com?tools=join_session,get_html
```
This limits tool discovery to only the listed tools, which is useful when the server exposes more tools than your agent needs.
## Manage MCP integrations
After connecting, you can:
* **Edit settings** — Click an MCP integration card to open the configuration panel and update the server URL, timeout, or authentication
* **Refresh tools** — Click **Refresh** in the configuration panel to re-discover available tools from the server
* **Toggle tools** — Enable or disable individual tools without disconnecting the server
* **Disconnect** — Remove an MCP integration entirely. Your agent will no longer have access to its tools
## Troubleshooting
Verify that the MCP server URL is correct and accessible over HTTPS. Check that any required authentication credentials are configured and that the associated secret has access to your project.
The MCP server may not expose any tools, or the discovery request may have timed out. Try increasing the timeout value and clicking **Refresh**.
Confirm that the tools are toggled on in the configuration panel. Only enabled tools are accessible during conversations.
Projects that previously configured MCP through experimental config will continue to work – existing servers keep running in the background. The old config is only replaced when you add a new MCP server through the Integrations UI. Once migrated, manage all MCP servers from the UI.
# Authentication
Source: https://docs.poly.ai/mcp/authentication
Create a PolyAI account API key and configure your MCP client to authenticate against Builder MCP or Data MCP.
Both of PolyAI's MCP servers — [Builder MCP](/mcp/builder/introduction) and [Data MCP](/mcp/data/introduction) — authenticate the same way: an **account API key** sent in the `X-API-KEY` header. One key works for either server.
## Get an account API key
Go to the **API Keys** tab on your workspace homepage in Agent Studio. See [API keys](/secrets/api-keys) for details.
Select **API key**, name it, choose the agents it can access, and set its permissions. Scope it to the least it needs — see [Security & safe use](/mcp/builder/security).
The full key is shown only once. Copy it immediately and store it in your client's secret settings — never paste it into a chat prompt.
## Configure your client
Pass the key in the `X-API-KEY` header. Both servers use the streamable HTTP transport, so clients also send an `Accept` header:
| Header | Value |
| ----------- | ------------------------------------- |
| `X-API-KEY` | Your PolyAI account API key |
| `Accept` | `application/json, text/event-stream` |
Most clients set `Accept` automatically. See [Quickstart](/mcp/quickstart#2-add-the-server-to-your-client) for per-client configuration.
## Pick your region
Both servers run in every region, on the same host family as the [platform APIs](/api-reference/introduction#pick-your-region). Your key is region-specific — a US key doesn't work against the UK endpoint — so connect to the matching endpoint. Self-serve (Studio) accounts are supported on both servers.
| Region | Builder MCP | Data MCP |
| ------ | ---------------------------------------- | ------------------------------------- |
| US | `https://api.us.poly.ai/builder-mcp` | `https://api.us.poly.ai/data-mcp` |
| UK | `https://api.uk.poly.ai/builder-mcp` | `https://api.uk.poly.ai/data-mcp` |
| EU | `https://api.eu.poly.ai/builder-mcp` | `https://api.eu.poly.ai/data-mcp` |
| Studio | `https://api.studio.poly.ai/builder-mcp` | `https://api.studio.poly.ai/data-mcp` |
## OAuth (coming soon)
Browser-based OAuth is on the roadmap — you'll be able to authorize a client without manually creating and pasting a key. **At launch, use API-key authentication as described above.** This page will cover OAuth once it ships.
# Capabilities
Source: https://docs.poly.ai/mcp/builder/capabilities
The tools Builder MCP exposes, grouped by what they do — each with its parameters and a link to the matching API reference.
Builder MCP exposes PolyAI's platform as MCP tools, grouped into the families below. Your client discovers them automatically on connection — you don't configure tools by hand. Each tool carries its own input schema, which the client reads directly from the server; the parameter tables below reproduce that schema so you can see what each tool takes without connecting first.
US, UK, and EU workspaces expose **106 tools**; self-serve Studio workspaces expose **100** (the Alerts family is enterprise-only). Tool names appear exactly as your client sees them in `tools/list`.
Every tool maps to a PolyAI REST endpoint. Parameters are named `path_*`, `query_*`, or nested under a request `body` in the raw schema — the tables below strip those prefixes and expand the body's top-level fields. For deeply nested request bodies, follow the **API reference** link on each tool for the complete schema. To see exactly what your client discovered, ask it to list its tools or query the endpoint's `tools/list` method (see [Quickstart → Verify](/mcp/quickstart#3-verify)).
## Build & manage agents
### Agents
#### `create-agent`
Create agent.
API reference: [Create agent](/api-reference/agents/endpoint/agents/create-agent).
| Parameter | Type | Description |
| -------------------- | ------ | ------------------------------------------------------------------------ |
| `accountId` | string | The account (workspace). **Required.** |
| `name` | string | 1–100 characters. **Required.** |
| `experimentalConfig` | object | Nested object — see the API reference for the full schema. |
| `responseSettings` | object | Nested object — see the API reference for the full schema. **Required.** |
| `llmSettings` | object | Nested object — see the API reference for the full schema. |
| `voiceSettings` | object | Nested object — see the API reference for the full schema. |
| `agentId` | string | — |
#### `list-agents`
List agents.
API reference: [List agents](/api-reference/agents/endpoint/agents/list-agents).
| Parameter | Type | Description |
| ----------- | ------ | -------------------------------------- |
| `accountId` | string | The account (workspace). **Required.** |
#### `duplicate-agent`
Duplicate agent.
API reference: [Duplicate agent](/api-reference/agents/endpoint/agents/duplicate-agent).
| Parameter | Type | Description |
| -------------- | ------ | ------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `newAgentName` | string | 1–100 characters. **Required.** |
| `newAgentId` | string | — |
#### `delete-agent`
Delete agent.
API reference: [Delete agent](/api-reference/agents/endpoint/agents/delete-agent).
| Parameter | Type | Description |
| --------- | ------ | ------------------------ |
| `agentId` | string | The agent. **Required.** |
### Behavior
#### `get-agent-behavior-rules`
Get agent behavior rules.
API reference: [Get agent behavior rules](/api-reference/agents/endpoint/behavior/get-agent-behavior-rules).
| Parameter | Type | Description |
| ---------- | ------ | ------------------------- |
| `branchId` | string | The branch. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `update-agent-behavior-rules`
Update agent behavior rules.
API reference: [Update agent behavior rules](/api-reference/agents/endpoint/behavior/update-agent-behavior-rules).
| Parameter | Type | Description |
| ---------- | ------ | ------------------------- |
| `agentId` | string | The agent. **Required.** |
| `branchId` | string | The branch. **Required.** |
| `behavior` | string | — **Required.** |
### Branches
#### `create-branch`
Create branch.
API reference: [Create branch](/api-reference/agents/endpoint/branches/create-branch).
| Parameter | Type | Description |
| ------------ | ------ | ------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `branchName` | string | 1–100 characters. **Required.** |
#### `list-branches`
List branches.
API reference: [List branches](/api-reference/agents/endpoint/branches/list-branches).
| Parameter | Type | Description |
| --------- | ------ | ------------------------ |
| `agentId` | string | The agent. **Required.** |
#### `delete-branch`
Delete branch.
API reference: [Delete branch](/api-reference/agents/endpoint/branches/delete-branch).
| Parameter | Type | Description |
| ---------- | ------ | ------------------------- |
| `branchId` | string | The branch. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `merge-branch`
Merge branch.
API reference: [Merge branch](/api-reference/agents/endpoint/branches/merge-branch).
| Parameter | Type | Description |
| --------------------- | ------ | ------------------------- |
| `agentId` | string | The agent. **Required.** |
| `branchId` | string | The branch. **Required.** |
| `conflictResolutions` | array | Array. |
| `deploymentMessage` | string | Default \`\`. |
#### `sync-branch-with-parent`
Sync branch with parent.
API reference: [Sync branch with parent](/api-reference/agents/introduction).
| Parameter | Type | Description |
| --------------------- | ------ | ------------------------- |
| `agentId` | string | The agent. **Required.** |
| `branchId` | string | The branch. **Required.** |
| `conflictResolutions` | array | Array. |
#### `deploy-a-branch`
Deploy a branch.
API reference: [Deploy a branch](/api-reference/agents/introduction).
| Parameter | Type | Description |
| ---------- | ------ | ------------------------- |
| `branchId` | string | The branch. **Required.** |
| `agentId` | string | The agent. **Required.** |
### Deployments
#### `publish-the-current-draft-to-an-environment`
Publish the current draft to an environment.
API reference: [Publish the current draft to an environment](/api-reference/agents/endpoint/deployments/publish-the-current-draft-to-an-environment).
| Parameter | Type | Description |
| ------------------- | ------ | ------------------------------------------------ |
| `agentId` | string | The agent. **Required.** |
| `environment` | string | Environment to publish to (defaults to sandbox). |
| `deploymentMessage` | string | Message describing this publish. Default \`\`. |
#### `promote-a-deployment-to-the-next-environment`
Promote a deployment to the next environment.
API reference: [Promote a deployment to the next environment](/api-reference/agents/endpoint/deployments/promote-a-deployment-to-the-next-environment).
| Parameter | Type | Description |
| ------------------- | ------ | ----------------------------- |
| `agentId` | string | The agent. **Required.** |
| `deploymentId` | string | The deployment. **Required.** |
| `deploymentMessage` | string | Default \`\`. |
| `targetEnvironment` | string | One of `pre-release`, `live`. |
#### `rollback-to-a-previous-deployment`
Rollback to a previous deployment.
API reference: [Rollback to a previous deployment](/api-reference/agents/endpoint/deployments/rollback-to-a-previous-deployment).
| Parameter | Type | Description |
| ------------------- | ------ | ----------------------------- |
| `agentId` | string | The agent. **Required.** |
| `deploymentId` | string | The deployment. **Required.** |
| `deploymentMessage` | string | Default \`\`. |
#### `list-deployments-for-an-environment`
List deployments for an environment.
API reference: [List deployments for an environment](/api-reference/agents/endpoint/deployments/list-deployments-for-an-environment).
| Parameter | Type | Description |
| ------------- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
| `agentId` | string | The agent. **Required.** |
| `environment` | string | Environment to list deployments for (sandbox, pre-release, live). One of `sandbox`, `pre-release`, `live`. **Required.** |
| `limit` | integer | Max number of deployments to return. |
#### `get-active-deployment-per-environment`
Get active deployment per environment.
API reference: [Get active deployment per environment](/api-reference/agents/endpoint/deployments/get-active-deployment-per-environment).
| Parameter | Type | Description |
| --------- | ------ | ------------------------ |
| `agentId` | string | The agent. **Required.** |
### Knowledge base
#### `create-knowledge-base-topic`
Create knowledge base topic.
API reference: [Create knowledge base topic](/api-reference/agents/endpoint/knowledge-base/create-knowledge-base-topic).
| Parameter | Type | Description |
| ---------------- | ------- | ---------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `branchId` | string | The branch. **Required.** |
| `name` | string | — **Required.** |
| `content` | string | — **Required.** |
| `actions` | string | — |
| `isActive` | boolean | — |
| `exampleQueries` | object | Nested object — see the API reference for the full schema. |
#### `get-knowledge-base-topic`
Get knowledge base topic.
API reference: [Get knowledge base topic](/api-reference/agents/endpoint/knowledge-base/get-knowledge-base-topic).
| Parameter | Type | Description |
| ---------- | ------ | --------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `topicId` | string | The knowledge base topic. **Required.** |
| `branchId` | string | The branch. **Required.** |
#### `list-knowledge-base-topics`
List knowledge base topics.
API reference: [List knowledge base topics](/api-reference/agents/endpoint/knowledge-base/list-knowledge-base-topics).
| Parameter | Type | Description |
| ---------- | ------ | ------------------------- |
| `branchId` | string | The branch. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `update-knowledge-base-topic`
Update knowledge base topic.
API reference: [Update knowledge base topic](/api-reference/agents/endpoint/knowledge-base/update-knowledge-base-topic).
| Parameter | Type | Description |
| ---------------- | ------- | ---------------------------------------------------------- |
| `branchId` | string | The branch. **Required.** |
| `topicId` | string | The knowledge base topic. **Required.** |
| `agentId` | string | The agent. **Required.** |
| `content` | string | — |
| `name` | string | — |
| `actions` | string | — |
| `isActive` | boolean | — |
| `exampleQueries` | object | Nested object — see the API reference for the full schema. |
#### `delete-knowledge-base-topic`
Delete knowledge base topic.
API reference: [Delete knowledge base topic](/api-reference/agents/endpoint/knowledge-base/delete-knowledge-base-topic).
| Parameter | Type | Description |
| ---------- | ------ | --------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `topicId` | string | The knowledge base topic. **Required.** |
| `branchId` | string | The branch. **Required.** |
### Variants
#### `create-variant`
Create variant.
API reference: [Create variant](/api-reference/agents/endpoint/variants/create-variant).
| Parameter | Type | Description |
| ----------------- | ------- | ------------------------- |
| `agentId` | string | The agent. **Required.** |
| `branchId` | string | The branch. **Required.** |
| `name` | string | — **Required.** |
| `attributeValues` | object | Default `{'values': {}}`. |
| `isDefault` | boolean | — |
#### `list-variants`
List variants.
API reference: [List variants](/api-reference/agents/endpoint/variants/list-variants).
| Parameter | Type | Description |
| ---------- | ------ | ------------------------- |
| `branchId` | string | The branch. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `update-variant`
Update variant.
API reference: [Update variant](/api-reference/agents/endpoint/variants/update-variant).
| Parameter | Type | Description |
| ----------------- | ------- | ---------------------------------------------------------- |
| `branchId` | string | The branch. **Required.** |
| `variantId` | string | The variant. **Required.** |
| `agentId` | string | The agent. **Required.** |
| `name` | string | — |
| `attributeValues` | object | Nested object — see the API reference for the full schema. |
| `isDefault` | boolean | — |
#### `delete-variant`
Delete variant.
API reference: [Delete variant](/api-reference/agents/endpoint/variants/delete-variant).
| Parameter | Type | Description |
| ----------- | ------ | -------------------------- |
| `agentId` | string | The agent. **Required.** |
| `variantId` | string | The variant. **Required.** |
| `branchId` | string | The branch. **Required.** |
### Attributes
#### `create-attribute`
Create attribute.
API reference: [Create attribute](/api-reference/agents/endpoint/variants/create-attribute).
| Parameter | Type | Description |
| ---------- | ------ | ------------------------- |
| `agentId` | string | The agent. **Required.** |
| `branchId` | string | The branch. **Required.** |
| `name` | string | — **Required.** |
#### `list-attributes`
List attributes.
API reference: [List attributes](/api-reference/agents/endpoint/variants/list-attributes).
| Parameter | Type | Description |
| ---------- | ------ | ------------------------- |
| `branchId` | string | The branch. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `update-attribute`
Update attribute.
API reference: [Update attribute](/api-reference/agents/endpoint/variants/update-attribute).
| Parameter | Type | Description |
| ------------- | ------ | ---------------------------- |
| `branchId` | string | The branch. **Required.** |
| `attributeId` | string | The attribute. **Required.** |
| `agentId` | string | The agent. **Required.** |
| `name` | string | — **Required.** |
#### `delete-attribute`
Delete attribute.
API reference: [Delete attribute](/api-reference/agents/endpoint/variants/delete-attribute).
| Parameter | Type | Description |
| ------------- | ------ | ---------------------------- |
| `agentId` | string | The agent. **Required.** |
| `attributeId` | string | The attribute. **Required.** |
| `branchId` | string | The branch. **Required.** |
### Connectors
#### `get-a-connector-by-id`
Get a connector by ID.
API reference: [Get a connector by ID](/api-reference/agents/endpoint/connectors/get-a-connector-by-id).
| Parameter | Type | Description |
| ------------- | ------ | ---------------------------- |
| `connectorId` | string | The connector. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `batch-get-connectors-by-id`
Batch get connectors by ID.
API reference: [Batch get connectors by ID](/api-reference/agents/endpoint/connectors/batch-get-connectors-by-id).
| Parameter | Type | Description |
| -------------- | ------ | ------------------------------------------------ |
| `agentId` | string | The agent. **Required.** |
| `connectorIds` | array | List of connector IDs to retrieve. **Required.** |
#### `list-all-connectors-for-a-project`
List all connectors for a project.
API reference: [List all connectors for a project](/api-reference/agents/endpoint/connectors/list-all-connectors-for-a-project).
| Parameter | Type | Description |
| --------- | ------ | ------------------------ |
| `agentId` | string | The agent. **Required.** |
#### `update-a-connector`
Update a connector.
API reference: [Update a connector](/api-reference/agents/endpoint/connectors/update-a-connector).
| Parameter | Type | Description |
| ------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `connectorId` | string | The connector. **Required.** |
| `voiceName` | string | Google TTS voice name (e.g. en-US-Neural2-A). tts\_lang\_code is auto-derived from the voice name prefix. Validated via TTS probe. |
| `variantId` | string | Variant ID to assign, or null to unassign. |
| `projectId` | string | Move connector to a different project. API key must have access to both projects. |
| `asrLangCode` | string | ASR language code (e.g. en-US). |
#### `batch-update-connectors`
Batch update connectors.
API reference: [Batch update connectors](/api-reference/agents/endpoint/connectors/batch-update-connectors).
| Parameter | Type | Description |
| --------- | ------ | ---------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `updates` | array | List of connector updates. **Required.** |
#### `delete-a-connector-and-its-phone-numbers`
Delete a connector and its phone numbers.
API reference: [Delete a connector and its phone numbers](/api-reference/agents/endpoint/connectors/delete-a-connector-and-its-phone-numbers).
| Parameter | Type | Description |
| ------------- | ------ | ---------------------------- |
| `connectorId` | string | The connector. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `look-up-connector-by-phone-number`
Look up connector by phone number.
API reference: [Look up connector by phone number](/api-reference/agents/endpoint/connectors/look-up-connector-by-phone-number).
| Parameter | Type | Description |
| ------------- | ------ | ----------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `phoneNumber` | string | Phone number to look up (E.164 format). **Required.** |
### Phone numbers
#### `get-a-specific-phone-number`
Get a specific phone number.
API reference: [Get a specific phone number](/api-reference/agents/endpoint/phone-numbers/get-a-specific-phone-number).
| Parameter | Type | Description |
| ------------- | ------ | --------------------------------------- |
| `phoneNumber` | string | The phone number (E.164). **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `batch-get-phone-numbers`
Batch get phone numbers.
API reference: [Batch get phone numbers](/api-reference/agents/endpoint/phone-numbers/batch-get-phone-numbers).
| Parameter | Type | Description |
| -------------- | ------ | ------------------------------------------------ |
| `agentId` | string | The agent. **Required.** |
| `phoneNumbers` | array | List of phone numbers to retrieve. **Required.** |
#### `list-all-phone-numbers-for-a-project`
List all phone numbers for a project.
API reference: [List all phone numbers for a project](/api-reference/agents/endpoint/phone-numbers/list-all-phone-numbers-for-a-project).
| Parameter | Type | Description |
| ------------- | ------ | ------------------------ |
| `agentId` | string | The agent. **Required.** |
| `connectorId` | string | Filter by connector ID. |
#### `import-a-single-phone-number`
Import a single phone number.
API reference: [Import a single phone number](/api-reference/agents/endpoint/phone-numbers/import-a-single-phone-number).
| Parameter | Type | Description |
| ------------- | ------ | ---------------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `phoneNumber` | string | The phone number (E.164). **Required.** |
| `clientEnv` | string | Client environment (sandbox, pre-release, live). Default `live`. |
#### `import-phone-numbers-into-a-project`
Import phone numbers into a project.
API reference: [Import phone numbers into a project](/api-reference/agents/endpoint/phone-numbers/import-phone-numbers-into-a-project).
| Parameter | Type | Description |
| -------------- | ------ | ---------------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `phoneNumbers` | array | List of phone numbers to import. **Required.** |
| `clientEnv` | string | Client environment (sandbox, pre-release, live). Default `live`. |
#### `delete-a-single-phone-number`
Delete a single phone number.
API reference: [Delete a single phone number](/api-reference/agents/endpoint/phone-numbers/delete-a-single-phone-number).
| Parameter | Type | Description |
| ------------- | ------ | --------------------------------------- |
| `phoneNumber` | string | The phone number (E.164). **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `delete-phone-numbers-from-a-project`
Delete phone numbers from a project.
API reference: [Delete phone numbers from a project](/api-reference/agents/endpoint/phone-numbers/delete-phone-numbers-from-a-project).
| Parameter | Type | Description |
| -------------- | ------ | ---------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `phoneNumbers` | array | List of phone numbers to delete. **Required.** |
#### `reassign-a-phone-number-to-a-different-connector`
Reassign a phone number to a different connector.
API reference: [Reassign a phone number to a different connector](/api-reference/agents/endpoint/phone-numbers/reassign-a-phone-number-to-a-different-connector).
| Parameter | Type | Description |
| ------------- | ------ | ------------------------------------------------------------------ |
| `agentId` | string | The agent. **Required.** |
| `phoneNumber` | string | The phone number (E.164). **Required.** |
| `connectorId` | string | Target connector ID to reassign the phone number to. **Required.** |
### Config pages (real-time config)
#### `get-a-config-page-by-environment`
Get a config page by environment.
API reference: [Get a config page by environment](/api-reference/agents/endpoint/real-time-configs/get-a-config-page-by-environment).
| Parameter | Type | Description |
| ----------- | ------ | ------------------------------------- |
| `clientEnv` | string | The client environment. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `list-all-config-pages`
List all config pages.
API reference: [List all config pages](/api-reference/agents/endpoint/real-time-configs/list-all-config-pages).
| Parameter | Type | Description |
| --------- | ------ | ------------------------ |
| `agentId` | string | The agent. **Required.** |
#### `update-config-variables-for-an-environment`
Update config variables for an environment.
API reference: [Update config variables for an environment](/api-reference/agents/endpoint/real-time-configs/update-config-variables-for-an-environment).
| Parameter | Type | Description |
| ----------- | ------ | ---------------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `clientEnv` | string | The client environment. **Required.** |
| `variables` | object | Key-value pairs to merge into the existing config. **Required.** |
#### `upsert-the-json-schema-for-a-config-page`
Upsert the JSON Schema for a config page.
API reference: [Upsert the JSON Schema for a config page](/api-reference/agents/endpoint/real-time-configs/upsert-the-json-schema-for-a-config-page).
| Parameter | Type | Description |
| ----------- | ------ | ----------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `clientEnv` | string | The client environment. **Required.** |
| `schema` | object | JSON Schema (Draft 7) definition. **Required.** |
### Functions
#### `start-function`
Start function.
API reference: [Start function](/tools/introduction).
| Parameter | Type | Description |
| ---------- | ------ | ------------------------- |
| `branchId` | string | The branch. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `end-function-`
End function.
API reference: [End function.](/tools/introduction).
| Parameter | Type | Description |
| ---------- | ------ | ------------------------- |
| `branchId` | string | The branch. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `execute-a-function`
Execute a function.
API reference: [Execute a function](/tools/introduction).
| Parameter | Type | Description |
| ------------ | ------ | ----------------------------- |
| `branchId` | string | The branch. **Required.** |
| `agentId` | string | The agent. **Required.** |
| `functionId` | string | Path parameter. **Required.** |
| `flowId` | string | — |
| `args` | object | Default `{}`. |
#### `duplicate-a-function`
Duplicate a function.
API reference: [Duplicate a function](/tools/introduction).
| Parameter | Type | Description |
| ------------ | ------ | ----------------------------- |
| `branchId` | string | The branch. **Required.** |
| `agentId` | string | The agent. **Required.** |
| `functionId` | string | Path parameter. **Required.** |
| `name` | string | — |
#### `replace-the-start-function-code-`
Replace the start function code.
API reference: [Replace the start function code.](/tools/introduction).
| Parameter | Type | Description |
| ---------- | ------ | ------------------------- |
| `agentId` | string | The agent. **Required.** |
| `branchId` | string | The branch. **Required.** |
| `code` | string | — **Required.** |
#### `replace-the-end-function-code-`
Replace the end function code.
API reference: [Replace the end function code.](/tools/introduction).
| Parameter | Type | Description |
| ---------- | ------ | ------------------------- |
| `agentId` | string | The agent. **Required.** |
| `branchId` | string | The branch. **Required.** |
| `code` | string | — **Required.** |
#### `get-python-type-stubs-for-a-function-`
Get Python type stubs for a function.
API reference: [Get Python type stubs for a function.](/tools/introduction).
| Parameter | Type | Description |
| ------------ | ------ | ----------------------------- |
| `functionId` | string | Path parameter. **Required.** |
| `agentId` | string | The agent. **Required.** |
## Voice & audio
### Voice library
#### `register-a-new-voice-in-the-voice-library`
Register a new voice in the voice library.
API reference: [Register a new voice in the voice library](/api-reference/agents/endpoint/voice-library/register-voice).
| Parameter | Type | Description |
| ----------------- | ------ | ------------------------------------------------------------- |
| `accountId` | string | The account (workspace). **Required.** |
| `voiceMetadata` | object | Additional voice metadata. |
| `name` | string | Human-readable voice name. **Required.** |
| `providerVoiceId` | string | Voice ID from the TTS provider. **Required.** |
| `provider` | string | TTS provider (e.g. GOOGLE, OPENAI, ELEVENLABS). **Required.** |
| `config` | object | Provider-specific voice config. **Required.** |
#### `update-a-voice-in-the-voice-library`
Update a voice in the voice library.
API reference: [Update a voice in the voice library](/api-reference/agents/endpoint/voice-library/update-voice).
| Parameter | Type | Description |
| ----------------- | ------ | -------------------------------------- |
| `voiceId` | string | The voice. **Required.** |
| `accountId` | string | The account (workspace). **Required.** |
| `voiceMetadata` | object | Additional voice metadata. |
| `name` | string | Human-readable voice name. |
| `providerVoiceId` | string | Voice ID from the TTS provider. |
| `provider` | string | TTS provider. |
| `config` | object | Provider-specific voice config. |
#### `get-a-single-voice-from-the-voice-library`
Get a single voice from the voice library.
API reference: [Get a single voice from the voice library](/api-reference/agents/endpoint/voice-library/get-voice).
| Parameter | Type | Description |
| ----------- | ------ | -------------------------------------- |
| `accountId` | string | The account (workspace). **Required.** |
| `voiceId` | string | The voice. **Required.** |
#### `list-all-voices-in-the-account-s-voice-library`
List all voices in the account's voice library.
API reference: [List all voices in the account's voice library](/api-reference/agents/endpoint/voice-library/list-voices).
| Parameter | Type | Description |
| ------------- | ------- | -------------------------------------------------------------------- |
| `accountId` | string | The account (workspace). **Required.** |
| `gender` | string | Filter by voice metadata gender. One of `male`, `female`, `neutral`. |
| `provider` | string | Filter by TTS provider, e.g. 'GOOGLE', 'ELEVENLABS'. |
| `language` | string | Filter by voice metadata language, e.g. 'en-US'. |
| `isPolyVoice` | boolean | If set, restrict to (or exclude) Poly's curated voices. |
#### `synthesize-a-short-audio-sample-of-a-voice`
Synthesize a short audio sample of a voice.
API reference: [Synthesize a short audio sample of a voice](/api-reference/agents/endpoint/voice-library/synthesize-voice-sample).
| Parameter | Type | Description |
| ----------- | ------ | -------------------------------------- |
| `accountId` | string | The account (workspace). **Required.** |
| `voiceId` | string | The voice. **Required.** |
#### `set-the-project-s-active-voice`
Set the project's active voice.
API reference: [Set the project's active voice](/api-reference/agents/introduction).
| Parameter | Type | Description |
| --------- | ------ | --------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `voiceId` | string | Voice ID to set as the project voice. **Required.** |
### Deployed voice configs
#### `create-or-update-a-deployed-voice-configuration`
Create or update a deployed voice configuration.
API reference: [Create or update a deployed voice configuration](/api-reference/agents/introduction).
| Parameter | Type | Description |
| ------------- | ------ | ------------------------------------------------------ |
| `agentId` | string | The agent. **Required.** |
| `voiceId` | string | Voice ID to deploy. **Required.** |
| `id` | string | Deployed voice ID — omit to create, provide to update. |
| `probability` | number | Traffic weight (0.0–1.0). **Required.** |
#### `list-deployed-voice-configurations`
List deployed voice configurations.
API reference: [List deployed voice configurations](/api-reference/agents/introduction).
| Parameter | Type | Description |
| --------- | ------ | ------------------------ |
| `agentId` | string | The agent. **Required.** |
#### `delete-a-deployed-voice-configuration`
Delete a deployed voice configuration.
API reference: [Delete a deployed voice configuration](/api-reference/agents/introduction).
| Parameter | Type | Description |
| ----------------- | ------ | ----------------------------------------------- |
| `deployedVoiceId` | string | The deployed voice configuration. **Required.** |
| `agentId` | string | The agent. **Required.** |
### Voice & disclaimer tuning
#### `get-voice-tuning-settings`
Get voice tuning settings.
API reference: [Get voice tuning settings](/api-reference/agents/introduction).
| Parameter | Type | Description |
| --------- | ------ | ----------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `voiceId` | string | The voice. **Required.** |
| `purpose` | string | Tuning purpose filter, e.g. 'disclaimer'. |
#### `update-voice-tuning-settings`
Update voice tuning settings.
API reference: [Update voice tuning settings](/api-reference/agents/introduction).
| Parameter | Type | Description |
| ----------------- | ------- | ------------------------------------ |
| `agentId` | string | The agent. **Required.** |
| `voiceId` | string | The voice. **Required.** |
| `resetToDefault` | boolean | Reset settings to provider defaults. |
| `similarityBoost` | integer | Similarity boost (0–100). |
| `modelId` | string | Provider model ID override. |
| `speed` | number | Playback speed multiplier. |
| `stability` | integer | Voice stability (0–100). |
#### `get-disclaimer-voice-tuning-settings`
Get disclaimer voice tuning settings.
API reference: [Get disclaimer voice tuning settings](/api-reference/agents/introduction).
| Parameter | Type | Description |
| --------- | ------ | ------------------------ |
| `voiceId` | string | The voice. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `update-disclaimer-voice-tuning-settings`
Update disclaimer voice tuning settings.
API reference: [Update disclaimer voice tuning settings](/api-reference/agents/introduction).
| Parameter | Type | Description |
| ----------------- | ------- | ------------------------------------ |
| `agentId` | string | The agent. **Required.** |
| `voiceId` | string | The voice. **Required.** |
| `resetToDefault` | boolean | Reset settings to provider defaults. |
| `similarityBoost` | integer | Similarity boost (0–100). |
| `modelId` | string | Provider model ID override. |
| `speed` | number | Playback speed multiplier. |
| `stability` | integer | Voice stability (0–100). |
### Audio cache
#### `list-audio-cache-entries`
List audio cache entries.
API reference: [List audio cache entries](/api-reference/agents/endpoint/audio-cache/list-audio-cache).
| Parameter | Type | Description |
| --------- | ------- | -------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `sort` | string | Sort expression, e.g. "hit\_count:desc", "duration:asc". |
| `offset` | integer | Pagination offset. Min 0. Default `0`. |
| `limit` | integer | Max entries to return (1-200). 1–200. Default `50`. |
#### `delete-audio-cache-entry`
Delete audio cache entry.
API reference: [Delete audio cache entry](/api-reference/agents/endpoint/audio-cache/delete-audio-cache-entry).
| Parameter | Type | Description |
| --------- | ------ | ------------------------------------ |
| `entryId` | string | The audio cache entry. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `bulk-delete-audio-cache-entries`
Bulk delete audio cache entries.
API reference: [Bulk delete audio cache entries](/api-reference/agents/endpoint/audio-cache/bulk-delete-audio-cache).
| Parameter | Type | Description |
| --------- | ------ | --------------------------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `ids` | array | List of audio cache entry IDs to delete (max 20). 1–20 items. **Required.** |
#### `download-cached-audio-file`
Download cached audio file.
API reference: [Download cached audio file](/api-reference/agents/endpoint/audio-cache/download-audio-file).
| Parameter | Type | Description |
| --------- | ------ | ------------------------------------ |
| `entryId` | string | The audio cache entry. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `replace-audio-file-for-a-cache-entry`
Replace audio file for a cache entry.
API reference: [Replace audio file for a cache entry](/api-reference/agents/endpoint/audio-cache/replace-audio-file).
| Parameter | Type | Description |
| --------- | ------ | ------------------------------------ |
| `entryId` | string | The audio cache entry. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `update-cache-entry-file-and-settings`
Update cache entry file and settings.
API reference: [Update cache entry file and settings](/api-reference/agents/endpoint/audio-cache/update-audio-cache-details).
| Parameter | Type | Description |
| --------- | ------ | ------------------------------------ |
| `entryId` | string | The audio cache entry. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `generate-a-tts-audio-preview`
Generate a TTS audio preview.
API reference: [Generate a TTS audio preview](/api-reference/agents/endpoint/audio-cache/synthesize-audio-preview).
| Parameter | Type | Description |
| ----------- | ------- | ------------------------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `config` | object | Provider-specific synthesis config. **Required.** |
| `language` | string | BCP-47 language tag, e.g. 'en-US'. **Required.** |
| `provider` | string | TTS provider key, e.g. 'eleven\_labs', 'cartesia'. Default `eleven_labs`. |
| `text` | string | Text to synthesize. **Required.** |
| `storeOnS3` | boolean | Store audio on S3 and return a presigned URL. |
#### `preview-tts-audio-for-a-cache-entry`
Preview TTS audio for a cache entry.
API reference: [Preview TTS audio for a cache entry](/api-reference/agents/endpoint/audio-cache/synthesize-audio-preview).
| Parameter | Type | Description |
| ---------- | ------ | -------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `entryId` | string | The audio cache entry. **Required.** |
| `config` | string | Voice tuning settings (provider-specific). **Required.** |
| `language` | string | BCP-47 language tag, e.g. 'en-US'. |
| `text` | string | Text to synthesize. **Required.** |
## Run, test & inspect
### Debug chat
#### `create-a-new-debug-chat-session`
Create a new debug chat session.
API reference: [Create a new debug chat session](/api-reference/debug-chat/create-debug-chat-session).
| Parameter | Type | Description |
| ----------------------- | ------ | ----------------------------------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `integrationAttributes` | object | Custom attributes accessible via conv.integration\_attributes in project functions. |
| `variantId` | string | Variant ID to use. |
| `channel` | string | Channel type (e.g. chat.polyai). |
| `asrLangCode` | string | ASR language code (e.g. en-US). Defaults to server config. |
| `conversationId` | string | Custom conversation ID. Auto-generated if omitted. |
| `ttsLangCode` | string | TTS language code (e.g. en-US). Defaults to server config. |
| `clientEnv` | string | Client environment (sandbox, pre-release, live). **Required.** |
#### `send-a-message-to-a-debug-chat-session`
Send a message to a debug chat session.
API reference: [Send a message to a debug chat session](/api-reference/debug-chat/send-debug-chat-message).
| Parameter | Type | Description |
| ---------------- | ------ | -------------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `conversationId` | string | The conversation. **Required.** |
| `asrLangCode` | string | ASR language code (e.g. en-US). Defaults to server config. |
| `metadata` | object | User input metadata. |
| `ttsLangCode` | string | TTS language code (e.g. en-US). Defaults to server config. |
| `message` | string | User input message. Default \`\`. |
| `clientEnv` | string | Client environment (sandbox, pre-release, live). **Required.** |
### Conversations
#### `get-a-conversation-by-id`
Get a conversation by ID.
API reference: [Get a conversation by ID](/api-reference/conversations/v3/endpoint/get-conversation-by-id).
| Parameter | Type | Description |
| ---------------- | ------ | ------------------------------- |
| `conversationId` | string | The conversation. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `list-conversations-for-a-project`
List conversations for a project.
API reference: [List conversations for a project](/api-reference/conversations/v3/endpoint/get-conversations).
| Parameter | Type | Description |
| --------- | ------- | ----------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `limit` | integer | Max number of conversations to return. Min 1. Default `50`. |
| `offset` | integer | Number of conversations to skip. Min 0. Default `0`. |
#### `get-audio-recording-for-a-conversation`
Get audio recording for a conversation.
API reference: [Get audio recording for a conversation](/api-reference/conversations/v3/endpoint/get-conversation-audio).
| Parameter | Type | Description |
| ---------------- | ------- | -------------------------------------------------------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `conversationId` | string | The conversation. **Required.** |
| `redacted` | boolean | Whether to return redacted audio. Default `False`. |
| `direction` | string | Audio direction: "combined", "user", or "agent". One of `combined`, `user`, `agent`. Default `combined`. |
#### `add-annotations-to-a-conversation`
Add annotations to a conversation.
API reference: [Add annotations to a conversation](/api-reference/conversations/introduction).
| Parameter | Type | Description |
| ---------------- | ------ | ------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `conversationId` | string | The conversation. **Required.** |
| `annotations` | array | Array. **Required.** |
#### `upsert-a-note-on-a-conversation`
Upsert a note on a conversation.
API reference: [Upsert a note on a conversation](/api-reference/conversations/introduction).
| Parameter | Type | Description |
| ---------------- | ------ | ------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `conversationId` | string | The conversation. **Required.** |
| `note` | string | — **Required.** |
### Test suites
#### `trigger-a-test-run`
Trigger a simulation test run. Select test cases explicitly by id, by severity, or by tag via the `select` field. The legacy flat `testCaseIds` body is deprecated but still accepted.
API reference: [Trigger a test run](/testing/introduction).
| Parameter | Type | Description |
| ------------- | ------ | ------------------------ |
| `agentId` | string | The agent. **Required.** |
| `select` | string | — **Required.** |
| `branchId` | string | — **Required.** |
| `testCaseIds` | string | — |
#### `get-a-test-run-by-id`
Get a test run by ID.
API reference: [Get a test run by ID](/testing/introduction).
| Parameter | Type | Description |
| ----------- | ------ | --------------------------- |
| `testRunId` | string | The test run. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `list-test-runs-for-a-project`
List test runs for a project.
API reference: [List test runs for a project](/testing/introduction).
| Parameter | Type | Description |
| ----------- | ------- | -------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `offset` | integer | Number of test runs to skip. Min 0. Default `0`. |
| `testSetId` | string | Filter by test set ID. |
| `limit` | integer | Max number of test runs to return. Min 1. Default `100`. |
| `branchId` | string | Filter by branch ID. |
#### `list-test-cases`
List test cases.
API reference: [List test cases](/testing/introduction).
| Parameter | Type | Description |
| ---------- | ------ | ------------------------- |
| `branchId` | string | The branch. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `get-test-case`
Get test case.
API reference: [Get test case](/testing/introduction).
| Parameter | Type | Description |
| ------------ | ------ | ---------------------------- |
| `agentId` | string | The agent. **Required.** |
| `testCaseId` | string | The test case. **Required.** |
| `branchId` | string | The branch. **Required.** |
#### `get-test-execution-history-for-a-project`
Get test execution history for a project.
API reference: [Get test execution history for a project](/testing/introduction).
| Parameter | Type | Description |
| ------------ | ------- | -------------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `offset` | integer | Number of history entries to skip. Min 0. Default `0`. |
| `limit` | integer | Max number of history entries to return. Min 1. Default `100`. |
| `branchId` | string | Filter by branch ID. |
| `testCaseId` | string | Filter by test case ID. |
### Outbound calling
#### `trigger-an-outbound-call`
Trigger an outbound call.
API reference: [Trigger an outbound call](/api-reference/agents/endpoint/outbound-calls/trigger-outbound-call).
| Parameter | Type | Description |
| ------------- | ------ | ----------------------------------------------------------------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `encryption` | string | Telephony encryption mode. Defaults to tls\_srtp. |
| `variantId` | string | Variant ID to use for the call. If omitted, the default connector for the environment is used. |
| `metadata` | object | Arbitrary key-value metadata passed through to the call. Must be under 26 KB when base64-encoded. |
| `countryCode` | string | ISO 3166-1 alpha-2 country code for number parsing (e.g. GB). Only needed when to\_number lacks a country prefix. |
| `toNumber` | string | Phone number to dial in E.164 format (e.g. +442012345678). **Required.** |
| `environment` | string | Target environment: sandbox, pre-release, or live. **Required.** |
#### `get-the-status-of-an-outbound-call`
Get the status of an outbound call.
API reference: [Get the status of an outbound call](/api-reference/agents/endpoint/outbound-calls/get-outbound-call-status).
| Parameter | Type | Description |
| ------------- | ------ | ---------------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `callSid` | string | The outbound call SID. **Required.** |
| `environment` | string | Target environment: sandbox, pre-release, or live. **Required.** |
## Manage agent infrastructure
### MCP servers on an agent
#### `add-mcp-server`
Add MCP server.
API reference: [Add MCP server](/mcp/agent-studio-integrations).
| Parameter | Type | Description |
| ------------ | ------ | ---------------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `branchId` | string | The branch. **Required.** |
| `providerId` | string | — **Required.** |
| `timeout` | number | 0–30. |
| `auth` | object | Nested object — see the API reference for the full schema. |
| `url` | string | — **Required.** |
#### `list-mcp-servers`
List MCP servers.
API reference: [List MCP servers](/mcp/agent-studio-integrations).
| Parameter | Type | Description |
| ---------- | ------ | ------------------------- |
| `branchId` | string | The branch. **Required.** |
| `agentId` | string | The agent. **Required.** |
#### `connect-mcp-server`
Connect MCP server.
API reference: [Connect MCP server](/mcp/agent-studio-integrations).
| Parameter | Type | Description |
| ---------- | ------ | ----------------------------- |
| `serverId` | string | The MCP server. **Required.** |
| `agentId` | string | The agent. **Required.** |
| `branchId` | string | The branch. **Required.** |
#### `update-mcp-server`
Update MCP server.
API reference: [Update MCP server](/mcp/agent-studio-integrations).
| Parameter | Type | Description |
| ---------- | ------ | ---------------------------------------------------------- |
| `branchId` | string | The branch. **Required.** |
| `serverId` | string | The MCP server. **Required.** |
| `agentId` | string | The agent. **Required.** |
| `timeout` | number | 0–30. |
| `auth` | object | Nested object — see the API reference for the full schema. |
| `url` | string | — |
#### `delete-mcp-server`
Delete MCP server.
API reference: [Delete MCP server](/mcp/agent-studio-integrations).
| Parameter | Type | Description |
| ---------- | ------ | ----------------------------- |
| `serverId` | string | The MCP server. **Required.** |
| `agentId` | string | The agent. **Required.** |
| `branchId` | string | The branch. **Required.** |
### Secrets
#### `create-a-secret`
Create a secret.
API reference: [Create a secret](/secrets/introduction).
| Parameter | Type | Description |
| ------------- | ------ | ----------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `value` | string | Secret value. **Required.** |
| `name` | string | Display name of the secret. **Required.** |
| `description` | string | Description of the secret. |
#### `update-a-secret`
Update a secret.
API reference: [Update a secret](/secrets/introduction).
| Parameter | Type | Description |
| ------------- | ------ | ------------------------------------------------- |
| `agentId` | string | The agent. **Required.** |
| `secretName` | string | The secret name. **Required.** |
| `description` | string | Description of the secret (unchanged if omitted). |
| `value` | string | New secret value. **Required.** |
#### `delete-a-secret`
Delete a secret.
API reference: [Delete a secret](/secrets/introduction).
| Parameter | Type | Description |
| ------------ | ------ | ------------------------------ |
| `secretName` | string | The secret name. **Required.** |
| `agentId` | string | The agent. **Required.** |
## Monitor deployed agents
The Alerts family is **enterprise-only** — it's available on US, UK, and EU workspaces, not on self-serve Studio workspaces.
Watch live agents and react to issues. Backed by the [Alerts API](/api-reference/alerts/introduction).
### Alerts
| Tool | What it does |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `create-alert-rule` | Create a new alert rule. The `account_id` is derived from the `X-API-KEY` header. |
| `get-alert-rule` | Get a specific alert rule by ID. |
| `list-alert-rules` | List alert rules with optional filters. Query params: `enabled` (filter by enabled status), `metric` (filter by metric type), `project_id` (filter by project ID), `state` (filter by current alert state — `OK`, `ALERT`, `NO_DATA`, or `UNKNOWN`). |
| `update-alert-rule` | Update an alert rule. Supports partial updates of alert rule attributes. |
| `delete-alert-rule` | Delete an alert rule and its associated state, including the Datadog monitor. |
| `get-active-alerts` | Get active alerts (rules currently in `ALERT` state). Optional query params: `project_id` (filter by project ID), `metric` (filter by metric type). |
## Limiting what a client can do
To narrow what a client can reach, scope the [API key](/mcp/authentication) itself. Create a [least-privilege key](/mcp/builder/security#restrict-the-tool-surface) limited to the agents and permissions that client needs — for example, a read-only key for a monitoring assistant. The key's scope governs which calls succeed, so a narrowly scoped key can't create, delete, or deploy even though the tools are still discovered.
# Builder MCP
Source: https://docs.poly.ai/mcp/builder/introduction
Build, test, and deploy PolyAI agents from your IDE or MCP client, without calling the API directly.
**Builder MCP** is PolyAI's authenticated [MCP](https://modelcontextprotocol.io) server for building agents. Connect an MCP client — [Claude Code](https://www.claude.com/product/claude-code), [Cursor](https://cursor.com), [Claude Desktop](https://claude.ai/download), or [Codex](https://developers.openai.com/codex/cli/) — and you can create, test, and deploy PolyAI agents from your IDE without calling the [REST API](/api-reference/introduction) yourself. When the client connects, it discovers the available tools and their input schemas.
Looking to query conversation data — transcripts, metrics, analytics — rather than build? That's the sibling [Data MCP](/mcp/data/introduction) server.
## Builder MCP vs Agent Studio MCP integrations
The two point in opposite directions:
| | **Builder MCP** | **Agent Studio MCP integrations** |
| -------------------------- | --------------------------------------- | --------------------------------------------------------------- |
| **Direction** | An MCP client connects **in** to PolyAI | Your PolyAI agent connects **out** to a third-party server |
| **Who acts as the client** | Your IDE / AI coding tool | Your live PolyAI agent |
| **What it's for** | Build, test, and deploy agents | Give a live agent extra tools during a conversation |
| **Docs** | This page | [Agent Studio MCP integrations](/mcp/agent-studio-integrations) |
## What you can do
Builder MCP exposes PolyAI's platform as tools grouped into three areas:
Create agents, edit behavior and knowledge, manage branches, variants, connectors, and phone numbers.
Retrieve conversations and transcripts to test and inspect agent behavior.
Create alert rules and read active alerts to monitor deployed agents.
See [Capabilities](/mcp/builder/capabilities) for the full tool list.
## Why use it
* **No API plumbing.** The client reads each tool and its schema from the server, so you don't hand-write requests, hosts, or headers.
* **Conversational workflows.** Ask your assistant to branch an agent, update its greeting, test it, and publish to staging, and it runs the tool calls in order.
* **Full deployment lifecycle.** Variants, environments, and deployments are available as tools. See the [walkthrough](/mcp/builder/walkthrough).
* **One API key.** A single [account API key](/mcp/authentication) authenticates every tool.
## Get started
Create an [account API key](/mcp/authentication).
Add Builder MCP in one command. See [Quickstart](/mcp/quickstart).
Follow the [end-to-end walkthrough](/mcp/builder/walkthrough).
# Security & safe use
Source: https://docs.poly.ai/mcp/builder/security
How to use Builder MCP safely — least-privilege keys, gating destructive actions, and read-first workflows.
Builder MCP gives an AI client real control over your agents — including actions that affect live callers. Treat it like any other privileged credential and follow the practices below.
## Prompt injection is the main risk
The client acting on your behalf is an LLM, so content it reads — a conversation transcript, a knowledge base topic, a tool result — can contain instructions that try to redirect it. Assume any text the model ingests could attempt to trigger a tool call you didn't intend. The mitigations below limit the blast radius when it does.
## Recommendations
Create an [API key](/mcp/authentication) with the minimum permissions and agent scope the task needs. A key that only needs to read conversations shouldn't be able to publish. Scope keys to specific agents where possible, and use separate keys for read-only versus build workflows.
Store the API key in your client's secret settings or environment — never paste it into a chat message. Anything typed into the conversation can end up in logs or model context.
Fetch and confirm the current state of a resource before you update or delete it. "Show me the agent's current behavior rules" before "update them" prevents the model from acting on stale assumptions.
Tools that delete, publish, promote, or roll back should require explicit user intent. Enable **confirm before running tools** in your MCP client so these are never executed silently, and read the [deployment steps](/mcp/builder/walkthrough#7-roll-back-if-needed) before promoting to production.
Don't pull full transcripts or conversation data into the model's context unless you need them — retrieve only the fields required. Smaller results keep prompts focused and reduce what's exposed downstream.
## Restrict the tool surface
Where a client only needs a subset of tools, expose only those using the `tools` query parameter on the endpoint URL — see [Capabilities → Limiting what a client can do](/mcp/builder/capabilities#limiting-what-a-client-can-do). A read-only analysis client should never be handed `create`, `publish`, or `delete` tools in the first place.
# Build, test & deploy
Source: https://docs.poly.ai/mcp/builder/walkthrough
An end-to-end walkthrough of building, testing, and promoting a PolyAI agent through environments with Builder MCP.
The lifecycle below is where Builder MCP does more than a thin API wrapper: branches, variants, environments, and deployments are all first-class, so you can take an agent from an idea to production — and roll back — entirely from your MCP client. This is the flow to prompt your assistant through.
These steps map to tools in [Capabilities](/mcp/builder/capabilities). You don't call them by hand — describe the outcome you want and let the client chain the tool calls. Example prompts are shown throughout.
## 1. Branch
Never edit a live agent directly. Create a [branch](/api-reference/agents/introduction) so your changes are isolated until you're ready to merge.
> "Create a branch of the Acme support agent called `greeting-update`."
## 2. Build
Edit the agent on the branch — behavior rules, knowledge base topics, connectors, and [variants](/knowledge/variants/introduction) for A/B experiments.
> "On that branch, update the greeting and add a knowledge base topic for refund policy."
## 3. Test
Use a [debug chat](/api-reference/debug-chat/introduction) session to exercise the branch before it goes anywhere. Inspect the resulting [conversation data](/api-reference/conversations/v3/endpoint/get-conversations) to confirm the agent behaves as intended.
> "Start a debug chat against this branch and ask it about refunds. Show me the transcript."
## 4. Merge
Once the branch checks out, merge it back into the agent's main line.
> "Merge `greeting-update` back into the agent."
## 5. Deploy to staging
[Publish](/api-reference/agents/endpoint/deployments/publish-the-current-draft-to-an-environment) the current draft to a non-production environment first. Test again against staging with real routing.
> "Publish the current draft to staging."
## 6. Promote to production
When staging looks good, [promote](/api-reference/agents/endpoint/deployments/promote-a-deployment-to-the-next-environment) the deployment to production. Nothing reaches live callers until this step.
> "Promote the staging deployment to production."
## 7. Roll back if needed
Every deployment is versioned. If something regresses, [roll back](/api-reference/agents/endpoint/deployments/rollback-to-a-previous-deployment) to a previous deployment immediately.
> "Roll production back to the previous deployment."
## Why this matters
Most MCP servers stop at create-and-update. PolyAI's environments and deployment model mean the entire promotion path — draft → staging → production, with versioned rollback — runs through the same MCP client you build in. Combine it with [variants](/knowledge/variants/introduction) to test changes on a slice of traffic before a full promotion.
Publishing and promotion affect live agents. Gate these behind explicit intent — see [Security & safe use](/mcp/builder/security#review-destructive-actions).
# Capabilities
Source: https://docs.poly.ai/mcp/data/capabilities
The tools Data MCP exposes, grouped by what they do — each with its parameters.
Data MCP exposes PolyAI's conversation data as MCP tools. Your client discovers them automatically on connection — you don't configure tools by hand. Each tool carries its own input schema, which the client reads directly from the server; the parameter tables below reproduce that schema so you can see what each tool takes without connecting first.
Data MCP exposes **5 tools**. Tool names appear exactly as your client sees them in `tools/list`.
To see exactly what your client discovered, ask it to list its tools or query the endpoint's `tools/list` method. Parameters map to a PolyAI REST endpoint — they're sent as path segments, query parameters, or a request body — but your client handles that wiring from the schema; the tables below just list what you can set.
## Search & read conversations
Find conversations by their metrics, search across transcript text, and pull back the full turn-by-turn record.
### `search-conversations`
Search conversations by metric filters, with sorting and pagination.
| Parameter | Type | Description |
| ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `from` / `to` | datetime | Bounds on conversation start time (`from` inclusive, `to` exclusive). |
| `filters` | array | Metric filters: `{metric, op, value}`. Operators: `eq`, `gt`, `gte`, `lt`, `lte`, `in`, `ex`, `contains`, `not_contains`, `exists`. `conversation_id` is also accepted as a filter (`eq`/`in`) to fetch one or many conversations' metrics. |
| `filter_operator` | string | How filters combine: `and` (default) or `or`. |
| `fields` | array | Metric keys to include in each conversation's `metrics` map (case-insensitive). Omit to return all metrics — prefer passing only the metrics you need, especially on large searches. |
| `channel` | array | Restrict to channels: `VOICE-SIP`, `CHAT`, `WEBCHAT`, `SMS`, `RCS`. |
| `client_env` | array | Restrict to environments: `test`, `sandbox`, `pre-release`, `live`, `scenarios`. |
| `sort` | object | `{field, order}` — field is `started_at`, `duration`, or `conversation_id`. Defaults to `started_at` descending. |
| `limit` / `offset` | integer | Pagination. `limit` 1–100 (default 20), `offset` 0–1000. |
| `project_id` | string | Optional project scope. |
### `search-transcripts`
Full-text search over transcript turns across conversations, returning matches with surrounding turns for context.
| Parameter | Type | Description |
| ------------------ | -------- | -------------------------------------------------------- |
| `q` | string | Search phrase, 3–300 characters. **Required.** |
| `from` / `to` | datetime | Bounds on conversation start time. |
| `context_turns` | integer | Turns of context around each match, 0–3 (default 1). |
| `limit` / `offset` | integer | Pagination. `limit` 1–100 (default 20), `offset` 0–1000. |
| `project_id` | string | Project scope. Required when authenticating with a PAT. |
### `get-conversation`
Fetch every turn of a single conversation.
| Parameter | Type | Description |
| ----------------- | ------ | ---------------------------------------- |
| `conversation_id` | string | The conversation to fetch. **Required.** |
| `project_id` | string | Optional project scope. |
## Metrics & analytics
Discover which metrics you can filter and aggregate on, then roll one up over time into buckets you can chart.
### `get-metrics`
List the filterable metrics for an account, optionally scoped to a project — built-in metrics plus any custom metrics the project defines. Use this first to discover what you can filter and aggregate on.
| Parameter | Type | Description |
| ------------ | ------ | ----------------------- |
| `project_id` | string | Optional project scope. |
### `aggregate-metrics`
Aggregate a metric over a time window into buckets you can chart — with grouping, cohort filters, and post-aggregation thresholds.
This tool is **experimental** — its schema may change. Your client discovers the current name from `tools/list`.
| Parameter | Type | Description |
| ------------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `metric` | string | Metric to aggregate (case-insensitive, e.g. `poly_score`). **Required.** |
| `aggs` | array | Aggregations to compute: `sum`, `avg`, `min`, `max`, `p50`, `p95`, `p99`, `conversation_count`, `distinct_count`. **Required.** |
| `from` / `to` | datetime | Window bounds (`from` inclusive, `to` exclusive). |
| `interval` | string | Time bucket: `hourly`, `daily`, `weekly`, `monthly`. Omit for a single unbucketed result. |
| `timezone` | string | IANA timezone (e.g. `Europe/London`) applied to `hourly` and `daily` bucketing; naive `from`/`to` are interpreted in it. Ignored for other intervals. |
| `group_by` | array | Group by `project_id`, `channel`, `deployment_id`, `variant_id`, `client_env`, or — for string metrics — `value_string`. |
| `filters` | array | Cohort filters on other metrics: `{metric, op, value}`. Operators: `eq`, `gt`, `gte`, `lt`, `lte`, `in`, `ex`, `exists`. |
| `filter_operator` | string | How filters combine: `and` (default) or `or`. |
| `having` | array | Post-aggregation thresholds: `{field, op, value}` where `field` is one of the `aggs`. |
| `channel` | array | Restrict to channels: `VOICE-SIP`, `CHAT`, `WEBCHAT`, `SMS`, `RCS`. |
| `client_env` | array | Restrict to environments: `test`, `sandbox`, `pre-release`, `live`, `scenarios`. |
| `deployment_id` / `variant_id` | array | Restrict to specific deployments or variants. |
| `sort` | object | Secondary sort on a `group_by` or `aggs` field, applied after the ascending bucket sort. |
| `limit` / `offset` | integer | Pagination. `limit` 1–100 (default 20), `offset` 0–1000. |
| `project_id` | string | Optional project scope. |
## Errors
| Status | Meaning |
| ------ | ------------------------------------------------------------------------------------- |
| `400` | Request validation error. |
| `401` | Missing or invalid `X-API-KEY`. |
| `403` | Authenticated, but missing the required permission. |
| `404` | Not found or empty result — also returned when a credential can't access the account. |
| `422` | Unsupported filter. |
# Data MCP
Source: https://docs.poly.ai/mcp/data/introduction
Query PolyAI conversation data — transcripts, metrics, and analytics — from any MCP client.
**Data MCP** is PolyAI's authenticated [MCP](https://modelcontextprotocol.io) server for conversation data. Connect an MCP client — [Claude Code](https://www.claude.com/product/claude-code), [Cursor](https://cursor.com), [Claude Desktop](https://claude.ai/download), or [Codex](https://developers.openai.com/codex/cli/) — and you can search conversations, read transcripts, and aggregate metrics in plain language, without calling the [REST API](/api-reference/introduction) directly.
## Data MCP vs Builder MCP
Both are PolyAI-hosted servers you connect **in** to. They cover different jobs and authenticate differently.
| | **Data MCP** | **Builder MCP** |
| ----------------- | -------------------------------------------------------- | ---------------------------------------- |
| **What it's for** | Query conversation data: transcripts, metrics, analytics | Build, test, and deploy agents |
| **Authenticates** | Account API key | Account API key |
| **Endpoint path** | `/data-mcp` | `/builder-mcp` |
| **Docs** | This page | [Builder MCP](/mcp/builder/introduction) |
To connect a client **out** from a live agent to a third-party server instead, see [Agent Studio MCP integrations](/mcp/agent-studio-integrations).
## What you can do
Find conversations by their metrics and scores — filter and sort on quality scores, duration, and outcomes over a date range.
Read the full, turn-by-turn record of a conversation, or search across many conversations for a word or phrase with surrounding context.
List the metrics available for an account or project — the built-in ones plus any custom metrics your projects define.
Roll a metric up over time into buckets you can chart — grouped, filtered, and thresholded however you like.
## Get started
### 1. Get an account API key
Every request carries an API key in the `X-API-KEY` header. Data MCP uses an **account API key**, created from your account page in Agent Studio — it's scoped to the account and can query any project in it. See [Authentication](/mcp/authentication) for details.
### 2. Set your key and endpoint
Keys are region-specific — a US key doesn't work against the UK endpoint. Match the region to where your account lives. Self-serve (Studio) accounts are supported too.
| Region | Endpoint |
| ------ | ------------------------------------- |
| US | `https://api.us.poly.ai/data-mcp` |
| UK | `https://api.uk.poly.ai/data-mcp` |
| EU | `https://api.eu.poly.ai/data-mcp` |
| Studio | `https://api.studio.poly.ai/data-mcp` |
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export POLYAI_API_KEY="your_api_key_here"
export POLYAI_DATA_MCP_URL="https://api.us.poly.ai/data-mcp" # use your region
```
### 3. Add the server to your client
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
claude mcp add --transport http polyai-data "$POLYAI_DATA_MCP_URL" \
--header "X-API-KEY: $POLYAI_API_KEY" \
--header "Accept: application/json, text/event-stream"
```
Verify with `claude mcp list`, then ask Claude to list its PolyAI data tools.
Add to `~/.cursor/mcp.json`:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"mcpServers": {
"polyai-data": {
"url": "https://api.us.poly.ai/data-mcp",
"headers": { "X-API-KEY": "your_api_key_here" }
}
}
}
```
Restart Cursor, then open the MCP settings to confirm the tools loaded.
Add to your `claude_desktop_config.json`:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"mcpServers": {
"polyai-data": {
"url": "https://api.us.poly.ai/data-mcp",
"headers": { "X-API-KEY": "your_api_key_here" }
}
}
}
```
Restart Claude Desktop. The tools appear in the tools menu.
Add to your Codex MCP config:
```toml theme={"theme":{"light":"github-light","dark":"github-dark"}}
[mcp_servers.polyai-data]
url = "https://api.us.poly.ai/data-mcp"
headers = { "X-API-KEY" = "your_api_key_here" }
```
Working across regions? Add the server once per region under distinct names (e.g. `polyai-data-us`, `polyai-data-uk`, `polyai-data-eu`), each with that region's key.
## Capabilities
Data MCP exposes 5 tools — conversation search, transcript search, single-conversation fetch, a metrics catalog, and metric aggregation. See [Capabilities](/mcp/data/capabilities) for each tool and its parameters.
Every Data MCP tool, grouped by what it does, with its parameters.
# Security & safe use
Source: https://docs.poly.ai/mcp/data/security
How to use Data MCP safely — scoping access, keeping keys secret, and handling untrusted transcript content.
Data MCP gives an AI client read access to your conversation data. Treat the API key like any other privileged credential and follow the practices below.
## The main risks
Data MCP is read-only — it can't change your agents — so the risks are about **what data leaves your control** and **what the model is told to do with it**:
* **Data exposure.** Anything pulled into the model's context can end up in logs or downstream output. Retrieve only what a task needs.
* **Prompt injection.** The client acting on your behalf is an LLM, and transcript text it reads can contain instructions that try to redirect it. Assume any conversation content the model ingests could attempt to trigger actions you didn't intend.
## Recommendations
Don't pull full transcripts into the model's context unless you need them — filter to the conversations that matter and request only the fields required. Smaller results keep prompts focused and reduce what's exposed downstream.
An account API key can query any project in the account. Where a task only concerns one project, pass its `project_id` so results — and any data that reaches the model — stay scoped to it. See [Authentication](/mcp/authentication).
Store the API key in your client's secret settings or environment — never paste it into a chat message. Anything typed into the conversation can end up in logs or model context. Rotate a key immediately if it's exposed.
A transcript is user-generated text — it can contain instructions aimed at the model. Don't let the client act on directions found inside conversation data, and be wary of summaries that quote it verbatim back into a shared channel.
Data MCP can't create, update, or delete anything — it only reads. To build, test, and deploy agents, that's the [Builder MCP](/mcp/builder/introduction), which has its own [security guidance](/mcp/builder/security).
# Query your data
Source: https://docs.poly.ai/mcp/data/walkthrough
An end-to-end walkthrough of exploring conversation data with Data MCP — from discovering metrics to reading a single transcript.
Data MCP turns conversation data into something you can interrogate in plain language. The flow below goes from "what can I even filter on?" to a single transcript, using the [tools](/mcp/data/capabilities) in the order you'd naturally reach for them.
You don't call these tools by hand — describe what you want and let the client chain the tool calls. Example prompts are shown throughout. Every tool takes an optional `project_id`; scope your questions to a project when your account spans several.
## 1. Discover what you can filter on
Start with [`get-metrics`](/mcp/data/capabilities#get-metrics) to see the metrics available for your account — the built-in ones plus any custom metrics your projects define. This tells you what you can filter and sort conversations by.
> "What metrics can I filter conversations on for project `acme-support`?"
## 2. Find the conversations you care about
Use [`search-conversations`](/mcp/data/capabilities#search-conversations) to filter on those metrics over a date range, then sort and page through the results. Combine filters with `and`/`or` and sort by start time, duration, or ID.
> "Find last week's conversations with a poly\_score below 3, longest first."
## 3. Search across transcripts
When you're chasing a phrase rather than a metric, [`search-transcripts`](/mcp/data/capabilities#search-transcripts) does full-text search over transcript turns and returns each match with surrounding turns for context.
> "Search transcripts for 'cancel my policy' in the last 30 days and show a couple of turns either side."
## 4. Read a full conversation
Once you've found a conversation worth reading, [`get-conversation`](/mcp/data/capabilities#get-conversation) returns every turn, top to bottom.
> "Show me the full transcript for conversation `abc-123`."
## Why this matters
Each tool is small, but together they cover the loop analysts actually run: *what can I measure → which conversations fit → where does the phrase show up → what happened in this one.* Because it all runs through your MCP client, you can follow a thread — from an aggregate concern to a single call — without leaving your chat.
Keep results tight — filter to the conversations that matter and scope to a `project_id` — so the model only ingests what the task needs. See [Security & safe use](/mcp/data/security).
# MCP
Source: https://docs.poly.ai/mcp/overview
PolyAI's authenticated MCP servers let any MCP client build agents and query their data. Learn the two directions MCP works and which one you need.
The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) is an open standard for connecting AI systems to tools. PolyAI works with MCP in **two directions** — and it's worth being clear which one you want before you start, because they solve opposite problems.
## Two directions
**Connect a client in.** Point an MCP client — Cursor, Claude Code, Claude Desktop, Codex — at one of PolyAI's authenticated, hosted MCP servers to build agents or query their data from your IDE. Your tool acts as a client of PolyAI's server.
**Consume external tools.** Add a third-party MCP server inside Agent Studio so your live agent can call its tools during conversations. This is your agent acting as a client of someone else's server.
| You want to… | Use | Direction |
| ------------------------------------------------------------------------- | --------------------------------------------------------------- | ------------------------ |
| Build and manage PolyAI agents from an IDE or AI coding tool | [Builder MCP](/mcp/builder/introduction) | Client → PolyAI |
| Query conversation data — transcripts, metrics, analytics — from a client | [Data MCP](/mcp/data/introduction) | Client → PolyAI |
| Let your live agent look up data or trigger actions in another system | [Agent Studio MCP integrations](/mcp/agent-studio-integrations) | PolyAI → external server |
The rest of this section documents **PolyAI's authenticated MCP servers** — the ones you connect a client to. For the connect-out direction, see [Agent Studio MCP integrations](/mcp/agent-studio-integrations).
## PolyAI's MCP servers
PolyAI hosts two authenticated MCP servers. They cover different jobs, but connect the same way — both use the streamable HTTP transport, run in the same regions, and take the same **account API key** in the `X-API-KEY` header.
Build, test, and deploy PolyAI agents — agents, data, and alerts as tools.
Query conversation data — search conversations, read transcripts, aggregate metrics.
| | **Builder MCP** | **Data MCP** |
| ----------------- | ------------------------------ | --------------------------------------------- |
| **What it's for** | Build, test, and deploy agents | Query conversations, transcripts, and metrics |
| **Authenticates** | Account API key | Account API key |
| **Endpoint path** | `/builder-mcp` | `/data-mcp` |
| **Regions** | US, UK, EU, Studio | US, UK, EU, Studio |
## Get connected
Create the API key your server needs and configure your client's auth. See [Authentication](/mcp/authentication).
Add the server to Cursor, Claude Code, Claude Desktop, or Codex with a single command. See [Quickstart](/mcp/quickstart).
Use the [Builder MCP](/mcp/builder/introduction) to create, test, and deploy agents, or the [Data MCP](/mcp/data/introduction) to query conversation data — all conversationally.
# Quickstart
Source: https://docs.poly.ai/mcp/quickstart
Connect one of PolyAI's MCP servers to your MCP client in one command.
Connect one of PolyAI's authenticated MCP servers to your MCP client — [Cursor](https://cursor.com), [Claude Code](https://www.claude.com/product/claude-code), [Claude Desktop](https://claude.ai/download), or [Codex](https://developers.openai.com/codex/cli/) — and you can drive PolyAI from your IDE in minutes.
Pick the server for the job:
Build, test, and deploy agents. Uses an **account API key** and the `/builder-mcp` endpoint.
Query conversations, transcripts, and metrics. Uses an **account API key** and the `/data-mcp` endpoint.
The steps below use Builder MCP. Data MCP connects the same way — swap the endpoint path to `/data-mcp`; the same account API key works for both. See [Data MCP → Get started](/mcp/data/introduction#get-started) for its exact commands.
## Prerequisites
You build agents inside a PolyAI workspace, so you need access to one first.
* **Enterprise customers** — PolyAI provisions your workspace during onboarding; your PolyAI representative sets it up and grants you access. Enterprise workspaces are region-specific (US, UK, or EU).
* **Getting started via the website** — sign up at [poly.ai](https://poly.ai) to create a self-serve workspace, which lives in the Studio region.
Create an **account API key** from the **API Keys** tab on your workspace homepage in Agent Studio (see [API keys](/secrets/api-keys)). The same key works for both Builder MCP and Data MCP — see [Authentication](/mcp/authentication).
Copy the value when it's shown — the full key only appears once.
Treat the key like a password. Don't commit it or put it in client-side code.
Open Agent Studio. Your account ID is the first path segment in the URL:
```
https://studio.{region}.poly.ai/{account_id}/{project_id}/agent
```
For example, `https://studio.uk.poly.ai/acme-uk/acme-team-4/agent` → `account_id=acme-uk`.
**"Account ID" and "Workspace ID" are the same thing.** Agent Studio's UI calls this the **Workspace ID** and shows it in a prefixed form (`ws-xxxxxxxx`). The API parameter is named `accountId` (Agents and Data APIs) or `account_id` (Conversations, Chat, Webhooks, and most other APIs) — same value, different casing convention depending on which API family you're calling. Both the slug form from the URL (`acme-uk`) and the prefixed form (`ws-xxxxxxxx`) work in API calls.
## 1. Set your key and endpoint
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
export POLYAI_API_KEY="your_api_key_here"
export POLYAI_MCP_URL="https://api.us.poly.ai/builder-mcp" # use your region and server — see below
```
| Region | Builder MCP | Data MCP |
| ------ | ---------------------------------------- | ------------------------------------- |
| US | `https://api.us.poly.ai/builder-mcp` | `https://api.us.poly.ai/data-mcp` |
| UK | `https://api.uk.poly.ai/builder-mcp` | `https://api.uk.poly.ai/data-mcp` |
| EU | `https://api.eu.poly.ai/builder-mcp` | `https://api.eu.poly.ai/data-mcp` |
| Studio | `https://api.studio.poly.ai/builder-mcp` | `https://api.studio.poly.ai/data-mcp` |
Match the region to the workspace or account your key belongs to. Connecting to the wrong region authenticates against the wrong place. See [Authentication](/mcp/authentication#pick-your-region) for how regions map.
## 2. Add the server to your client
Add to `~/.cursor/mcp.json`:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"mcpServers": {
"polyai": {
"url": "https://api.us.poly.ai/builder-mcp",
"headers": { "X-API-KEY": "your_api_key_here" }
}
}
}
```
Restart Cursor, then open the MCP settings to confirm the PolyAI tools loaded.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
claude mcp add --transport http polyai "$POLYAI_MCP_URL" \
--header "X-API-KEY: $POLYAI_API_KEY" \
--header "Accept: application/json, text/event-stream"
```
Verify with `claude mcp list`, then ask Claude to list its PolyAI tools.
Add to your `claude_desktop_config.json`:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"mcpServers": {
"polyai": {
"url": "https://api.us.poly.ai/builder-mcp",
"headers": { "X-API-KEY": "your_api_key_here" }
}
}
}
```
Restart Claude Desktop. The PolyAI tools appear in the tools menu.
Add to your Codex MCP config:
```toml theme={"theme":{"light":"github-light","dark":"github-dark"}}
[mcp_servers.polyai]
url = "https://api.us.poly.ai/builder-mcp"
headers = { "X-API-KEY" = "your_api_key_here" }
```
Most clients set the `Accept: application/json, text/event-stream` header for you. If a client can't connect, add it explicitly — both servers use the streamable HTTP transport.
Connecting to both servers at once? Add them under distinct names (e.g. `polyai-builder` and `polyai-data`), each with its own endpoint and key.
## 3. Verify
Ask your client to list its available tools, or test the endpoint directly:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -s -X POST "$POLYAI_MCP_URL" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H "X-API-KEY: $POLYAI_API_KEY" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
| grep '^data: ' | sed 's/^data: //' | jq '.result.tools | length'
```
A number greater than zero means the server is reachable and your key is valid.
## Next steps
Browse the tools, grouped by agents, data, and alerts.
Walk through the full lifecycle from branch to production.
Search conversations, read transcripts, and aggregate metrics.
Connect a live agent out to a third-party MCP server.
# Agent Attachments in Web Chat
Source: https://docs.poly.ai/messaging-channel/advanced/agent-attachments
Send interactive charts, YouTube videos, and images inline in the web chat widget, so customers get visual answers without leaving the conversation.
Agents can send interactive graphs, YouTube videos, and images directly into the web chat conversation. Attachments render inline in the widget so customers see the answer in the thread instead of clicking out to a separate page.
## Why Inline Attachments
Previously, anything visual (a trend, a comparison, a product demo) meant the agent had to send a link and hope the customer followed it. Every link out is a break in the conversation and a chance to lose the customer. Inline attachments keep the answer in the widget: the agent shows the chart, plays the clip, or enlarges the image without moving the customer off your page.
## Supported Attachment Types
Interactive charts rendered from a chart spec, using the same chart object as Smart Analyst.
Hosted YouTube videos, embedded as a consent-gated facade that only loads on click.
Images with click-to-enlarge lightbox and a proper error state when the source fails to load.
PDFs render as a link card that opens the file in a new tab.
### Graph
When the agent needs to show a trend or comparison, it sends a **chart spec**, the same Smart Analyst chart object used elsewhere in the platform. The widget renders it inline with the shared chart library, so styling and interactions match the rest of the product.
Use graphs when a number, a distribution, or a time series answers the question better than a sentence.
### Video
Videos are embedded as a **click-to-load facade**: the widget shows a poster and a play button, and nothing loads from YouTube until the customer clicks. This keeps the conversation lightweight, avoids autoplay, and gives the customer explicit consent before third-party content is fetched.
Only hosted YouTube videos are supported today.
### Image
Images render inline in the message thread. Click any image to open it in a **lightbox** for a full-size view. If an image fails to load, the widget shows an explicit error state rather than a broken tile.
## Accessibility
Attachments are keyboard-operable by default. Interactive controls (video play, lightbox open and close, chart tooltips) can be reached and triggered from the keyboard, and accessible text is composed automatically from each attachment's title so screen readers announce meaningful content.
## Related Pages
LLM, greeting, safety filters, and shared settings for messaging channels.
Brand the widget and embed it on your site.
# Messaging Agent Configuration
Source: https://docs.poly.ai/messaging-channel/advanced/chat-configuration
Configure LLM, greeting, and safety filters across messaging channels in Agent Studio.
Configure your agent's messaging channels from the **Messaging** page in Agent Studio, under **Channels > Messaging**. The page has three tabs: **General**, **Web chat** and **SMS**. Create [widgets](/widgets/configure) at Widget.
The **General** tab holds settings shared across all messaging channels: which channels are active, the language model, channel behaviour, the default greeting and safety filters. The **Web chat** and **SMS** tabs hold channel-specific settings.
Settings on the **General** tab apply across all messaging channels unless overridden in a channel's configuration. Anything set on a channel tab overrides the matching General setting for that channel only.
## Channels
The **Channels** card is where messaging channels are switched on and off. Each channel has its own row with a status tag and toggle. A channel that needs setup before it can be activated shows **Not configured** with a prompt to complete setup; an activated channel shows **Active**.
Deactivating a channel stops it sending and receiving messages, but keeps its configuration so it can be reactivated at any time. You are asked to confirm before a channel is deactivated.
## Language Model
Choose the primary LLM for messaging. All messaging channels use the same model. We recommend PolyAI's Raven, designed for conversational AI and tuned for both voice and chat, with support for custom styling, reasoning and 24+ languages.
## Channel Behaviour
Tailor tone, format and etiquette for your messaging agent with the **Messaging channel behaviour prompt**. This optional multiline prompt applies across messaging channels, and sits between the agent's core behaviour and any channel-specific style prompt.
Prompts are applied in this order: **channel style prompt > messaging channel behaviour > agent behaviour**. Use this prompt for style shared across messaging channels, a channel tab's style prompt for channel-specific style, and the Agent page to change what the agent knows or does.
Example: “Use concise responses with bullet points. Include links when helpful.” The style prompt is optional. If left empty, the agent uses its default persona across all channels.
## Greeting
Set the **Default greeting**, the opening message used across messaging channels. Keep it short and welcoming, so it invites the first message rather than obscuring the conversation. Each channel tab can override this with its own greeting.
Example: “Hello, how can I help?”
## Safety Filters
Configure filters to block harmful user inputs and generated outputs across four categories: violence, hate, sexual content and self-harm. Each category has its own toggle and a severity slider from **Lenient** to **Strict**; stricter settings increase protection against harmful content. Channel tabs can override these defaults per channel. See Safety filters for the full reference on categories, severity levels, and how filters interact with Guardrails.
The jailbreak attack filter is always enabled and cannot be turned off. It blocks user inputs that attempt to bypass safety measures.
## Agent Attachments
Agents can share rich attachments inline in the web chat conversation, so customers get visual answers without leaving the widget. Supported types:
* **Images** open in a lightbox on click, and show a clear error state if the image fails to load.
* **Graphs** render inline from a chart spec (the same Smart Analyst chart object used elsewhere), so agents can drop a live trend or comparison straight into the thread.
* **YouTube videos** embed as a consent-gated, click-to-load facade. Nothing autoplays or loads from YouTube until the customer opts in.
* **Links and PDFs** open as a card the customer can open in a new tab, the same as any other link.
Attachments are accessible by default: controls are keyboard-operable and accessible text is composed automatically from the attachment's title.
## Good to Know
* **Changes apply on save.** Messaging configuration takes effect immediately when saved, unlike settings that go through environment promotion.
* **Markdown is supported.** Chat messages render bold, italics, lists, links and code blocks across testing and review panels.
## Branching behavior
Chat configuration changes take effect immediately when saved. Unlike other project settings, webchat does not follow the environment branching system, there is no Sandbox, Pre-release, or Live promotion step. Use the Test Agent Chat panel to verify changes before saving.
## Markdown support
Chat messages support formatted markdown rendering:
* Bold and italics
* Bulleted and numbered lists
* Links
* Code blocks
Formatted markdown appears in both the Test Agent Chat panel and Conversation Review.
## Next steps
After configuring chat settings:
1. [Configure the widget](/widgets/configure) - Style and customize the chat widget appearance
2. [Deploy to your website](/widgets/install) - Generate and embed the script tag
You must configure chat settings before the widget will function. In-product banners will guide you through this process.
## Configure Each Channel
Channel-specific settings live on their own tabs and pages:
Widget configuration, disclaimers and privacy policies, and website deployment.
SMS behaviour, greetings and templates.
# Disclaimers and privacy policies
Source: https://docs.poly.ai/messaging-channel/advanced/disclaimers-and-privacy-policies
Configure the legal disclosures your web chat shows, where they appear, and whether users must accept them before chatting.
Set what legal disclosures your web chat shows, where they appear, and whether users must accept them before chatting. You configure everything in Agent Studio, and changes apply to your widget without a code deploy.
All disclaimer settings live in one place: **Agent Studio → Messaging → Widgets → Content** tab. There are two sections:
* **Disclaimers**: your disclaimer message, consent and visibility settings.
* **Company policies**: links to your hosted Privacy Policy and Terms & Conditions.
These settings help you meet your disclosure obligations, but what you are required to display depends on your industry and the regions you operate in. Check with your legal or compliance team before choosing your setup.
## The disclaimer message
The disclaimer message is the text shown to users in the chat widget, typically covering AI disclosure, call recording and data use. A default message is provided:
> This is an AI-powered system and responses should be verified. All chats are recorded in order to respond to your query and for training and quality control purposes.
Replace this with your own approved wording in the **Disclaimer message** field. The preview on the right updates as you type, so you can see exactly what users will see before you publish.
### Add links to your disclaimer
You can link out to your hosted legal pages from within the disclaimer itself, rather than packing the full legal copy into the message.
To add a link:
1. Highlight the text you want to turn into a link (for example, `Privacy Policy`).
2. Select the **Link** button in the toolbar that appears.
3. Enter the URL and confirm.
The link renders as clickable text in the widget and opens in a new tab, so users never lose their place in the conversation.
URLs must use HTTPS. Anything else is rejected when you save, with a clear error telling you why.
## Consent button
The **Consent button** setting controls whether users must actively accept your disclaimer before the conversation starts.
* **Show**: users see the disclaimer with an "I consent and start chat" button. They cannot send a message until they accept.
* **Hide**: the chat opens straight away, with the disclaimer displayed but no acceptance step.
Use **Show** where you need evidence of acceptance, for example where you process sensitive data or your legal team requires explicit consent to recording. Use **Hide** where disclosure alone is sufficient and you want the lowest possible barrier to starting a chat.
## Disclaimer visibility
The **Disclaimer visibility** setting controls how long the disclaimer stays on screen.
* **Always show**: the disclaimer remains visible below the message input throughout the conversation.
* **Hide after first message**: the disclaimer displays until the user sends their first message, then clears to give the conversation more room.
**Always show** keeps the disclosure permanently in view, which suits deployments with stricter compliance requirements. **Hide after first message** ensures every user sees the disclaimer at the start, then prioritises the conversation itself.
## Company policies
The **Company policies** section adds standing links to your hosted Privacy Policy and Terms & Conditions, separate from the disclaimer message. Turn the section on, then add either or both URLs:
* **Privacy policy URL** (optional)
* **Terms & conditions URL** (optional)
Then choose a **Display location**:
* **Header menu**: the links sit in the widget's three-dot menu, alongside **End chat**. Discoverable at any point in the conversation, without taking up space in the chat window.
* **Below message input**: the links display beneath the message input throughout the conversation, permanently visible.
## Translating your disclaimer
If your widget supports multiple languages, you can author disclaimer copy per language, so users see the correct legal disclosure in their own language without needing separate widget deployments per region.
Select a language above the disclaimer content field and enter the approved copy for that language. Two things to know:
* **Content is authored manually.** Nothing is auto-translated, so the wording your legal team approved is exactly what users see.
* **Your default language disclaimer is required**, and acts as the fallback wherever a translation has not been provided.
For full details on configuring languages for your widget, see [Web chat localisation](/messaging-channel/advanced/localisation).
## Choosing the right setup
There is no single correct configuration. The right combination depends on your regulatory environment, your users and how much friction you can accept at the start of a chat. Below are the most common scenarios, but it's essential you confirm your compliance requirements with your legal team as they are subject to change.
### Regulated industries or sensitive data
Healthcare, financial services, insurance, or any deployment handling sensitive personal data.
| Setting | Recommendation |
| --------------------- | -------------------------------- |
| Consent button | Show |
| Disclaimer visibility | Always show |
| Disclaimer links | Link to your full privacy notice |
| Company policies | Both URLs, Below message input |
Users explicitly accept the disclaimer before chatting, and the disclosure stays visible for the whole conversation. This gives your compliance team the strongest posture: acceptance is required, and the disclosure is never off screen.
### Retail, hospitality and low-friction support
Deployments where speed to first message matters and disclosure alone is sufficient.
| Setting | Recommendation |
| --------------------- | ------------------------ |
| Consent button | Hide |
| Disclaimer visibility | Hide after first message |
| Company policies | Both URLs, Header menu |
Every user sees the disclaimer when the chat opens, then it clears once they engage. Your legal links remain one tap away in the header menu without occupying the conversation window.
### Long or complex legal copy
Your legal team's required wording is too long to sit comfortably in a chat widget.
Keep the disclaimer message short (a sentence or two covering AI disclosure and recording) and link out to the full hosted document from within the disclaimer. The canonical legal copy stays on your website where your legal team maintains it, and the widget stays readable. Update the hosted page and the link keeps pointing to the current version, with no widget changes needed.
### Multi-market deployments
One widget serving users across several countries or languages.
Author your disclaimer per language, with your default language set to your primary market. Users see the disclosure in their own language, and any user whose language does not yet have a translation sees the default. Pair this with per-market legal review: disclosure requirements differ between regions, and your translated copy should reflect local requirements rather than being a direct translation.
### Explicit AI disclosure requirements
Regions or sectors where you are required to clearly inform users they are talking to an AI system.
Keep the AI disclosure in the disclaimer message itself rather than behind a link, and set visibility to **Always show** so the disclosure remains on screen. Whether you also require consent is a question for your legal team.
## Good to know
* **Changes preview live.** Edits in the Content tab appear in the on-page preview immediately, before you save and publish.
* **No code deploy needed.** Published changes reach your widget without any engineering work on your side.
* **Settings are audit-logged.** Changes to your disclaimer configuration are recorded, so your compliance team can evidence what was displayed and when.
## Related
Serve disclaimer copy and widget UI in your users' languages.
Configure the rest of your web chat widget's appearance and behaviour.
# Web chat localisation
Source: https://docs.poly.ai/messaging-channel/advanced/localisation
Localise your web chat widget experience to serve customers in multiple languages.
Web chat Localisation makes the widget interface feel native in every language the customer supports. When a visitor lands on the page, the widget reads their browser language and renders all static UI text (Headers, labels, status messages) in that language. The language signal is passed to the agent so responses follow suit.
## How it works
* **Browser-language detection.** When a visitor opens the widget, it reads their browser language setting and renders all static UI text (headers, labels, status messages) in that language.
* **Mid-session language following.** If the agent switches language during a conversation (for example, a visitor starts in English and the agent responds in Spanish once it detects the shift), the widget UI switches to match. No half-English, half-Spanish interface.
* **Manual disclaimer translation.** Because the disclaimer is customer-editable static text, it is not auto-translated. You supply the disclaimer copy per language directly in Agent Studio, and the correct version is shown when the widget renders in that language.
## What can be localised
Customer-editable content in the widget supports multiple translations. In the widget's **Content** section you can add a translation for each language enabled on the widget, including:
* The **disclaimer** shown before the conversation starts.
* Anything that is **not** customer-editable in the widget configuration can be localised.
Each field accepts one translation per enabled language, and the widget shows the version that matches the visitor's selected language.
## What cannot be localised
For static text you can edit (disclaimer, widget header, greeting overrides, custom buttons), auto-translation is not applied. You supply each language version directly in Agent Studio.
## Configure languages on your widget
Localisation is configured entirely in the "**widgets"** configuration page. In the widget's language dropdown you select which languages the widget should offer. The only languages available in that dropdown are those already configured on your chat agent.
1. Open the widget configuration.
2. In the language dropdown, select the languages you want the widget to offer.
* Only languages configured on your chat agent appear in the dropdown. If a language you need is missing, add it to the agent first.
3. Save the widget configuration.
If required you can create multiple variants of widgets and apply different language settings per widget depending on your clients needs.
## Add translations for content
1. In the widget configuration, open the **Content** section.
2. For each translatable field (for example, the disclaimer), add a translation for each language enabled on the widget.
3. Save the widget configuration.
The widget serves the translation that matches the visitor's active language. If no translation is provided for a given language, the default language version is used.
## Language support
### Available today
The core European and North American languages ship in production. If your markets sit inside this list, you can go live with no additional gating:
* English (US) `en-US`
* English (UK) `en-GB`
* Spanish (US) `es-US`
* French (Canada) `fr-CA`
### Additional languages
Further languages are rolling out incrementally. If you need a language that is not yet available, contact your PolyAI account team so we can confirm timelines or scope coverage for your deployment.
## Example use cases
* **Multi-region rollout.** A US retailer expands to Canada. The Canadian French market gets a French-Canadian widget experience automatically when the visitor's browser is set to `fr-CA`.
* **Regulated multilingual market.** A financial services firm in Quebec is legally required to offer service in French. Browser language detection means the widget renders in French from the moment a francophone visitor lands.
* **Mid-conversation language switch.** A visitor starts in English and then asks a question in Spanish. The agent switches to Spanish, and the widget UI switches with it.
* **Localised disclaimers.** A healthcare provider serving English and French customers enters both language versions of their consent disclaimer in Agent Studio. The correct one is shown based on the visitor's browser language.
## Constraints and caveats
* Localisation is driven by the visitor's browser language. Routing to different widget configurations by domain, subdomain, or brand is not part of this release. Talk to your account team if you need that shape.
* Agent response quality in non-English is trained per language. If you need a specific regional variant (for example, French-Canadian rather than French), confirm training coverage with your account team.
## Related
* [Chat configuration](/messaging-channel/advanced/chat-configuration)
* [Multilingual agents](/behavior/language/multilingual)
* [Translations](/behavior/language/translations)
* [Language coverage](/behavior/language/language-coverage)
# Verified Context Injection
Source: https://docs.poly.ai/messaging-channel/advanced/verified-context-injection
Pass verified user context (e.g. signed-in identity) into your web chat agent securely at session start.
Verified Context Injection lets your backend pass trustworthy, cryptographically verified user context into a PolyAI conversation. Your backend creates a short-lived signed token (a JWT, or JSON Web Token) using a signing key you generate in Agent Studio. PolyAI verifies the signature, stores the verified values against the conversation, and makes them available to the agent's functions as read-only fields at every turn.
Think of it as a per-conversation lockbox. Only your backend, which holds the signing key, can put values into it. The agent's functions can only read them out. Nothing in the browser, the widget, or any other layer can forge or tamper with the values.
## The problem it solves
A basic implementation of context injection lets you pass casual context into the widget for the agent to use, but with that approach the widget can't vouch for it. Anyone can edit a value in the browser, so it can't be relied on for anything that matters.
Verified Context Injection closes that gap. Your backend **signs** the context; PolyAI **verifies** the signature server-side before the agent sees anything. That makes a value like `account_tier: "premium"` trustworthy enough to gate entitlements on.
## What it does
* **Sets context from a trusted origin.** Your backend signs a JWT with a key you generated in Agent Studio. The token has a 30-second lifetime and carries up to 8 KB of context data (fields such as customer identifier, account tier, or a short-lived auth token). The widget carries the opaque token but never sees the signing key.
* **Verifies the signature and stores verified values.** PolyAI validates the signature, checks the token binding and expiry, then stores the verified context payload against the conversation. Nothing bypasses this validation path.
* **Reads into functions as read-only fields.** Every turn, the function runtime receives a read-only object (accessed as `conv.context_vault`) with the verified fields. Functions can act on them, for example calling your API with the customer identifier, checking the account tier, or using a short-lived auth token to call a downstream service. Values are never surfaced in prompts or transcripts unless a function explicitly copies them into state.
## Benefits
* **Trust.** You know the identity and entitlement values came from your own systems, not from a manipulated browser session. That is the difference between an assistant that can act on account data and one that cannot.
* **Personalised from the first turn.** The agent skips identity verification questions when the user is already authenticated on your side. Cuts turns, cuts abandonment.
* **Compliance-friendly.** Verified context lives only as long as the conversation, is never surfaced in prompts by default, and is scoped strictly to a single session. The security posture holds up in procurement conversations.
## How it works
* **A conversation starts.** PolyAI mints a unique `context_id` for that specific conversation and hands it to the widget. Just before the agent joins, the widget calls your `onContextRequired` callback with it.
* **Your backend signs a token.** A short-lived JWT whose subject is exactly that `context_id`, carrying the context payload. Signing always happens on your server, never in the browser.
* **PolyAI verifies and attaches it.** The vault checks the signature and that the token was minted for this conversation, then stores the context. The agent reads it on turn 1.
Because verification is server-side and bound to a PolyAI-issued `context_id`, a token can't be replayed against a different conversation, and a forged token is rejected.
**Two usage patterns, one integration.** The same callback powers both:
* **At conversation start** (default, recommended). Verified context is present before the agent joins.
* **Mid-conversation refresh.** If the user's context changes while chatting, your page calls `refreshVerifiedContext()`. Same callback, fresh token, last write wins, no interruption to the live conversation.
## Example use cases
* **Entitlement gating.** An `account_tier` of `premium` is signed by your backend, so the agent can act on it with confidence rather than taking the browser's word for it.
* **Identified-from-turn-1 conversations.** A `customer_id` is attached before the agent joins, so there's no "can I take your account number?" opening.
* **Passing a short-lived auth token.** Your backend mints and signs it; the agent receives it as trusted, read-only context.
* **User logs in partway through a chat.** Your page calls `refreshVerifiedContext()` from your login success handler. The same callback re-runs, a fresh token replaces the old context, and the conversation carries on uninterrupted.
## What you need to build
* A **signing key** provisioned for your project (a key ID and a secret from Agent Studio or your PolyAI representative). The key ID is stable across rotation.
* A **backend endpoint** that signs a JWT with that secret.
* A small amount of JavaScript on your page to register the `onContextRequired` callback inside `onReady`.
You never fetch, store, or submit the `context_id` or the token, the widget owns all of that. You only sign and return.
## Key facts
| Fact | Value |
| ----------------------------------- | ------------------------------------------------------------- |
| Token type | JWT, HS256, signed with a shared secret |
| Token lifetime | ≤ 30 seconds (short-lived, single-use in practice) |
| Context payload | Flat JSON: string / number / boolean values, ≤ 8 KB |
| Bound to | A per-conversation `context_id`, cannot be replayed elsewhere |
| Typical round-trip | 200–500 ms; agent-join is held only for the callback path |
| Where signing happens | Your backend, never in the browser |
| Impact on integrations not using it | None, zero delay, behaviour unchanged |
| Availability | Web chat widget via the browser SDK |
## It always fails open
The conversation **always** starts. Verified context is best-effort and never blocks the user.
| Situation | What the user experiences | Context attached? |
| ----------------------------------------------------- | --------------------------------------- | ----------------- |
| Valid token, vault accepts it | Normal conversation | Yes |
| No callback registered | Normal conversation, zero delay | No |
| Callback returns nothing / throws | Normal conversation | No |
| Callback hangs | Proceeds after a short timeout (\~4–5s) | No |
| Token rejected (bad signature / expired / mismatched) | Normal conversation | No |
## Constraints and caveats
* **Signed, not encrypted.** The token proves *origin*, not *secrecy*, values travel in plaintext inside it. Hyper-sensitive data isn't a fit today; a server-to-server path is planned. Speak to your PolyAI representative if you need it.
* **Web chat only.** Verified Context Injection today covers the web chat widget via the browser SDK. Server-to-server and voice transports are future work.
* **V2 widget only.** V1 is deprecated.
* **Your agent must degrade gracefully.** Because context can legitimately be absent, agents should fall back sensibly, for example asking the user to identify themselves, rather than assuming verified context is always there.
## Good to know
* **Signing key provisioning.** Get a key from Agent Studio or your PolyAI representative.
* **One integration point, two behaviours.** You only ever implement `onContextRequired`. The mid-conversation refresh reuses it rather than needing anything new wired up.
* **The vault set is idempotent.** A refresh fully replaces the previous context, last write wins.
## Related pages
Step-by-step integration: register the callback, sign the token, handle refresh and timeouts.
# Android SDK
Source: https://docs.poly.ai/messaging-channel/android-sdk
Embed a PolyAI messaging agent natively in your Android app with a headless Kotlin library.
The Android SDK is a native Kotlin library that embeds PolyAI's messaging agent directly inside your Android app. It's **headless by design** — you own the UI, PolyAI provides the AI layer underneath. Your app connects to the same agent logic used across voice, webchat, and other channels. As of v0.9.0 it covers two channels, chat (`ai.poly:messaging`) and live two-way [voice calls](/messaging-channel/android-sdk-voice) (`ai.poly:voice`).
The Android SDK wraps the [Messaging API](/api-reference/messaging/introduction). All WebSocket events, streaming, and handoff behavior documented in the API reference apply.
polyai/android-sdk — Kotlin library, Maven Central, and example apps.
## How it works
The SDK handles authentication, session management, WebSocket connections, and reconnection logic. Your app sends and receives messages through the SDK and renders them however you choose — in **Jetpack Compose** or **Android Views**.
Voice calling ships as a separate artifact, `ai.poly:voice`, so chat-only apps stay lean. It reuses the same configuration and vocabulary as messaging, so there are no new concepts to learn if you already run chat. See [Voice calling (Android)](/messaging-channel/android-sdk-voice) for the full guide.
Add the SDK to your project via **Maven Central**.
Add your API key (from Agent Studio) and ensure your app's **package name** (`applicationId`) matches the host registered in Agent Studio for your API key.
Initialize the SDK once in `Application.onCreate()`, then call `PolyMessaging.chat()` to get a `ChatSession`. The SDK handles access token exchange and WebSocket connection automatically.
Observe `ChatSession` state via Kotlin `StateFlow` — collect messages, connection status, typing indicators, and more. Render the conversation in your own UI components.
## Installation
The SDK is published to Maven Central as `ai.poly:messaging`. Ensure `mavenCentral()` is in your repositories (it's there by default in new Android projects).
```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
// build.gradle.kts
dependencies {
implementation("ai.poly:messaging:0.9.0")
implementation("ai.poly:voice:0.9.0") // only if you need voice calling
}
```
```groovy theme={"theme":{"light":"github-light","dark":"github-dark"}}
// build.gradle
dependencies {
implementation 'ai.poly:messaging:0.9.0'
implementation 'ai.poly:voice:0.9.0' // only if you need voice calling
}
```
```toml theme={"theme":{"light":"github-light","dark":"github-dark"}}
# libs.versions.toml
[versions]
polyMessaging = "0.9.0"
[libraries]
poly-messaging = { module = "ai.poly:messaging", version.ref = "polyMessaging" }
poly-voice = { module = "ai.poly:voice", version.ref = "polyMessaging" } # only if you need voice calling
```
```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
// build.gradle.kts
dependencies {
implementation(libs.poly.messaging)
implementation(libs.poly.voice) // only if you need voice calling
}
```
### Requirements
| Requirement | Minimum |
| ------------------ | --------------------------------- |
| **Android** | API 24 (Android 7.0) |
| **compileSdk** | 36 |
| **Kotlin** | 2.2+ |
| **JDK** (to build) | 17 |
| **Java consumers** | Supported |
| **R8 / minify** | Works without extra configuration |
No permissions to declare — the SDK's manifest merges `INTERNET` and `ACCESS_NETWORK_STATE` into your app automatically. (Voice calls need `RECORD_AUDIO` — see [Voice calling permissions](/messaging-channel/android-sdk-voice#permissions).)
## Authentication setup
The Android SDK authenticates using a connector token and your app's package name.
Voice calling needs one further credential from the same page, the **WebRTC token**, a distinct value that authenticates the media connection. Both tokens come from the same connector you use for chat. See [Voice calling credentials](/messaging-channel/android-sdk-voice#credentials).
In Agent Studio, go to **Messaging > API Configuration** and generate a new Messaging API key.
Your app's `applicationId` is sent as the `X-Host` header. It must match the host registered in Agent Studio for your API key.
## Quick start
Initialize the SDK once in `Application.onCreate()`, then create a `ChatSession` and render messages.
### Initialize once
```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
// HelloApplication.kt
import ai.poly.messaging.Configuration
import ai.poly.messaging.PolyMessaging
import android.app.Application
class HelloApplication : Application() {
override fun onCreate() {
super.onCreate()
PolyMessaging.initialize(
this,
Configuration(apiKey = "YOUR_API_KEY"),
)
}
}
```
Register it in your manifest with `android:name=".HelloApplication"`. No network happens at init — the work starts when you call `chat()`.
### Build the chat UI
```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
// MainActivity.kt
import ai.poly.messaging.ChatMessage
import ai.poly.messaging.ChatSession
import ai.poly.messaging.PolyMessaging
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
@Composable
fun ChatScreen() {
val session: ChatSession = remember { PolyMessaging.chat() }
val messages by session.messages.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope()
var input by remember { mutableStateOf("") }
Column(Modifier.fillMaxSize().imePadding()) {
LazyColumn(Modifier.weight(1f)) {
items(messages, key = { it.id }) { message ->
val mine = message is ChatMessage.User
Box(Modifier.fillMaxWidth().padding(vertical = 4.dp, horizontal = 8.dp)) {
Text(
message.text ?: "",
Modifier.align(if (mine) Alignment.CenterEnd else Alignment.CenterStart),
)
}
}
}
Row(Modifier.padding(8.dp)) {
TextField(value = input, onValueChange = { input = it }, modifier = Modifier.weight(1f))
Button(onClick = {
val body = input.trim()
if (body.isNotEmpty()) {
input = ""
scope.launch { runCatching { session.send(body) } }
}
}) { Text("Send") }
}
}
}
```
```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
// ChatActivity.kt
import ai.poly.messaging.ChatMessage
import ai.poly.messaging.ChatSession
import ai.poly.messaging.PolyMessaging
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.launch
class ChatActivity : ComponentActivity() {
private lateinit var binding: ActivityChatBinding
private val session: ChatSession by lazy { PolyMessaging.chat() }
private val adapter = MessageAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityChatBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.list.layoutManager = LinearLayoutManager(this).apply { stackFromEnd = true }
binding.list.adapter = adapter
binding.send.setOnClickListener {
val body = binding.composer.text.toString().trim()
if (body.isNotEmpty()) {
binding.composer.setText("")
lifecycleScope.launch { runCatching { session.send(body) } }
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
session.messages.collect { messages ->
adapter.submit(messages)
if (messages.isNotEmpty()) binding.list.scrollToPosition(messages.size - 1)
}
}
}
}
}
```
## Key features
### Session persistence
Conversations survive an app relaunch. `PolyMessaging.chat()` resumes the stored session automatically if it's still valid, or starts a fresh one — you don't need to check anything first. Sessions can only be resumed within the session timeout of **\~10 minutes** (matching the backend's WebSocket idle timeout); after that, `chat()` starts a new conversation.
Use `PolyMessaging.start()` when you want to *always* begin fresh (an explicit "New chat" entry point), and `PolyMessaging.hasResumableSession()` when you want to offer the user the choice before showing the chat:
```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
if (PolyMessaging.hasResumableSession()) {
// offer "Resume previous chat?" → PolyMessaging.chat()
// or "Start new" → PolyMessaging.start()
}
```
### Streaming responses
Streaming is **on by default** — agent replies grow token-by-token. The SDK reassembles chunks and updates `session.messages` automatically. To switch to complete-message bubbles, set `streamingEnabled = false` on the `Configuration`.
```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
PolyMessaging.initialize(
this,
Configuration(apiKey = "YOUR_API_KEY", streamingEnabled = false),
)
```
### Handoff to live agents
The full [handoff flow](/api-reference/messaging/handoff) is supported. When the PolyAI agent triggers a handoff, the SDK delivers the same handoff events via `ChatMessage.System` messages with typed `SystemEvent` cases (`HandoffStarted`, `QueueStatus`, `LiveAgentJoined`, etc.). Live agent messages arrive as `ChatMessage.Agent` with `agentKind == AgentKind.LIVE`.
### Response suggestions
Agent messages can include `suggestions` — pre-written reply options. Render these as tappable chips in your UI. When the user taps one, call `clearSuggestions(messageId)` then `send(suggestion.messageText)`.
### Attachments
Agent messages may include rich content via the `attachments` field — images (`AttachmentContentType.IMAGE`), link cards (`AttachmentContentType.URL`), and call-to-action phone buttons (`callActions`).
### Delivery tracking
User messages appear immediately as `Delivery.PENDING`, then settle to `SENT` or `FAILED`. The SDK never auto-resends — one send is one send, so a message can't be delivered twice. An unconfirmed message is marked `FAILED` as soon as it can't be confirmed: immediately if it was sent while offline, at the moment the connection drops if it was still in flight, or after a 10-second wait if the server never echoes it back. **Your UI must offer its own retry affordance** — it isn't a backstop for SDK retries, it's the only way a failed message gets sent. On `FAILED`, call `removeMessage(draftId)` then re-send the text.
### Connection & reconnect
The SDK *reconnects* automatically with exponential backoff and jitter. Reconnection applies to the socket only — failed messages are never resent automatically (see [Delivery tracking](#delivery-tracking) above). Observe `session.connection` to show a reconnect banner:
```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
// StateFlow —
// Idle / Connecting / Open / Reconnecting(attempt) / Closing / Closed(event) / Failed(reason)
session.connection.collect { status ->
showBanner = status is ConnectionStatus.Reconnecting
}
```
When the reconnect budget is exhausted (`ConnectionStatus.Failed`), recover with `session.client.startNewSession()`.
### Voice calling
`ai.poly:voice` places live, two-way WebRTC [voice calls](/messaging-channel/android-sdk-voice) to the same agent that powers your chat. Calls are user-initiated: the user taps to call your agent.
```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
import ai.poly.voice.PolyVoice
val call = PolyVoice.call(
context,
Configuration(apiKey = "YOUR_API_KEY"), // connector token — Agent Studio › Connector Settings
VoiceOptions(webrtcToken = "YOUR_WEBRTC_TOKEN"), // WebRTC token — same place, a distinct value
)
// Observe the call lifecycle (Idle → Connecting → Connected → Ended / Failed).
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
call.state.collect { state -> render(state) }
}
}
// After the RECORD_AUDIO permission is granted:
lifecycleScope.launch { call.start() }
// In-call controls:
call.setMuted(true) // mute the mic
call.end() // hang up and release the mic
```
A call needs **two credentials** (the API key plus a separate WebRTC token, both from Agent Studio › Connector Settings) and the `RECORD_AUDIO` runtime permission granted before starting; the SDK's manifest declares everything else a basic call needs. Beyond that the SDK handles the hard parts for you: accessory-aware audio routing, automatic reconnection on transient network drops, and graceful handling of interruptions like incoming phone calls.
The full voice guide: installation, credentials, permissions, audio output, interruptions, background calls, and R8.
## Configuration reference
| Field | Default | Description |
| ---------------------- | ---------------- | ------------------------------------------------------------------ |
| `apiKey` | — (required) | API key from Agent Studio |
| `environment` | `Environment.US` | `US` / `UK` / `EUW` / `cluster("name")` / `custom(restUrl, wsUrl)` |
| `hostIdentifier` | package name | `X-Host` for connector validation |
| `streamingEnabled` | `true` | Token-by-token (`true`) or complete bubbles (`false`) |
| `logLevel` | `LogLevel.ERROR` | `NONE` / `ERROR` / `WARN` / `INFO` / `DEBUG` |
| `maxReconnectAttempts` | `10` | Reconnect budget before `Failed` |
The same `environment` also selects the gateway for voice calls. The full configuration reference on GitHub covers the remaining options, error handling and connection states.
### Environments
| Environment | Endpoint |
| ----------------------------- | -------------------------- |
| `Environment.US` (default) | `messaging.us-1.poly.ai` |
| `Environment.UK` | `messaging.uk-1.poly.ai` |
| `Environment.EUW` | `messaging.euw-1.poly.ai` |
| `Environment.cluster("name")` | `messaging..poly.ai` |
## ChatSession reference
### State (read-only `StateFlow` properties)
| Property | Type | Description |
| ---------------- | ------------------------------ | -------------------------------------------------------------------- |
| `messages` | `StateFlow>` | Full transcript — `ChatMessage.User` / `.Agent` / `.System` |
| `isReady` | `StateFlow` | Connected and ready to send |
| `connection` | `StateFlow` | Socket state |
| `isAgentTyping` | `StateFlow` | Show typing indicator |
| `agentAvatarUrl` | `StateFlow` | Latest agent avatar |
| `hasEnded` | `StateFlow` | Conversation is over |
| `failureReason` | `StateFlow` | Terminal failure (invalid key, reconnect exhausted, session expired) |
### Methods
| Method | Description |
| ----------------------------- | --------------------------------------- |
| `suspend send(text)` | Send a user message (optimistic) |
| `suspend sendTyping()` | Broadcast typing (throttled internally) |
| `suspend end()` | End the conversation |
| `removeMessage(draftId)` | Drop a failed draft before re-sending |
| `clearSuggestions(messageId)` | Clear quick-reply pills for a message |
| `clearChat()` | Wipe the transcript |
| `close()` | Tear down this session's observers |
## Platform values
When the SDK creates a session, it sets `platform` to `android` automatically, alongside a `device_type` (mobile / tablet). This is visible in Agent Studio analytics and can be used in your agent logic to tailor behavior for mobile users. It identifies the **device**, not the channel — for the channel a conversation arrives on, see [Multichannel](#multichannel) below.
| Platform value | Source |
| -------------- | ---------------------------- |
| `android` | Android SDK (native) |
| `ios` | iOS SDK (native) |
| `ios-web` | Webchat widget on iOS Safari |
| `web` | Webchat widget on desktop |
## Limitations
These limitations apply to the initial release. Check the [release notes](/releases/overview) for updates.
| Limitation | Details |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **No remote notifications** | The SDK has no remote push integration (no FCM). Messages arrive over the WebSocket while the app is open and for a short period after backgrounding; what's missing is lock-screen delivery once Android kills the app. Local notifications are possible while the session is live. |
| **User-initiated calls only** | Calls are always started by the user from inside your app. The agent cannot ring the user (no push-triggered incoming calls). |
| **Android only** | Native Kotlin only; no React Native or Flutter wrapper. |
## Example apps
The android-sdk repository ships runnable example apps for both chat and voice, each mirrored across Compose and Views: a full chat implementation with streaming, suggestions and handoff, and a one-screen tap-to-call demo with the audio-output picker. With a 7 rung example ladder:
| Level | What it adds |
| --------------------- | --------------------------------------------------------- |
| **01 Hello** | Initialize, render, send |
| **02 Standard** | Typing, suggestions, delivery, reconnect, end + start-new |
| **03 Rich Content** | Attachments, link cards, tel: actions, Markdown |
| **04 Resilience** | Offline banner, loading skeleton, terminal error + retry |
| **05 Handoff** | Full live-agent ladder |
| **06 Full Reference** | Production resume + start-new flows |
| **07 Playground** | Diagnostics, runtime config, streaming toggle |
Browse the examples on [GitHub](https://github.com/polyai/android-sdk/tree/main/examples).
## Multichannel
The Android SDK connects to the same agent project as your voice and webchat channels. Agent behavior, knowledge, and flows are shared — only channel-specific settings (greetings, formatting) differ. See [multichannel agents](/messaging-channel/multichannel) for how to tailor behavior per channel.
Two values tell your agent where a conversation came from, and they answer different questions:
* **`conv.channel_type`** identifies the **channel**. Its possible values are `webchat.polyai`, `chat.polyai`, `sms.twilio`, `sms.polyai`, `rcs.polyai`, `whatsapp.polyai`, and `sip.polyai` — it is never `"android"` or `"ios"`.
* **`platform`** identifies the **device**: the SDK sends `platform: "android"` (alongside `device_type`) when the session is created. Use this to tailor behavior for native app users.
To branch on the channel in your agent's start function:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start(conv):
if conv.channel_type == "webchat.polyai":
conv.state.greet_message = "Hi there! How can I help?"
elif conv.channel_type == "whatsapp.polyai":
conv.state.greet_message = "Hi! How can I help you today?"
```
## Related pages
WebRTC voice calls with ai.poly:voice: setup, permissions, audio output, and background calls
Full WebSocket protocol, events, streaming, and handoff
Access tokens, session creation, and platform values
Build agents that work across voice, webchat, and mobile
# Voice calling (Android SDK)
Source: https://docs.poly.ai/messaging-channel/android-sdk-voice
Add live two-way WebRTC voice calls to your Android app with ai.poly:voice.
`ai.poly:voice` places live, two-way WebRTC voice calls to a PolyAI agent — the companion artifact to the [Android SDK's](/messaging-channel/android-sdk) chat product. It ships **separately** so chat-only apps stay lean (the call path pulls in the native libwebrtc audio engine), and it reuses the messaging `Configuration` plus the same `CallState` / `PolyError.Voice` / `Environment` vocabulary — no new concepts.
Calls are user-initiated: the user taps to call your agent. Inbound (push-triggered) calls are not supported.
polyai/android-sdk — includes the full polyvoice technical guide and runnable voice example apps.
## Installation
```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
// build.gradle.kts
dependencies {
implementation("ai.poly:messaging:0.9.0")
implementation("ai.poly:voice:0.9.0")
}
```
## Quickstart
A call needs the **`RECORD_AUDIO`** runtime permission. The SDK declares it in its manifest, but your app must request the grant from the user before starting a call.
```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
import ai.poly.voice.PolyVoice
val call = PolyVoice.call(
context,
Configuration(apiKey = "YOUR_API_KEY"), // connector token — Agent Studio › Connector Settings
VoiceOptions(webrtcToken = "YOUR_WEBRTC_TOKEN"), // WebRTC token — same place (see Credentials below)
)
// Observe the call lifecycle (Idle → Connecting → Connected → Ended / Failed).
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
call.state.collect { state ->
when (state) {
is CallState.Connected -> showInCallUi()
is CallState.Failed -> showError(state.error) // a PolyError.Voice
is CallState.Ended -> dismissInCallUi()
else -> Unit
}
}
}
}
// After RECORD_AUDIO is granted:
lifecycleScope.launch { call.start() } // suspends until the call is connecting; throws on setup failure
// In-call controls:
call.setMuted(true) // mute the mic
call.end() // hang up and release the mic
```
`CallState`, `PolyError.Voice.*`, `Configuration`, and `Environment` are the same types from `ai.poly:messaging`. Java callers get `Executor` + `Callback` overloads of `start` / `end` / `setMuted`, mirroring the chat API.
## Credentials
A voice call needs **two credentials**, both on your agent in [Agent Studio](https://studio.poly.ai) › Connector Settings (the same connector you use for chat):
| Value | What it is | Required? | Sent as |
| --------------------------------------------- | ------------------------------------------------------------------------------------------- | --------- | ----------------------------------------- |
| **API key** — `Configuration.apiKey` | your **connector token** | **Yes** | `X-Token` (authenticates the call) |
| **WebRTC token** — `VoiceOptions.webrtcToken` | the **gateway auth token** for the media connection — a **distinct** token from the API key | **Yes** | the offer `authToken` + ICE-servers fetch |
Both are always required and always distinct: the API key authenticates the *connector* and is shared by chat and voice, so it lives on the shared `Configuration`; the WebRTC token authenticates the *media gateway* only, so it lives on `VoiceOptions`.
Two more values have sensible defaults, so most apps don't set them:
* **`environment`** defaults to `Environment.US`. Set `.UK` / `.EUW` — or `.cluster("…")` for a named cluster — only if your agent lives in another region. The same `Environment` you use for chat also selects the voice gateway.
* **`hostIdentifier`** (sent as `X-Host`) defaults to your app's package name (`applicationId`).
**Custom / self-hosted gateway:** the WebRTC gateway host is derived from your `Environment`. If you run a dev or self-hosted gateway, set `VoiceOptions.signalingHost` (no scheme, e.g. `"webrtc-gateway.example.com"`) — it's **required** with `Environment.Custom`, since the gateway host can't be derived from a custom messaging endpoint.
## Permissions
The SDK's manifest **auto-merges** the three permissions every call needs — you don't declare these:
```xml theme={"theme":{"light":"github-light","dark":"github-dark"}}
```
`RECORD_AUDIO` is a **runtime** permission — request it before `start()` (the call fails fast with `PolyError.Voice.MediaFailed` if it's missing). Add the rest **only for the optional features you use**:
```xml theme={"theme":{"light":"github-light","dark":"github-dark"}}
```
## Audio output
By default the call **follows the connected accessory** — a wired or Bluetooth headset is used automatically (and auto-switches when you plug/unplug one mid-call); when nothing's connected it falls back to the **loudspeaker** (`VoiceOptions(speakerphone = false)` falls back to the earpiece instead). To let users **pin a specific output**, observe `call.audio` and call `setAudioDevice` — and pass `null` to return to automatic:
```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
// A consistent snapshot: the outputs available now + the active one.
lifecycleScope.launch {
call.audio.collect { state ->
renderPicker(state.availableDevices, selected = state.selectedDevice)
}
}
call.setAudioDevice(speakerDevice) // route to a device from availableDevices
call.setAudioDevice(null) // revert to automatic routing (wired > Bluetooth > earpiece/speaker)
```
* `AudioDevice.type` is one of `EARPIECE` / `SPEAKER_PHONE` / `WIRED_HEADSET` / `BLUETOOTH`; `name` is a picker-friendly label. The list updates live as headsets connect/disconnect.
* **Switching is asynchronous** — Bluetooth can take a few seconds to engage. Drive your UI off `call.audio`, not off `setAudioDevice` returning. Selecting an unavailable device is a no-op.
* **Bluetooth needs `BLUETOOTH_CONNECT`.** The library does **not** declare this runtime permission for you; add it to your app and request the grant if you want Bluetooth outputs to appear. Without it, Bluetooth is simply absent — no crash.
## Interruptions (incoming calls, other apps)
The SDK manages audio focus for you — it takes focus on `start()` and releases it on `end()`/teardown. It also **reacts to losing focus** while a call is live, so you don't have to:
* A **transient** loss (a notification, a navigation prompt) **mutes the mic** for the duration and restores it automatically when focus returns — the call stays `Connected`. Nothing to handle.
* A **permanent** loss (the user answers an **incoming phone call**, or another app starts an exclusive audio session) **ends the call**: it surfaces as `CallState.Failed(PolyError.Voice.Interrupted)` and the mic is released.
So all you do is observe `state` and tell the user — the mic is already released for you:
```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
lifecycleScope.launch {
call.state.collect { state ->
if (state is CallState.Failed && state.error is PolyError.Voice.Interrupted) {
showBanner("Call interrupted — tap to call again") // e.g. an incoming phone call ended it
}
}
}
```
## Background calls
A `VoiceCall` is a plain object on its own coroutine scope — it is **not** tied to your Activity/Fragment lifecycle, so the SDK won't end a call just because your UI is backgrounded. **However**, when your app goes to the background Android 9+ **cuts mic capture** and **throttles the WebRTC media/network threads**, so the connection silently dies within \~15s and the SDK reports `CallState.Failed(PolyError.Voice.Disconnected)`. To keep a call alive in the background you need **two things** while the call is active:
1. A **microphone foreground service** — grants background mic + keeps the process foregrounded.
2. A **partial wake lock** — keeps the CPU running for the media/network threads.
```xml theme={"theme":{"light":"github-light","dark":"github-dark"}}
```
```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
// in the service's onStartCommand, after startForeground(...):
wakeLock = getSystemService(PowerManager::class.java)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "yourapp:voice-call").apply { acquire() }
// release it in onDestroy()
```
Start the service **before** `call.start()` and stop it when the call ends. The SDK is headless and deliberately doesn't impose a service (it has no notification UI) — that's your app's call. A foreground-only call works fine without any of this. Both voice example apps ship a complete, working `CallForegroundService` you can copy.
## R8 / ProGuard
**No keep rules needed in your app.** The `ai.poly:voice` AAR ships **consumer R8 rules** (applied automatically) that keep `org.webrtc.**` — libwebrtc is reached by name over JNI from native code, which R8 can't see, so stripping it would crash the audio engine. If you maintain a global `proguard-rules.pro` that's unusually aggressive, the shipped consumer rules still protect the SDK; you don't add anything.
## Example apps
A one-screen **tap-to-call** demo with the audio-output picker ships in both toolkits — drop your connector token and WebRTC token into the `PolyVoice.call(...)` block and run:
* [`examples/voice/compose`](https://github.com/polyai/android-sdk/tree/main/examples/voice/compose)
* [`examples/voice/views`](https://github.com/polyai/android-sdk/tree/main/examples/voice/views)
## Related pages
Chat with ai.poly:messaging: installation, authentication, sessions, and UI
Build agents that work across voice, webchat, and mobile
# Web Chat
Source: https://docs.poly.ai/messaging-channel/introduction
Deploy a webchat version of your agent alongside voice.
Use webchat to deploy your agent on your website alongside voice. Webchat lets customers interact with the same agent logic through text, with channel-specific styling, safety filters, and widget configuration.
Configure the chat experience from **Messaging** in the sidebar.
## Chat channel pages
Configure LLM, style prompt, greeting, and safety filters
Create, style, and deploy widgets to your website
To embed the webchat widget on your website, see the [widget deployment guide](/widgets/configure). This requires adding a code snippet to your site.
## Getting started
Setting up webchat involves two main steps:
Go to **Messaging > Chat Configuration** to:
* Select your language model
* Add an optional style prompt for chat-specific tone
* Set your greeting message
* Configure safety filters
Go to **Widgets** to:
* Create a new widget for your website
* Style the appearance (header, colors, agent avatar)
* Configure disclaimers and consent
* Generate and embed the script tag
## Key features
### LLM and style control
Select the language model powering chat responses – PolyAI's [Raven 3.5](/behavior/models/raven) is recommended for chat, delivering natural paraphrasing and strong grounding. Optionally add a style prompt to tailor tone and formatting specifically for the chat channel.
### Multiple widgets
Create separate widgets for different websites, variants, or environments. Each widget can have unique styling and configuration.
### Safety filters
Configure channel-specific content filtering with adjustable strictness levels for violence, hate, sexual content, and self-harm categories.
### Markdown support
Chat messages support formatted markdown including bold, italics, lists, links, and code blocks. See [chat configuration](/messaging-channel/advanced/chat-configuration#markdown-support) for details.
### Variant support
Select which [variant](/knowledge/variants/introduction) each widget connects to for location-specific or configuration-specific chat experiences.
### Environment alignment
Chat configuration changes take effect immediately when saved. Unlike other project settings, webchat does not follow the environment branching system – there is no Sandbox, Pre-release, or Live promotion step. Use the **Test Agent Chat** panel to verify changes before saving.
## Related pages
Configure LLM, greeting, and safety filters
Create and deploy chat widgets
Embed the widget on your website
Configure multi-site setups
# iOS SDK
Source: https://docs.poly.ai/messaging-channel/ios-sdk
Embed a PolyAI messaging agent natively in your iOS app with a headless Swift library.
The iOS SDK is a native Swift library that embeds PolyAI's messaging agent directly inside your iOS app. It's **headless by design** — you own the UI, PolyAI provides the AI layer underneath. Your app connects to the same agent logic used across voice, webchat, and other channels. As of v0.9.0 it covers two channels, chat (`PolyMessaging`) and live two-way [voice calls](/messaging-channel/ios-sdk-voice) (`PolyVoice`).
The iOS SDK wraps the [Messaging API](/api-reference/messaging/introduction). All WebSocket events, streaming, and handoff behavior documented in the API reference apply.
polyai/ios-sdk — Swift package, CocoaPods, and example apps.
## How it works
The SDK handles authentication, session management, WebSocket connections, and reconnection logic. Your app sends and receives messages through the SDK and renders them however you choose.
Voice calling ships as a separate product, `PolyVoice`, so chat-only apps never link the WebRTC binary. It reuses the messaging `Configuration` and the same `CallState` / `PolyError` vocabulary, so there are no new concepts to learn if you already run chat. See [Voice calling (iOS)](/messaging-channel/ios-sdk-voice) for the full guide.
Add the SDK to your project via **Swift Package Manager** or **CocoaPods**.
Add your API key (from Agent Studio) and register your app's **bundle identifier** in Agent Studio. The backend rejects connections from unregistered bundle identifiers.
Initialize the SDK and start a messaging session. The SDK handles access token exchange and WebSocket connection automatically.
Listen for incoming messages and events from the SDK, and send user messages through it. Render the conversation in your own UI components.
## Requirements
| Requirement | Minimum |
| :----------- | :--------------------------------------------------------------------------------------- |
| iOS | 15.0+ |
| Swift | 5.9+ |
| Xcode | 15.0+ |
| Dependencies | None for chat (`PolyMessaging` is source-only); `PolyVoice` pulls the WebRTC xcframework |
`PolyMessaging` (chat) also supports macOS; `PolyVoice` is iOS-only.
## Installation
The SDK is pre-1.0, so a **minor** version bump is allowed to include breaking changes. Always pin to the next minor version — don't use Xcode's default "Up to Next Major" rule.
In Xcode, go to **File > Add Package Dependencies** and enter the repository URL:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
https://github.com/polyai/ios-sdk
```
Set the **Dependency Rule** to *Up to Next Minor Version* from `0.9.0`, then tick the **PolyMessaging** library for your app target. If you're using [voice calling](/messaging-channel/ios-sdk-voice), also tick **PolyVoice** — it's a separate product and isn't added automatically.
Or in `Package.swift`:
```swift theme={"theme":{"light":"github-light","dark":"github-dark"}}
// Pre-1.0: breaking changes bump the MINOR version, so pin to next-minor.
.package(url: "https://github.com/polyai/ios-sdk.git", .upToNextMinor(from: "0.9.0"))
// target dependencies:
.product(name: "PolyMessaging", package: "ios-sdk")
.product(name: "PolyVoice", package: "ios-sdk") // only if you need voice calling
```
Add the pod(s) to your `Podfile`:
```ruby theme={"theme":{"light":"github-light","dark":"github-dark"}}
pod 'PolyMessaging', '~> 0.9.0'
pod 'PolyVoice', '~> 0.9.0' # only if you need voice calling
```
Then run:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
pod install
```
## Authentication setup
The iOS SDK authenticates using a connector token and your app's bundle identifier.
In Agent Studio, go to **Connector Settings** and generate a connector token for your agent.
When generating the token, set the **host identifier** to your app's bundle identifier (e.g. `com.yourcompany.app`). This must match the `CFBundleIdentifier` in your app's `Info.plist`. The backend rejects connections from apps with a mismatched bundle identifier.
For voice calling you need one further credential from the same page, the **web calling token** — a separate value that authenticates the voice media gateway. See [Voice calling credentials](/messaging-channel/ios-sdk-voice#credentials).
## Key features
### Session persistence
Sessions persist across app launches. If the user leaves and returns, the SDK reconnects to the existing session and replays the conversation history automatically. Network changes (Wi-Fi to cellular, brief drops) are handled with automatic reconnection and retry.
### Streaming responses
Enable streaming when creating a session to show agent responses as they're generated, word by word. The SDK surfaces streaming chunks as they arrive — your UI can render them incrementally.
### Handoff to live agents
The full [handoff flow](/api-reference/messaging/handoff) is supported. When the PolyAI agent triggers a handoff, the SDK delivers the same handoff events (`HANDOFF_ACCEPTED`, `HANDOFF_QUEUE_STATUS`, `LIVE_AGENT_JOINED`, etc.) so your app can show queue status and live agent messages.
### Response suggestions
Agent messages can include `response_suggestions` — pre-written reply options. Render these as tappable buttons in your UI. When the user taps one, send its text as a message through the SDK.
### Attachments
Agent messages may include rich content like links and images via the `attachments` field. Render these inline in your chat UI.
### Voice calling
`PolyVoice` places live, two-way WebRTC [voice calls](/messaging-channel/ios-sdk-voice) to the same agent that powers your chat. Calls are user-initiated: the user taps to call your agent. Call `PolyVoice.call` from the **main actor**:
```swift theme={"theme":{"light":"github-light","dark":"github-dark"}}
import PolyMessaging
import PolyVoice
let call = try PolyVoice.call(
config: Configuration(apiKey: "YOUR_CONNECTOR_TOKEN"), // connector token — Agent Studio › Connector Settings
options: VoiceOptions(webrtcToken: "YOUR_WEB_CALLING_TOKEN") // web calling token — same page, a distinct value
)
// Observe the lifecycle: .idle → .connecting → .connected → .ended / .failed
Task { for await state in call.states { render(state) } }
try await call.start() // after the microphone permission is granted
await call.setMuted(true)
await call.end()
```
Your app needs the microphone permission (`NSMicrophoneUsageDescription` — a call without it **crashes**, it doesn't fail gracefully) and the `audio` background mode so calls survive backgrounding. Beyond that the SDK handles the hard parts for you: accessory-aware audio routing (headsets and Bluetooth are used automatically, with a speaker/earpiece toggle for the user), automatic reconnection on transient network drops, and graceful handling of interruptions like incoming phone calls. Optional CallKit support runs the call as a system call, with lock-screen controls and phone-call audio priority — it requires specific delegate wiring, covered on the [voice page](/messaging-channel/ios-sdk-voice#callkit).
The full voice guide: installation, credentials, CallKit integration, audio routing, resilience, and troubleshooting.
## Platform values
The platform identifier is sent automatically when the session is created, alongside a `device_type` (mobile / tablet). It identifies the **device**, not the channel — for the channel a conversation arrives on, see [Multichannel](#multichannel) below.
| Platform value | Source |
| :------------- | :--------------------------- |
| `ios` | iOS SDK (native) |
| `android` | Android SDK (native) |
| `ios-web` | Webchat widget on iOS Safari |
| `web` | Webchat widget on desktop |
## Limitations
| Limitation | Details |
| :-------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **No remote notifications** | The SDK has no remote push integration (no APNs). Messages arrive over the WebSocket while the session is live; if your app holds a `beginBackgroundTask`, the connection keeps delivering for roughly 30 seconds after backgrounding and you can raise local banners from that — without one, iOS suspends the app immediately and nothing arrives. |
| **User-initiated calls only** | Calls are always started by the user from inside your app. The agent cannot ring the user (no push-triggered incoming calls). |
| **Voice needs a physical device** | WebRTC media does not run on the iOS simulator. |
| **iOS only** | Native Swift only; no React Native or Flutter wrapper. |
## Multichannel
The iOS SDK connects to the same agent project as your voice and webchat channels. Agent behavior, knowledge, and flows are shared — only channel-specific settings (greetings, formatting) differ. See [multichannel agents](/messaging-channel/multichannel) for how to tailor behavior per channel.
Two values tell your agent where a conversation came from, and they answer different questions:
* **`conv.channel_type`** identifies the **channel**. Its possible values are `webchat.polyai`, `chat.polyai`, `sms.twilio`, `sms.polyai`, `rcs.polyai`, `whatsapp.polyai`, and `sip.polyai` — it is never `"ios"` or `"android"`.
* **`platform`** identifies the **device**: the SDK sends `platform: "ios"` (alongside `device_type`) when the session is created. Use this to tailor behavior for native app users.
To branch on the channel in your agent's start function:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start(conv):
if conv.channel_type == "webchat.polyai":
conv.state.greet_message = "Hi there! How can I help?"
elif conv.channel_type == "whatsapp.polyai":
conv.state.greet_message = "Hi! How can I help you today?"
```
## Related pages
WebRTC voice calls with PolyVoice: setup, CallKit, audio routing, and troubleshooting
Full WebSocket protocol, events, streaming, and handoff
Access tokens, session creation, and platform values
Build agents that work across voice, webchat, and mobile
# Voice calling (iOS SDK)
Source: https://docs.poly.ai/messaging-channel/ios-sdk-voice
Add live two-way WebRTC voice calls to your iOS app with PolyVoice.
`PolyVoice` places live, two-way WebRTC voice calls to a PolyAI agent — the companion to the [iOS SDK's](/messaging-channel/ios-sdk) chat product. It ships as a **separate product/pod** so chat-only apps never link the WebRTC binary, and it reuses the messaging `Configuration` plus the same `CallState` / `PolyError` vocabulary, so there are no new concepts if you already run chat.
Calls are user-initiated: the user taps to call your agent. Inbound (push-triggered) calls are not supported.
polyai/ios-sdk — includes the full PolyVoice technical guide and runnable voice example apps.
## Installation
The SDK is pre-1.0, so pin to the next minor version — a minor bump is allowed to include breaking changes.
Add the package and depend on the `PolyVoice` product (alongside `PolyMessaging`):
```swift theme={"theme":{"light":"github-light","dark":"github-dark"}}
// Pre-1.0: breaking changes bump the MINOR version, so pin to next-minor.
.package(url: "https://github.com/polyai/ios-sdk.git", .upToNextMinor(from: "0.9.0"))
// target dependencies (the package identity is the repo name, `ios-sdk`):
.product(name: "PolyMessaging", package: "ios-sdk")
.product(name: "PolyVoice", package: "ios-sdk")
```
In Xcode, this means ticking **both** the PolyMessaging and PolyVoice libraries for your app target when adding the package.
```ruby theme={"theme":{"light":"github-light","dark":"github-dark"}}
pod 'PolyVoice', '~> 0.9.0' # chat-only apps use `pod 'PolyMessaging', '~> 0.9.0'` instead
```
`PolyVoice` transitively pulls the WebRTC xcframework; `PolyMessaging` stays source-only. With CocoaPods the WebRTC dependency lives solely in `PolyVoice`, so a chat-only install pulls nothing extra.
## Quickstart
Call `PolyVoice.call` from the **main actor** (it won't compile otherwise):
```swift theme={"theme":{"light":"github-light","dark":"github-dark"}}
import PolyMessaging
import PolyVoice
let call = try PolyVoice.call(
config: Configuration(apiKey: "YOUR_CONNECTOR_TOKEN"), // connector token — Agent Studio › Connector Settings
options: VoiceOptions(webrtcToken: "YOUR_WEB_CALLING_TOKEN") // web calling token — same page, a distinct value
) // throws PolyError.invalidConfiguration on a blank token, or a .custom environment without signalingHost
// Observe the lifecycle: .idle → .connecting → .connected → .ended / .failed
Task { for await state in call.states { render(state) } }
try await call.start() // after the microphone permission is granted
await call.setMuted(true)
await call.end()
```
`CallState`, `PolyError`, and `Configuration` are the same types from `PolyMessaging`.
## Credentials
A voice call needs **two credentials**, both on your agent in [Agent Studio](https://studio.poly.ai) › Connector Settings (the same connector you use for chat):
| Value | What it is | Sent as |
| :------------------------------------------------- | :--------------------------------------------------------------------------- | :---------------------------------------------- |
| **Connector token** — `Configuration.apiKey` | your connector token | `X-Token` (authenticates the call) |
| **Web calling token** — `VoiceOptions.webrtcToken` | the media-gateway auth token — a **distinct** value from the connector token | the offer `authToken` and the ICE-servers fetch |
**Region:** calls default to the US gateway. For a UK / EUW / other-region agent, set the environment on the shared `Configuration` — e.g. `Configuration(apiKey: …, environment: .cluster("…"))` — the same `Configuration` you use for chat.
**Custom / self-hosted gateway:** pass `VoiceOptions(webrtcToken:, signalingHost:)` to point at a specific gateway host (required when the environment is `.custom`).
## Microphone permission
A call needs the microphone. Add **`NSMicrophoneUsageDescription`** to your app's `Info.plist` — a call without it **crashes** on iOS, it does not fail gracefully. The system prompts the user on the first call; the SDK activates the `AVAudioSession` for you (under [CallKit](#callkit), the system activates it instead — the permission requirement is unchanged).
## Backgrounding
To keep a call running while your app is in the background (the norm for a voice call), enable the **`audio` background mode** in your `Info.plist`:
```xml theme={"theme":{"light":"github-light","dark":"github-dark"}}
UIBackgroundModesaudio
```
Without it, iOS suspends the app when backgrounded and the call drops. By default a call is a normal app audio session, not a system phone call — for the system call UI, see [CallKit](#callkit), which additionally requires the **`voip`** background mode alongside `audio`.
## Audio routing
The call is **accessory-aware** by default: a connected wired or Bluetooth headset is used automatically (and followed if connected or removed mid-call); otherwise it falls back to the loudspeaker. Set `VoiceOptions(speakerphone: false)` to fall back to the earpiece instead.
iOS keeps one active output and routes accessories for you, so the output an app reliably controls is speaker ↔ earpiece. Observe the live route via `call.audioStates` and switch with `call.setAudioDevice(_:)`:
```swift theme={"theme":{"light":"github-light","dark":"github-dark"}}
Task { for await snapshot in call.audioStates {
show(current: snapshot.selectedDevice) // e.g. "Output: AirPods"
} }
// speaker ↔ earpiece — the entries come from snapshot.availableDevices
await call.setAudioDevice(speakerDevice) // .kind == .speakerphone
await call.setAudioDevice(earpieceDevice) // .kind == .earpiece
let muted = await call.isMuted
```
`availableDevices` also lists connected headsets and Bluetooth devices (`.kind` is `.earpiece` / `.speakerphone` / `.wiredHeadset` / `.bluetooth`) for display. To let users pick among connected outputs the iOS-standard way, drop in the system route picker (`AVRoutePickerView`).
## CallKit
Opt in with **`VoiceOptions(callKit: true)`** to run a call as a **system call**: the green in-call indicator, lock-screen / AirPods / car-Bluetooth controls, phone-call audio priority, and hold arbitration when a cellular call arrives.
In this mode the SDK never activates or deactivates the audio session itself — CallKit does — and your `CXProviderDelegate` **must** forward three moments to the SDK. Without this forwarding, the call connects but has no sound.
```swift theme={"theme":{"light":"github-light","dark":"github-dark"}}
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
PolyVoice.callKitConfigureAudioSession() // configure EARLY — never self-activate
Task { try? await call.start() }
action.fulfill()
provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)
}
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
PolyVoice.callKitAudioSessionDidActivate(audioSession) // audio starts HERE
}
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
PolyVoice.callKitAudioSessionDidDeactivate(audioSession)
}
```
Rules the integration must follow:
* **Declare the `voip` background mode** (alongside `audio`) in `UIBackgroundModes`. Without it every `CXCallController` transaction is refused (`com.apple.CallKit.error.requesttransaction Code=1`) and the call never starts.
* **Request, don't command:** start / end / mute go through `CXCallController` actions and are executed in the matching `perform` callback, so the system can arbitrate and the system UI stays in sync. Remote endings (the agent hangs up, a failure) are **reported** via `reportCall(with:endedAt:reason:)` instead.
* **Never call `AVAudioSession.setActive(true)`** during a CallKit call — a self-activated session blocks CallKit's elevated activation and `didActivate` never fires (the classic "call connects, no audio" bug).
* **System interruptions move to CallKit:** a cellular call arrives as a hold action plus `didDeactivate`, not as the SDK's interruption handling. After the interrupting call ends, iOS may not resume you automatically — offer a manual un-hold path.
* **Simulator:** CallKit is broken there (iOS 17+ auto-ends calls). Gate on `targetEnvironment(simulator)` and fall back to a plain call.
* **China:** Apple rejects CallKit UI for the Chinese App Store. Keep `callKit:` behind a region or remote-config gate if you ship there.
## Resilience
* **Connectivity:** STUN/TURN servers are fetched from the gateway per call, so calls connect behind symmetric NAT / CGNAT (falls back to public STUN if the fetch fails).
* **Reconnect:** a dropped signaling socket reconnects automatically (backoff 1s / 2s / 4s) on the same session before the call is failed.
* **Interruptions:** an incoming phone call or Siri mutes the mic and restores it; a non-resumable interruption ends the call as `PolyError.voice(.interrupted)`.
* **Retryable errors:** a post-connect drop surfaces as `PolyError.voice(.disconnected)`. Both it and `.interrupted` are `isRetryable`, so you can offer a one-tap retry.
## Troubleshooting
| Symptom | Likely cause |
| :--------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Token rejected / fails while connecting | Both tokens come from the *same* connector in Agent Studio › Connector Settings, and the `Configuration` must match that connector's environment (region/cluster) and registered host (bundle identifier). A token from one environment silently 401s on another. |
| Call connects but is silent (CallKit) | The app isn't forwarding the provider callbacks — all three `PolyVoice.callKit*` calls are required, and `UIBackgroundModes` must include `voip`. |
| Call connects but is silent (no CallKit) | Check the mic permission was granted (Settings › your app › Microphone) and that nothing else in the app deactivated the `AVAudioSession` mid-call. |
| `failed(.voice(.timedOut))` after \~30 s | Signaling reached the gateway but media never connected — usually a firewalled or relay-only network where the TURN fetch failed. |
| Works on Wi-Fi, dies on the move | Transient drops reconnect automatically; a `.disconnected` failure is retryable (`error.isRetryable`) — offer a redial button. |
| Nothing works on the simulator | Expected: WebRTC media needs a physical device, and CallKit is additionally broken on iOS 17+ simulators. |
## Architecture
`PolyVoice` provides a real `CallMediaEngine` (an `RTCPeerConnection` audio engine) and an `AVAudioSession` controller, injected into the existing `PolyMessaging` `CallCoordinator` via `PolyCall.wired(config:webrtcToken:signalingHost:mediaEngine:)` (SPI — `@_spi(PolyVoice)`, not public API). The signaling pipeline (auth → session → link → signaling → offer/answer/ICE) lives in `PolyMessaging` and is exercised end-to-end by its test suite.
## Example apps
A one-screen tap-to-call demo ships in both toolkits — drop your connector token and web calling token into the `PolyVoice.call(...)` block and run:
* [`Examples/SwiftUI/Voice/01-Hello`](https://github.com/polyai/ios-sdk/tree/main/Examples/SwiftUI/Voice/01-Hello)
* [`Examples/UIKit/Voice/01-Hello`](https://github.com/polyai/ios-sdk/tree/main/Examples/UIKit/Voice/01-Hello)
The **02-CallKit** examples encode all of the CallKit rules above.
## Related pages
Chat with PolyMessaging: installation, authentication, sessions, and UI
Build agents that work across voice, webchat, and mobile
# Multichannel agents
Source: https://docs.poly.ai/messaging-channel/multichannel
Build agents that work across voice and webchat from a single project.
A multichannel agent is a single virtual agent that works consistently across different channels – such as **voice** and **web chat** – while adapting to each channel's unique needs. The design separates what stays the same (core behavior) from what changes depending on the channel (greetings, formatting, available actions).
## How voice and chat differ
Voice and web chat interactions differ in meaningful ways:
* **Voice** is shaped by latency and audio limitations – users speak casually, may interrupt, and prefer short responses
* **Web chat** is more deliberate – users type at their own pace, can scroll back, and expect clear visual structure. Chat also enables rich UI elements like buttons and quick replies that aren't possible in voice
Despite these differences, the **core agent logic remains the same** across channels.
## Core principles
### Keep core behavior unified
The core behavior of your agent should remain consistent across all channels. This includes:
* Prompting logic and knowledge
* Reasoning steps and flows
* Metrics and reporting
### Handle channel differences in one place
Channel-specific differences – such as greetings or formatting – should be handled in a single location. This avoids duplicating code and makes updates easier.
The platform automatically selects capabilities based on the channel. For example, voice-specific delays are turned off automatically for web chat at runtime.
## Channel-specific greetings
Each channel can have its own greeting message. Store greetings as **variables** so they're easy to update.
* **Web chat** greetings might include emoji and HTML formatting
* **Voice** greetings should use a more conversational tone
Set channel variables in your start function:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start(conv):
if conv.channel_type.startswith("webchat"):
conv.state.greet_message = (
f"Hi, you're speaking with Poly, a virtual agent for "
f"{conv.variant.webchat_site_name}
How may I help you today?"
)
else:
conv.state.greet_message = (
f"Hi there, you're speaking with a virtual agent for "
f"{conv.variant.site_name}. How can I help?"
)
```
## LLM and style prompt
You don't need to manually add style guidelines to your behavior prompt for voice. A **prompt decorator** is automatically applied per channel – for example, the Raven voice model includes built-in style guidelines.
Configure which LLM powers each channel in **Voice > Advanced > [Call settings](/voice-channel/advanced/call-settings)** or **Messaging > Advanced > [Chat configuration](/messaging-channel/advanced/chat-configuration)**. For multichannel agents, **[Raven 3.5](/behavior/models/raven)** is the recommended model – it is the only Raven model tuned for both voice and chat, providing a consistent experience across channels.
## Knowledge and links
* To share a **URL** in chat, include it directly in the knowledge topic – the system presents it correctly inline
* **SMS** is only available for voice, so don't rely on it for other channels
* Keep your content as channel-agnostic as possible to minimize maintenance
Where voice sends an SMS with a link, webchat can display the link or content inline instead. Adjust the action per channel when needed.
## Metrics and reporting
The same metrics system applies across all channels. Metrics designed for voice (like transfer rates or conversation length) also apply to web chat. All results are stored in the same database for unified reporting.
## Deploy and test
From **Messaging**, configure your greeting, LLM, and style prompt for the chat channel.
From **Widgets**, generate a script tag and embed it in your website's HTML headers. See the [widget deployment guide](/widgets/install) for details.
Use the **Preview** button to test your webchat agent. Verify that formatting displays correctly and channel-specific behaviors work as expected.
## Summary
| Concern | Approach |
| ------------------------ | ---------------------------------------------------------------------------------------------------- |
| Core agent behavior | Unified across channels |
| Greetings and formatting | Channel-specific variables |
| LLM model | [Raven 3.5](/behavior/models/raven) (recommended) for voice and chat; GPT also available for webchat |
| Style guidelines | Handled by prompt decorator per channel |
| Metrics | Same system, unified database |
| SMS actions | Voice only – use inline content for chat |
## Related pages
Configure greetings, LLM, and style for the chat channel
Embed the webchat widget on your website
Configure your agent's voice for the voice channel
# SMS Agent Configuration
Source: https://docs.poly.ai/messaging-channel/sms-and-rcs/advanced/sms-agent-configuration
Configure your SMS agent's behavior, style, and responses.
Configure SMS alongside your other messaging channels from the **Messaging** page in Agent Studio, under **Channels > Messaging**. The page has three tabs: **General**, **Web chat** and **SMS**.
Settings on the **General** tab apply across all messaging channels unless overridden in a channel's configuration. Anything you set on the **SMS** tab overrides the matching General setting for SMS only.
## Activate and Deactivate SMS
SMS is switched on and off from the **Channels** card on the **General** tab, where each messaging channel has its own toggle and status tag.
Turn the SMS toggle on to activate the channel. The status tag changes to **Active**, the SMS tab's settings unlock with defaults pre-filled, and the SMS tab confirms the channel is active and ready to handle SMS conversations. Publish your changes to go live.
Turning the toggle off deactivates the channel. You are asked to confirm first: SMS stops sending and receiving messages, but your configuration is kept and can be reactivated at any time. While SMS is deactivated, the SMS tab shows "SMS is currently disabled" with a link back to the General tab, and its settings are locked.
## SMS Behaviour
SMS is a different medium from web chat: shorter, plainer and billed per message segment. The **SMS behaviour** card on the SMS tab controls how your agent writes on SMS, without touching its underlying knowledge or logic.
Select from the style options to generate an SMS prompt, or write your own:
| Option | Choices |
| ---------------------- | ------------------------------------------------------------ |
| Tone (select multiple) | Neutral, Friendly, Professional, Concise, Empathetic, Casual |
| Emoji use | No emojis, Enabled |
| Sign-off | No sign-off, Agent name, Custom |
| Opener | No opener, Custom |
| Date format | For example "Fri 16 May", "16 May 2026", "May 16, 2026" |
| Time format | For example "3pm", "3:00pm", "15:00" |
Your selections generate the **SMS style prompt** automatically, and adjust it as you change options. You can edit the generated prompt directly. For example, a friendly, concise tone with no emojis and a custom sign-off produces:
```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
- Use a warm and approachable tone while remaining professional. Keep responses brief and focused on the essential information.
- Do not use emojis.
- End responses with "Thanks, Project ABC".
- Format dates as "Fri 16 May".
- Format times as "3pm".
```
These behaviour settings apply to the SMS channel only. Prompts are applied in this order: **SMS style prompt > Messaging channel behaviour > Agent behaviour**. Use the SMS style prompt for how the agent writes on SMS; use the Messaging channel behaviour prompt on the General tab for style shared across messaging channels. To change what the agent knows or does, edit the agent's behaviour on the Agent page.
## Greeting
Set the opening message sent when an SMS conversation begins. The default is "Hello, how can I help?" and it overrides the default greeting configured on the General tab.
The field shows a live **character count** and **SMS segment count** as you type. A single SMS carries 160 GSM-7 characters; longer messages split into segments of 153 characters each, and each segment is billed as a message. Keeping your greeting to one segment keeps your cost per conversation down.
## From Draft to Live
1. Activate SMS in the **Channels** card on the **General** tab.
2. On the **SMS** tab, generate your SMS style prompt from the style options, or write your own. If you do not choose to edit the styling prompt, there is a default generic SMS styling prompt applied to your agent for SMS with the appropriate formatting. (Including character count per message)
3. Tailor the greeting, keeping an eye on the segment counter.
4. **Publish**.
Your agent is then live on SMS with SMS-specific style, while web chat and any other messaging channels keep their own configuration. One agent, one set of knowledge and logic, with per-channel style on top.
# RCS
Source: https://docs.poly.ai/messaging-channel/sms-and-rcs/rcs
Deploy your agent over RCS to bring rich, branded messaging to Android and iOS users.
PolyAI RCS brings Rich Communication Services to your agent, adding branded sender identity, rich media, and interactive elements on supported Android and iOS devices. The same agent brain that handles voice, web chat, and SMS also supports RCS, so customers get a consistent experience across every channel.
The current capability supports transactional use cases use cases, campaign orchestration and marketing use case support is coming soon.
## What it is
RCS (Rich Communication Services) is the carrier-grade successor to SMS, defined by the GSMA. It runs on the same delivery network as SMS, so it keeps universal reach and the no-app-install property, but adds:
* Branded verified sender
* Images and video
* Rich UI components (customer provided)
* Read receipts and typing indicators
Where a device or network cannot receive RCS, the message arrives as SMS automatically. The business version is RCS Business Messaging, where the sender is a brand registered and verified with the carriers.
## Key features
### Branded sender identity
Verified sender profile with your brand name, logo, and colors on supported devices.
### Rich media
Images, videos, rich cards, carousels and inline links inside the message thread.
### SMS fallback
Automatic fallback to SMS when a device or carrier does not support RCS, so no message is lost.
### Unified conversation flow
RCS runs on the same shared messaging architecture as SMS and web chat. Live agent handoff, CSAT, and events are inherited, and every conversation is visible in Agent Studio Conversation Review.
### Same agent brain
One configuration powers voice, web chat, SMS, and RCS. Persistent memory travels with the customer across conversations.
## Setting up RCS
Register a branded RCS Business Messaging profile (name, logo, colors, contact info) with Twilio. PolyAI coordinates registration and verification.
Provision a fallback SMS sender so messages are delivered to devices without RCS support.
Point your CRM at the PolyAI outbound webhook to fire branded messages on events. Two-way replies route into the standard inbound flow.
Connect the CCaaS or ticketing platform your live agents work in.
Monitor traffic in Agent Studio **Conversation Review** alongside voice, web chat, and SMS.
## Use cases
Confirmations, alerts, and notifications delivered from a verified brand sender profile.
Rich cards with tracking imagery and one-tap actions to reroute or reschedule delivery.
Quick reply chips for confirm, reschedule, or cancel routed straight into the agent flow.
Guided flows with structured buttons instead of free-text prompts to accelerate resolution.
Rich cards or carousels that link to product pages. (Please note carousels incur significant costs to serve)
Transfer the thread to a live agent with full conversation history and customer context.
## Related pages
Two-way SMS messaging on the same agent brain as voice and chat.
Send outbound SMS messages through the PolyAI outbound webhook API.
Send rich RCS messages with media, carousels, and SMS fallback.
CCaaS handoff integrations to transfer RCS threads to live agents.
# SMS
Source: https://docs.poly.ai/messaging-channel/sms-and-rcs/sms
Deploy your agent over SMS with the same brain that powers voice and web chat.
PolyAI SMS extends your agent to text messaging on a PolyAI-managed Twilio number. The same agent brain that handles voice and web chat now supports SMS, so customers get consistent answers, live agent handoff with full context, and end-to-end resolution over text.
The current capability supports transactional use cases, campaign orchestration and marketing use case support is coming soon.
## Three SMS modes
Through Twilio PolyAI is able to offer three directional modes of 2-way SMS. What changes is who starts the conversation and whether replies are expected.
Customer texts the agent number. Best for FAQ, account servicing, and queue callback replies from a live voice queue.
CRM event fires a HTTPS POST. Best for re-engagement, utility use cases, and status updates that expect a reply.
Agent sends via the `send_sms` function in Agent Studio. Best for reminders, confirmations, OTPs, and post-call follow-up.
Outbound and Triggered use the same webhook. Once a customer replies to an outbound message, the conversation routes automatically into the inbound flow with the same agent.
## Key features
### PolyAI-managed Twilio
PolyAI provisions and manages the Twilio infrastructure. No customer Twilio account is needed, and the agent phone number is provisioned in Agent Studio.
### Reuse your voice number
If PolyAI already handles your voice deployment, the same Twilio number serves both voice and inbound SMS. Once an SMS camapign has been approved by Twilio, you can also use the same number for outbound SMS. No extra number management.
### Unified conversation flow
SMS runs on the shared messaging architecture, so it inherits live agent handoff, CSAT, and events from voice and web chat. Multi-turn dialogue keeps context across the thread, and every conversation is visible in Agent Studio Conversation Review.
### Live agent handoff
Native handoff to Salesforce, Zendesk, NICE, Amazon Connect, and Webex. When the customer needs a person, the agent transfers the thread with full conversation context.
### Compliance and safety
* STOP/START/HELP keyword enforcement at Twilio level (case-insensitive, tolerant of trailing punctuation).
* End-users are have opt-in and opt-out consent enabled, with blocklist enforcement.
* Repeated-message detection and abuse auto-block.
* Per-sender and per-project rate limiting.
### Operational control
Conversation lifecycle controls for inactivity timeout, end-of-conversation logic, and termination messaging.
## How it works
### Inbound
The customer texts the PolyAI agent number from their phone.
Twilio fires a webhook to the PolyAI messaging service.
Rate limit, blocklist, and opt-in checks run before the message is accepted. A conversation record and session are created by PolyAI.
The shared messaging architecture handles the turn, including handoff and CSAT events.
The agent reply is routed via Twilio back to the customer.
### Outbound and Triggered
Salesforce, Zendesk, Oracle, or Dynamics raises an event.
The CRM posts to the PolyAI outbound webhook with the recipient and message.
A specific agent number is selected. A conversation record and session are created.
Rate limit, blocklist, and opt-in checks run, then Twilio delivers the SMS.
If the customer replies, the reply lands in the standard inbound flow and the same agent picks up.
## Setting up SMS
PolyAI provisions and manages the Twilio number in Agent Studio. If you already have PolyAI voice, the same number can serve inbound SMS.
Enable SMS on the agent in Agent Studio. Set the greeting, customise your styling prompt, and set channel specific configuration where required.
Complete 10DLC brand and campaign registration via Twilio. PolyAI handles platform-level keyword compliance. You manage consent records.
Point your CRM at the PolyAI outbound webhook using a single HTTPS POST. Salesforce, Zendesk, Oracle (OIC / Eloqua), and Dynamics 365 are natively supported. Other CRMs work via Zapier middleware or a custom HTTPS POST.
Connect the CCaaS or ticketing platform your live agents work in (Salesforce, Zendesk, NICE, Amazon Connect, Webex).
Monitor traffic in Agent Studio **Conversation Review**. Filter SMS conversations using the `TW_SMS_` Call SID prefix.
## Compliance posture
| Region | Behavior |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| TCPA (US) | 10DLC brand and campaign registration via Twilio. Platform-level keyword compliance. Customer manages consent records. |
| GDPR / PECR (UK and EU) | Customer manages opt-in. Platform enforces STOP at the keyword layer. |
| CASL (Canada) and DLT (India) | Express consent and audit trail managed in the customer CRM. PolyAI does not store consumer consent records. |
### Default rate limits
* Per-sender: 10 messages / 60 seconds. STOP/START/HELP keywords are exempt.
* Per-project: 100 messages / minute across all senders.
* Repeated-message auto-block: same message sent 5+ times in 5 minutes triggers a 10-minute auto-block that lifts automatically.
## Use cases
Store hours, eligibility, and status checks. Highest volume, lowest complexity SMS traffic on the platform.
Deflect from a busy voice queue. Customers wait outside the queue while agents work down the backlog.
Booking system fires a templated confirmation. Reschedule and cancel replies route to a live agent.
Send a CSAT link or an open survey prompt after a voice call ends to capture feedback in the moment.
OMS or WMS fires status updates over SMS. Tracking questions route back to an inbound agent for resolution.
Marketing CRM fires templated sends. Two-way replies route into the inbound agent flow for follow-up.
Monitoring platform triggers alerts to affected customers. Cuts inbound voice volume on outage days.
Banking or FS system triggers alerts with a one-tap reply flow for identity verification and dispute.
Fire-and-forget one-time passwords and authentication confirmations sent from Agent Studio functions.
## Related pages
Send outbound SMS messages through the PolyAI outbound webhook API.
10DLC brand and campaign registration requirements for US SMS traffic.
Rich, branded messaging over the RCS channel with SMS fallback.
CCaaS handoff integrations to transfer SMS threads to live agents.
# SMS & RCS Registration
Source: https://docs.poly.ai/messaging-channel/sms-and-rcs/sms-and-rcs-campaign-registration-compliance-requirements-us
What US carriers require to register SMS and RCS campaigns, and how to prepare your brand, policies and opt-in flow for a first-time approval.
Before your PolyAI agent can send outbound SMS or RCS messages to customers in the United States, the messaging campaign must be registered and approved. This page explains what registration involves, what carriers require from your business, and how to prepare so approval happens once, quickly, and before your go-live date.
These are industry rules, not vendor preferences. Registration requirements are set by The Campaign Registry (TCR), the [CTIA Messaging Principles and Best Practices](https://api.ctia.org/wp-content/uploads/2023/05/230523-CTIA-Messaging-Principles-and-Best-Practices-FINAL.pdf) and the TCPA, and are enforced by every US mobile carrier. You will meet the same ruleset with any legitimate SMS provider, and non-compliance carries real exposure: TCPA statutory damages run \$500 to \$1,500 per message, with no aggregate cap.
The detailed registration process on this page applies to the United States. Messaging rules differ by country: sender ID types, pre-registration requirements, consent rules and permitted sending hours all vary. If you are launching outside the US, see the country table at the end of this page and speak to your PolyAI representative before planning your go-live.
The current capability supports transactional use cases use cases, campaign orchestration and marketing use case support is coming soon.
Before committing to deployment, review the [customer compliance checklist](https://docs.google.com/document/d/1Qo4FBac_Uglwc0m9PXLpwu_m8PGqRt_NRMmusIe5vhs/edit?usp=sharing) to confirm you are ready to register.
## Register Early
Campaign vetting is a manual review process. SMS campaign approval typically takes days to weeks; RCS carrier approval takes four to six weeks, longer if you are launching in multiple regions. Each rejection costs a full review cycle, so registration should begin at project kickoff, not during integration testing. Your PolyAI deployment team will drive the registration process, but the items below need input from your web, marketing and legal teams, and they are the most common cause of delay.
## SMS: What Carriers Check (A2P 10DLC)
Every SMS campaign registration is reviewed against your brand, your website, your published policies, your opt-in flow and your sample messages ([Twilio's A2P 10DLC registration guide](https://www.twilio.com/docs/messaging/compliance/a2p-10dlc/quickstart) covers the full process). Reviewers look for consistency across all of them, and a mismatched or unrelated website is a [documented reason for rejection](https://www.twilio.com/en-us/blog/insights/best-practices/improving-your-chances-of-a2p10dlc-registration-approval).
Since 30 June 2026, all new A2P 10DLC campaign registrations [must include two publicly accessible URLs](https://www.twilio.com/en-us/changelog/a2p-10dlc-campaign-registration-will-require-privacy-policy-and-): a Privacy Policy and Terms and Conditions. Both must be relevant to the business registering the campaign, and Twilio's guidance is that they should be hosted on the same domain as your business website. This is why PolyAI cannot host these pages on your behalf: the registered brand is your business, and reviewers, carriers and regulators expect the policies to live with you.
### Your Privacy Policy Must State
Your privacy policy needs one specific commitment, worded to cover SMS consent data:
> Text messaging opt-in data and consent will not be shared with, sold to, or bought by third parties or affiliates for marketing or promotional purposes.
Two scoping points your legal team will want to know. First, the requirement covers SMS opt-in data and consent only, not "mobile information" in the broad sense, so it does not restrict how your website or app collects data generally. Second, the prohibition is on sharing for marketing or promotional purposes. Sharing with service providers to deliver messages and operate the programme is permitted and can be stated explicitly.
### Your Terms Must Include
* The programme name and a description of what it sends
* "Message and data rates may apply"
* Message frequency (for example, "1 to 3 messages per interaction")
* A customer support contact (email or phone)
* Complete HELP and STOP opt-out instructions, in bold
* A link to your privacy policy
* The statement "carriers are not liable for delayed or undelivered messages"
### Publish a Standalone Page, Not a Policy Amendment
The fastest route through review, and the one Twilio's own compliance team recommends in writing, is a single new page on your website (for example, yourcompany.com/sms-terms) scoped only to the SMS programme, rather than an amendment to your corporate privacy policy. A standalone page needs far lighter legal review, leaves your corporate policies untouched, and insulates them from future carrier requirement changes. Your PolyAI deployment team can provide a pre-vetted template covering every required element; your legal team reviews one page and fills in the brackets.
### Consistency Between Opt-In and Registration
The opt-in flow described in your campaign submission must match what your customers actually experience. If your registration says callers verbally consent during a call with the assistant, that is what the assistant must do, and your sample messages must reflect the real messages the programme sends. Consent must be sender-specific and informed, and proof of consent must be retained.
## RCS: Agent Registration and Verification
RCS is branded messaging, so registration has a few more requirements than SMS and runs through both Google and the US carriers ([Twilio's RCS onboarding guide](https://www.twilio.com/docs/rcs/onboarding) covers the full process). Plan for four to six weeks. RCS senders cannot be onboarded programmatically at scale; each one goes through the same review.
Creating the sender requires your brand assets and public details: a unique display name, a logo (PNG or JPEG, 224 x 224 pixels, under 50kB), a banner image (1140 x 448 pixels, under 200kB), an accent colour meeting minimum contrast, a description of the sender and how users interact with it, at least one labelled phone number, email address or website, and your privacy policy and terms of service URLs. The same policy pages you publish for SMS can serve here, provided they cover the RCS programme.
Compliance registration then requires, for Google: an authorised representative (name, business email, title, website), your opt-in and opt-out policies with publicly accessible image URLs evidencing them, a use case description with a video demonstration, agent access instructions so reviewers can test the sender, and a notification contact. For US carriers, additionally: your legal business name as it appears on IRS documentation, company type and EIN, business address, industry classification, a brand contact number, current and projected message volumes, campaign description and message flow, sample HELP and STOP responses, and confirmation of CTIA handbook compliance.
## Pre-Flight Before You Submit
Twilio provides a pre-submission compliance checker at [a2pcheck.com](http://a2pcheck.com). It assesses your privacy policy, terms and campaign details against the carrier vetting criteria before anything is formally submitted, and it is the same tool Twilio's own team uses. Your deployment team will run your campaign through it before submission, and after any rejection the next step is a pre-review with Twilio's compliance team rather than a blind resubmission. If a campaign is rejected, the rejection code maps to Twilio's error documentation ([30908](https://www.twilio.com/docs/api/errors/30908), [30932](https://www.twilio.com/docs/api/errors/30932), [30933](https://www.twilio.com/docs/api/errors/30933) are the common privacy-policy and terms failures).
## What You Provide and What PolyAI Handles
Your business provides: the published policy and terms pages on your domain, brand details (legal name, EIN, address), brand assets for RCS, and sign-off on the opt-in flow and sample messages. PolyAI handles: campaign drafting and submission, pre-flight checks, liaison with Twilio's compliance and partner teams, and tracking of registration status through to approval.
## Country-Specific Requirements Outside the US
Every country in the table below shares the same baseline: opt-in consent before any non-essential message, HELP and STOP support in the recipient's local language, sending during daytime hours unless urgent, and respect for do-not-call registries. On top of that baseline, sender ID rules, registration requirements and sending windows vary by country. The table summarises the headline differences; each country name links to Twilio's full guideline page, which is the authoritative and current source. If your deployment covers a country not listed here, or you are unsure which rules apply, contact your PolyAI representative.
| Country | Sender ID and registration | Key differences to note |
| :--------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [United States](https://www.twilio.com/en-us/guidelines/us/sms) | No alphanumeric sender IDs. 10DLC long codes require A2P campaign registration; toll-free numbers require verification; short codes take 6-10 weeks | Privacy Policy and Terms URLs required for all new campaigns since 30 June 2026 (this page) |
| [Canada](https://www.twilio.com/en-us/guidelines/ca/sms) | No alphanumeric sender IDs. Long codes need no pre-registration; toll-free requires verification; short codes take 12-16 weeks | Strict carrier A2P filtering; short codes limited to 160 ASCII characters |
| [United Kingdom](https://www.twilio.com/en-us/guidelines/gb/sms) | Alphanumeric supported without pre-registration, except brand-related "Protected Sender IDs" (MEF/BT) which must be pre-registered; short codes take 8-12 weeks | Generic sender IDs (SMS, TEXT, INFO, VERIFY) are blocked by operators; short codes require express consent |
| [Ireland](https://www.twilio.com/en-us/guidelines/ie/sms) | Alphanumeric sender IDs must be pre-registered (about 2 weeks); domestic long codes and short codes not supported | Since 3 July 2025, unregistered alphanumeric senders are delivered labelled "Likely Scam"; MEF-protected IDs need a Letter of Authorisation |
| [Germany](https://www.twilio.com/en-us/guidelines/de/sms) | Alphanumeric and long codes supported with no registration; short codes take 6-8 weeks | Sender IDs preserved as sent |
| [France](https://www.twilio.com/en-us/guidelines/fr/sms) | Alphanumeric supported without pre-registration; sensitive sender IDs may need a Letter of Authorisation; short codes take 8-10 weeks | Marketing SMS only 08:00-21:30 Monday to Saturday (transactional exempt); using a mobile number as the opt-out mechanism is forbidden and can draw operator fines |
| [Netherlands](https://www.twilio.com/en-us/guidelines/nl/sms) | Alphanumeric and long codes supported with no registration; short codes not supported | Lightest-touch market in this table |
| [Croatia](https://www.twilio.com/en-us/guidelines/hr/sms) | Alphanumeric supported but the A1 network may overwrite sender IDs; domestic long codes not supported | Two-way SMS not supported, so replies to the agent are not possible |
| [Czech Republic](https://www.twilio.com/en-us/guidelines/cz/sms) | Sender ID pre-registration required for T-Mobile and O2 networks (about 3 weeks); dynamic alphanumeric not supported on those networks | Since 14 July 2025, unregistered sender IDs are blocked on T-Mobile and O2 |
| [Slovakia](https://www.twilio.com/en-us/guidelines/sk/sms) | Alphanumeric supported with no registration; international long code sender IDs are replaced by operators; short codes not supported | Two-way SMS not supported, so replies to the agent are not possible |
| [Italy](https://www.twilio.com/en-us/guidelines/it/sms) | Alphanumeric supported without pre-registration; short codes take 7-9 weeks | Marketing SMS prohibited 22:00-08:00 and all day Sunday; sender IDs must comply with the AGCOM code of conduct |
## Related pages
Two-way SMS messaging on the same agent brain as voice and chat.
Rich, branded messaging over the RCS channel with SMS fallback.
Send outbound SMS messages through the PolyAI outbound webhook API.
CCaaS handoff integrations to transfer SMS threads to live agents.
# Platform overview
Source: https://docs.poly.ai/platform/introduction
PolyAI's Agentic Dialog Platform is an enterprise platform for building voice and chat agents.
PolyAI's **Agentic Dialog Platform** runs production voice and chat agents for banks, hotels, healthcare providers, and other businesses where the agent is expected to resolve the conversation — bookings, billing, claims, escalations — rather than route it to a human. The same platform is available to every team building on PolyAI. Most agents run on [Raven](/behavior/models/raven), PolyAI's proprietary LLM: sub-300ms latency, 24+ languages, grounded in your knowledge.
To start from a description, use [Wren](/wren/introduction). It scaffolds the flows, knowledge, and guardrails from your prompt. To work locally, use the [ADK](/extend/adk): a CLI, self-serve API keys, and a Git-based workflow. Every change goes to a [shareable test environment](/widgets/test) for validation and stakeholder review before it can be promoted to production.
## Surfaces
You can manage the same agent through any of these:
**Build with or without code**
Build and maintain agents, view analytics, and manage deployments through a visual interface.
**Build like an engineer**
CLI and Python package for building agents locally with a Git-native workflow.
**Integrate with your systems**
REST APIs for updating and configuring agents programmatically.
## Choose your surface
| | Agent Studio | ADK | APIs |
| ----------------- | ---------------------------------------- | -------------------------------------- | ------------------------------------- |
| **Who it's for** | Non-technical users, visual workflow | Technical teams, engineering workflows | Enterprises with internal tooling |
| **How you work** | Visual UI – point and click | CLI and Python – local dev with Git | REST endpoints – programmatic control |
| **Best for** | Building and tuning agents interactively | Bulk authoring, version control, CI/CD | Programmatic control, automation |
| **Code required** | Optional | Yes (Python) | Yes (any language) |
An agent built in Studio can be updated through the ADK or APIs, and vice versa.
## Platform capabilities
All three surfaces share the same platform capabilities:
Proprietary LLM – sub-300ms latency, 24+ languages, no hallucination
Multi-provider TTS and ASR, plus webchat and SMS
FAQs, Sources sources, and retrieval-augmented generation
Multi-step conversation workflows – no-code or code-driven
Five9, NICE, Twilio, Genesys, Salesforce, ServiceNow, and more
Dashboards, conversation review, and PolyScore
## Get started
Build your first agent in Agent Studio in minutes
Install the CLI and start building agents locally
Explore PolyAI's REST APIs
# Real-time config
Source: https://docs.poly.ai/real-time-config/introduction
Let non-technical managers update agent settings without publishing new versions using structured configuration forms.
Use the Configuration Builder to let non-technical managers update agent settings – opening hours, feature toggles, fallback phone numbers – without publishing a new version or writing code. Changes take effect immediately in each environment, avoiding the need for developers to edit code and promote through the deployment pipeline.
Schema definition requires Python familiarity — a developer defines the configuration schema (fields, types, defaults). Once the schema is set up, non-technical users can update values in the Data tab without writing code.
The **Configuration builder** is found under **Real-time config**.
## How it works
Configuration Builder separates **structure** (schema) from **values** (data).
| Tab | Purpose |
| ---------- | ------------------------------------------------------ |
| **Schema** | Define what fields exist (like opening hours, toggles) |
| **Data** | Fill in the environment-specific values |
The schema enforces structure and validation. The data defines what the agent uses at runtime.
The configuration builder is not tied to the main publish lifecycle.
The builder sits **outside the agent's draft/publish system**, so data value changes take effect immediately within each environment. Changes to Live values affect all active calls instantly – verify values in Sandbox or Pre-release first.
This means:
* An empty config file that receives a schema will instantly expose real-time fields to fill.
* **Data** changes take effect immediately within the environment where they are made.
* **Schemas** must be created separately in each environment (they do not propagate automatically).
### Two tasks: schema definition and value entry
This page covers both schema definition (requires Python familiarity) and value entry (UI only). If you only need to fill in runtime values, skip to [step 2](#2-optional-add-environment-specific-values). For the full technical setup including Python code, continue reading from the top.
The Configuration Builder serves two different tasks:
* **Schema definition** – Define the structure and wire up `conv.real_time_config` in functions. This requires Python familiarity and is done in the **Configuration Builder** tab.
* **Value entry** – Fill in runtime values via the **Real Time Configuration** UI. No code required.
These tasks may be performed by different people with different access levels. For customers who self-manage their configuration (for example, updating settings across many locations), the distinction matters – value entry is done through the UI without touching the schema definition.
## Step-by-step
### 1. Create a schema
In **Configuration Builder → Schema**, define the fields. For example:
* A text field for `opening_hours`
* A toggle for `after_hours_enabled`
* A validated phone number for `fallback_number`
These fields are written in JSON Schema format. The schema drives the form layout in the next step. Use clear `title` values – these labels will be visible in the real-time UI.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"title": "Agent Settings",
"description": "Set call handling parameters for this agent",
"type": "object",
"properties": {
"opening_hours": {
"type": "string",
"title": "Opening Hours",
"description": "Hours during which the agent should be available"
},
"after_hours_enabled": {
"type": "boolean",
"title": "After Hours Message",
"description": "Enable this toggle to play a message outside business hours"
},
"fallback_number": {
"type": "string",
"title": "Fallback Contact Number",
"description": "Phone number to call if no agent is available",
"pattern": "^\\+44\\d{10}$"
}
}
}
```
Boolean toggles are also useful as **feature flags**, so you can enable or disable capabilities in live environments without a deployment. For example, you could add a `transfer_enabled` toggle to control whether the agent offers call transfers.
**The schema must be created separately in each [environment](/environments-and-versions/introduction).** If you build a schema in Sandbox, it does not automatically exist in Live. You need to set up the schema in every environment where you want the configuration UI to appear.
### 2. (Optional) Add environment-specific values
Once a schema is saved, the Real Time Configuration UI will appear automatically, even if no values are set.
You *can*, however, populate values manually, at any time, in the **Data** tab, where each environment (Sandbox, Pre-release, Live) maintains its own data.
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"opening_hours": "Mon–Fri, 9am to 6pm",
"after_hours_enabled": true,
"fallback_number": "+442071234567"
}
```
Fields can be left blank unless marked required.
### 3. Publish
You **do not need to publish** your agent for configuration changes to take effect.
However, publishing may still be useful if you want to include these changes in a documented release.
Once your schema is added, the **Real Time Configuration** tab becomes available:
* **Draft and Sandbox**
* **Pre-release**
* **Live**
### 4. Add the read config in your functions
Use the [conv.real\_time\_config](/tools/classes/conv-object/#real-time-config) helper to read real-time values.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
config = conv.real_time_config
hours = config.get("opening_hours")
after_hours = config.get("after_hours_enabled")
fallback = config.get("fallback_number")
if after_hours:
conv.say("We're currently closed. Please call back during business hours.")
conv.transfer_call(fallback)
```
All values are returned as a dictionary. Use `.get("key")` to safely access fields.
### Working with nested and complex schemas
The Configuration Builder supports arrays and nested objects in addition to flat fields. This is useful for projects that manage per-site settings, employee directories, or structured data.
For example, a schema might define an array of site-specific hours:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"type": "object",
"properties": {
"site_hours": {
"type": "array",
"title": "Site hours",
"items": {
"type": "object",
"properties": {
"site_name": { "type": "string", "title": "Site name" },
"hours": { "type": "string", "title": "Opening hours" }
}
}
}
}
}
```
To access nested values in your functions, index into the structure:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
sites = conv.real_time_config.get("site_hours", [])
first_site_hours = sites[0]["hours"] if sites else None
```
## Programmatic management via API
For deployments managing configuration across many locations, the [Agents API](/api-reference/agents/introduction) exposes the same real-time config surface the UI uses:
| Method | Endpoint | Description |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
| `GET` | [`/v1/agents/{agentId}/real-time-configs`](/api-reference/agents/endpoint/real-time-configs/list-all-config-pages) | List configs across environments |
| `GET` | [`/v1/agents/{agentId}/real-time-configs/{clientEnv}`](/api-reference/agents/endpoint/real-time-configs/get-a-config-page-by-environment) | Get config for a specific environment |
| `PATCH` | [`/v1/agents/{agentId}/real-time-configs/{clientEnv}/variables`](/api-reference/agents/endpoint/real-time-configs/update-config-variables-for-an-environment) | Update config variables |
| `PUT` | [`/v1/agents/{agentId}/real-time-configs/{clientEnv}/schema`](/api-reference/agents/endpoint/real-time-configs/upsert-the-json-schema-for-a-config-page) | Update the config schema |
This is especially useful when customers need to update settings themselves at scale, rather than editing values manually in the UI.
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os, requests
BASE = "https://api.us.poly.ai"
HEADERS = {"x-api-key": os.environ["POLYAI_API_KEY"]}
# Push the latest opening hours from your internal scheduling system
requests.patch(
f"{BASE}/v1/agents/{AGENT_ID}/real-time-configs/live/variables",
headers=HEADERS,
json={
"variables": {
"opening_hours": scheduling.get_hours_for_today(),
"after_hours_enabled": scheduling.is_out_of_hours(),
}
},
)
```
Changes take effect immediately in the target environment — no promotion required.
## Best practices
Design schemas for clarity. Labels help non-technical users navigate the configuration UI.
Validate critical fields (like phone numbers) with regex.
Test behavior across all environments before deploying to Live.
Keep track of which flows and functions use which configuration fields.
Set up the schema in every environment where you need the config UI – schemas are not shared across environments.
Standard JSON Schema features including arrays, nested objects, and validation patterns are supported.
## What happens if the schema changes?
If the schema is edited in a way that invalidates existing data, the system will prevent publishing until all environments are valid again.
## Can each environment have different values?
Yes. For example, you can test one phone number in **pre-release** while using a different one in **live**.
## Related pages
Understand sandbox, pre-release, and live deployment.
Access configuration data in your agent logic with conv.real\_time\_config.
Manage schema and variables per environment via the Agents API.
# 10.2024
Source: https://docs.poly.ai/releases/notes/24.10
October 2024 release notes.
The **October 2024** release brings targeted updates to ASR, regional expansion, version management, voice customization, and content filtering.
Expand the items for details:
The **ASR biasing for flows** feature allows builders to improve ASR performance by configuring biasing for specific types of inputs at particular [flow](/flows/introduction) steps.
**Key highlights:**
* Bias for input types including alphanumeric, numeric, precise date, string number, yes/no, name spelling, party size, relative date, and time.
* Optimal biasing effectiveness is achieved by selecting the minimal necessary inputs.
**Benefits:**
* **Improved ASR performance**: Targeted biasing enhances recognition accuracy for specific input types.
* **Higher recognition rate**: Custom biasing ensures critical inputs are accurately captured.
**How to use:**
* Open the **"Flows"** section under the Build tab.
* Configure biasing for input types at designated Flow steps.
The studio now operates in **UK and EU regions** as well as the default **US**.
**Key updates:**
* **EUW-1** and **UK-1** production environments.
* New regional domain addresses:
* **US:** [studio.us.poly.ai](https://studio.us.poly.ai)
* **UK:** [studio.uk.poly.ai](https://studio.uk.poly.ai)
* **EUW:** [studio.eu.poly.ai](https://studio.eu.poly.ai)
* Authentication formats for regional production environments remain the same.
Manage and review **historical, draft, sandbox, pre-release, and live versions** of your agent for seamless change management.
**Key highlights:**
* View and manage all agent versions.
* Access historical versions for a comprehensive overview.
**Benefits:**
* **Development and Deployment**: Efficiently manage changes to align with business objectives.
* **Ongoing Maintenance**: Implement updates without impacting user experience.
Visit the [environments and versions](/environments-and-versions/introduction) page for more details.
Set a distinct **greeting message and dial tone** before the agent interaction begins.
**Key highlights:**
* Customize greeting messages with a separate voice.
* Upload custom dial tones for tailored interactions.
**How to use:**
* Go to the **"Agent"** section.
* Enable the disclaimer message and configure the greeting and tone settings.
Gain full control over **Safety Filters** directly in the studio UI. Builders can fine-tune severity settings or disable specific filters to better align with project requirements.
**Key highlights:**
* Adjust filter severity to match client risk tolerance.
* Disable specific filters when necessary.
**How to use:**
* In the **"Settings"** section, modify filter thresholds using the slider or disable filters as needed.
# 11.2024
Source: https://docs.poly.ai/releases/notes/24.11
November 2024 release notes.
The **November 2024** release brings significant improvements to SMS capabilities, debugging tools, and Knowledge export functionality, alongside enhancements for barge-in and dialogue management.
Expand the items for details:
Builders can now easily upgrade [SMS](/messaging-channel/sms) templates into advanced, customizable [functions](/tools/introduction) to support complex use cases.
**Key highlights:**
* **Alternative SMS number:** Collect alternative phone numbers from callers for SMS delivery.
* **Upgradable SMS functions:** Add custom logic to basic SMS templates for advanced configurations.
You can now view and copy real-time input, output, and error messages for [functions](/tools/introduction) directly:
* After calls, in the **Conversation** review page.
* Live, when testing with [Agent Chat](/get-started/quickstart#test-your-agent).
Export your [Managed Topics](/knowledge/faqs/introduction) as a CSV file.
The file naming convention of exported Managed Topics is:
`{agent Name}_{Env}_{Timestamp}_{version code}_KB.csv`.
Note that draft exports will exclude the version code. The timestamp reflects the export time.
This settings toggle allows you to decide if callers can interrupt agents. This can improve conversation flow and create natural, human-like interactions.
Functionally, this feature shortens the [Voice Activation Detection (VAD)](https://machinelearning.apple.com/research/comparative-analysis-personalized-voice) time and reduces [response latency](/voice-channel/audio-library).
To test this feature, find **Enable barge-in** in the **"Settings"** menu.
Stop keywords give builders control over dialogue flow by halting agent responses upon detecting specific words or phrases. These can
trigger custom functions or prevent unwanted interactions. Use this feature to configure stop keywords using **RegEx patterns.**
**Key highlights:**
* **Halt responses:** Interrupt the agent's response when certain words or phrases (configured with RegEx) are detected in user input.
* **Trigger functions:** Use Stop Keywords to activate specific functions or workflows.
**How to use:**
1. Create a function
2. Open **Response Controls** in the Build tab.
3. Add Stop Keywords and define their behavior using RegEx patterns.
4. Optionally, configure the agent to trigger a custom function or workflow when a Stop Keyword is detected.
Visit the [stop keywords](/voice-channel/advanced/call-settings#stop-keywords) section for more details.
# 12.2024
Source: https://docs.poly.ai/releases/notes/24.12
December 2024 release notes.
The **December 2024** release adds variants, which enable multi-site configuration with individual agents now able to access multiple partitioned Knowledge configurations. This release also refines ASR capabilities,
latency customization, and builder tools.
Expand the items for details:
Visit the full [variant management](/knowledge/variants/introduction) page for details.
Use variants to manage [Knowledge](/knowledge/faqs/introduction) content for multiple locations in a single agent, and use one agent to handle
enquiries about a business with outlets in different timezones, legal jurisdictions, or tax codes.
Each variant has content for custom attributes like phone numbers, addresses, and opening hours. You can use variants and variables in [SMS](/messaging-channel/sms),
[functions](/tools/introduction), and [Knowledge](/knowledge/faqs/introduction) entries, with the `${variant_foo}` format:
> Imagine you have an attribute `site_number`, your SMS content field would be:
> `Please contact the office at ${variant_site_number}`
You can add logic to your [start function](/tools/start-tool) to direct the agent to likely variant Knowledge items
based on phone number geolocation.
Use transcript corrections to aid agents in recognizing domain-specific terms, and avoid issues with homonyms like:
* "I see you" for a hospital's ICU.
* "Money" for an art museum dealing with works by Monet.
Go to **Voice > Advanced settings > Speech** in the studio to use this feature, and visit the full [documentation](/voice-channel/advanced/call-settings#transcript-corrections) page for more details.
Control latency to alter the agent's conversational tone to match audience preferences.
**Use case:**
* Configure thoughtful delays to prevent interruptions.
* Set faster response times for rapid user queries.
**How to use:**
* Go to **"Settings"**, then **"Interaction Style"**, and select the desired response delay time.
Visit the [audio management](/voice-channel/audio-library#interaction-style) page for more details.
You can now copy and paste entire [flows](/flows/introduction) or specific nodes between projects, by using `Cmd/Ctrl + C` to copy and `Cmd/Ctrl + V` to paste flows or nodes.
**Key features:**
* Copy entire flows or partial nodes within or across projects.
* Import any subsidiary global or transition [functions](/tools/introduction) without overwriting existing templates.
Users can now download call recordings directly from the studio for offline review and analysis.
**How to use:**
* Open the **"Conversation"** page.
* Click on the **"Download"** icon to save the call recordings locally.
You may not have permissions enabled by default. Speak to your PolyAI advisor or email support for more information on enabling permissions.
# 01.2025
Source: https://docs.poly.ai/releases/notes/25.01
January 2025 release notes.
The **January 2025** PolyAI Agent Studio release introduces easier in-browser calling, a new safety dashboard, custom analytics dashboards,
a new Knowledge structure, and updates to functions and the Conversation API.
Expand the items for details:
In-app calling is a quicker way to [test voiced conversations](/get-started/quickstart#test-your-agent) with your agent, removing the need for telephony services like [Twilio](/voice-channel/numbers/twilio/introduction).
**How to use:**
* Click the phone icon in the top-right corner of the project page.
* Select an [agent version](/environments-and-versions/introduction) in the dropdown to start testing.
The new **text-to-speech audio cache** detects common phrases across calls, enabling easy customization.
**Key Benefits:**
* Enhance voice quality by editing utterances directly in-studio.
* Streamline testing and iteration for commonly used phrases.
Visit the full [audio management](/voice-channel/audio-library) page for details.
A dedicated **Questions** field has been added to the Knowledge UI, allowing for clearer separation between sample questions and content.
**Key Benefits:**
* Improves readability for builders.
* Simplifies the process of adding and editing sample questions.
Visit the [Managed Topics Introduction](/knowledge/faqs/introduction) page for more details.
The enterprise safety dashboard provides insights into flagged calls and safety filter activations.
**Key Benefits:**
* Monitor flagged calls across projects.
* Understand which safety filters were triggered and why.
Visit the full [self-serve dashboards](/analytics/dashboards/introduction) page for details.
Support for custom dashboards is now available for all projects.
**Key Benefits:**
* Tailored dashboards aligned with project-specific goals.
* Improved clarity and focus on critical success metrics.
Visit the full [custom dashboard](/analytics/dashboards/custom) page for details.
End functions allow unconditional function calls at the conclusion of conversations.
**Use Cases:**
* **Data logging:** Save conversation details to a [CRM](/integrations/introduction).
* **Task automation:** Trigger workflows or send follow-up [SMS](/messaging-channel/sms).
Visit the full [End Function](/tools/end-tool) page for implementation details.
The Conversations API now includes `variant_id` and `variant_name` in the following endpoints:
* [Get Conversations](/api-reference/conversations/v3/endpoint/get-conversations): `/v1/{account_id}/{project_id}/conversations`
* [Get Maximum Concurrent Call Numbers](/api-reference/concurrent-calls/endpoint/get-max-concurrent): `/v1/{account_id}/{project_id}/conversations/concurrency`
These additions allow for better tracking and management of variant-specific data.
# 02.2025
Source: https://docs.poly.ai/releases/notes/25.02
February 2025 release notes.
The **February 2025** PolyAI Agent Studio release introduces new tools for Knowledge versioning, function latency control, and improved conversation review.
Expand the items for details:
You can now use the [Environments & versions](/environments-and-versions/diffs) tab to compare two [Knowledge](/knowledge/faqs/introduction) versions
side-by-side and track modifications across the **sandbox, pre-release, and live** environments.
Versions are displayed as **split diffs**.
* ** Additions** mean a new Knowledge entry.
* ** Deletions** means a Knowledge entry has been deleted.
* ** Edits** is the symbol applied to any existing but altered Knowledge entry.
* Quickly identify additions, deletions, and changes between Knowledge versions.
* Easily attribute changes to their authors.
Visit the [Environments & versions](/environments-and-versions/diffs) page for more details.
Improve the experience for users when a function takes longer than expected by playing filler utterances during the delay.
**How it works:**
* Set up **delay responses** that play while a function is still processing.
* Define the **initial delay** before the first filler utterance is played.
* Configure the **interval** between utterances to control pacing.
* Specify the **expected length** of each utterance for smoother timing.
This feature ensures that users receive real-time feedback instead of silence, making interactions feel more natural. It's useful for scenarios like **booking confirmations** or **data lookups**, where a response takes a few seconds.
Visit the [Function delay control](/tools/delay-control) page for more details.
Upload pre-recorded audio directly from the **audio management resource** in Agent Studio.
**Key benefits:**
* Easily replace TTS with high-quality voice actor recordings.
* Improve voice consistency across different agent responses.
* Initial release supports single file uploads. Bulk upload functionality is planned for a future release.
Reference variant attributes dynamically within Knowledge responses.
**How it works:**
* Add variant attributes by typing `/` or selecting the `+` icon in content fields.
* Reference existing attributes or create new ones dynamically.
* Personalize responses by encoding structured variant data.
Transition functions are now managed centrally within the flow editor.
**Key benefits:**
* Prevents accidental deletions when modifying flows.
* Enables reusing, duplicating, and managing transition functions from a single interface.
* Streamlines agent configuration and debugging.
Visit the [flows](/flows/introduction) page for more details.
Users can now annotate user conversation turns for:
* **Missed topics**
* **Incorrect ASR transcriptions**
Agent conversation turns can be annotated for **wrong information**.
You can now configure handoff using SIP, with [INVITE](https://datatracker.ietf.org/doc/html/rfc3261#section-13.3.1), [REFER](https://www.ietf.org/rfc/rfc3515.txt), or [BYE](https://www.rfc-editor.org/rfc/rfc3261.html).
You can also add [SIP headers](https://www.iana.org/assignments/sip-parameters/sip-parameters.xhtml) to the handoff template.
**How it works:**
* The default handoff method is set to REFER.
* You can adjust this to other methods according to the specific needs of your projects.
* You can add SIP headers to the handoff.
Visit the [call handoff](/voice-channel/handoffs) page for more details.
A new **Enterprise overview dashboard** provides key operational insights and is the new homepage of a project.
**Key benefits:**
* Monitor total calls, call duration, words generated, and key metrics.
* Improve oversight of agent performance and containment rates.
* Can be configured to work alongside custom dashboards for deeper analytics.
# 03.2025
Source: https://docs.poly.ai/releases/notes/25.03
March 2025 release notes.
The **March 2025** PolyAI Agent Studio release includes enhancements to dashboards, voice management, Knowledge performance, and real-time collaboration features.
Expand the items for details:
A redesigned [self-serve dashboards](/analytics/dashboards/introduction) now includes additional metrics and improved performance.
**What's new:**
* Containment rate, total calls, SMS stats, and function call metrics now available at a glance.
* Hourly refresh cycle brings data closer to real time.
* Dark header and improved layout boost usability and visual clarity.
The new [self-serve dashboards](/analytics/dashboards/introduction) mirrors the updated overview dashboard experience.
**What's improved:**
* Faster load times and more responsive data rendering.
* Hourly refreshes ensure up-to-date monitoring.
* Unified styling with clearer visual hierarchy for incident triage.
You can now click matched [Knowledge](/knowledge/faqs/introduction) topics in [conversation review](/analytics/conversations/review) to open them in a new browser tab.
This allows you to explore topic content without disrupting the review flow, making it easier to cross-reference and validate responses.
Reviewed conversations now include the **agent version** that was active at the time.
**Why it matters:**
* Ensures reviewers can account for environment-specific behavior.
* Useful for debugging across staging vs. production versions.
* Supports clearer QA documentation and compliance audits.
You can now upload new voices directly in Agent Studio without needing backend intervention.
Refer to your voice provider's documentation to locate the correct voice ID for upload.
The voice selection interface has been redesigned with filter controls to streamline voice browsing.
**Available filters:**
* Language
* Accent
* Gender
SMS messages are now triggered **inline** during [function](/tools/introduction) execution, rather than being queued afterward.
**Why it matters:**
* Enables immediate error handling for failed sends.
* Prevents agents from falsely confirming delivery before it's complete.
* Supports tighter runtime logic and better user feedback.
The Knowledge search bar now fully indexes action keywords like [SMS](/messaging-channel/sms), [handoff](/voice-channel/handoffs), [function](/tools/introduction), and [variant](/knowledge/variants/introduction).
**What's new:**
* These terms are now searchable across the Knowledge area.
# 04.2025
Source: https://docs.poly.ai/releases/notes/25.04
April 2025 release notes.
The **April 2025** PolyAI Agent Studio release includes new tools for call review, debugging, and utility development.
Expand the items for details:
We have refined the sidebar layout to better group tools by function and clarify where key capabilities live. Think of this as a smart re-balancing of where things go and a review of section naming.
### What's new, renamed, or moved:
**Analyze** renamed to **Manage**
* **Agent Analysis** → New feature. See below for more details.
**Build**
* **Agent** → Renamed from **About**.
* **Model Training** → Moved here from **Annotations**, previously under **Conversation Review**.
* **Variant Management** → Moved here from the **Configure** menu.
**Voice**
* **Agent Voice** → Moved here from Configure (combines features previously under **Voice** and **Rules**)
* **Cache Management** → Previously the **Audio Management** tab.
**Configure**
* **Environments** and **Project History** → New sections, previously under **Environment & Versions**.
### Why it matters
These adjustments make it easier to find what you need, reduce clutter, and prepare the platform for future capabilities.
You can now generate **LLM-powered performance metrics** in the PolyAI Agent Studio using the new **Agent Analysis** flow. Define a custom prompt and category set, then run it across a batch of calls to generate insight-rich evaluations and visual summaries.
**How it works:**
* Create a custom evaluation prompt, with categories and descriptions
* Select a batch of calls using the Agent Analysis feature.
* Automatically assess agent performance across key custom criteria like tone, intent success, or compliance (or any other potential variables)
* View results in a dedicated **Agent Analysis UI**, with graphs showing how your calls measure up against each evaluation axis
Agent Studio now supports real-time collaboration and edit tracking across your team.
Multi-user refresh view in action
**How it works:**
* See who else is editing a draft in real time.
* Receive notifications when changes are saved by others.
* Copy unsaved edits before refreshing to avoid overwriting.
* View full change logs using the **history** panel.
You can now use an LLM to review and rate calls.
**What you can do:**
* Rate calls for tone, intent success, or compliance
* Use project-specific categories
* Get automatic summaries
You can now filter conversations by workspace on the Conversations page.
**Why it helps:**
* Focus on the calls your team is responsible for
* Cleaner review experience for large orgs
This is an opt-in feature and will not be enabled by default. Contact PolyAI for access.
You can now extract structured address data from free-text input using a built-in LLM utility: `extract_address()`.
**What it does:**
* Parses the most recent user message for address info
* Returns an `Address` object with fields like street, postcode, and country
* Raises an `ExtractionError` if parsing fails
**Usage:**
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
try:
address = conv.utils.extract_address(country="US")
conv.state.parsed_address = address
except ExtractionError as e:
conv.state.address_error = str(e)
```
**Address object:**
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
@dataclass
class Address:
street_number: Optional[str]
street_name: Optional[str]
city: Optional[str]
state: Optional[str]
postcode: Optional[str]
country: Optional[str]
```
**Other details:**
* Calls an LLM, so may take a few seconds
* You can optionally validate against a list of known addresses
* Some fields may be missing depending on input quality
You can now access earlier conversation turns from within utility functions using `conv.history`.
**What it does:**
* Returns a list of events: user inputs, agent replies, function calls, etc.
* Useful for writing context-aware logic
**Usage:**
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def some_function(conv: Conversation):
history = conv.history
for event in history:
print(event.to_dict())
return {
"utterance": "Here's a response that depends on previous turns.",
"content": f"Conversation so far: {str(history)}"
}
```
**Why it matters:**
* Build logic based on what's already happened in the conversation
* Avoid passing state manually
* Better debugging and flow control
You can now export and import variants between agents or environments.
**What you can do:**
* Export all variant data to CSV
* Edit things like contact numbers or opening hours in bulk
* Reimport to update the same or a different agent
**Why it matters:**
* Makes it easy to scale across multiple sites
* No need to rebuild variant logic from scratch
* Keeps your CSVs in sync with other tools
See the full [variant management](/knowledge/variants/introduction) page for advanced use cases like SMS personalization or routing.
Reviewed conversations now show the **variant ID** used in each call.
**Why it matters:**
* See exactly which variant was active
* Easier to debug and compare different versions
* Adds context for QA and bug tracking
The built-in handoff now supports **handoff reasons** and **custom utterances**, and you can now use a `handoff()` method directly inside functions.
**What you can do:**
* Specify a reason for the handoff (e.g. compliance, escalation, etc.)
* Set a custom utterance to be spoken at handoff
* Use `handoff()` programmatically inside a [function](/tools/introduction).
**Example usage:**
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def escalate(conv: Conversation):
return conv.call_handoff(
reason="policy_violation",
utterance="Let me transfer you to a specialist who can help."
)
```
You can now configure multiple TTS voices for your agent and distribute them across calls. Use percentage values adding up to 100% to distribute the likelihood of each agent answering a call.
**Why it matters:**
* A/B test different voices for performance or tone
* Assign different voices to different intents or caller types
* Create more natural, human-like team dynamics
**What you can do:**
* Select and assign multiple voices
* Preview how your agent sounds across scenarios
For help or feedback, reach out to your PolyAI representative.
# 05.2025
Source: https://docs.poly.ai/releases/notes/25.05
May 2025 release notes.
The **May 2025** PolyAI Agent Studio release includes enhancements to variant testing, input handling, KB creation, and LLM feedback.
Expand the items for details:
[DTMF (Dual Tone Multi-Frequency)](https://www.techtarget.com/searchnetworking/definition/DTMF) is the method phones use to turn keypad presses into audio tones that telecom systems can detect and process.
You can now configure DTMF collection directly inside a flow step. DTMF is often used for collecting phone numbers, booking IDs, or confirmation codes during a conversation.
Click the app grid icon to open the DTMF menu.
**You can configure:**
* **Inter-digit timeout**: Set the number of seconds to wait between key presses before timing out of the collection process.
* **Number of digits expected**: Specify how many digits the agent should expect.
* **End key**: Choose an optional key (like `#` or `*`) to signal the end of input.
* **Collect while speaking**: Enable data collection even while the agent is still talking.
* **Mark as PII**: Flag the collected value as [Personally Identifiable Information](https://ico.org.uk/for-organizations/uk-gdpr-guidance-and-resources/personal-information-what-is-it/what-is-personal-information-a-guide/).
Users can now test how different [variants](/knowledge/variants/introduction) respond using a drop down in the chat panel.
**What you can do:**
* Select a specific variant to observe differences in behavior, tone, or logic
**Why it matters:**
* Easier testing
* Better confidence in how variants perform in production
You can now duplicate a variant and assign a new default variant directly from the [variant management](/knowledge/variants/introduction) page.
**What's new:**
* Open the options menu to **Edit** or **Duplicate** an existing variant.
* Set a variant as default using the toggle in the side of the **Edit** panel.
Switching to a new default automatically deactivates the previous one.
You can now create new [Knowledge](/knowledge/faqs/introduction) topics by [uploading a PDF](/knowledge/variants/csv-imports#pdf) or simply by [providing a website URL](/knowledge/variants/csv-imports#csv).
**Features:**
* Pulls page content from public websites
* This means you can give your agent a link to your company's FAQ and it will generate a full draft of Knowledge topics.
* Upload PDF files to generate KB topic drafts. When creating KBs using URL import, the system will provide a real-time crawl status and error messaging if the page cannot be scraped.
When using the URL option, remember some websites block scraping and that HTML-to-text fidelity may vary. Always review content carefully after importing.
A shortcut button has been added to the chat panel to open the conversation in the Review page.
**Benefits:**
* Speeds up debugging
* Useful when testing flows or functions in studio
The built-in **handoff** template and the `conv.handoff()` helper now accept two optional, structured fields for clearer call logging and routing:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def handoff(conv: Conversation):
return conv.call_handoff(
destination="DESTINATION",
reason="SPEAK_TO",
utterance="Okay, no problem. Just give a moment to connect you with someone who can help"
)
```
| Field | Purpose | Example |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
| `reason` | Machine-readable code explaining *why* the call is being escalated (e.g. `policy_violation`, `needs_human`, `no_availability`). Surfaces in [Conversation Review](/analytics/conversations/review) and the [Conversations API](/api-reference/conversations). | `policy_violation` |
| `utterance` | The exact phrase the agent should speak *before* initiating the transfer. Logged alongside the handoff. | "Let me transfer you to a specialist who can help." |
**Where it shows up**
* **[Flows](/flows/introduction) & [KB Actions](/knowledge/faqs/actions/introduction)** – When using the `builtin-handoff` action, you can now optionally specify `reason` and `utterance` fields inside the action configuration (visible in the JSON behind the scenes, not in the Handoff Destination UI).
* [Functions](/tools/introduction) – Call `conv.handoff(reason="...", utterance="...")` directly in the Function Editor to trigger a handoff with these fields.
* [Conversation Review](/analytics/conversations/review) – Both fields appear in the metadata panel for visibility and QA.
* [Conversations API](/api-reference/handoff/endpoint/get-handoff) – Returned inside the `handoff` object for downstream routing.
These fields do not appear in the [Call Handoff UI](/voice-channel/handoffs) (where you configure SIP methods, URIs, or SIP headers).
They are attached during configuration in the Knowledge area fields later, and are at runtime, when the agent decides to escalate the call, where they are logged alongside the handoff action.
**Why it matters**
* You can fine-grain routing rules in telephony or CRM systems.
* Ensures the exact agent wording before handoff is logged and auditable.
[Conversation Review](/analytics/conversations/review) now shows detected intents and extracted entities directly in the Diagnosis panel, making it easier to debug and QA conversations that use intents or entities.
**What's new:**
* **Intents**: Shows the triggered intent from the agent's recognition model (for pre-GenAI intent-based projects).
* **Entities**: Lists extracted key data points, such as booking numbers, customer IDs, or locations.
The speech recognition engine has been updated to provide improved accuracy for several supported languages.
**What's improved:**
* Better recognition of numbers and dates in French, Spanish, and German.
* Reduced transcription errors in noisy environments.
* Improved support for accented speakers and regional dialects.
**Why it matters:**
* Higher transcription accuracy improves agent understanding and reduces failure rates.
* Improves customer experience in multi-lingual deployments.
A new filter has been added to [conversation review](/analytics/conversations/review) to allow sessions to be filtered by duration.
**Benefits:**
* Focus on very short or very long conversations.
* Easily identify drop-offs, failures, and edge cases.
# 06.2025
Source: https://docs.poly.ai/releases/notes/25.06
June 2025 release notes.
The **June 2025** PolyAI Agent Studio release focuses on version comparison, configuration flexibility, and UI streamlining.
Expand the items for details:
You can now compare versions across **functions**, **flows**, and **Knowledge** items–not just KBs.
**What's new:**
* The sidebar UI highlights additions, edits, and deletions
* View full publish history per item
This update supports better change management for teams working across large or complex agents, especially when multiple people are working on a single item.
Bring dynamic, real-time settings to your agent–without touching flows or code.
**How it works:**
* Builders define a JSON **schema** to create structured config forms
* Managers populate **values** per environment (Draft, Pre-release, Live)
* Forms appear in the **Configuration** tab once the schema is published
This setup lets teams safely update logic–like hours, routing, or toggles–without risk to underlying functions.
**Example uses:**
* Opening hours
* After-hours message toggles
* Fallback numbers
* Environment-specific logic flags
You can now invoke actions, like functions and SMS, by typing `/name` directly into the editor.
**Improvements include:**
* Cleaner UI for selecting actions
* Matches top-level categories and partial names
* Can better handle nested actions in the UI
Disclaimer messages are now cached in the Cache under **Audio Management**.
**Why this helps:**
* Ensures consistent delivery of disclaimer audio
* Reduces response time when disclaimers are reused across flows or agents
* Simplifies troubleshooting when disclaimers fail to play
This feature is currently in open beta. Reach out to your PolyAI contact for access and setup.
Smart Analyst is a new chatbot in Studio that reviews a random sample of your recent calls and answers natural-language questions about them.
**Ask questions like:**
* "How long are my calls on average?"
* "Which intents failed yesterday?"
* "What's the most common escalation reason?"
**How it works:**
* Reviews call data from the past 24 hours
* Summarizes behavior patterns and performance
* Lets you explore agent usage conversationally
Agent Memory allows agents to persist and recall information across conversations–like user preferences or recent bookings–using a secure key-value store.
**How it works:**
* Memory is stored per **identifier** (e.g. phone number)
* Functions can access memory using `conv.memory`
* Values are written using `conv.state` and saved at call end
* Multiple identifiers (like phone and email) can be linked to the same profile
This is especially useful for repeat callers or omnichannel use cases, where remembering previous interactions improves containment and personalization.
**Example use cases:**
* Recall a user's previous delivery date or cheese preference
* Link web and phone interactions to a shared memory profile
* Personalise greetings based on previous contact history
**Developer usage:**
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Read memory
cheese = conv.memory.get("cheese_type")
# Persist memory
conv.state["cheese_type"] = "gouda"
```
Before using Agent Memory in production, a Data Protection Impact Assessment (DPIA) may be required. Contact your PolyAI representative for guidance on compliance requirements.
The [Test Suite](/testing/simulation-tests) lets you save real conversations as test cases and re-run them against draft or sandbox versions to check for behavioral drift.
**What's new:**
* Save test cases from the chat panel or conversation review
* Re-run conversations with new parameters like contact name or phone number
* Run tests individually or batch test cases in groups
* Review success rates in the **Test Runs** tab
Use this to catch issues when refining agents, especially when upgrading functions or dealing with variants.
PolyScore introduces automated LLM-based scoring for conversations–directly in Conversation Review.
**What it does:**
* Assigns a score from **0–10** per call, where higher is better
* Weighs multiple behavioral and qualitative signals
* Offers fast, standardized feedback across all calls
PolyScore also appears as a filterable field in the **Conversations** tab to help teams quickly identify high- or low-performing calls.
# 07.2025
Source: https://docs.poly.ai/releases/notes/25.07
July 2025 release notes.
The **July 2025** PolyAI Agent Studio release focuses on improved editor tooling–with powerful updates to analytics, voice control, and conversational intelligence.
Expand the items for details:
Smart Analyst has been upgraded with enhanced logic, query capabilities, and visualization tools–making it even easier to surface insights from call data.
**What's new?**
* **Keyword-driven transcript filtering**
Search transcripts with keyword queries to focus analysis on specific terms, topics, or call types.
> "Show me all calls where the customer said 'cancel my subscription'."
* **Aggregated data querying**
> "How many calls resulted in an escalation yesterday, broken down by length of call?"
Smart Analyst can run SQL queries against an aggregated data table and identify trends and performance patterns.
* **Integrated data visualization**
> "Create a bar chart showing the number of failed intents over the last 7 days."
Visual responses now include auto-generated charts mapped from query results–no need to leave the interface.
This builds on the Smart Analyst beta introduced in June 2025, offering deeper analytical capabilities directly within the QA tooling.
Agent Studio now opens to a redesigned homepage dashboard with a clearer overview of platform usage and agent performance.
**What's new:**
* **At-a-Glance Metrics**: Call volume, Containment Rate, AHT, and estimated Agent Cost Savings
* Metrics like **Top 10 QA Reasons** or **Top 10 GenAI Topics**: Horizontal bar charts with drill-down access.
* **Recent Conversations**: Visual cards for "Best Calls" and "Problem Calls" over the past 24 hours.
* **Agent Analysis Batch**: Interactive pie chart with prompt tooltips.
Input text fields have been rebuilt to support **rich text**, so adding a reference to a flow, function, SMS, or other platform element is as easy as typing **/** and then searching for it by name.
**Key improvements:**
* You can now search by top-level menu item names, like "SMS" or "Handoff"
* The **Escape key** now closes popups.
* Open the insert menu by typing `/` or clicking the **plus icon**.
* Maintains support for `{{mention}}` formatting. So, for example `{{fn:SELL-abc123de}}` will show up as the function named `SELL-abc123de`.
You can now add your own [Amazon Polly](https://aws.amazon.com/polly/) voices to your Agent Studio project.
**What's new:**
* New **"Add New Voice"** flow for [Amazon Polly TTS](https://aws.amazon.com/polly/).
* Fill in attributes for the voice like **language** (and **accent**).
* Lets you assign **attribute** tags to help organize and apply voices consistently.
This is part of a broader move toward multi-provider TTS support. Voices from PlayHT and other vendors will follow soon.
You can now fine-tune the **speaking rate** of synthetic voices in Agent Studio to match your preferences or use case.
**What's new:**
* New **voice speed slider** available in voice settings
* Ranges from **0.5× to 1.5×**, starting at the default **1.0×**
* Fine-grained increments (down to **0.01×**) allow for precise control
* Marked 1.0× reference point for easy orientation
**Example use cases:**
* Speed up a slow voice without changing TTS provider
* Slow down speech slightly for accessibility or clarity
* Match pace across multiple voices for consistency
Available across all voice integrations, including Amazon Polly.
Agent Studio functions can now call [Claude](https://www.anthropic.com/index/claude) for generative reasoning and summarization.
**What's new:**
* Add Claude as a model option in function settings
* Supports prompt templating and async response handling
* Useful for summarizing user inputs, drafting replies, or generating structured outputs
**Designed for enterprise users** looking to augment existing flows with LLM-powered reasoning.
If applicable, inbound, and webchat calls are now clearly distinguished across Agent Studio.
**What's new:**
* New **channel label**: Calls are now tagged as either *Inbound*, *Outbound*, *Agent chat* (webchat).
* **Conversation Review table** shows which number the call was placed to (callee) rather than the outbound number used.
* Column renamed to a neutral label like **"Phone Number"**
* Filtering improvements let you easily find all calls to the same callee, especially useful for outbound flows
This update improves outbound use cases and makes it easier to navigate high-volume call logs.
Outbound calls and their statuses are now visible in Agent Studio:
* **Delivery status added** to Conversation Review filters and custom columns
* Call outcomes now grouped into intuitive labels:
* `Success` → Call connected
* `Unavailable` → Temporarily unreachable
* `Busy` → Line engaged
* `Invalid Number` → Not found
* `Declined` → Explicitly rejected
* `Error` → All other outcomes
# 08.2025
Source: https://docs.poly.ai/releases/notes/25.08
August 2025 release notes.
The **August 2025** PolyAI Agent Studio release focuses on performance improvements, voice configuration flexibility, and more powerful call review tools, as well as introducing
OpenAI's [new model GPT-5](https://openai.com/index/introducing-gpt-5/) as an option for agents.
Expand the items for details:
The Agent Studio homepage now loads faster and presents key metrics more cleanly. The displayed metrics can be customized to meet the needs of a particular project, so what you see will be unique to your deployment.
**What's new:**
* Reduced page load time
* Improved widget layout for better readability
* More consistent alignment across metric cards and charts
Agent Studio now displays an **AI-generated summary** at the top of the Conversation Review screen.
**What's new:**
* New **AI Summary** module in the **Conversation Review** screen
* Generated post‑call using GPT summarization logic
AI Call Summaries are available for eligible projects. Contact your PolyAI representative to confirm availability for your project.
Projects can now have **up to 10 active voices**, updated from the previous limit of **5** voices.
Voice limits apply per project and can be managed from the settings page.
It is now much easier to filter calls in Conversation Review by delivery status, channel, and other metadata.
**What's new:**
* Faster filter response time
* Filter combinations can be applied without resetting the view
Use saved filters to make these configurations instantly reusable.
Conversation Review now displays additional call attributes and improved channel labels for quicker scanning.
**What's new:**
* Clearer channel tags for *Inbound*, *Outbound*, and *Agent Chat*
* Additional metadata columns available for custom table views
* Streamlined column naming for consistency across the platform
We've added **GPT‑5** options to Agent Studio for evaluation and feedback.
**What's available:**
* **GPT‑5 nano** and **GPT‑5 mini**: Recommended for most Agent Studio use cases (best balance of quality/latency)
* **GPT‑5 (large)**: Included for testing; note it's currently slow at 3–4 seconds or more.
* **GPT‑5 chat (router)**: Routes dynamically across the above models so you can experiment quickly.
Switch models from function settings or your project's model configuration panel.
You can now test calls using a selected variant directly in the Test panel.
**What's new:**
* Variant **dropdown + search** in **Test ▸ Call**
* Calls launched with the chosen variant's config
* Transcripts tagged with `variant_id` for review and filtering
Use this to test prompts, policies, or routing logic before [upgrading your deployment to production](/environments-and-versions/introduction).
It's now easier to jump from answers to exact conversations. New and more complex SQL data queries have also been added to the Analyst.
**What's new:**
* **Clickable Call IDs:** Any Call ID in a Smart Analyst answer opens that call's **Conversation Review** in a new tab.
* **Expanded SQL range:** Queries can include additional tables, such as **PolyScore** and **Custom Metrics**.
If a **Start** or **End** function has an error that prevents execution, Agent Studio now shows a warning and guidance to fix it.
Agent Analysis now supports multiple analysis tasks per project.
**What's new:**
* Configure up to **10 analysis tasks** per project. Each task can have its own prompt, categories, and color-coding.
* A redesigned chart creation flow with filters, random call selection, and manual call selection.
* A dedicated **Batch UI Page** where you can see task results, call breakdowns, filters, and categories in one view.
* The ability to click into a call to open Conversation Review, with categorizations still editable at the call level
Use multiple tasks to mimic QA workflows, track quality across different call types, and monitor continuous improvement.
# 09.2025
Source: https://docs.poly.ai/releases/notes/25.09
September 2025 release notes.
The **September 2025** PolyAI Agent Studio release focuses on the addition of new Webchat configuration options and individual Test Cases
to complement the previously introduced Test Sets.
Expand the items for details:
Configure, preview, and test non-voice Webchat deployments directly in Studio.
**What's new:**
* There is a new **Webchat** configuration section in the Agent Studio **Settings** tab.
* You can customize the appearance of the chat widget quickly using the live preview.
* You can test the widget quickly in-browser with **Preview Demo** or in any site using the embed script tag:
The Test Suite now supports individual **Test Cases**.
**What's new:**
* You can use a conversation to create a **Test Case** against a single scenario.
* These are run directly from the **Test Cases tab**, after selecting the agent version (Draft/Sandbox) to test against.
* Outcomes and last-run timestamps are tracked automatically.
* Organize cases into **Test Sets** to rerun whole groups at once and view aggregated results with charts.
* Cases can belong to **multiple sets**, so you can target them by feature or however else is required.
The Agent Studio home page now surfaces more trend data and a clearer quality summary.
**What's new:**
* **Average PolyScore** added to Quick Insights
* New charts for **Containment rate** and **PolyScore** over time
* Consistent "**PolyScore**" naming across tables, filters, and details
Smart Analyst now supports structured responses using reasoning logic. A persistent chat history has also been added.
**What's new:**
* **Structured responses**: answers now include reasoning logic along with the final answer, as well as more clearly labeled sources and steps.
* **Chat history**: resume previous analyses; conversations stored with redacted transcript data
* **Auto titles** with the ability to rename and delete old analyses from the history list.
You can now buy and assign phone numbers from the new **Numbers** tab in the Agent Studio UI, and you can optionally link these to individual multi-site **variants**.
**What's new:**
* New **Numbers** page under *Configure* shows all numbers by environment
* Add, view, and delete numbers in Sandbox and Pre-release.
* Assign numbers to variants for multi-site projects.
Functions can now write structured logs [Conversation Review](/analytics/conversations/annotations) and the [Conversations API](/api-reference/conversations) using [`conv.log`](/tools/classes/conv-log).
**What's new:**
* Use `conv.log.info`, `conv.log.warning`, and `conv.log.error` inside functions
* Entries appear in **Conversation Review → Diagnosis** and using the **Conversations API**.
* Mark entries as **PII** so they are only visible to users with permission.
Third-party QA tools and automated test suites can now read **tool/function call events** from the Conversations API.
**What's new:**
* Every function call made during a conversation is now surfaced in the API response
* Makes it possible to verify not just the dialogue, but also the actions taken behind the scenes
* Access is scoped per environment (Sandbox, Pre-release, Live) so external tools don't touch production data
# 10.2025
Source: https://docs.poly.ai/releases/notes/25.10
October 2025 release notes.
The **October 2025** PolyAI Agent Studio release introduces live call monitoring, advanced Webchat configuration, automated regression tests, smarter analytics features, and improved deployment control.
Expand the items for details:
Monitor calls as they happen in real time in [Conversation Review](/analytics/conversations/introduction).
**What's new:**
* In-progress calls now appear in the **Conversations** list with live duration indicators.
* Watch turns stream live as they occur, including timestamps and caller metadata.
* Access is permission-controlled – users without [PII-level](/tools/classes/conv-log#pii) access cannot view transcripts.
* A new tabbed view separates *Live*, *Ended*, and *All* calls for easier navigation.
Webchat now supports configurable disclaimer messages and consent prompts to meet legal and privacy requirements.
**What's new:**
* Add a **custom disclaimer** message above the input field.
* Include **clickable Privacy Policy** and **Terms & Conditions** links.
* Optionally require an **"I consent"** button before starting a chat.
* Configure everything from **Settings → Webchat**.
You can now run Test Sets automatically when publishing to Sandbox, or promoting to Pre-release or Live.
**What's new:**
* Configure **Test Sets** to run automatically on publish or promote.
* Runs trigger before deployment to Sandbox, Pre-release, or Live environments.
* Enforces regression testing for complex, multi-variant projects.
Write structured logs directly from inside functions using `conv.log`, viewable in **Conversation Review → Diagnosis** and the **Conversations API**.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def handle_booking(conv):
conv.log.info("Validating booking", booking_id=conv.state.get("booking_id"))
conv.log.warning("Slow API response", vendor="booking_api", latency_ms=4800)
conv.log.error("Payment failed", code="CARD_DECLINED", retriable=False)
conv.log.info("User phone captured", phone=conv.caller_number, is_pii=True)
```
**What's new:**
* Use `conv.log.info`, `conv.log.warning`, and `conv.log.error` to record structured logs.
* Entries appear inline in Conversation Review and in API responses.
* Mark entries as **PII** so only users with permission can view them.
Smart Analyst now opens with a new **Templates** interface – offering ready-made, categorized prompts for instant insight generation.
**What's new:**
* Start faster with **pre-built analysis templates**, organized by category: *Customer Insights*, *Agent Performance*, and *Custom Research*.
* Templates include optimized queries and short descriptions for clear outcomes.
* Selecting a template auto-fills the query field with a pre-configured analysis prompt.
* Each template is designed to help non-technical users explore real data and build useful reports instantly.
* Templates will expand over time with more analysis types and custom management tools.
You can now schedule Agent Analysis to run **daily** or **weekly**. Automating recurring analysis tasks is a good way to continuously track the performance of your project after deployment or when it is in QA.
# 11.2025
Source: https://docs.poly.ai/releases/notes/25.11
November 2025 release notes.
The **November 2025** PolyAI Agent Studio release introduces the Connected Knowledge tab (now part of the Knowledge area), KB topic activation controls, an in-app integrations catalog, multi-channel dashboards, Smart Analyst visual updates, and improved SMS configuration.
Expand the items for details:
Upload and manage multiple knowledge sources to power your agent – files, URLs, and external integrations – all from one place within the Knowledge area.
This is an improvement on the previous knowledge collection functionality, and allows mature organizations with a lot of content to build and maintain quickly.
**What's new:**
* Add **multiple external knowledge sources** to populate your agent's knowledge.
* Supported formats include PDFs, text files, Word docs, CSVs, JSON, URLs, and CRM/helpdesk integrations.
* Assign each source to specific **client environments** and **variants**, so large deployments stay organized.
* Group sources for easier navigation and maintenance.
* Re-upload or re-scrape sources to refresh content when upstream material changes.
* Per-chunk **citations** appear in Conversation Review, showing exactly which source text informed a response and helping teams trust the agent's reasoning.
Browse a catalog of third-party tools that Agent Studio can integrate with.
**What's new:**
* New **Integrations** page listing supported tools across Telephony, Chat, CRM, Vertical, and Knowledge categories.
* Search by integration name or description to find what you need quickly.
* Filter by category to focus on telephony, CRM, healthcare, restaurant, hotel, or KB integrations.
* Each card shows the integration logo and a short summary of what the connection enables.
Temporarily turn off Knowledge topics without deleting or restructuring content.
**What's new:**
* You can now **activate or deactivate** any KB topic from the ellipsis menu.
* Deactivated topics are **ignored by the LLM** and clearly labeled *Inactive* in the UI.
* Activation state is versioned and can differ across **Sandbox**, **Pre-release**, and **Live**.
* New **sort by inactive topics** option to help find dormant or seasonal content.
Agent Studio's homepage now reflects how projects actually run: if a project uses both **voice** and **chat**, each channel now has its own homepage analytics tab.
The redesigned dashboards give clearer insight into performance, and for chat-only deployments now only relevant chat metrics will appear.
**What's new:**
* Split the homepage into **Voice** and **Chat** tabs, with visibility based on which channels your project uses.
* Chat tab highlights key KPIs such as engaged conversations, containment rate, total messages, cost savings, and average handle time.
* New charts for chat deployments: engaged conversations over time, messages per conversation, total message volume, top QA reasons, and containment evolution.
* Currency selector for core metrics (USD, GBP, EUR), with automatic conversion for cross-regional teams.
Agent Studio now matches how production SMS integrations are actually configured.
Messaging Service IDs replace the old, restrictive phone-number-only fields, making outbound SMS setup more flexible for enterprise deployments.
**What's new:**
* UI accepts Twilio-style Messaging Service IDs like `MGxxxxxxxxxx`.
* Replaces the previous phone-number-only validator, aligning the UI with existing API behavior.
# 12.2025
Source: https://docs.poly.ai/releases/notes/25.12
December 2025 release notes.
The **December 2025** PolyAI Agent Studio release focuses on scale and collaboration: multi-draft branching, low-code API integrations, and introduces no-code flow building.
Expand the items for details:
Agent Studio now supports **multi-draft development** using branches, allowing multiple people or teams to work on the same project in parallel without colliding.
This is especially valuable for organizations that need to ship urgent fixes quickly while continuing longer-term feature work – without turning Sandbox into a single shared bottleneck.
**What's new:**
* Whenever you make a change, create **branches** from the main project so work can happen in parallel.
* Branches are listed under each project, with information like the owner, creation time, and last edited.
* Merge a branch back into the main project using **Merge** (replacing **Publish**).
* Built-in **conflict resolution** shows changes side-by-side (re-using the version comparison experience) and requires resolving all conflicts before merge.
No-code flows make it possible to build and evolve conversational logic without writing custom [functions](/tools/introduction) for every step.
This lowers the barrier for external builders while still allowing advanced steps and functions where needed.
**What's new:**
* Build flows using **default nodes** with instructions, entity collection, and conditional routing.
* Drag-and-drop connections between steps without writing [transition functions](/flows/transition-functions).
* Combine no-code steps with advanced steps in the same flow for complex scenarios.
A new **API integrations** area lets you configure external endpoints directly in the UI – making API-backed agents easier to build and reuse.
**What's new:**
* New **API integrations** page listing configured endpoints with name, operation, and description.
* Configure **base URLs and authentication per environment** (Sandbox / Pre-release / Live) while keeping operation details consistent.
* Supported auth types include **No Auth**, **Basic Auth**, **API Key**, and **OAuth 2.0**.
* Use variables inside headers and body fields for dynamic runtime values.
* Call configured APIs from functions using `conv.api` (for example: `conv.api..(...)`).
* API request and response status can be surfaced in logs for debugging and QA review.
Test Cases can now show the **variant** they were saved against, so teams can reliably run QA against the correct configuration in variant-driven deployments.
This matters in real builds because the same prompt running on the wrong variant is effectively a different agent.
**What's new:**
* Test Case list and detail pages now display the **variant** used when the test case was saved.
* The variant column only appears for projects that use variants.
* If a referenced variant has been deleted, the UI flags the test case so it can be updated or removed.
Conversation Review now provides clearer, more transparent signals about **call quality and outcomes**, making it easier to understand *why* a conversation succeeded or failed.
**What's new:**
* **Redesigned conversation table and review UI** with clearer summaries and improved readability.
* **PolyScore breakdowns** now explain how scores are calculated, including:
* Agent understanding
* Task success and resolution quality
* Inline explanations and tooltips make scoring criteria explicit.
Smart Analyst now streams its reasoning steps live while an analysis is running, giving users immediate visibility into how conclusions are formed.
**What's new:**
* Analysis stages are shown **progressively**.
* Users can follow the model's thinking in real time, so complex analyses are faster and more transparent.
# 02.2026
Source: https://docs.poly.ai/releases/notes/26.02
February 2026 release notes.
The **February 2026** PolyAI Agent Studio release introduces a new multi-channel navigation structure and expands visibility, control, and configuration across voice, chat, integrations, and review workflows.
Expand the items below for details:
Agent Studio now includes a dedicated **Channels** section in the sidebar. It separates channel-specific configuration from shared project resources such as flows, Knowledge content, and variants.
**What's new:**
* Added a new **Channels** section below **Build**.
* Created dedicated pages for:
* **Voice Configuration**
* **Chat Configuration**
* **Widget Configuration**
* Moved **Chat configuration** from **Settings** into **Channels**.
* Moved greeting and disclaimer settings from the **Agent** page into **Voice** and **Chat** configuration pages.
* Placed **Widget** configuration under **Channels**.
* Structured the section to support additional channels in future.
**Navigation updates:**
* Added a new **Deployments** section.
* Moved **Environments** and **Project History** into **Deployments**.
* Moved **Real-time Configuration** and **Configuration Builder** into **Build**.
* Updated user permissions to reflect the new structure.
The new **Voice library** makes it easier to browse, preview, and select voices for your agent.
**What's new:**
* Open the **Voice library** directly from the **Agent Voice** page.
* Browse voices in the **Explore** tab or return to saved voices in **Favorites**.
* Preview supported voices with custom test text before selecting them.
* Filter voices by **language**, **region**, and **gender**.
* View provider, language, and style tags for each voice.
* Select a new voice directly from the library and apply it to the agent.
**Agent Voice page updates:**
* The **Agent Voice** page now links directly to the **Voice library**.
* Voice selection is managed separately for the main **Agent** voice and the **Disclaimer** voice.
* If disclaimer audio is configured elsewhere, the page links users to **Channels / Voice configuration**.
Latency insights are now available directly in the product, helping teams understand where response time is spent during a call.
**What's new:**
* View detailed latency breakdowns for conversations.
* Inspect timing for **LLM requests**.
* Inspect timing for **function calls**.
* Use latency data to identify and reduce slow responses.
You can now configure in-call voice CSAT surveys directly in Agent Studio.
**What's new:**
* Dedicated **CSAT** page under **Configure**.
* Toggle to enable or disable **in-call voice survey**.
* Guided setup for routing callers into the survey flow from the hang-up function.
* **Copy code** action for `conv.goto_csat_flow()`.
* **Go to Functions** shortcut for updating hang-up logic.
* Editable fields for the survey **lead-in message** and **survey question**.
* Built-in guidance to use a **1 to 5 rating scale** for consistent feedback.
**Dashboard support:**
* CSAT results can now be tracked for voice interactions in dashboards.
February expands Webchat deployment and improves chat configuration and rendering.
**Chat configuration:**
* Enable chat from the **Chat Configuration** page under **Channels**.
* Manage **Widget** configuration separately and push changes directly to environments.
* Display in-product banners to guide chat enablement before widget setup.
* Align chat configuration with branching behavior.
**Markdown rendering:**
* Render formatted markdown in the **Test Agent Chat** panel.
* Display formatted markdown in **Conversation Review**.
* Support **bold**, *italics*, lists, links, and code blocks.
Agent Studio now supports click-based integrations with leading CCaaS providers.
**What's new:**
* Integrate **Dialpad**, **Twilio**, and **Five9** directly from the **Integrations** page.
* Route directly to the **Handoffs** page after setup.
* Update the **Handoffs** page to reflect telephony integrations for unified routing.
**Note:** Zendesk integration support is coming in a future release.
You can now assign **mixed permissions** across different areas of Agent Studio. Give users edit access where they build, and restrict access where they only need visibility.
**What's new:**
* Set permissions per navigation area using **None / Read / Edit**.
* Mix permissions across sections in the same project (for example: **Edit Knowledge** while **Read-only Functions**).
* Expand sections (for example: **Analytics → Conversations**) and set more specific access where needed.
February also delivers smaller refinements across testing and review.
**What's new:**
* Increase sample question limit from **10 to 20** per topic.
* Mask secret values by default in the **Secrets** UI.
* Unmask on focus and re-mask on blur.
* Add copy-to-clipboard support for secret values.
* Update user permissions to match the new navigation structure.
* Upgrade **PolyScore** and **Call Summaries** to use **GPT-5**.
# 03.2026
Source: https://docs.poly.ai/releases/notes/26.03
March 2026 release notes.
The **March 2026** PolyAI Agent Studio release improves how teams review and debug conversations, with deeper visibility into source content and large-scale conversation analysis.
Expand the items below for details:
Each conversation turn now shows a **Sources** tag listing the Connected Knowledge files the agent retrieved. Click a source name to open a side panel showing the exact document content used to generate the response.
**What's new:**
* **Sources** tag appears beneath each turn, alongside **Matched topics**.
* Click any source name to open an inline preview panel.
* The panel shows the retrieved text chunks from the source document.
* An **Open in Knowledge** button links directly to the source in the Knowledge area.
* Toggle **Sources** in the **Diagnosis** dropdown to show or hide this layer.
**Why it matters:**
* See exactly what content the agent was working from on any given turn.
* Faster debugging when retrieved content is unexpected or incomplete.
* No need to leave Conversation Review to check the source document.
Smart Analyst now uses a sub-agent to analyze up to **500 conversations** for patterns – a 10× increase from the previous 50-conversation limit.
**What's new:**
* Analyze up to **500 conversations** per query (previously 50).
* Conversations are sampled either **randomly** or **based on a metric / PolyScore**.
* Ask questions like *"What do customers complain about?"* or *"How can we reduce handoffs where users ask to speak to a human?"*
* Get **directional insights on behavior frequency** where no metric is present.
* Deep sampling helps uncover patterns that smaller samples might miss.
**Example use cases:**
* Identify common complaint themes across a large sample
* Analyze handoff reasons with statistically meaningful data
* Discover edge cases and rare conversation patterns
* Validate hypotheses about customer behavior at scale
# 04.2026
Source: https://docs.poly.ai/releases/notes/26.04
April 2026 release notes.
The **April 2026** PolyAI Agent Studio release introduces first-class multi-language support – configure multilingual agents entirely from the UI, manage translations in a dedicated page, connect external tools with MCP integrations, and script agent configuration through the new Agents API.
Expand the items below for details:
Build a single agent that handles multiple languages – no separate projects, no manual `start_function` workarounds. The new **Multi-language** settings page lets you configure everything from the UI.
**What's new:**
* **Add languages in the UI** – go to **Behavior** and add up to 10 additional languages with a couple of clicks.
* **Per-language voices** – assign a dedicated Agent voice and Disclaimer voice for each language under **Voice > Settings**, with multi-voice support per language. The agent switches voice automatically when the conversation language changes.
* **Test in any language** – a new language dropdown in Agent Chat lets you start conversations in a specific language to verify behavior before going live.
* **Conditional content tags** – wrap language-specific content in `` / `` tags to serve the right version from a single prompt.
* **Test in any language** – a new language dropdown in Agent Chat lets you start conversations in a specific language to verify behavior before going live.
* **Language metadata** – see which language was used for each conversation in Conversation Review (list and detail), and manage language-tagged audio in Audio Management.
See [Multi-language](/behavior/language/multilingual) for full setup instructions.
Fine-tune how your agent speaks in every language. The new **Translations** page under **Voice > Advanced** gives you a central place to manage auto-translated content and override anything that doesn't sound right.
**What's new:**
* **Translation cards** – each card holds a piece of content (like a greeting or confirmation message) with versions for every configured language.
* **Automatic translation** – when you save a card, all configured languages are translated instantly. Cards that haven't been manually reviewed are marked as "Auto Translated".
* **Manual overrides** – edit any translation to fix awkward phrasing, adjust formality, or handle idioms that don't carry over. Overridden entries are marked as "Manually Translated".
* **Insert anywhere** – reference translation cards via the action menu in greetings, disclaimers, behavior rules, prompts, delay control responses, and SMS templates.
* **Access in functions** – use `conv.translations.your_key` to pull the right translation programmatically for hard-coded utterances.
See [Translations](/behavior/language/translations) for setup instructions.
Script agent configuration and deployment through a new public REST API. The **Agents API** covers the same surface as Agent Studio's build pipeline — create agents, edit behavior, manage the knowledge base, configure variants, and promote deployments — all from code.
**What's new:**
* **Full CRUD for agent configuration** – create, update, duplicate, and delete agents; read and update behavior rules on any branch.
* **Knowledge base management** – list, create, update, and delete knowledge base topics programmatically, making bulk migrations and CMS sync straightforward.
* **Variants and attributes** – sync multi-site variants from a source of truth (locations database, CRM) instead of editing them by hand.
* **Deployment pipeline** – publish drafts to Sandbox, promote through Pre-release to Live, and roll back from CI.
* **Telephony plumbing** – import phone numbers, reassign them to different connectors, and look up which connector serves a given number.
* **Branches and real-time configs** – manage branches and real-time configuration values alongside the rest of the agent.
See the [Agents API reference](/api-reference/agents/introduction) for the full endpoint list.
Agent Studio now supports [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) integrations, letting you connect external tool servers to your agent directly from the UI.
**What's new:**
* A dedicated **MCP** tab is now available under **Integrations**.
* Add an MCP server by providing a URL, authentication details (Header, Query parameter, or OAuth), and an optional timeout.
* Agent Studio **auto-discovers** the tools available on the connected server and displays them in a configuration panel.
* **Toggle individual tools** on or off to control which functions are exposed to the agent, helping manage context window usage.
* Supports **Header**, **Query parameter**, and **OAuth** authentication methods using secrets from the Secrets Vault.
Projects that previously configured MCP through experimental config will continue to work – the old MCP tab is hidden once the feature is enabled, but existing servers keep running in the background. The old config only stops being used if you add a new MCP server through the new UI. See [MCP integrations](/mcp/agent-studio-integrations) for setup instructions.
# 05.2026
Source: https://docs.poly.ai/releases/notes/26.05
May 2026 release notes.
The **May 2026** PolyAI Agent Studio release ships a redesigned **Conversations** workspace built for high-volume review, introduces the **Agent Development Kit (ADK)** for local agent development, and adds a one-click **Tripleseat** integration for restaurant agents.
Expand the items below for details:
The Conversations page is now organized around channel scopes, saved views, layered diagnosis, and an integrated review side panel — purpose-built for analyzing conversations at scale.
* **Voice and Webchat as separate scopes** — each with their own saved views, column layouts, and an integrated review side panel that opens without leaving the table.
* **Saved views** — built-in **Production** and **Test** System Views, plus your own **Custom Views** as pinned tabs. Duplicate, set as default, and share filters as a link.
* **Diagnosis toggle group** — debugging layers (tool calls, flows, variables, latency, logs, sources) are now independent toggles that persist as you move between conversations.
See [Conversation review](/analytics/conversations/review), [Views](/analytics/conversations/views), and [Diagnosis](/analytics/conversations/diagnosis) for the full walkthrough.
When an SMS conversation goes idle and the session times out due to inactivity, the agent now sends a brief closing SMS — *"I am going to end conversation now"* — before tearing the session down. Users get an explicit signal that the conversation is over instead of silently being dropped.
This applies only to abandoned (timed-out) sessions. Conversations that end naturally or because the user explicitly leaves are unchanged.
See [SMS overview → Session timeouts](/messaging-channel/sms#session-timeouts) for details.
Build PolyAI agents locally with the new **Agent Development Kit (ADK)** — a Python CLI (`polyai-adk` on PyPI) that brings a pull-edit-push workflow to Agent Studio projects.
* **Pull your project as code** — `poly pull` downloads your agent as human-readable YAML files organized by resource type (topics, flows, entities, agent settings) plus Python for functions.
* **Edit anywhere** — work in your IDE, with scripts, or with AI coding assistants like Claude, and version everything in Git.
* **Push back to Agent Studio** — `poly push` diffs local changes against the remote state and applies updates to the target environment immediately.
* **Self-serve API keys** — generate keys from your account at [studio.poly.ai](https://studio.poly.ai). Fully compatible with Agent Studio and the Agents API — switch between surfaces at any time.
See [Agent Development Kit (ADK)](/extend/adk) for installation, authentication, and usage instructions.
Connect PolyAI to **Tripleseat** to capture event and large-party leads from voice, webchat, and SMS conversations — without writing custom code. Projects on the PLG restaurant template can connect Tripleseat through one-click OAuth from the Integrations page; other projects continue to be supported through Managed Services.
See [Tripleseat](/integrations/tripleseat) for the setup guide.
# 06.2026
Source: https://docs.poly.ai/releases/notes/26.06
June 2026 release notes.
The **June 2026** PolyAI Agent Studio release introduces **platform Guardrails** – five pre-built safety protections that are applied automatically to every conversation, observable in transcripts, and maintained by PolyAI – adds **A/B testing (Beta)** for running two Live versions in parallel with a controlled traffic split, and expands **Agent Builder** to author and maintain simulation tests and to build chat agents alongside voice.
Expand the items below for details:
Agent Builder can now author and manage [simulation tests](/testing/simulation-tests) for your agent in the same chat where you build it. Ask it to cover a scenario and it creates the test cases, runs them, and updates them as your agent changes — no hand-writing every case to keep coverage in step with the build.
* **Author from a description** — *"add tests for the refund flow, including the 30-day cutoff and non-original payment method"* turns into individual test cases on your branch.
* **Run from chat** — kick off runs against the current draft or sandbox and see results inline.
* **Maintained as the agent evolves** — when a flow or topic changes, Agent Builder updates the affected tests instead of leaving them stale.
* **Lives where your other tests do** — generated cases land in the **Simulation tests** workspace alongside any cases you've saved by hand, so reviewing, batching, and re-running stay unchanged.
See [Test suite](/testing/simulation-tests) for the workspace and [Prompting Agent Builder](/wren/prompting#test) for prompt patterns.
Agent Builder now tailors its work to the channel you're building for. When the active project is a chat agent, it pulls in chat-specific guidance — message length, formatting, turn-taking, async patterns — so the output fits how text conversations actually work. Voice remains the default for voice projects.
* **Channel-aware planning** — plans, prompts, and step wording reflect chat conventions for chat projects and voice conventions for voice projects.
* **Same workflow** — branches, plan review, and merge stay identical across channels; only the generated content changes.
* **Works with existing chat features** — pairs with the [Chat channel](/messaging-channel/introduction) configuration and [multichannel agents](/messaging-channel/multichannel) for projects that serve both.
Ongoing optimisations reduce the per-step overhead Agent Builder carries during a session, so longer chats and larger projects stay responsive. No configuration change is required — existing chats benefit automatically.
**A/B testing** promotes a second version to Live alongside the current one and splits real caller traffic between them, so you can compare key metrics in your dashboards before promoting a winner to 100% of traffic. Use it for any change where you want evidence before fully rolling out — a new prompt, a reworked flow, a different routing rule, a model swap.
* **Control vs. variant** — the current Live version is the control (A); the version you promote from Pre-release is the variant (B).
* **Configurable split** — set the traffic split at test start, from 5/95 to 95/5 in 5% steps (defaults to 50/50). Calls are routed at the start of the conversation and stay on the assigned version for the whole call.
* **Real metrics, side by side** — both versions write to the same analytics tables tagged with their deployment version. Filter dashboards by deployed version to compare CSAT, containment, latency, handover rate, function errors, and anything else you already track.
* **Safe guardrails on the pipeline** — only one active test per project; promotions to Live and rollbacks of the control are blocked while a test is running.
* **End on your terms** — pick a winner when you have enough data; the chosen version is promoted to Live immediately and the test appears in **Live Version History**.
Available in Beta on US and UK enterprise clusters behind the `ab_tests` feature flag — ask your PolyAI representative to enable it for your project.
See [A/B testing](/testing/ab-testing) for the full walkthrough.
Platform **Guardrails** ship as a managed Agent Studio feature. Five safety protections that previously had to be pasted into every agent's behavior prompt by hand are now applied automatically and maintained centrally.
* **Jailbreak & Prompt Defence** – blocks attempts to extract instructions, override behavior, or impersonate a different AI system.
* **Scope & Hallucination Control** – restricts the agent to its knowledge base and prevents fabrication of phone numbers, prices, or policies.
* **AI Identity & Confidentiality** – prevents disclosure of the underlying LLM, provider, or platform.
* **Emergency & Crisis Escalation** – escalates immediately on suicidal ideation, self-harm, threats, or medical emergencies. Catches conversational distress signals that content filters miss.
* **Tool Call Integrity** – stops the agent from speaking internal function or tool names aloud.
All five are enabled by default on new and existing projects, can be toggled individually in **Behavior**, travel with the project through [environments and versions](/environments-and-versions/introduction), and are observable inline in transcripts via the **Guardrails** display toggle in [Conversation review](/analytics/conversations/review). Filter by guardrail in the **QA category** filter to find every conversation where a specific guardrail fired.
See [Guardrails](/behavior/guardrails/introduction) for the full walkthrough and guidance on when to keep each one on.
# 07.2026
Source: https://docs.poly.ai/releases/notes/26.07
July 2026 release notes.
The **July 2026** PolyAI release brings two developer-focused ways to build and manage agents outside the Agent Studio UI: the **PolyAI Agent Development Kit (ADK)** for a local, Git-like workflow, and **Builder MCP** for building agents straight from your IDE or AI coding tool. Both ship with full documentation.
Expand the items below for details:
The **PolyAI ADK** gives you a local, Git-like workflow for Agent Studio projects: pull a project to your machine, edit it with standard tooling, validate, and push it back to Agent Studio to review and deploy.
* **Zero to a local project in a few commands** — install with `pip install polyai-adk`, then `poly start` handles self-serve sign-up, API key, and your first project in one go.
* **Local, code-first editing** — work on flows, functions, topics, entities, variables, and variants as files, using the tooling you already have.
* **Git-like project lifecycle** — pull, branch, validate, and push, with branch merging to bring changes back safely.
* **Tutorials and reference** — build-an-agent and restaurant-booking walkthroughs, core-concept guides, worked examples, and a full CLI and configuration reference.
See the [ADK docs](/adk) to get started, or jump straight to [Getting started](/adk/get-started/get-started) and the [CLI reference](/adk/reference/cli).
**Builder MCP** is PolyAI's authenticated [MCP](https://modelcontextprotocol.io/) server for building agents. Point an MCP client — Claude Code, Cursor, Claude Desktop, or Codex — at it and create, test, and deploy PolyAI agents from your IDE without calling the REST API yourself.
* **Build from your IDE** — the client discovers the available tools and their input schemas on connect, so your AI coding tool can drive Agent Studio directly.
* **Full agent lifecycle** — create and edit agents, run tests, and deploy, all over MCP.
* **Authenticated and scoped** — connect with your workspace API key; see [Authentication](/mcp/authentication) for setup.
* **Two directions, clearly separated** — Builder MCP is a client → PolyAI connection for *building* agents. To let a live agent call external tools during a conversation, see [Agent Studio MCP integrations](/mcp/agent-studio-integrations).
See the [MCP overview](/mcp/overview) and [Builder MCP](/mcp/builder/introduction) for the full walkthrough.
# 08.2026
Source: https://docs.poly.ai/releases/notes/26.08
August 2026 release notes.
The **August 2026** PolyAI release introduces the new **PolyScore** — a simpler 1–5 quality score with a fully transparent rubric that works across voice, messaging, and email.
Expand the items below for details:
**Rollout date:** 5 August 2026.
PolyScore — the automated quality score assigned to every conversation with your agent — is moving from a 0–10 scale to a **1–5 scale**, backed by a new, fully transparent evaluation rubric.
Charts on the Analytics pages and scores on individual conversations in [Conversation review](/analytics/conversations/review) will reflect the new score. The new score is calculated for the preceding week.
**Why we're making this change**
* **One score across every channel.** The original PolyScore was designed for voice calls. The new rubric natively evaluates voice, messaging, and email — inbound and outbound — with channel-appropriate judgement. For example, a customer silently leaving a webchat after their question is answered is treated as success, not abandonment.
* **Simpler and more accurate.** The previous score combined five overlapping categories (conversation quality, repetition, frustration, resolution, and task completion). The new rubric condenses these into two scored dimensions plus an engagement gate, each with explicit decision rules — reducing ambiguity and making the score more consistent.
* **A familiar scale.** The 1–5 scale mirrors CSAT, so PolyScore reads naturally alongside the customer-satisfaction metrics you already use.
**How the new score works**
Every conversation is evaluated on two questions:
| Dimension | Question | Outcomes |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- |
| **Agent quality** | Did the agent handle the exchange competently — understanding the user, avoiding forced repeats, not causing frustration through its own faults? | Good / Fair / Poor |
| **Task success** | Did the conversation deliver on its objective, so the user won't need to make contact again for the same reason? | Good (Completed) / Fair (Handoff or decline honored) / Poor (Not completed) |
As with the previous PolyScore, conversations must meet two criteria to be scored, and both reasons for skipping scoring are now made explicit:
* **User engaged** — it has to be possible for the agent to do its job. Spam calls, silent calls, or conversations where a user instantly requests a human count as not engaged.
* **More than 3 user turns** — only conversations with real interaction are scored.
The two dimensions combine into a single 1–5 score:
| PolyScore | Typical conversation |
| --------- | ------------------------------------------------------------------------------------------------------------------------ |
| **5** | Understood cleanly and fully resolved (or a clear self-service path given) |
| **4** | Strong on one dimension — for example, handled well but ended in a handoff, or resolved despite a minor misunderstanding |
| **3** | Middling on both — for example, some friction and a handoff |
| **2** | Weak on both dimensions |
| **1** | The agent got stuck or repeatedly misunderstood, and the conversation ended unresolved with no handoff |
**Key rubric decisions**
* **Handoffs are neutral on task success.** From the user's perspective, being routed to a person is a completed outcome. A handoff is never scored as "not completed" — that rating is reserved for genuine dead-ends where the user got nothing and nobody. A handoff driven by a struggling agent is penalized through the **Agent quality** sub-score instead.
* **Self-service paths count as completed.** If the agent gives the user a concrete way to finish the task themselves — for example, *"you can reset your PIN any time at acme.com/pin"* — that scores the same as resolving it in-conversation.
* **Frustration only counts against the agent when the agent caused it.** Unhappiness with a policy or outcome doesn't penalize the agent; being stuck in a loop does.
* **Design choices aren't penalized.** If your agent is configured to deflect or decline certain requests, executing that correctly scores as competent handling.
* **Outbound declines are neutral on task success.** A polite *"not interested, remove me,"* honored cleanly, is scored as the agent doing its job.
See the full [PolyScore reference](/analytics/polyscore) for details.
Questions? Reach out to your PolyAI account team.
# Overview
Source: https://docs.poly.ai/releases/overview
PolyAI releases monthly Studio updates.
Explore the update cards for release summaries:
Click the **bolded** titles for a breakdown of what's new, or go to the [notes tab](./notes/26.08) for full details.
* **[New PolyScore](./notes/26.08#new-polyscore)**: Released 5 August. PolyScore moved from a 0–10 scale to a **1–5 scale**, backed by a rubric that natively scores voice, messaging, and email (inbound and outbound). Two dimensions — **Agent quality** and **Task success** — combine into the overall score, with explicit rules for handoffs, self-service paths, and design-driven declines. See [PolyScore](/analytics/polyscore).
Click the **bolded** titles for a breakdown of what's new, or go to the [notes tab](./notes/26.07) for full details.
* **[PolyAI ADK](./notes/26.07#adk)**: A local, Git-like workflow for Agent Studio projects — pull, edit with standard tooling, validate, and push. Install with `pip install polyai-adk` and see the [ADK docs](/adk).
* **[Builder MCP](./notes/26.07#builder-mcp)**: Point an MCP client — Claude Code, Cursor, Claude Desktop, or Codex — at PolyAI's authenticated MCP server to build, test, and deploy agents from your IDE. See the [MCP overview](/mcp/overview).
**New Agent Studio layout.** On 24 June we're rolling out a cleaner Studio sidebar that consolidates 25+ pages into 15 grouped sections. No functionality changes — agents behave exactly as before.
See **[Meet the new Agent Studio](/get-started/whats-new)** for the full migration guide, including a section-by-section "was → now" map and a list of features that have moved.
Highlights:
* **Wren** is the new homepage — our prompt-based tool for building agents in natural language.
* **Analytics** consolidates KPIs, custom metrics, dashboards, Conversations, PolyScore, and CSAT. The standalone **Agent Analysis** page is sunset; per-call diagnosis lives under [Conversations > Diagnosis](/analytics/conversations/diagnosis).
* **Behavior** merges Agent settings and general configuration into one section with **General**, **Language**, **Guardrails**, and **Models** subsections.
* **Knowledge** renames Managed topics → **FAQs**, Connected knowledge → **Sources**, and pulls Variants in as a tab.
* **Testing** replaces Test suite (which is being sunset). See [Testing](/testing/simulation-tests).
* **Real-time config** combines the real-time config and the configuration builder.
* **Voice** consolidates Settings, Numbers, Handoffs, Audio library, and speech/response-control tuning onto one page, with deeper options under **Advanced**.
* **Messaging** follows the same pattern for chat and SMS.
* **Integrations** becomes its own section (App, API, MCP).
* **Widgets** moves out of Messaging into a standalone section for managing widgets across channels.
The new navigation is **on by default** for all users from 24 June — a temporary **revert toggle** in your profile menu lets you switch back while you adjust.
Click the **bolded** titles for a breakdown of what's new, or go to the [notes tab](./notes/26.06) for full details.
* **[Platform Guardrails](./notes/26.06#guardrails)**: Five pre-built safety protections – jailbreak defence, scope and hallucination control, AI identity, emergency escalation, and tool call integrity – are now applied automatically to every conversation, observable in transcripts, and toggleable per project in [Behavior](/behavior/guardrails/introduction).
* **[A/B testing (Beta)](./notes/26.06#ab-testing)**: Promote a second version to Live alongside the current one with a configurable traffic split, compare real-traffic metrics in your dashboards, and pick a winner – available behind a feature flag on US and UK enterprise clusters. See [A/B testing](/testing/ab-testing).
* **[Agent Builder writes simulation tests](./notes/26.06#agent-builder-test-suite)**: [Agent Builder](/wren/introduction) can now author, run, and maintain [simulation test cases](/testing/simulation-tests) alongside the agent it's building, keeping coverage in step with your changes.
* **[Agent Builder builds chat agents](./notes/26.06#agent-builder-chat)**: Agent Builder now tailors plans and prompts to the project's channel — chat-specific guidance for chat agents, voice for voice agents.
* **[Leaner Agent Builder runtime](./notes/26.06#agent-builder-perf)**: Per-step overhead reductions keep long sessions and large projects responsive.
Click the **bolded** titles for a breakdown of what's new, or go to the [notes tab](./notes/26.05) for full details.
* **[Conversations workspace revamp](./notes/26.05#conversations-revamp)**: Voice and Web chat are now separate scopes with their own saved views, column layouts, and an integrated review side panel that opens without leaving the table.
* **[Diagnosis toggle group](./notes/26.05#diagnosis-toggle-group)**: Debugging layers (tool calls, flows, variables, latency, logs, sources) are now independent toggles that persist as you move between conversations.
* **[Tripleseat integration](./notes/26.05#tripleseat-integration)**: Connect PolyAI to Tripleseat to capture large-party and event leads from voice, webchat, and SMS – no custom code required.
Click the **bolded** titles for a breakdown of what's new, or go to the [notes tab](./notes/26.04) for full details.
* **[Multi-language support](./notes/26.04#multilingual)**: Configure a single agent to handle [multiple languages](/behavior/language/multilingual) from the UI – add languages, assign per-language voices with automatic switching, use conditional content tags, and test in any language from Agent Chat.
* **[Translations](./notes/26.04#translations)**: A new [Translations](/behavior/language/translations) page under [Advanced voice settings](/voice-channel/advanced/call-settings) lets you manage auto-translated content and manually override specific phrases.
* **[MCP integrations](./notes/26.04#mcp-integrations)**: Connect [external tool servers](/mcp/agent-studio-integrations) to your agent from **Integrations > MCP**.
Click the **bolded** titles for a breakdown of what's new, or go to the [notes tab](./notes/26.03) for full details.
* **[Sources in Conversation Review](./notes/26.03#source-hub-file-preview)**: Each turn now shows which Connected Knowledge files the agent retrieved. Click a source name to preview the document content inline, without leaving Conversation Review.
* **[Smart Analyst deep sampling](./notes/26.03#deep-sampling)**: Analyze up to 500 conversations (10× the previous limit) with random or metric-based sampling to uncover patterns and get directional insights at scale.
Click the **bolded** titles for a breakdown of what's new, or go to the [notes tab](./notes/26.02) for full details.
* **[Channels and navigation reorganization](./notes/26.02#channels-navigation-reorg)**: Agent Studio now includes a dedicated **Channels** section for Voice, Chat, and Widget configuration, alongside broader navigation updates across Build, Configure, and Deployments.
* **[Voice library](./notes/26.02#voice-library)**: Browse, preview, and select voices more easily with a redesigned voice page and expanded provider support.
* **[Latency visualization](./notes/26.02#latency-visualization)**: View detailed latency breakdowns, including LLM request and function timing, to understand and improve response performance.
* **[CSAT by voice](./notes/26.02#csat-by-voice)**: Configure in-call voice CSAT surveys with a dedicated UI and dashboard support for voice interactions.
* **[Webchat enhancements](./notes/26.02#webchat-markdown)**: Chat/Webchat is easier to configure, with a dedicated Chat configuration page, separate Widget setup, and markdown rendering in Test Agent Chat and Conversation Review.
* **[Easy telephony integrations](./notes/26.02#ccaas-integrations)**: Set up **Dialpad**, **Twilio**, and **Five9** directly from the Integrations page, then continue routing setup in Handoffs.
* **[User permissions](./notes/26.02#user-permissions)**: Assign mixed access per area (None/Read/Edit) – for example, let users edit Knowledge while keeping Functions read-only – aligned to the new navigation structure.
* **[Security and usability updates](./notes/26.02#security-usability-updates)**: Expanded sample questions, simplified conversation filtering, improved secret handling, updated permissions, and GPT-5-powered PolyScore and call summaries.
Click the **bolded** titles for a breakdown of what's new, or go to the [notes tab](./notes/25.12) for full details.
* **[No-code flows](./notes/25.12#no-code-flows)**: For projects that have opted in, flows can be built using drag-and-drop nodes, conditions, and entity collection – reducing reliance on custom functions for common logic.
* **[Multi-draft (Branches)](./notes/25.12#multi-draft-branches)**: Work in parallel using branches and resolve conflicts visually.
* **[API integrations in Agent Studio](./notes/25.12#api-integrations-in-agent-studio)**: Configure and manage external APIs directly in the UI using environment-specific authentication and reusable operations.
* **[Variant info in Test Cases](./notes/25.12#variant-info-in-test-cases)**: See which variant each test case was saved against.
* **[Conversation review & PolyScore enhancements](./notes/25.12#conversation-review-and-polyscore-enhancements)**: Clearer call summaries and PolyScore breakdowns in the table and Conversation Review.
* **[Smart Analyst live reasoning steps](./notes/25.12#smart-analyst-live-reasoning)**: Analysis steps now stream live as Smart Analyst runs.
Click the **bolded** titles for a breakdown of what's new, or go to the [notes tab](./notes/25.11) for full details.
* **[Connected Knowledge](./notes/25.11#knowledge-source-hub)**: Add, group, and refresh multiple knowledge sources in the Knowledge area – files, URLs, and third-party integrations – with environment/variant scoping and per-chunk citations in Conversation Review.
* **[Integrations overview page](./notes/25.11#integrations-overview-page)**: Browse a searchable, filterable catalog of supported telephony, chat, CRM, vertical, and KB integrations directly inside Agent Studio.
* **[Multi-channel dashboard](./notes/25.11#multi-channel-dashboard)**: New Voice/Chat homepage tabs with channel-specific KPIs and charts, plus automatic currency selection.
* **[SMS messaging-service IDs](./notes/25.11#sms-messaging-service-ids)**: Use Twilio Messaging Service IDs (`MGxxxx`) in SMS fields, replacing the old phone-number-only restriction.
Click the **bolded** titles for a breakdown of what's new, or go to the [notes tab](./notes/25.10) for full details.
* **[Live call listening](./notes/25.10#live-call-listening)**: Watch calls stream live on the Conversations page – turns appear in real time with clear "in progress" indicators and duration tracking.
* **[Advanced Webchat customization](./notes/25.10#advanced-webchat-customization)**: Configure disclaimer messages, consent buttons, and clickable Privacy Policy or T\&C links for GDPR-compliant chat widgets.
* **[Auto-run Test Sets](./notes/25.10#auto-run-test-sets)**: Test Sets can now run automatically when publishing or promoting to Pre-release or Live.
* **[Function logging (conv.log)](./notes/25.10#function-logging-conv-log)**: Add structured logs directly inside functions for easier debugging, visible in Conversation Review → Diagnosis.
* **[Smart Analyst templates](./notes/25.10#smart-analyst-templates)**: New pre-built analysis templates to help build out a quick, categorized set of insights.
* **[Agent Analysis – scheduled batches](./notes/25.10#agent-analysis-scheduled-batches)**: Automate daily or weekly analytics runs with trend charts and filters to track changes over time.
Click the **bolded** titles for a breakdown of what's new, or go to the [notes tab](./notes/25.09) for full details.
* **[Webchat configuration](./notes/25.09#webchat-config)**: Customize the webchat agent's name, button text, and widget colors; live preview in-browser and deploy quickly with an embed or script tag.
* **[Test cases](./notes/25.09#test-cases)**: Define and run individual scenarios, then bundle them into reusable Test Sets for regression testing and workflow coverage.
* **[Home page updates](./notes/25.09#home-page-v2)**: Average PolyScore summary, evolution charts for Containment and PolyScore, and clearer distribution labeling.
* **[Smart Analyst: chat history + structured answers](./notes/25.09#smart-analyst-history)**: Resume past analyses, rename or delete conversations, and view answers with sources and steps.
* **[Numbers tab](./notes/25.09#phone-numbers-variants)**: Buy and manage numbers quickly and assign them to variants for multi-site deployments.
* **[Conversations API: tool calls](./notes/25.09#conversations-api-tool-calls)**: Programmatic access to function/tool execution events; environment-scoped endpoints.
Click the **bolded** titles for a breakdown of what's new, or go to the [notes tab](./notes/25.08) for full details.
* **[Home page improvements](./notes/25.08#home-page-updates)**: Faster load times and a cleaner layout on the Agent Studio homepage dashboard.
* **[AI call summaries](./notes/25.08#ai-call-summaries)**: GPT-based summaries now surface at the top of Conversation Review and as tooltips in Conversations.
* **[Multi-voice limit increase](./notes/25.08#multi-voice-limit-increase)**: Configure up to **10 voices** per project for multi-lingual or multi-persona agents.
* **[Enhanced call review filters](./notes/25.08#new-call-review-filters)**: New options for delivery status, channel, and other metadata–plus snappier filtering.
* **[Updated channel and metadata display](./notes/25.08#updated-channel-and-metadata-display)**: Clearer *Inbound/Outbound/Agent Chat* tags and more column options.
* **[GPT-5 models in Agent Studio (experimental)](./notes/25.08#gpt-5-models-in-agent-studio-experimental)**: Try **GPT-5 nano** and **GPT-5 mini** (recommended), plus **GPT-5 large** and **GPT-5 chat (router)** for experimentation. Not yet ready for production traffic; quota available on request.
* **[In-app calling with variants](./notes/25.08#in-app-calling-with-variants)**: You can run voice test calls against specific [variants](/knowledge/variants/introduction), with transcripts tagged by `variant_id`.
* **[Smart Analyst improvements](./notes/25.08#smart-analyst-improvements)**: Clickable call IDs added to [Smart Analyst](/wren/analyze) to link directly to Conversation Review, SQL query range expanded, and improved search performance.
* **[Start and End function error warnings](./notes/25.08#start-end-function-error-warnings)**: Clear UI alerts when Start or End functions contain errors that prevent execution.
* **[Agent Analysis – Multi-task](./notes/25.08#agent-analysis-multi-task)**: Configure up to **10 analysis tasks** per project with their own prompts, categories, and charts.
Click the **bolded** titles for a breakdown of what's new, or go to the [notes tab](./notes/25.07) for full details.
* **[Smart Analyst enhancements](./notes/25.07#smart-analyst-enhancements)**: Keyword filtering, SQL-style aggregation, and integrated charts now make LLM-powered QA review faster and more insightful.
* **[New homepage](./notes/25.07#new-homepage)**: A redesigned dashboard shows key agent metrics, best/worst recent calls, and quick links to support tools.
* **[Rich text refactor](./notes/25.07#rich-text-refactor)**: Input fields now support slash-command **rich text**, allowing inline references to flows, functions, and more.
* **[Add Amazon Polly voices](./notes/25.07#add-amazon-polly-voices)**: Add new voices with language/accent tags with the improved Amazon Polly integration.
* **[Adjust voice speed](./notes/25.07#adjust-voice-speed)**: Control TTS speed between 0.5× and 1.5× with a precision slider for pacing and accessibility.
* **[Claude integration for Agent Studio functions](./notes/25.07#claude-integration-for-agent-studio-functions)**: Use Claude to generate summaries, text, or structured outputs inside functions.
* **[Channel filter and call metadata improvements](./notes/25.07#channel-filter-and-call-metadata-improvements)**: Conversation Review now shows clearer call direction tags (*Inbound*, *Outbound*, *Agent chat*) and improved filtering.
* **[Outbound call delivery tracking](./notes/25.07#outbound-call-delivery-tracking)**: Delivery outcomes like *Busy*, *Invalid Number*, and *Declined* are now visible in Conversation Review filters and columns for outbound calls.
Click the **bolded** titles for a breakdown of what's new, or go to the [notes tab](./notes/25.06) for full details.
* **[Agent configuration](./notes/25.06#agent-configuration)**: You can now define settings like opening hours or routing rules in a JSON config form, without editing flows.
* **[Version comparison enhancements](./notes/25.06#version-comparison-v2)**: The diff version structure introduced in the **25.02** release is now supported across functions and flows.
* **[Action name search and autofill](./notes/25.06#action-name-search-and-autofill)**: Type `/name` in any rich text field to trigger action insertion.
* **[Cached disclaimer messages](./notes/25.06#cached-disclaimer-messages)**: Disclaimer messages are now stored in the Cache under [audio management](/voice-channel/audio-library), reducing latency and improving reliability across multiple flows.
* **[Smart analyst (Beta)](./notes/25.06#smart-analyst-beta)**: This LLM-powered QA tool can rate calls, summarize agent behavior, and flag weak containment. Contact your PolyAI rep to participate in the Smart Analyst **beta**.
* **[Agent memory](./notes/25.06#agent-memory)**: Retrieve persistent values from previous conversations with the `conv.memory` object.
* **[Test suite](./notes/25.06#test-suite)**: Save real conversations as [test cases](/testing/simulation-tests) and re-run them against draft or sandbox versions to check for changes in behavior.
* **[PolyScore](./notes/25.06#polyscore)**: New automatic scoring for conversation quality using defined behavioral metrics.
Click the **bolded** titles for a breakdown of what's new, or go to the [notes tab](./notes/25.05) for full details.
* **[DTMF support](./notes/25.05#dtmf-support)**: Configure tone-based input (DTMF) during specific steps to collect keypad responses from callers with support for timeouts, digit length, and early listening.
* **[Chat with variants](./notes/25.05#chat-with-variants)**: You can now test how different variants respond in the chat panel–choose any draft, sandbox, or variant on demand.
* **[Duplicate and set default variants](./notes/25.05#duplicate-and-set-default-variants)**: From the ellipsis menu on the variant page, duplicate an existing variant or assign a new default with a simple toggle.
* **[Build KBs with URL and PDF upload](./notes/25.05#build-kbs-with-url-and-pdf-upload)**: Create Knowledge topics directly from a website or uploaded PDF. Real-time crawl status and error reporting help troubleshoot issues when importing.
* **[Chat-to-review shortcut](./notes/25.05#chat-to-review-shortcut)**: Quickly jump from a chat session to the conversation review page with one click, streamlining your debugging process.
* **[Handoff reason and utterance](./notes/25.05#handoff-reason-and-utterance)**: You can now include `reason` and `utterance` fields when writing a handoff implementation in a KB topic, flow, or function call.
* **[Entities and Intents in Conversation Review](./notes/25.05#entities-and-intents-in-conversation-review)**: View detected intents and extracted entities directly inside the [Conversation Review](/analytics/conversations/review) page.
* **[Improved multi-language ASR accuracy](./notes/25.05#improved-multi-language-asr-accuracy)**: Speech recognition updates improve accuracy for numbers, dates, and accented speakers across several languages.
* **[Conversation review: session duration filter](./notes/25.05#conversation-review-session-duration-filter)**: Filter conversations by session length to focus QA on very short or long calls.
Click the **bolded** titles for a breakdown of what's new, or go to the [notes tab](./notes/25.04) for full details.
* **[Information architecture updates](./notes/25.04#information-architecture-updates)**: Tools have been regrouped by function. "Analyze" is now "Manage", and sections like Voice and Environments & Versions have moved or been renamed.
* **[Metrics](./notes/25.04#metrics)**: Use the new Agent Analysis tab to run custom metrics across batches of calls and generate insight-rich evaluations.
* **[Live collaboration support](./notes/25.04#live-collaboration-and-edit-history)**: Agent Studio now shows who else is editing a draft, alerts you when updates are saved, and lets you review edit history to avoid accidental overwrites.
* **[Call categorization](./notes/25.04#call-categorization)**: Use an LLM to rate calls for tone, intent, or compliance. Categories are defined per project. Contact your PolyAI rep to configure.
* **[Workspace filter in Conversations](./notes/25.04#workspace-filter-in-conversations)**: Filter conversation review by workspace to focus on your team's calls.
* **[Utility function – extract\_address](./notes/25.04#utility-function--utilsextract_address)**: Extracts a structured address from a user message. Returns a typed object, or a clear error. Opt-in only.
* **[Conversation history access – conv.history](./notes/25.04#conversation-history-access--convhistory)**: Use `conv.history` to access earlier turns from inside a utility function. Supports custom logic and debugging.
* **[Import and export variants](./notes/25.04#import-and-export-variants)**: Export all variant data to CSV, edit it, then re-import across agents. Includes overwrite protection.
* **[Variant ID added to conversations](./notes/25.04#variant-id-added-to-conversations)**: Each call now shows the variant ID used–helpful for QA and tracking.
* **[Multi-voice agents](./notes/25.04#multi-voice-agents)**: Assign multiple TTS voices to an agent and control their distribution. Great for A/B testing or simulating real-world voice teams.
Click the **bolded** titles for a detailed breakdown of the released feature, or open the [notes tab](./notes/25.03) on the sidebar for the full release notes.
* **[Overview dashboard enhancements](./notes/25.03#overview-dashboard-enhancements)**: Additions to the dashboard include call volumes, durations, containment rate, SMS and function stats–refreshed hourly.
* **[Safety dashboard enhancements](./notes/25.03#safety-dashboard-enhancements)**: A refreshed version of the safety dashboard to match the new overview, with faster load times and a clearer layout.
* **[Open matched topics in a new tab](./notes/25.03#open-matched-topics-in-a-new-tab)**: In conversation review, clicking a matched topic opens it in a new browser tab–letting you keep your place while checking the KB.
* **[Version tracking in conversation review](./notes/25.03#version-info-added-to-conversation-review)**: Each conversation now shows which version of the agent it ran on.
* **[Add voices directly in Agent Studio](./notes/25.03#add-new-voices-in-agent-studio)**: Voices can now be added or managed without leaving the platform.
* **[Filter voices by language, accent, or gender](./notes/25.03#filter-voices-by-language-accent-and-gender)**: The voices page now supports helpful filters to speed up selection.
* **[Send SMS immediately during function calls](./notes/25.03#send-sms-during-a-function-call)**: SMS messages now send in real time as part of the function, so agents won't falsely confirm delivery if the message fails.
* **[Improved KB search for actions and tags](./notes/25.03#improved-kb-search-for-actions)**: You can now search for SMS, handoff, and function names in the Knowledge area using the main search bar.
Click the **bolded** titles for a detailed breakdown of the released feature, or open the [notes tab](./notes/25.02) on the sidebar for the full release notes.
* **[View Knowledge differences between versions](./notes/25.02#view-knowledge-base-differences-between-versions)**: Easily compare changes across different versions of your Knowledge for better content tracking and updates.
* **[Utterances for high latency functions](./notes/25.02#function-latency-control)**: Manage function latency effectively by configuring filler utterances and dynamic delay timing.
* **[Upload custom audio to replace cached items](./notes/25.02#upload-audio-to-replace-cached-items)**: Upload pre-recorded audio directly from the Agent Studio audio management resource.
* **[Add variants to Knowledge items](./notes/25.02#reference-variants-in-the-knowledge-base)**: Add variant attributes to Knowledge content dynamically.
* **[Manage transition functions in a flow](./notes/25.02#managing-transition-functions-in-a-flow)**: Improve transition function management within flows, preventing unintended deletions.
* **[New conversation review annotations](./notes/25.02#conversation-review-annotations)**: Add annotations and comments to conversations for better review and collaboration.
* **[A new version of the overview dashboard](./notes/25.02#enterprise-overview-dashboard)**: A new standardized dashboard displaying key operational data like total calls, duration, and handover rates.
Click the **bolded** titles for a detailed breakdown of the released feature, or open the [notes tab](./notes/25.01) on the sidebar for the full release notes.
* **[Enterprise safety dashboard](./notes/25.01#safety-dashboard)**: A new dashboard focused on flagged calls and safety filter trigger analysis.
* **[Custom dashboards](./notes/25.01#custom-dashboards)**: Track project-specific success metrics with tailored dashboards.
* **[In-app calling](./notes/25.01#in-app-calling)**: Test voice interactions directly in-studio without telephony setup, enabling faster and simplified workflows.
* **[Audio management](./notes/25.01#audio-management)**: Delete or regenerate cached TTS audio for improved flexibility.
* **[A new Knowledge structure](./notes/25.01#knowledge-base-structure)**: Separate questions and content for better organization.
* **[End-of-conversation functions calls](./notes/25.01#end-functions)**: Trigger automatic function calls when a conversation ends to log data, create tickets, or call APIs.
* **[API enhancements](./notes/25.01#variants-in-the-conversations-api)**: Variant information now included in the Conversations API.
Click the **bolded** titles for a detailed breakdown of the released feature, or open the [notes tab](./notes/24.12) on the sidebar for the full release notes.
* **[Multi-site config](./notes/24.12#multi-site-config)**: Manage multiple site-specific Knowledge configurations in a single agent, with location-specific variants for tailored responses.
* **[Global ASR corrections, biasing, and keyword boosting](./notes/24.12#asr-updates)**: Customize Automatic Speech Recognition (ASR) corrections to refine transcribed text, improving LLM input accuracy.
* **[Audio management](./notes/24.12#audio-management)**: Browse, delete, regenerate, or upload cached TTS audio for better quality control.
* **[Customize latency response delay](./notes/24.12#customize-latency-response-delay)**: Set dynamic response delays to fit conversational tone and audience preferences.
* **[Copy and paste flows](./notes/24.12#copy-paste-flows)**: Transfer flows or nodes between projects to reuse configurations across builds.
* **[Call recordings download](./notes/24.12#call-recordings-download)**: Download call recordings directly from the conversation detail page, with required permissions.
Click the **bolded** titles for a detailed breakdown of the released feature, or open the [notes tab](./notes/24.11) on the sidebar for the full release notes.
* **[In-app function debugging](./notes/24.11#in-app-function-debugging)**: View and copy input, output, and error messages for function calls directly in the Conversation Review and agent Chat Panel.
* **[Enhanced SMS](./notes/24.11#enhanced-sms-functionality)**: agents can send SMS messages to alternative numbers specified by callers and upgrade SMS templates to advanced functions with added logic and configurations.
* **[Knowledge export](./notes/24.11#knowledge-base-export)**: Export Knowledge topics to a CSV file for version control and reuse.
* **[Barge-in feature](./notes/24.11#barge-in-feature)**: Enable agents to be interrupted by callers, improving natural conversation flow and reducing latency.
* **[Stop keywords](./notes/24.11#stop-on-keyword)**: Configure specific words or phrases to automatically halt the agent's response and trigger a function.
Click the **bolded** titles for a detailed breakdown of the released feature, or open the [notes tab](./notes/24.10) on the sidebar for the full release notes.
* **[Separate voice disclaimer with ringing tone](./notes/24.10#disclaimers)**: Customize greetings and dial tones to enhance user interaction.
* **[ASR biasing for flows](./notes/24.10#asr-biasing-for-flows)**: Configure speech recognition biasing for specific input types, like alphanumerics, times, or precise dates, and improve ASR performance and accuracy.
* **[UK and EU regions](./notes/24.10#uk-and-eu-regions)**: Regional expansion for improved performance and compliance.
* **[View versions](./notes/24.10#view-versions)**: Access and manage historical and live versions of agents for better development and maintenance.
* **[Content safety filters](./notes/24.10#content-safety-filters)**: Full control over content filter severity and configurations for project-specific needs.
***
Updates will be added to this page sequentially with product releases.
# API keys
Source: https://docs.poly.ai/secrets/api-keys
Create and manage API keys for accessing PolyAI's external APIs.
API keys control access to PolyAI's external APIs – including the [Agents API](/api-reference/agents/introduction), [Conversations API](/api-reference/conversations/introduction), [Handoff API](/api-reference/handoff/introduction), and [External Events API](/api-reference/external-events/introduction). Create and manage keys from the **API Keys** tab in the workspace top bar (**Agents / Users / Usage / API Keys / Secrets**) — a workspace-level area separate from the per-agent sidebar.
API keys require the [Conversations API v3](/api-reference/conversations/introduction). If you are using v1, contact your PolyAI representative to migrate to v3 first.
## Creating an API key
1. Go to the **API Keys** tab on the workspace homepage in Agent Studio
2. Select **API key**
3. Configure the key:
* **Name** – a label to identify this key
* **Agents** – choose "All agents" or select specific agents (this cannot be changed after creation)
* **Permissions** – select which APIs the key can access:
| Permission | Grants access to |
| ---------------------- | --------------------------------------------------------------------------------------------------------------- |
| **Conversations data** | [Conversations API](/api-reference/conversations/introduction) – retrieve conversation transcripts and metadata |
| **Handoff** | [Handoff API](/api-reference/handoff/introduction) – read handoff details |
| **External events** | [External Events API](/api-reference/external-events/introduction) – send events to active conversations |
4. Select **Create** to generate the key
Copy your API key immediately after creation. The full key value is only shown once.
## Viewing and managing keys
Your API keys appear on the **API Keys** page. Each entry shows:
* Key name and scope (which agents it covers)
* Expiry date
* A masked key value with a copy button
* Edit and delete options
Keys are independent – modifying or deleting one does not affect others.
## Configuring metrics
Metrics control what data appears in API responses. Configure them separately from API keys:
1. Select **Configuration** at the top of the **API Keys** page
2. Enable the metrics you want available in API responses:
* Response metrics (e.g., `CALL_IN_PROGRESS`, `CALL_COMPLETED`, `HANDOFF_REASON`, `HANDOFF_TO`)
* Conversation transcript access
Metrics are configured at the **project level**, not per key. For data to appear in an API response, both conditions must be met: the API key must have the relevant permission **and** the project must have the metric enabled.
## Using your API key
Include the API key in the `x-api-key` header of your requests:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "https://api.us-1.platform.polyai.app/v3/ws-xxxxxxxx/PROJECT-xxx/conversations" \
-H "x-api-key: YOUR_API_KEY"
```
For detailed endpoint documentation, see the [API reference](/api-reference/introduction).
## Key lifecycle
* Keys have an **expiry date** set at creation time
* Expired keys stop working immediately
* You can delete keys at any time from the API Keys page
* There is no limit to the number of keys you can create
## Best practices
* Create separate keys for different integrations (e.g., one for your CRM, one for analytics)
* Use the most restrictive permissions needed for each integration
* Rotate keys periodically by creating a new key before deleting the old one
* Monitor key usage through your API response logs
## Related pages
Retrieve conversation metadata and transcripts programmatically.
Access handoff context and manage transitions.
Programmatically configure agents, variants, and deployments.
Send events to active conversations.
# Access control
Source: https://docs.poly.ai/secrets/how-to-access-control
Manage which agents can access secrets
Each secret has its own access list that controls which agents can retrieve it at runtime. An agent without access will fail if its function attempts to call `conv.utils.get_secret()` for that secret.
## Managing access
From the workspace homepage, click the **Secrets** tab in the top bar, then click the secret you want to configure.
The Secrets tab is only visible to **workspace admins** on supported workspaces. If you're an admin and can't see it, the Secrets Vault may not be enabled for your account yet — contact PolyAI support.
Scroll to the **Agent access** section. You will see a list of all agents in your account.
Check the box next to each agent that should be able to retrieve this secret. Uncheck any agents that no longer need access.
Click **Save** to apply your changes. Access updates take effect immediately.
## Principle of least privilege
Grant access only to agents that require a specific secret for their [functions](/tools/introduction). For example, if only your booking agent calls the reservations API, only that agent should have access to the `reservations_api_key` secret.
When rotating credentials, update the secret value first, then test the affected agents before revoking old credentials in the external service.
## Next steps
Build flows with transition functions for complex conversation paths
Write Python functions that call external APIs using your stored credentials
# How to create a secret
Source: https://docs.poly.ai/secrets/how-to-setup
Create a secret in the Secrets Vault
**This page requires Python familiarity.** Secrets are accessed from Python [functions](/tools/introduction) using `conv.utils.get_secret()`.
Create secrets to store API keys, tokens, or other credentials that your agent's [functions](/tools/introduction) need at runtime.
## Create a secret
From the workspace homepage, click the **Secrets** tab in the top bar (next to **Agents**, **Users**, and **API Keys**).
The Secrets tab is only visible to **workspace admins** on supported workspaces. If you're an admin and can't see it, the Secrets Vault may not be enabled for your account yet — contact PolyAI support to request access.
Click **Add secret** in the top right corner.
* **Name** – Use a descriptive, lowercase name with underscores (e.g., `stripe_api_key`, `booking_service_token`). This is the identifier you pass to `conv.utils.get_secret()` in your functions.
* **Description** (optional) – Explain what the secret is for and which integration it supports.
* **Value** – Store as a single value (a plain string like an API key) or key/value pairs (a dictionary for grouped credentials like `client_id` and `client_secret`).
Under **Agent access**, select which agents can retrieve this secret. Only selected agents can use `conv.utils.get_secret()` to access this value.
Click **Add** to create the secret.
Agents without access granted here cannot retrieve the secret. You can update access later from the [access control](/secrets/how-to-access-control) page.
## Using secrets in functions
The `conv.utils.get_secret` method lets you securely retrieve the contents of secrets added to your account. Use it inside any function that has access to the `conv` object:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
secret_value = conv.utils.get_secret('name of your secret')
```
### Return values
**For a key-value type secret**, the function returns a dictionary of key-value pairs:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"secret_key": "secret_value"
}
```
You can access this like a standard Python dictionary in your function code.
**For a single type secret**, the function returns the string containing the secret value.
### Tips
* **Check available secrets**: There is a box called "Secrets" on the top right corner of the "Function Definition" box in the Function Editor. Click that to see what secrets your agent has access to.
* **Permission issues**: If you cannot see the secret in the "Secrets" box, refer to the [access control for secrets](/secrets/how-to-access-control) page to add permission for your function.
* **Copy code snippet**: In the "Secrets" box, you can also copy the code snippet for accessing the secret directly into your function code.
## Next steps
Grant secrets to specific agents and control permissions
Write Python functions that call external APIs using your stored credentials
# Overview
Source: https://docs.poly.ai/secrets/introduction
Store API keys and credentials securely in the Secrets Vault.
Store API keys, tokens, and credentials securely. Your agent's [tools](/tools/introduction) retrieve them at runtime – no hardcoded secrets in code, logs, or version history.
Secrets are managed at the **account level**, not inside individual agents. Access the Secrets Vault from the **Secrets** tab in the workspace top bar (**Agents / Users / Usage / API Keys / Secrets**) — separate from the per-agent sidebar.
The Secrets tab is only visible to **workspace admins** on supported workspaces. If you are an admin and the tab is not visible in your workspace, the Secrets Vault may not be enabled for your account yet — [contact PolyAI support](/learn/maintain/common-issues) to request access.
## How it works
1. You [create a secret](/secrets/how-to-setup) in the Secrets Vault with a name, optional description, and value.
2. You [grant access](/secrets/how-to-access-control) to specific agents.
3. Functions retrieve the secret at runtime using `conv.utils.get_secret("secret_name")`.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
api_key = conv.utils.get_secret("stripe_api_key")
response = requests.get(url, headers={"Authorization": f"Bearer {api_key}"})
```
## Storage formats
Secrets can be stored as:
* **Single value** – A plain string (e.g., an API key).
* **Key/value pairs** – A dictionary of related credentials (e.g., `client_id` and `client_secret`).
## Next steps
Add a new secret to the Secrets Vault.
Grant secrets to specific agents.
Retrieve secrets at runtime in your code.
# Account
Source: https://docs.poly.ai/settings/introduction
Account settings: safety filters, defaults, and admin controls.
Project-wide settings that apply across all channels and environments. Review safety filters before going live.
Access these settings from **Behavior** in the sidebar.
## Safety filter defaults
Default content safety filters. Apply when a channel does not have its own overrides enabled.
Safety filters are configured on a **per-channel basis**. The defaults set here only apply when a channel's safety filters are not explicitly enabled. Each channel can override these settings independently.
| Category | Description |
| ------------------ | -------------------------------------------------------------------------- |
| **Violence** | Controls filtering of violent content (Lenient → Strict) |
| **Hate** | Controls filtering of hateful or discriminatory content (Lenient → Strict) |
| **Sexual content** | Controls filtering of sexually explicit content (Lenient → Strict) |
| **Self-harm** | Controls filtering of self-harm related content (Lenient → Strict) |
For the full reference – categories, severity behavior, language support, monitoring, and how filters interact with [Guardrails](/behavior/guardrails/introduction) – see [Safety filters](/behavior/guardrails/safety-filters). Override the defaults per channel in [Voice configuration](/voice-channel/advanced/call-settings#safety-filters) and [Chat configuration](/messaging-channel/advanced/chat-configuration#safety-filters).
## Related pages
Full reference for content filter categories, severity levels, and per-channel overrides.
Build a dashboard to monitor flagged conversations and safety metrics.
Channel-specific chat safety and behavior settings.
Channel-specific voice and safety settings.
# A/B testing
Source: https://docs.poly.ai/testing/ab-testing
Run two live agent versions in parallel, split real traffic between them, and pick a winner based on real performance data.
**Availability (Beta).** A/B testing is available on US and UK enterprise clusters behind a feature flag. Ask your PolyAI representative to enable it for your project.
A/B tests promote a second version to Live alongside the current one and split real caller traffic between them. You compare key metrics in your dashboards before ending the test and promoting a winner to receive 100% of traffic.
Use it for any change where you want evidence before fully rolling out — a new prompt, a reworked flow, a different routing rule, a model swap. Until now every change went to 100% of traffic on promotion; A/B testing gives you a controlled rollout.
## How it works
* The **current Live version** is the **control (A)**. The **version you promote from Pre-release** is the **variant (B)**.
* At test start you set a traffic split between A and B (between 5% / 95% and 95% / 5%, in 5% steps; defaults to 50 / 50).
* Both versions handle real customer traffic. Calls are routed at the start of the conversation and stay on the assigned version for the whole call.
* The split is fixed for the duration of the test. (Mid-test adjustments are on the roadmap.)
* Only one A/B test can be active per project at a time.
* You end the test by picking a winner. The chosen version is promoted to Live and receives 100% of traffic. The losing version stays in its previous environment.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
stateDiagram-v2
direction LR
[*] --> Live: Promote (no test)
Live --> ABTest: Start A/B test\nfrom Pre-release
ABTest --> Live: End test\npick winner
note right of ABTest: Traffic split A / B\nFixed for test duration
```
## Before you start
You need:
* An active Live deployment (this becomes the control).
* A version in Pre-release that you want to test against it (this becomes the variant). Get there with the standard [promote flow](/environments-and-versions/introduction#promoting-a-version).
* No other A/B test currently running on the project.
* The `ab_tests` feature flag enabled for the project.
You also need write access to environments. Run a regression pass on the variant against your [simulation testing](/testing/simulation-tests) before promoting it to Pre-release — A/B testing measures real-world performance, not correctness.
## Start a test
1. Open **Deployments** in the sidebar and go to the **Pre-release** tab.
2. On the Pre-release version you want to test, open the overflow menu (three dots) and select **Run A/B test**.
3. In the **Start A/B test** modal:
* **Name** — defaults to the current date and time. Override with something you'll recognize in history (for example, `Refund flow rewrite`).
* **Traffic split** — use the slider to set the split between A (control / current Live) and B (variant / Pre-release). Steps of 5%, from 5/95 to 95/5.
* Review both version cards to confirm you're testing the right deployments.
4. Tick **Please confirm both versions will start receiving live customer traffic** and click **Start test**.
Both versions now serve live traffic at the configured split. The Environments page groups them together under the test name with **Live A** and **Live B** tags showing each version's traffic share.
**Both versions are live.** Once you start the test, every caller is routed to either A or B and receives a real, production interaction. Don't start a test with a variant you wouldn't be comfortable shipping to all customers if it had to.
## While a test is running
* Both versions stay visible on the **Pre-release tab** with their traffic share shown next to each row (for example, *Live A 50%* and *Live B 50%*), and the active test appears as a grouped card on the **Live tab**.
* The **Agent Studio chat and call panels** show a banner: *"A/B test in progress, you may be served either live version."* Either version may answer when you test from inside Studio.
* **Other promotions to Live are blocked** until the test ends — you'll see *"End A/B test before promoting a new version to live"* on the promote action.
* **Rollback** of the control version is also blocked while a test is active. End the test first.
* You can still promote other versions through Sandbox → Pre-release; only the final promotion to Live is gated.
## Track performance
Compare A vs B in your existing dashboards. Both versions write to the same analytics tables, tagged with their deployment version.
* Open **Analytics > Dashboards** (QuickSight).
* Filter by **deployed version** to slice any metric — CSAT, containment, latency, handover rate, function errors, anything you already track.
* Compare the two version IDs side by side over the duration of the test.
**Pick metrics before the test starts.** Decide up front what "winning" means (for example, *containment must be ≥ current Live without increasing average handle time*). Reviewing dashboards after the fact and choosing the metric that looks best is how you ship regressions.
Conversation Review filtering by version is on the roadmap; for now, use dashboard filters or the deployment version on each conversation row.
## End the test
1. On the **Environments** page, click **End A/B test** on the active test group (top-right of the grouped card).
2. In the **End A/B test** modal, select the version you want to keep as Live — either the control (A) or the variant (B).
3. Click **Confirm**.
The chosen version is promoted to Live and receives 100% of traffic immediately. The other version stays in its previous environment (Live becomes Pre-release if the variant won; the variant stays in Pre-release if the control won).
**No automated significance testing yet.** You decide when there's enough data to call a winner based on your own thresholds. Statistical comparison is on the roadmap.
## History
Ended A/B tests appear in the **Live Version History** section of the Environments page, grouped under the test name with:
* Both versions and their traffic shares at the time the test ran.
* An indicator on the chosen winner.
* The end timestamp.
Expand any past test to view either version's full deployment details or compare it against another version.
## Limits and roadmap
Today:
* One active A/B test per project.
* Traffic split is set at test start and fixed for the test's duration.
* Variant must be promoted from Pre-release.
* No automated significance testing — you read the dashboards and decide.
* Conversation Review can't yet filter by deployment version.
Planned:
* Mid-test split adjustments.
* Conversation Review filtering by version.
* Automated significance testing and statistical comparison.
## Related pages
How versions move through Sandbox, Pre-release, and Live.
Side-by-side diff of any two versions before promoting.
Audit trail of published versions, including A/B test history.
Automated regression checks to run before promoting a variant.
# Automate with API
Source: https://docs.poly.ai/testing/ci-automation
Gate deployments on simulation test results using the Agents API.
Test runs are most useful when they gate deployments. The [Agents API](/api-reference/agents/introduction) gives you publish and promote actions you can chain behind a passing test set.
## Gate promotions on test results
A typical CI job [publishes](/api-reference/agents/endpoint/deployments/publish-the-current-draft-to-an-environment) the current draft to Sandbox, runs the relevant test set, and only [promotes](/api-reference/agents/endpoint/deployments/promote-a-deployment-to-the-next-environment) to Pre-release if the set passes.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
flowchart LR
A[Publish Draft → Sandbox] --> B[Run test set]
B -->|Pass| C[Promote → Pre-release]
B -->|Fail| D[Block promotion]
```
Use the [Deployments endpoints](/api-reference/agents/endpoint/deployments/publish-the-current-draft-to-an-environment) to wire this into your CI pipeline.
## Related pages
Author the test cases that CI runs.
Organize tests into sets for CI.
How versions move through Sandbox, Pre-release, and Live.
Publish, promote, and rollback endpoints.
# Testing
Source: https://docs.poly.ai/testing/introduction
Validate your agent before and after going live with simulation tests and A/B experiments.
Testing in Agent Studio gives you three ways to validate your agent:
* **Simulation tests** — automated conversations that verify your agent handles specific scenarios correctly. Author a test by describing what a caller will do and asserting how the agent should respond, then run it against Draft or Sandbox to catch regressions before they reach production.
* **A/B tests** — split live traffic between two versions and compare real-world performance before fully rolling out a change.
* **Wren** — generate simulation test scenarios from natural language. Describe a flow and [Wren](/wren/introduction) authors the test cases for you, keeping them in sync as your agent changes.
Use simulation tests throughout development to catch issues early. Use A/B tests when you need real-world evidence that a change improves outcomes. Use Wren when you want to quickly scaffold test coverage without writing each case by hand.
Author scenario-based tests with assertions and system actions.
Execute tests, inspect results, and organize with test sets.
Split live traffic between two versions and pick a winner.
Gate deployments on test results from CI.
# Run and review
Source: https://docs.poly.ai/testing/run-and-review
Execute simulation tests, inspect results, and organize tests into sets.
## Test sets
A test set is a named collection of test cases. Use sets to cover a feature area or release scope (for example, "Payments," "Shipping," "Core intents"). A test case can belong to **multiple** sets.
To create a set:
1. Go to **Testing > Test Sets** and select **New set**.
2. Give the set a **name** and add cases from the picker.
Create focused sets ("Refunds," "Shipping address changes," "Escalations") so failures point straight to the right area.
## Run tests
Tests run against non-production versions. Select **Draft** or **Sandbox** when you start a run.
You can run a **single case** or an entire **set**.
1. Open the case in **Test Cases**.
2. Choose **Draft** or **Sandbox**.
3. Select **Run** to execute just this scenario.
The case shows **Outcome** and **Last run** after completion.
1. Open the set in **Test Sets** and select **Run set**.
2. Choose **Draft** or **Sandbox**.
3. Start the run to execute all member cases together.
The set displays an aggregated view with pass/fail counts and trend charts.
## Review results
When a run completes, select it to open the **Test run** panel. The panel shows:
* **Prompt assertions** — each assertion with a pass/fail indicator and a short explanation of why it passed or failed.
* **Conversation** — the full transcript of the simulated conversation, showing both caller and agent turns.
For test sets, the set view provides:
* **Pass/fail counts** – how many cases succeeded vs. failed in the run.
* **Trend charts** – historical pass/fail rates across multiple runs, so you can spot regressions over time.
If a previously passing test case fails after a change, review the conversation transcript to identify what broke. Common causes include:
* Knowledge topic changes that altered routing
* Function logic updates that changed return values
* Flow modifications that skipped or reordered steps
## Edit test case parameters
Each test case stores the function call values from the original conversation. You can edit these to test variations of the same scenario without creating a new case.
1. Open the test case from **Test Cases**.
2. Select the parameters you want to modify.
3. Adjust values to simulate a different scenario – for example, change a date, customer ID, or location.
4. Save the case.
Editing parameters is useful for testing edge cases. For example, duplicate a booking test case and change the party size to test large-group handling.
## Best practices
* **Create focused sets** – group cases by feature area so failures point to the right area.
* **Re-run after knowledge changes** – topic edits can silently break other flows. Test sets catch this.
* **Run after every significant change to Draft** – catching regressions early saves time and prevents issues from reaching Sandbox or Live.
# Simulation tests
Source: https://docs.poly.ai/testing/simulation-tests
Author scenario-based tests with assertions, tags, and system actions.
Simulation tests run automated conversations against your agent. You describe what a simulated caller will do, assert how the agent should respond, and optionally verify that specific system actions are triggered.
## Create a test
On the **Tests** tab, select **+ Test** to open the **Add tests** panel.
Give the test a descriptive name that summarizes the scenario (for example, "Language test" or "After-hours booking").
Select **Voice** or **Chat** from the **Channel** dropdown. The channel determines how the conversation is simulated.
Describe the caller's behavior in plain language. The simulator uses this to generate realistic caller turns.
**Example:** *A customer calls and requests to speak in French during the call.*
Assertions describe what the agent **should** do during the conversation. After the test runs, each assertion is evaluated and marked as passed or failed.
Select **+ Assertion** to add more.
**Example:** *The agent immediately switches to French and repeats the initial greeting.*
Write assertions that focus on observable agent behavior — language choice, information provided, actions taken — rather than internal implementation details.
Add **tags** to organize and filter tests. Type a tag name and press Enter. A test can have multiple tags (for example, `language` and `french`).
Use tags with the **Filter tests** control on the Tests tab to find related tests quickly.
Expand **Advanced configuration** to access optional settings:
* **Simulated variant** — select a specific [variant](/knowledge/variants/introduction) to run the test against. Leave blank to use the default.
* **System actions** — define actions the agent is expected to take during the conversation. See [System actions](#system-actions) below.
Select **Add 1 test** to save. The test appears in the Tests tab ready to run.
### System actions
System actions verify that the agent triggers the right backend operations — not just that it says the right things. Each action has:
| Field | Description |
| --------------- | --------------------------------------------------------------------------- |
| **Action name** | The system action the agent should invoke (for example, `switch_language`). |
| **Name** | The parameter name (for example, `language`). |
| **Value** | The expected parameter value (for example, `french`). |
| **Type** | The parameter data type — **String**, **Number**, or **Boolean**. |
Select **+ Action** to add more system actions to the test.
## Best practices
* **Name tests descriptively** – use names that describe the scenario, not the expected outcome (for example, "Caller cancels booking" rather than "Test 1").
* **Cover happy paths and edge cases** – include both successful flows and failure scenarios (invalid input, missing data, handoff triggers).
* **Use system actions for backend verification** – don't just test what the agent says; verify it triggers the right actions with the right parameters.
* **Use real conversations as a starting point** – save cases from [Conversation Review](/analytics/conversations/review) to test against real-world scenarios.
# Tool classes
Source: https://docs.poly.ai/tools/classes
Use Conversation, Voice, and utility classes to manage state and configure TTS providers in functions.
**This page requires Python familiarity.** If you are a non-technical operator, work with your developer to configure these classes.
Use these classes in your [functions](/tools/introduction) to manage conversation state and voice configuration.
## Core class
### `Conversation`
The primary class for managing conversation state at runtime. Every function receives a `Conversation` instance as its first argument.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def get_conversation_id(conv: Conversation):
return conv.id
```
For full attributes and methods, see the [`conv` object](/tools/classes/conv-object) page.
## Voice classes
Use voice classes to configure TTS providers programmatically – for example, in a [start function](/tools/start-tool) or when using [multi-voice](/voice-channel/multi-voice).
### `VoiceWeighting`
Assign specific weightings to voices for [multi-voice](/voice-channel/multi-voice) setups, adjusting their prominence in a given context.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
VoiceWeighting(
voice=ElevenLabsVoice(
provider_voice_id="LcfcDJNUP1GQjkzn1xUU",
similarity_boost=0.2,
stability=0.4
),
weight=0.25
)
```
### TTS provider classes
Configures voice settings for [ElevenLabs](https://docs.elevenlabs.io/api-reference/voices) TTS, with control over stability and similarity.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
elevenlabs_voice = ElevenLabsVoice(
provider_voice_id="a1b2C3d4E5f6G7h8I9j0",
stability=0.5,
similarity_boost=0.7
)
```
| Parameter | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `provider_voice_id` | The [ElevenLabs voice ID](https://help.elevenlabs.io/hc/en-us/articles/14599760033937-How-do-I-find-my-voices-ID-of-my-voices-via-the-website-and-through-the-API) |
| `stability` | Consistency of tone and delivery (0.0–1.0) |
| `similarity_boost` | How closely the voice matches the original (0.0–1.0) |
Configures voice settings for [Cartesia](https://docs.cartesia.ai/api-reference/tts/tts) TTS. Uses `Emotion` objects for expressive control.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from polyai.voice import CartesiaVoice, Emotion, EmotionKind, EmotionIntensity
cartesia_voice = CartesiaVoice(
provider_voice_id="a1b2c3d4",
speed=0.0,
emotions=[Emotion(EmotionKind.POSITIVITY, EmotionIntensity.HIGH)]
)
```
| Parameter | Description |
| ------------------- | ----------------------------------------------------------------------- |
| `provider_voice_id` | The Cartesia voice to use |
| `speed` | Speech rate: -1.0 (slowest) to 1.0 (fastest) |
| `emotions` | List of `Emotion` objects (see [voice reference](/tools/classes/voice)) |
| `model_id` | `"sonic"` or `"sonic-preview"` |
Configures voice settings for [PlayHT](https://docs.play.ht/reference/api-getting-started) TTS.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
playht_voice = PlayHTVoice(
provider_voice_id="en_us_male_1",
style="conversational"
)
```
| Parameter | Description |
| ------------------- | ----------------------- |
| `provider_voice_id` | The PlayHT voice to use |
| `style` | Speaking style or tone |
Configures voice settings for [Rime](https://docs.rime.ai/api-reference/voices) TTS.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
rime_voice = RimeVoice(
provider_voice_id="voice_id",
speech_alpha=1.0,
model_id="mistv2"
)
```
| Parameter | Description |
| ------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `provider_voice_id` | Your configured Rime voice identifier. Contact your PolyAI rep or [Rime](https://docs.rime.ai) for the correct value. |
| `speech_alpha` | Speech rate multiplier: \<1.0 faster, >1.0 slower |
| `model_id` | `"mistv2"` or `"mist"` |
Configures voice settings for Minimax TTS.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
minimax_voice = MinimaxVoice(
model_id="speech-02-hd",
voice_id="voice_id",
speed=1.0,
emotion="happy"
)
```
| Parameter | Description |
| ---------- | ------------------------------------------------------------------------------------- |
| `model_id` | `"speech-02-hd"`, `"speech-02-turbo"`, `"speech-01-hd"`, or `"speech-01-turbo"` |
| `voice_id` | The Minimax voice ID |
| `speed` | Speech rate (0.5–2.0) |
| `emotion` | `"happy"`, `"sad"`, `"angry"`, `"fearful"`, `"disgusted"`, `"surprised"`, `"neutral"` |
Configures voice settings for [Hume](https://www.hume.ai/) TTS.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
hume_voice = HumeVoice(
provider_voice_id="voice_uuid_or_name",
version="2"
)
```
| Parameter | Description |
| ------------------- | ------------------------------------------ |
| `provider_voice_id` | Voice UUID or name |
| `voice_description` | Optional description for voice personality |
| `version` | `"1"` (octave-1) or `"2"` (octave-2) |
| `instant_mode` | Ultra-low latency mode (boolean) |
Define voice configurations for a custom TTS provider.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
voice_config = CustomVoice(
provider="MY_PROVIDER",
provider_voice_id="voice_id"
)
```
| Parameter | Description |
| ------------------- | ---------------------------- |
| `provider` | Custom TTS provider name |
| `provider_voice_id` | Voice ID within the provider |
For full voice configuration examples including ElevenLabs model IDs, Cartesia emotions, and cache behavior, see the [Voice class reference](/tools/classes/voice).
# Agent memory
Source: https://docs.poly.ai/tools/classes/agent-memory
Remember information about returning callers across separate conversations using persistent key-value storage.
Use Agent Memory when your agent needs to remember information about returning callers – preferences, past bookings, or verification status – across separate conversations. Without it, every call starts from scratch.
Early access — contact your PolyAI representative before production use.
## How it works
Agent Memory is a key-value store attached to a user identifier (such as a phone number). Persist small, structured data between conversations so your agent can:
* Skip repeated questions for returning callers
* Greet callers by name or reference past interactions
* Resume interrupted conversations where the caller left off
* Track handoff history and resolution status
Memory is read **at the start of each turn** and cached for that turn. It is written **at the end of a conversation** – not on every turn.
You can:
* **Read memory** using `conv.memory.get("key")`
* **Write memory** by setting `conv.state["key"] = value` (if the key is listed in `state_keys`)
## Configuration
### Enable Agent Memory
Agent Memory is configured by PolyAI, not self-served. Contact your PolyAI representative to enable it for your project. They will apply a configuration equivalent to the example below on your behalf:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"memory": {
"repeat_caller": {
"analytics_enabled": true,
"state_keys": ["booking_day", "preferred_language"],
"identifier_blacklist": ["+440000000000"]
}
}
}
```
Use the field table below to decide what to request – which `state_keys` to persist, whether you need analytics, and any identifiers to exclude.
| Field | Description |
| ---------------------- | ------------------------------------------------------------------------------------ |
| `analytics_enabled` | Adds repeat caller metrics to Studio analytics dashboards |
| `state_keys` | Keys from `conv.state` that should be saved to memory at the end of the conversation |
| `identifier_blacklist` | Optional list of identifiers to exclude (e.g., test phone numbers) |
Only include keys you explicitly need in `state_keys`. Memory is persisted at the end of the conversation, so only values set during the call are saved.
## Using memory in functions
### Reading memory
Use `conv.memory.get("key")` in any function to retrieve a previously stored value:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start_function(conv: Conversation):
name = conv.memory.get("caller_name")
if name:
return {"utterance": f"Welcome back, {name}. How can I help you today?"}
```
### Writing memory
Set values in `conv.state` – they are persisted to memory at the end of the conversation if the key is listed in `state_keys`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def collect_name(conv: Conversation, caller_name: str):
conv.state["caller_name"] = caller_name
return {"content": f"Thanks, {caller_name}. I'll remember you next time."}
```
Only keys listed in `state_keys` in your config are persisted. Setting `conv.state["key"] = value` for a key not in `state_keys` will not save it to memory.
### Full example: returning caller
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start_function(conv: Conversation):
booking_day = conv.memory.get("booking_day")
cheese_type = conv.memory.get("cheese_type")
if booking_day and cheese_type:
return {
"utterance": f"I see you previously booked {cheese_type} for {booking_day}. Would you like to book again?"
}
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def book_delivery(conv: Conversation, cheese_type: str, booking_day: str):
conv.state["booking_day"] = booking_day
conv.state["cheese_type"] = cheese_type
return {"content": f"Booked {cheese_type} for {booking_day}."}
```
## Repeat caller analytics
When `analytics_enabled` is set to `true`, Agent Memory automatically tracks five metrics for each returning caller:
| Metric | Description |
| ------------------------------ | ------------------------------------------------- |
| `REPEAT_CALLER_CONV_ID` | Conversation ID of the caller's first interaction |
| `REPEAT_CALLER_DATETIME` | Timestamp of the first interaction |
| `REPEAT_CALLER_QA` | QA outcome from the first interaction |
| `REPEAT_CALLER_HANDOFF_REASON` | Reason for handoff in the first interaction |
| `REPEAT_CALLER_HANDOFF_TO` | Handoff destination from the first interaction |
These metrics appear in your [dashboards](/analytics/dashboards/introduction) and can be used to track repeat caller patterns and resolution rates.
Repeat caller metrics reference the **first** interaction in the retention window, not the most recent — chaining calls would allow identification beyond the retention period.
## Memory behavior
### Timing
* Memory is **fetched once per turn** from the memory service and cached. Multiple `conv.memory.get()` calls in the same turn do not trigger additional lookups.
* Memory is **written at the end of the conversation** (after the end function executes). Values set in `conv.state` during a function on turn 1 are not available through `conv.memory` on turn 2 of the same call.
### Persistence
Each write acts as a **patch** – it updates or adds specific keys without removing existing ones. If conversation 1 writes `{"cheese_type": "gouda"}` and conversation 2 writes `{"booking_day": "Friday"}`, the next lookup returns both fields.
### Expiry
All memory identifiers and fields expire at the end of your contracted retention period (for GDPR compliance). Retention periods vary by customer agreement – check your contract for specifics, or speak to your PolyAI representative. Once expired, data is automatically deleted and no longer accessible.
### Identifiers
The current supported identifier is the **caller's phone number**. This is set automatically – you do not need to configure it.
Additional identifiers (email, account ID) and cross-channel linking are planned.
## Compliance
Before going live with Agent Memory, inform your clients so they can update privacy notices and confirm compliance with applicable data protection laws.
Key compliance considerations:
* Agent Memory is **not intended for automated decision-making** that significantly impacts end users
* Do not store sensitive PII in a single field – separate fields allow independent expiry
* Custom memory fields **cannot** be used to build caller profiles
* Timestamps stored in memory should be rounded to the hour to prevent linking calls across the retention window
* All data expires at the end of your contracted retention period
If your use case goes beyond analytics, call recovery, or personalisation, raise a request with your legal team before proceeding.
## FAQ
Values are stored as JSON-encoded strings. You can store structured data in a single field, but it is recommended to keep fields flat so they can expire independently – especially for anything containing PII.
Yes. Use Python's `in` operator:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if "booking_day" in conv.memory:
# Memory exists for this key
```
Not natively – the latest write always wins. Guard against overwrites in your code:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if "cheese_type" not in conv.memory:
conv.state["cheese_type"] = value
```
Memory is persisted at the end of the conversation, after the [end function](/tools/end-tool) executes. Values set in `conv.state` during a function are not available in `conv.memory` until the next call.
Currently, memory is scoped by phone number. Support for linking identifiers across channels (voice, SMS, webchat) is planned for future releases.
Use the `fields()` method:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
all_memory = conv.memory.fields() # Returns a dict of all fields
```
`conv.memory` is read-only and behaves like a mapping. Use whichever pattern is clearest for your case:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Membership check
if "booking_day" in conv.memory:
...
# Item access (raises KeyError if missing)
last_order = conv.memory["last_order_id"]
# Safe access with default
visits = conv.memory.get("visit_count", 0)
# All stored fields
all_memory = conv.memory.fields()
# Number of stored fields
count = len(conv.memory)
```
Writes go through `conv.state` for keys configured in `state_keys`; assigning directly to `conv.memory` is not supported.
# ASR biasing from functions
Source: https://docs.poly.ai/tools/classes/asr-from-conv
Set and clear speech recognition biasing at runtime using conv.set_asr_biasing() for dynamic vocabulary.
**This page requires Python familiarity.** It covers dynamic ASR biasing from Python functions. For no-code ASR biasing in the flow editor, see [ASR biasing in flows](/flows/asr-biasing).
Use dynamic ASR biasing when you need to bias speech recognition toward values retrieved at runtime – names from a CRM lookup, product codes from an API, or locations specific to the caller's account. Static biasing (configured in the flow editor or Speech Recognition page) cannot handle these cases.
Set and clear biasing using:
* `conv.set_asr_biasing()` – add keywords and custom biases
* `conv.clear_asr_biasing()` – remove previously set biasing
## How it behaves
### Persists across turns
ASR biasing set with `conv.set_asr_biasing()` stays active across turns. It will continue to apply until you:
* call `conv.clear_asr_biasing()`, or
* call `conv.set_asr_biasing()` again with new values.
You do not need to re-apply biasing on every turn.
### Takes priority over other ASR settings
Function-set ASR biasing has the highest priority. It is merged with any ASR configuration defined elsewhere, including:
* global ASR settings (configured on the **Voice > Advanced settings > Speech** page)
* step-level ASR settings (configured on individual flow steps)
* language-specific ASR settings
If the same phrase appears in multiple places, the value set by this type of biasing takes precedence.
ASR biasing can't be set at the flow level. For a whole flow, use global biasing or set it per step. For dynamic biasing that persists across turns, use `conv.set_asr_biasing()` (described on this page).
## When to use this
Use ASR biasing from functions when:
* You retrieve data at runtime and want ASR to reliably capture it.
* You are about to ask the user to say something you already know, such as a reference number or surname.
* Certain domain terms are often misheard and need extra support.
Sets ASR biasing for the current conversation.
### Parameters
#### keywords (optional, list of strings)
A list of phrases that are all biased equally. Use this when you have a small set of expected words and do not need different strengths.
#### custom\_biases (optional, dictionary mapping strings to numbers)
A mapping of phrase to bias weight. Use this when some phrases should be recognized more strongly than others.
### Validation
Inputs are validated when the function runs:
* `keywords` must be a list of strings
* `custom_biases` must be a dictionary with string keys and numeric values
Invalid inputs raise a `ValueError`.
## Examples
### Bias equally toward a set of terms
Use this when all phrases are equally important.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def set_expected_terms(conv):
conv.set_asr_biasing(
keywords=["hotel", "restaurant", "booking", "cancellation"]
)
```
### Bias unevenly using custom weights
Use this when some phrases matter more than others.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def set_weighted_terms(conv):
conv.set_asr_biasing(
custom_biases={
"booking reference": 3.0,
"cancellation": 2.5,
"non refundable": 2.0,
}
)
```
### Combine keywords and custom biases
This is common when you want a general nudge plus one high-priority term:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def set_mixed_biasing(conv):
conv.set_asr_biasing(
keywords=["dermatology", "eczema", "psoriasis"],
custom_biases={"isotretinoin": 3.0}
)
```
### Bias toward values returned from an API
Set biasing after fetching data, before asking the user to say it.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def verify_booking(conv):
booking = conv.api.reservations.get_booking()
conv.set_asr_biasing(
custom_biases={
booking["surname"]: 3.0,
booking["reference"]: 3.0,
}
)
conv.say("Could you confirm the surname and booking reference?")
```
## conv.clear\_asr\_biasing()
Clears any ASR biasing that was previously set for future turns.
This is recommended when biasing is only needed for a short part of the conversation, such as a verification step.
### Example: set biasing, then clear it
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def verify_reference(conv):
ref = conv.api.orders.get_expected_reference()
conv.set_asr_biasing(custom_biases={ref: 3.0})
conv.say("Please say your reference number.")
conv.clear_asr_biasing()
```
## Related documentation
* [Flows: ASR biasing](/flows/asr-biasing)
# Conversation API client
Source: https://docs.poly.ai/tools/classes/conv-api
Call configured APIs from functions using conv.api with automatic environment and auth handling.
**This page requires Python familiarity.** It covers calling APIs from Python functions using `conv.api`.
Use `conv.api` when your function needs to call an external service – CRM lookups, booking systems, payment providers. API definitions (base URLs, auth, operations) are configured in the **APIs** tab in Agent Studio; your function code stays clean and environment-safe.
## How it works
1. You define an API in **Agent Studio → APIs**
* Name
* Base URL (per environment)
* Auth type
* One or more operations (method + resource path)
2. Agent Studio generates a client at runtime.
3. Inside a function, you call it via:
`conv.api..(...)`
The call is executed using the environment's base URL and auth settings automatically.
## Naming rules
* API name in the UI becomes the client name under `conv.api`
* `sweet_booking_api` → `conv.api.sweet_booking_api`
* Operation name becomes the callable method
* `create_booking` → `conv.api.sweet_booking_api.create_booking()`
Use lowercase `snake_case` names to keep calls readable.
## Basic example
API defined in UI:
* API name: `sweet_booking_api`
* Base URL: `https://api.sweets.example`
* Operation:
* Method: POST
* Operation name: `create_booking`
* Resource: `/bookings`
Function usage:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.sweet_booking_api.create_booking(
json={
"party_size": 4,
"date": "2026-02-01",
"contact_phone": conv.caller_number
}
)
```
## Path variables
Path parameters defined in the resource can be passed positionally or by name.
Resource:
`/bookings/{booking_id}`
Both of these are valid:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.sweet_booking_api.get_booking("abc123")
```
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.sweet_booking_api.get_booking(
booking_id="abc123"
)
```
## Query parameters
Use the `params` argument for query string parameters.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.sweet_booking_api.list_bookings(
params={
"from": "2026-02-01",
"to": "2026-02-07"
}
)
```
## Request body
Use `json` (recommended) or `data` depending on the API.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.sweet_booking_api.create_booking(
json={
"party_size": 6,
"notes": "Birthday booking"
}
)
```
## Custom headers
You can pass additional headers at call time.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.sweet_booking_api.create_booking(
json={ "party_size": 4 },
headers={
"X-Request-Source": "polyai-agent",
"X-Correlation-Id": conv.id
}
)
```
## Responses
The return value is a standard HTTP response object.
Typical fields you'll use:
* `response.status_code`
* `response.json()`
* `response.text`
Example:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.sweet_booking_api.create_booking(json=payload)
if response.status_code != 201:
conv.log.error(
"Booking API failed",
status=response.status_code,
body=response.text
)
conv.say("I couldn't complete the booking just now.")
return
booking_id = response.json().get("id")
conv.state.booking_id = booking_id
```
## Error handling
Always check status codes explicitly.
Recommended pattern:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.vendor.operation(...)
if response.status_code >= 500:
conv.log.error("Vendor API error", status=response.status_code)
conv.say("That system is unavailable right now.")
return
if response.status_code == 404:
conv.say("I couldn't find a matching record.")
return
```
## Logging API responses
For debugging and review, log responses explicitly with `conv.log_api_response()`.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.sweet_booking_api.create_booking(json=payload)
conv.log_api_response(response)
```
This records request metadata (URL, method, status code, response time, and error body on non-2xx responses) and makes it visible in:
* Conversation Review → Diagnosis
* Conversations API
### Grouping by URL pattern
Pass `override_url` when the real URL contains high-cardinality path variables (IDs, tokens) so calls group cleanly in analytics.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.sweet_booking_api.get_booking(booking_id="abc123")
conv.log_api_response(
response,
override_url="https://api.sweets.example/bookings/{booking_id}"
)
```
Without `override_url`, each booking ID would log as a distinct URL.
## Environment awareness
The same function code runs across environments.
`conv.api` automatically uses:
* Sandbox base URL in Sandbox
* Pre-release base URL in Pre-release
* Live base URL in Live
You should not branch on environment to change URLs.
# Conversation log
Source: https://docs.poly.ai/tools/classes/conv-log
Write structured log entries visible in Conversation Review diagnostics using conv.log methods.
**This page requires Python familiarity.** It covers structured logging from Python functions.
`conv.log` lets your function write small, structured log entries that show up in **Conversation Review → Diagnosis** and in the Conversations API. Use it for breadcrumbs, warnings, and errors. Without logging, function failures are invisible – you cannot debug what you cannot see.
Logging is part of the core `Conversation` object. It lives on `conv.log`, not `conv.utils`.
## Methods
**Description**: Add a routine breadcrumb.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.log.info("Validated inputs", validation_passed=True)
```
**Description**: Flag a soft failure or approaching limit.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.log.warning(
"Rate limit nearing cap",
vendor="maps_api", window_remaining=5, threshold=10
)
```
**Description**: Record a handled failure with context.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.log.error(
"CRM upsert failed", status=409, retriable=True, attempt=2
)
```
## PII
Set `is_pii=True` when the message or fields contain personally identifiable information.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.log.warning("Caller number captured", user_phone="+123…", is_pii=True)
```
Users without PII permission won't see PII logs in Review or API responses.
## Where it appears
* **Conversation Review → Diagnosis**: grouped under the turn's function event.
* **Conversations API**: returned on function events as `logs.conversation_logger`.
## Entry shape
Each call produces a JSON object like this:
```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
"level": "info|warning|error",
"content": "Message",
"is_pii": false,
"timestamp": "2025-01-01T10:00:00Z",
"logger": "conversation_logger",
"...": "any extra key–values you passed"
}
```
## Patterns
**Validation**
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.log.info("Validation ok", validation_passed=True, missing_fields=[])
```
**Retries**
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.log.error("Inventory API failed", error_code=500, attempt=1, retriable=True)
```
**Companion redaction**
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.log.warning("User email captured", email="j***@example.com", is_pii=True)
conv.log.info("User email captured [redacted]")
```
## Best practices
* Keep messages short; put details in fields.
* Log at decision points, not in tight loops.
* Prefer identifiers over payload dumps.
* Default to `is_pii=True` if you're unsure.
## See also
* [Conversation object](./conv-object)
* [Conversation utilities](./conv-utils)
* [`conv.log_api_response()`](./conv-object#log_api_response) – log full HTTP responses from API integrations to Conversation Review → Diagnosis
# Conversation
Source: https://docs.poly.ai/tools/classes/conv-object
The Conversation object, its attributes, and methods.
**This page requires Python familiarity.** It is a reference for developers writing functions inside Agent Studio.
The Conversation object (conv) provides access to conversation data and tools for managing the agent's behavior. It handles state management, flow transitions, SMS interactions, environment details, and voice selection.
## Attributes
**Description**: Unique identifier of the conversation.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
log.info(f"Conversation ID: {conv.id}")
# Example output: "conv_abc123xyz789"
```
**Description**: PolyAI account ID (the **Workspace ID**, prefixed with `ws-`) that owns this project. This is the same account ID visible in your Studio URL: `https://studio..poly.ai//...` (where `` is `us`, `uk`, or `eu` — Agent Studio is region-specific).
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
log.info(f"Account: {conv.account_id}")
# Example output: "acc_abc123"
```
**Description**: Project ID of the current agent. This is the same project ID visible in your Studio URL: `https://studio..poly.ai///...` (where `` is `us`, `uk`, or `eu` — Agent Studio is region-specific).
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
log.info(f"Project ID: {conv.project_id}")
# Example output: "proj_abc123"
if conv.project_id == "proj_123":
print("Running in the main deployment")
```
**Description**: Current environment.
**Values**: "sandbox", "pre-release", "live"
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if conv.env == "live":
log.info("Production traffic")
```
**Description**: Type of channel the conversation is taking place on.
**Possible values**:
* `"sip.polyai"` – Voice calls (telephony/SIP)
* `"webchat.polyai"` – Webchat widget
* `"chat.polyai"` – Agent Studio in-browser chat
* `"sms.twilio"` – SMS text messages
Use `.startswith()` for broader matching (e.g., `conv.channel_type.startswith("sms")`) when you don't need to distinguish between SMS sub-types.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if conv.channel_type == "sip.polyai":
log.info("Running a voice call")
elif conv.channel_type.startswith("sms"):
log.info("Running an SMS conversation")
elif conv.channel_type == "webchat.polyai":
log.info("Running a webchat conversation")
```
**Description**: List of `Attachment` objects queued to be included with the next agent message (`list[Attachment]`). Append to it with [`conv.add_attachments(...)`](#add_attachments). Attachments are only supported on **webchat** channels.
**Attachment object structure**:
* `content_url` (str) – URL to the main content of the attachment
* `content_type` (str) – The type of the attachment (`"image"`, `"weblink"`, or `"unspecified"`)
* `title` (str, optional) – Title of the attachment
* `preview_image_url` (str, optional) – URL to a preview image for the attachment
* `call_to_action` (str, optional) – Text for the call-to-action button or link
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
for file in conv.attachments:
print(f"Type: {file.content_type}")
print(f"URL: {file.content_url}")
if file.title:
print(f"Title: {file.title}")
if file.preview_image_url:
print(f"Preview: {file.preview_image_url}")
```
**Description**: Dictionary of SIP headers (dict\[str, str]) provided by the carrier.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
source_ip = conv.sip_headers.get("X-Src-IP")
```
**Description**: Metadata passed from an external integration (dict\[str, Any]). These attributes are defined per project and per integration during setup.
**Supported integrations**:
* [DNIs Pooling](https://docs.poly.ai/integrations/voice/dnis-pool) – Provides `shared_id` for correlating conversation outcomes
**Best practice**: Validate and extract required attributes in the `start_function` to handle missing data appropriately.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# In start_function
if shared_id := conv.integration_attributes.get("shared_id"):
conv.state.shared_id = shared_id
log.info(f"Tracking with shared_id: {shared_id}")
else:
# Handle missing integration data
log.warning("No shared_id provided by integration")
```
**Description**: The caller's identifier.
**For inbound calls**: The phone number of the person calling in, in [E.164](http://twilio.com/docs/glossary/what-e164) format (e.g., `+14155551234` for USA numbers).
**For outbound calls**: The phone number being called by the agent.
**For chat channels**: This may be an email address or integration-provided user ID.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
identifier = conv.caller_number
log.info(f"Caller identity: {identifier}")
# Inbound example: "+14155551234"
# Chat example: "user@example.com"
```
**Description**: Number dialled by the caller.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if conv.callee_number.endswith("1001"):
conv.state.branch = "Priority"
```
**Description**: Dictionary-like store that persists values across turns. Validated entities are also written here under their entity name, coerced to their native Python type (`int` for numeric/quantity entities, `float` for decimal numerics and currency, `str` for everything else).
**Access patterns**: `conv.state` supports both bracket and attribute access — pick whichever reads more clearly. Reads and writes behave identically across the two styles:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Writes are equivalent
conv.state["is_verified"] = True
conv.state.is_verified = True
# Reads differ for missing keys
conv.state["is_verified"] # raises KeyError if missing
conv.state.is_verified # returns None if missing
conv.state.get("is_verified") # returns None if missing
```
Missing-key behavior differs by access style:
* `conv.state["missing_key"]` raises `KeyError`.
* `conv.state.missing_key` returns `None` — it does **not** raise `AttributeError`.
* `conv.state.get("missing_key")` returns `None` (or your default).
This means `if conv.state.is_verified == False:` is **not** the same as "missing or False" — when the key is missing the value is `None`, and `None == False` is `False`, so the branch is skipped. Prefer `if not conv.state.get("is_verified"):` to treat missing and falsy the same way.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Manual state
conv.state["attempts"] = conv.state.get("attempts", 0) + 1
# Auto-synced entity, available as a native int
if conv.state.party_size > 15:
flow.goto_step("Group Booking")
# Guarding a function on a verification flag
if not conv.state.get("is_verified"):
conv.state.original_topic = "check_account_balance"
conv.goto_flow("Identify & Verify User")
return
```
**Description**: Name of the flow currently executing, or None.
**Description**: Step name currently executing in the active flow.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
log.info(f"Current step: {conv.current_step}")
```
**Description**: List of `OutgoingSMS` / `OutgoingSMSTemplate` objects queued for dispatch at turn end.
**Description**: Name of the active variant, or None.
**Description**: Dictionary of all variant definitions (dict\[str, Variant]). Each key is the variant name and each value is a Variant object whose attributes match the columns defined in **Knowledge > Variants**.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Iterate variants to find one matching a custom attribute
for variant_name, variant in conv.variants.items():
log.info(f"{variant_name}: {variant.phone_number}")
# Build a lookup from a variant attribute to variant name
callee_map = {
variant.callee: name
for name, variant in conv.variants.items()
}
matched = callee_map.get(conv.callee_number)
if matched:
conv.set_variant(matched)
```
**Description**: Variant object for the active variant, or `None` if no variant has been set. Attribute values are returned as **strings** (the storage type used by Variants) — parse them yourself if you need structured data. Reading an attribute that doesn't exist on the active variant returns `None`, not an `AttributeError`.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if conv.variant:
print(conv.variant.description)
# Attribute values are strings — parse JSON yourself if needed
import json
hours = json.loads(conv.variant.opening_hours_json or "{}")
```
**Description**: Dictionary of SMS templates (dict\[str, SMSTemplate]).
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
template_body = conv.sms_templates["booking_confirmation"].content
```
**Description**: Pending TTSVoice change requested this turn, or None.
**Description**: Language code configured for the project, which may include a locale suffix (e.g. "en-US", "en-GB", "es-ES").
**Description**: Chronological list of UserInput and AgentResponse events so far.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
for event in conv.history:
print(event.role, event.text)
# Example output:
# user "I need to book a table"
# agent "I'd be happy to help you book a table"
# user "For 4 people at 7pm"
```
**Description**: Dictionary of configured hand-off destinations (dict\[str, HandoffConfig]).
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if "support" in conv.handoffs:
print("Support line is available")
```
**Description**: List of transcription alternatives (list\[str]) for the last user utterance, including the primary transcription.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
for alt in conv.transcript_alternatives:
print(f"Alternative: {alt}")
# Example output:
# Alternative: "book a table for two"
# Alternative: "book a table for too"
# Alternative: "book a table for to"
```
**Description**: Returns a dictionary of real-time configuration values defined in Configuration Builder.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
config = conv.real_time_config
if config.get("after_hours_enabled"):
conv.say("Our offices are currently closed.")
```
**Description**: Dictionary of memory fields previously stored for the caller, retrieved from Agent Memory.
**Customer identification**: Memory is retrieved using `caller_number` for voice calls or the integration-provided user identifier for chat channels. This lets the agent recognize returning customers and recall previous interactions.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
cheese = conv.memory.get("cheese_type")
if cheese:
conv.say(f"You're a fan of {cheese}, right?")
# Check if this is a returning customer
if conv.memory.get("last_order_date"):
conv.say("Welcome back!")
```
**Description**: Dictionary of entity validation results collected from the conversation (dict\[str, EntityValidationResult]). Each result's `.value` is the raw string captured from the caller.
For a typed value (`int`, `float`, or `str`) read the entity from [`conv.state`](#state) instead -- validated entities are auto-synced there under their entity name.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if "email" in conv.entities:
validated_email = conv.entities.email.value # str
# Numeric entity as a native int
party_size = conv.state.party_size
```
**Description**: Executor for calling other functions defined in the project. Access functions using dot notation.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
result = conv.functions.lookup_order()
```
**Description**: Executor for calling configured API integrations. Access APIs using `conv.api.{integration_name}.{operation_name}()`.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = conv.api.salesforce.get_contact(user_id="123")
if response.status_code == 200:
contact_data = response.json()
```
**Description**: Executor for calling pre-built third-party integrations (e.g., OpenTable, Tripleseat) configured from **Integrations**. Access using `conv.integrations.{integration_name}.{method}()`.
`conv.integrations` is for integrations PolyAI has built and maintains (proxied through Paragon). For custom HTTP APIs you define yourself in the **APIs** tab, use [`conv.api`](#api).
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# OpenTable
response = conv.integrations.opentable.get_reservation(confirmation_number="ABC123")
# Tripleseat
response = conv.integrations.tripleseat.create_lead(
public_key=conv.utils.get_secret("tripleseat_public_key"),
first_name="Jane",
last_name="Smith",
phone_number=conv.caller_number,
location_id="12345",
)
if response.status_code == 200:
lead_data = response.json()
```
See [Integrations](/integrations/introduction) for the list of available integrations and setup instructions.
**Description**: Webchat-specific interface for functionality that only applies on `webchat.polyai` channels. Access webchat-only methods via `conv.webchat.{method}()`.
**Available methods**:
* `set_chat_call_actions(actions)` – attach click-to-call buttons to the next agent message.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from polyai.webchat import ChatCallAction
if conv.channel_type == "webchat.polyai":
conv.webchat.set_chat_call_actions([
ChatCallAction(contact_number="+15551234567", title="Call support")
])
```
Calling webchat methods on non-webchat channels has no effect on the conversation, but you should still gate them on `conv.channel_type` for clarity.
**Description**: List of external events initiated by `generate_external_event`. Each event contains `ext_event_id`, `send_to_llm`, `created_at`, `data`, and `content_type`.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
for event in conv.generic_external_events:
if event.data:
log.info(f"Received webhook data: {event.data}")
```
**Description**: Proxy for accessing localized translations. Access translation keys as attributes to get the translated text for the current language.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
greeting = conv.translations.welcome_message
conv.say(greeting)
```
**Description**: List of quick-reply suggestions for the next agent message. Only supported on webchat channels.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
for suggestion in conv.response_suggestions:
print(suggestion)
```
**Description**: Agentic dial data for the conversation, used for advanced dialing scenarios.
## Methods
**Description**: Override the next utterance.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.say("I've made that change for you.")
```
**Description**: Randomly choose a voice based on weighted probabilities.
**Parameters**:
* voice\_weightings (list\[VoiceWeighting]) – list of VoiceWeighting objects, each containing a voice and weight.
**Available voices**: Import from `polyai.voice` module (e.g., `ElevenLabsVoice`, `CartesiaVoice`, `PlayHTVoice`, `RimeVoice`). See [Voice classes](/tools/classes/voice) for the full list of available voices and their IDs.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from polyai.voice import VoiceWeighting, ElevenLabsVoice
conv.randomize_voice([
VoiceWeighting(voice=ElevenLabsVoice(provider_voice_id="voice1"), weight=0.7),
VoiceWeighting(voice=ElevenLabsVoice(provider_voice_id="voice2"), weight=0.3)
])
```
**Description**: Transition to another flow at turn end.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.goto_flow("verification")
```
**Description**: Exit the current flow.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.exit_flow()
```
**Description**: Manually set the active variant.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.set_variant("evening")
```
**Description**: Attach one or more visual tiles (images or web links) to the conversation. Only supported on webchat channels.
**Parameters**: attachments (list\[Attachment]) – list of Attachment objects.
**Attachment fields**:
* `content_url` (str) – URL to the main content of the attachment
* `content_type` (str) – The type of the attachment (`"image"`, `"weblink"`, or `"unspecified"`)
* `title` (str, optional) – Title of the attachment
* `preview_image_url` (str, optional) – URL to a preview image for the attachment
* `call_to_action` (str, optional) – Text for the call-to-action button or link
**Attachment types**:
* `"weblink"` – Displays the title, preview image, and call-to-action text. Clicking navigates the user to `content_url`.
* `"image"` – Displays the title (if present), but no call-to-action. `preview_image_url` should be a lower resolution image, and `content_url` should be the full resolution image. Clicking shows the higher resolution version.
* `"unspecified"` – No specific rendering behaviour is applied.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from polyai.attachments import Attachment
conv.add_attachments([
Attachment(
content_url="https://example.com/menu",
content_type="weblink",
title="View our menu",
preview_image_url="https://example.com/menu-preview.jpg",
call_to_action="Open menu"
),
Attachment(
content_url="https://example.com/logo.png",
content_type="image"
)
])
```
**Description**: Prevents saving the current call recording, e.g. when sensitive data is detected.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if conv.state.get("contains_pii"):
conv.discard_recording()
```
**Description**: Generates a unique external event ID linked to the current conversation. Use this ID to receive webhook data from an external provider through the `/v1/external-events/webhook` endpoint. The webhook payload is then accessible through `conv.generic_external_events`.
**Parameters**:
* send\_to\_llm (bool, keyword-only, optional) – if `True`, the webhook payload is also sent to the LLM as a system prompt. Default `False`.
**Returns**: str – the generated external event ID (expires after 1 hour).
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
event_id = conv.generate_external_event(send_to_llm=True)
# Pass event_id to an external provider, which can POST data back via the webhook
```
**Description**: Logs an external API response for visibility in Conversation Review → Diagnosis and the analytics pipeline.
**Where logs appear**:
* **Conversation Review**: View API responses in the Diagnosis tab for debugging
* **Analytics pipeline**: API response data is available for reporting and analysis
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = requests.get("https://api.example.com/user")
conv.log_api_response(response)
# Response will appear in Conversation Review → Diagnosis
```
**Description**: Sends a WhatsApp message through a Twilio subaccount. Both the sender phone number and the message template must be pre-approved by Meta. Once approved, Twilio provides a `content_id` that must be referenced when sending.
WhatsApp messaging requires a configured Twilio integration and is not a native PolyAI channel. Contact your PolyAI representative for setup.
**Parameters**:
* `to_number` (str) – recipient phone number.
* `from_number` (str) – sender phone number (must be WhatsApp-enabled in Twilio).
* `content_id` (str) – alphanumeric ID of the approved WhatsApp message template.
* `content` (str, optional) – text content. Default `""`.
* `retry_count` (int, optional) – number of retries to attempt on failure.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.send_whatsapp(
to_number=conv.caller_number,
from_number="+441234567890",
content_id="HX5678efgh",
)
```
For richer templating with variables, use [`send_content_template`](#send_content_template) with `whatsapp=True`.
**Description**: Sends a WhatsApp or SMS template message through Twilio's Content API. Requires the content template to be pre-approved in your Twilio account.
WhatsApp messaging requires a configured Twilio integration and is not a native PolyAI channel. Contact your PolyAI representative for setup.
**Parameters**:
* messaging\_service\_id (str) – Twilio messaging service ID.
* to\_number (str) – recipient phone number.
* content\_id (str) – alphanumeric ID of the approved message template.
* content (str, optional) – text content. Default `""`.
* whatsapp (bool, optional) – set to `True` to send over WhatsApp. Default `False`.
* content\_variables (dict, optional) – variables to pass to the message template.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.send_content_template(
messaging_service_id="MG1234abcd",
to_number="+441234567890",
content_id="HX5678efgh",
whatsapp=True,
content_variables={"1": "12345", "2": "shipped"}
)
```
**Description**: Queue a plain-text SMS.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.send_sms(to_number=conv.caller_number, from_number="+441234567890", content="Thanks for calling – here's your link: https://…")
```
**Description**: Queue a pre-configured SMS template.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.send_sms_template(to_number=conv.caller_number, template="booking_confirmation")
```
**Description**: Write a custom metric to the analytics pipeline. This is the only way custom metrics are recorded for a conversation – the agent does not write them automatically at runtime, so you must call `write_metric` explicitly from a function whenever you want a metric value captured.
**Parameters**:
* name (str) – metric key, as defined in your project's metrics configuration.
* value (str, int, float, bool, or None) – the metric value. Must match the type defined in the metric spec.
* write\_once (bool, optional) – if `True`, the metric can only be written once per conversation. Subsequent calls with the same name are ignored. Default `False`.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.write_metric("agent_handoff", 1)
conv.write_metric("call_outcome", "resolved", write_once=True)
```
**Description**: Transfer the call to a configured handoff destination.
**Parameters**:
* destination (str) – handoff target key, as defined in your handoff configuration.
* reason (str, optional) – escalation reason. Defaults to the destination name.
* utterance (str, optional) – message for the agent to say before transferring.
* sip\_headers (dict\[str, str], optional) – SIP headers to pass through. Merged with any headers configured in the handoff config, with passed headers taking precedence.
* route (str, optional) – phone number or route to override the configured route.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.call_handoff(
destination="BillingQueue",
reason="policy_violation",
utterance="Let me transfer you to a specialist who can help."
)
```
**Where it shows up**: In flows using builtin-handoff or using functions; visible in Conversation Review and API.
**Description**: Provides helper functions for extracting data, validating entities, and accessing secrets.
**Available utilities**:
* `get_secret(name)` – Retrieve a stored [secret](/secrets/introduction) by name
* `extract_address()` – Extract postal addresses from user input
* `extract_city()` – Extract city references from user input
* `prompt_llm()` – Perform a standalone LLM request
* `validate_entity()` – Validate a value against an entity config (email, phone, date, etc.)
**Note**: Some utilities require activation. If a method raises a `NotImplementedError`, contact your PolyAI representative to enable it for your account.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
api_key = conv.utils.get_secret("stripe_api_key")
address = conv.utils.extract_address(country="US")
```
See [Conversation utilities](./conv-utils) for the full list of available helpers and detailed documentation.
**Description**: Change the TTS voice for the current conversation moving forward.
**Parameters**: voice (TTSVoice) – the voice configuration to use.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from polyai.voice import ElevenLabsVoice
conv.set_voice(ElevenLabsVoice(provider_voice_id="abc123"))
```
**Description**: Change the language for the current conversation moving forward.
**Parameters**: language (str) – ISO 639 language code (e.g., "en-US", "es-ES", "fr-FR").
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.set_language("es-US")
```
**Description**: Dynamically configure ASR keywords and custom biases for improved speech recognition. Biasing persists across turns until cleared.
**Parameters**:
* keywords (list\[str], optional) – list of keywords to bias ASR recognition toward.
* custom\_biases (dict\[str, float], optional) – dictionary mapping phrases to bias weights.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.set_asr_biasing(
keywords=["reservation", "booking", "cancel"],
custom_biases={"reservation": 3.0, "cancellation": 2.5}
)
```
**Description**: Clear any previously set ASR biasing for future turns.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.clear_asr_biasing()
```
**Description**: Exclude one or more [FAQs](/knowledge/faqs/introduction) from RAG retrieval for the rest of the conversation. Useful when an upstream IVR or runtime context means certain topics should not be matched. The list persists across turns until cleared or replaced. See [Disable KB topics](/tools/classes/disable-kb-topics) for full details.
**Parameters**:
* topics (list\[str]) – Managed Topic names to disable. Replaces any previously disabled list.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if conv.state.get("ivr_intent") == "billing_handled":
conv.disable_kb_topics(["refund_policy", "billing_dispute"])
```
**Description**: Re-enable any [FAQs](/knowledge/faqs/introduction) that were previously disabled with `conv.disable_kb_topics()`.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.clear_disabled_kb_topics()
```
**Description**: Trigger a transition to the CSAT (Customer Satisfaction) survey flow for voice calls.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if conv.state.get("call_completed"):
conv.goto_csat_flow()
```
**Description**: Override whether the conversation is eligible for a [CSAT survey](/analytics/csat/introduction). When `eligible=False`, the conversation is excluded from CSAT regardless of call type, percentage rollout, or weekly caps. When `eligible=True` or this method isn't called, normal CSAT logic applies.
**Parameters**:
* `eligible` (bool) – whether the conversation is eligible for CSAT.
* `reason` (str, optional) – reason for the decision (logged for debugging).
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if conv.state.get("contains_pii"):
conv.set_csat_eligibility(False, reason="PII detected")
```
**Description**: Override the phone number used for CSAT SMS surveys. Useful when the real caller number is delivered via a SIP header (e.g., behind an IVR) rather than as the caller ID.
**Parameters**:
* `phone_number` (str) – phone number to send the CSAT SMS to, in [E.164](http://twilio.com/docs/glossary/what-e164) format.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
real_caller = conv.sip_headers.get("X-Real-Caller")
if real_caller:
conv.set_csat_phone_number(real_caller)
```
**Description**: Mark that the caller entered the CSAT survey flow. This is used internally by the platform's voice CSAT flow for analytics and is not typically called from custom functions.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.set_csat_survey_entered()
```
**Description**: Send an email from the function.
This function sends outbound emails during a conversation (e.g., confirmations or notifications). **Email is not a supported inbound channel for PolyAI agents.**
**SMTP configuration**: Emails are sent through a managed delivery service. For custom email delivery requirements, contact your PolyAI representative.
**Delivery considerations**:
* Emails are sent asynchronously after the turn completes
* Delivery failures are logged but do not interrupt the conversation
* For high-volume sending, consider rate limits and reputation management
**Parameters**:
* to (str) – recipient email address.
* body (str) – raw body of the email.
* subject (str) – subject line.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.send_email(
to="customer@example.com",
body="Thank you for your order!",
subject="Order Confirmation"
)
```
**Description**: Set quick-reply suggestions that appear as clickable options for the user. Only supported on webchat channels.
**Parameters**: suggestions (list\[str]) – list of suggested responses.
**Example**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.set_response_suggestions(["Yes, confirm", "No, cancel", "More options"])
```
# Conversation utilities
Source: https://docs.poly.ai/tools/classes/conv-utils
Access secrets and helper methods for extracting addresses and other structured data from conv.utils.
The `conv.utils` property provides helper methods for accessing secrets, extracting and validating information from user input, and making standalone LLM calls.
## `get_secret`
Retrieve a stored [secret](/secrets/introduction) by name. Returns the secret value as a string or dictionary (for key/value pair secrets).
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
api_key = conv.utils.get_secret("stripe_api_key")
response = requests.get(url, headers={"Authorization": f"Bearer {api_key}"})
```
Parameters:
* `secret_name` (str): The name of the secret as configured in the [Secrets Vault](/secrets/how-to-setup).
Returns:
* `str` or `dict`: The secret value.
Raises:
* `SecretNotFound` if the secret does not exist.
* `MissingAccess` if the current agent does not have [access](/secrets/how-to-access-control) to this secret.
Never hardcode credentials — use `conv.utils.get_secret()` for API keys, tokens, and passwords.
## `extract_address`
Extract a structured postal address from the latest user turn. Optionally validate against a list of known addresses.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
address = conv.utils.extract_address(country="US")
conv.state.user_address = address
```
Parameters:
* `addresses`: Optional list of `Address` objects to match against. Street name must be specified for each address.
* `country`: Optional country code to filter on (default `"US"`).
Returns:
* An `Address` instance with available fields populated. Some fields may be `None` if not provided.
Raises:
* `ExtractionError` if parsing fails.
Providing an `addresses` list improves extraction accuracy. Without it, street numbers or names may be missed or incorrect — always confirm extracted addresses with the caller before taking action.
## `extract_city`
Extract a valid city name (and optionally state/country) from the latest user turn.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
city_data = conv.utils.extract_city(states=["CA"], country="US")
conv.state.city = city_data.city
```
Parameters:
* `city_spellings`: Optional list of spelled-out city names to match.
* `states`: Optional list of states to filter on.
* `country`: Optional country code to filter on (default `"US"`).
Returns:
* An `Address` instance where the `city` field is guaranteed to be populated on a successful extraction; other fields may be `None`. If extraction fails, an `ExtractionError` is raised instead.
Raises:
* `ExtractionError` if parsing fails.
### `Address` type
Both utilities return the same `Address` type:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
@dataclass
class Address:
street_number: Optional[str]
street_name: Optional[str]
city: Optional[str]
state: Optional[str]
postcode: Optional[str]
country: Optional[str]
```
## `prompt_llm`
Perform a standalone LLM request with a given prompt. Useful for summarizing conversations or extracting specific information.
This method requires activation for your account. If you receive a `NotImplementedError`, contact your PolyAI representative to enable it.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
summary = conv.utils.prompt_llm(
"Summarize the key points from this conversation",
show_history=True,
return_json=False,
model="gpt-4o"
)
```
Parameters:
* `prompt` (str): The system-level prompt containing instructions for the model.
* `show_history` (bool, optional): Whether to include conversation history in the request. Default `False`.
* `return_json` (bool, optional): Whether to return the response as a parsed JSON dict. Default `False`.
* `model` (str, optional): The LLM to use. Default `"gpt-4o"`. Available options:
* `"gpt-4o"` – GPT-4o (default, balanced performance)
* `"gpt-4o-mini"` – GPT-4o Mini (faster, lower cost)
* `"gpt-4.1"` – GPT-4.1
* `"gpt-4.1-mini"` – GPT-4.1 Mini
* `"gpt-4.1-nano"` – GPT-4.1 Nano (fast, low cost)
* `"gpt-5"` – GPT-5 (full reasoning model, highest capability)
* `"gpt-5-mini"` – GPT-5 Mini (balanced speed and capability)
* `"gpt-5-nano"` – GPT-5 Nano (fastest, lowest latency)
* `"gpt-5-chat"` – GPT-5 Chat (optimised for conversation)
* `"claude-3.5-haiku"` – Claude 3.5 Haiku (fast)
* `"claude-sonnet-4"` – Claude Sonnet 4
Returns:
* `str` or `dict`: The LLM response, parsed as JSON if `return_json=True`.
Raises:
* `ChatCompletionError` if the request fails.
`prompt_llm` makes a synchronous LLM call during the conversation turn. This adds latency — choose a smaller model (e.g. `"gpt-5-nano"` or `"gpt-4.1-nano"`) when speed matters, and avoid calling it multiple times in a single turn.
## `validate_entity`
Validate an entity value against a configuration schema.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
result = conv.utils.validate_entity(
value="john@example.com",
entity_config=conv.utils.EmailConfig()
)
if result.valid:
conv.state.email = result.value
```
Parameters:
* `value` (str): The value to validate.
* `entity_config` (EntityConfig): Configuration for the entity type. Available configs:
* `conv.utils.EmailConfig()`
* `conv.utils.PhoneNumberConfig()`
* `conv.utils.DateConfig()`
* `conv.utils.TimeConfig()`
* `conv.utils.NumericConfig()`
* `conv.utils.QuantityConfig()`
* `conv.utils.CurrencyConfig()`
* `conv.utils.NameConfig()`
* `conv.utils.AlphanumericConfig()`
* `conv.utils.EnumConfig()`
* `conv.utils.FreeTextConfig()`
Returns:
* `EntityValidationResponse` with `valid`, `value`, and validation details. Some config types expose additional fields – for example, `PhoneNumberConfig` results include `country_code` and `number`. Available fields vary by config type.
Entity values are always returned as strings, even for numeric entity types. Cast with `int()` or `float()` before numeric comparisons.
## Notes
* **Latency**: Each method may take a few seconds to complete due to LLM processing.
* **Validation**: Providing allowed values (addresses, city spellings, or states) can improve accuracy.
* **Scope**: Operates on the most recent user input, including alternate transcript hypotheses.
## See also
* [`conv` object](./conv-object) – full list of conversation methods and attributes.
# Disable knowledge topics from functions
Source: https://docs.poly.ai/tools/classes/disable-kb-topics
Exclude specific FAQs from RAG retrieval at runtime using conv.disable_kb_topics() based on conversation context.
**This page requires Python familiarity.** It covers disabling [FAQs](/knowledge/faqs/introduction) from Python functions so they are excluded from retrieval for the rest of the conversation.
Use `conv.disable_kb_topics()` when you need to deterministically prevent the agent from matching certain topics – for example, when an upstream IVR has already handled a flow, the caller is in a context where some answers are not applicable, or a client request requires hiding a topic from the agent without editing the knowledge base.
Previously this could only be approximated through prompting. Disabling topics from a function gives you a reliable, deterministic way to scope the agent's knowledge mid-conversation.
Set and clear disabled topics using:
* `conv.disable_kb_topics()` – hide one or more topics from retrieval
* `conv.clear_disabled_kb_topics()` – re-enable topics that were previously disabled
## How it behaves
### Persists across turns
Topics disabled with `conv.disable_kb_topics()` stay disabled for the rest of the conversation. They continue to be excluded until you:
* call `conv.clear_disabled_kb_topics()`, or
* call `conv.disable_kb_topics()` again with a new list (the new list replaces the previous one).
You do not need to re-disable topics on every turn.
### Excluded everywhere RAG runs
Disabled topics are removed from:
* The list of topics shown to the LLM in the system prompt.
* Retrieval results used to ground the response.
* The function/tool list exposed to real-time (speech-to-speech) agents.
### Combines with channel exclusions
Disabled topics are merged with any topics already excluded for the current channel (configured under **Channels**). A topic is hidden if it is excluded by either mechanism.
## When to use this
Use `conv.disable_kb_topics()` when:
* An external IVR has already handled an intent and you do not want the agent to repeat it.
* The caller's account or segment should not see certain topics (for example, a "cancellations" topic for a non-cancellable plan).
* A client requests temporary suppression of a topic without changing the knowledge base.
* You want to scope retrieval to a subset of topics for a specific part of the conversation.
## conv.disable\_kb\_topics()
Disables one or more FAQs for the current conversation.
### Parameters
#### topics (list of strings)
A list of Managed Topic **names** to disable. Names must match the topic names configured under **Knowledge > FAQs**.
Calling `conv.disable_kb_topics()` replaces any previously disabled list. To add to the existing list, include the previous names alongside the new ones.
### Examples
#### Disable a single topic based on IVR context
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def on_start(conv):
if conv.state.get("ivr_intent") == "billing_handled":
conv.disable_kb_topics(["refund_policy", "billing_dispute"])
```
#### Disable topics based on account data
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def setup_account(conv):
account = conv.api.crm.get_account()
if not account["supports_cancellation"]:
conv.disable_kb_topics(["cancellation_policy"])
```
#### Replace the disabled list
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def update_scope(conv):
# Only "store_hours" is disabled after this call;
# any previously disabled topics are re-enabled.
conv.disable_kb_topics(["store_hours"])
```
## conv.clear\_disabled\_kb\_topics()
Re-enables any topics that were previously disabled for the conversation.
Use this when the reason for disabling no longer applies – for example, after the caller switches to a different intent or completes a verification step.
### Example: disable, then clear
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def handle_self_service(conv):
conv.disable_kb_topics(["human_handoff"])
conv.say("Let's try to sort this out together.")
# Later, if self-service fails:
if conv.state.get("self_service_failed"):
conv.clear_disabled_kb_topics()
```
## Related documentation
* [FAQs](/knowledge/faqs/introduction)
* [RAG](/knowledge/faqs/RAG/introduction)
* [Conversation object](/tools/classes/conv-object)
# History
Source: https://docs.poly.ai/tools/classes/history
Access conversation turn history as UserInput and AgentResponse objects from conv.history.
**This page requires Python familiarity.** It covers accessing conversation history from Python functions.
The `conv.history` attribute contains a chronological list of `UserInput` and `AgentResponse` objects representing the conversation so far.
Filter events with `isinstance()` rather than `event.role` — new event types may be added that don't have a `role` attribute.
## `UserInput`
Represents a user turn in the conversation.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
for event in conv.history:
if isinstance(event, UserInput):
print(f"User said: {event.text}")
```
### Properties
| Property | Type | Description |
| -------- | ---- | ----------------------------------------------------------------------------------------------- |
| `text` | str | The user's input text. Can be an empty string when the user is silent or no speech is detected. |
| `role` | str | Always `"user"` |
### Methods
| Method | Returns | Description |
| ------------- | ------- | ----------------------------------------- |
| `to_dict()` | dict | Returns `{"type": "user", "text": "..."}` |
| `to_string()` | str | Returns `"User: "` |
## `AgentResponse`
Represents an agent turn in the conversation.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
for event in conv.history:
if isinstance(event, AgentResponse):
print(f"Agent said: {event.text}")
```
### Properties
| Property | Type | Description |
| -------- | ---- | ------------------------- |
| `text` | str | The agent's response text |
| `role` | str | Always `"agent"` |
### Methods
| Method | Returns | Description |
| ------------- | ------- | ------------------------------------------ |
| `to_dict()` | dict | Returns `{"type": "agent", "text": "..."}` |
| `to_string()` | str | Returns `"Agent: "` |
## Example: format history for logging
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def log_conversation(conv: Conversation):
for event in conv.history:
conv.log.info(event.to_string())
```
## Example: get last user message
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def get_last_user_input(conv: Conversation) -> str:
for event in reversed(conv.history):
if isinstance(event, UserInput):
return event.text
return ""
```
## Notes
* **Empty `text` values**: `UserInput.text` can be an empty string (e.g., when a user is silent or no speech is detected). Guard against this in your functions if downstream logic depends on non-empty input:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
last_input = get_last_user_input(conv)
if last_input.strip():
# Process the input
pass
```
* **Metric events**: Metric data is not part of `conv.history`. Custom metrics are written separately via [`conv.write_metric`](./conv-object#write-metric).
## See also
* [`conv` object](./conv-object) – full list of conversation methods and attributes.
# Voice
Source: https://docs.poly.ai/tools/classes/voice
Configure TTS providers like ElevenLabs and Cartesia programmatically using voice classes.
**This page requires Python familiarity.** It covers programmatic voice configuration from Python functions.
The PolyAI platform supports flexible voice selection for external providers such as ElevenLabs, Cartesia, Rime, PlayHT, Minimax, Hume, and Google TTS.
## Provider classes
When picking models, adjusting stability, or accessing third-party providers – use provider-specific TTSVoice classes.
### Example: ElevenLabs
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from polyai.voice import ElevenLabsVoice
conv.set_voice(
ElevenLabsVoice(
provider_voice_id="gDnGxUcsitTxRiGHr904",
model_id="eleven_turbo_v2_5",
stability=1.0, # Recommended starting point (Robust); eleven_v3 only supports 0.0, 0.5, 1.0
similarity_boost=0.7,
speed=1.0, # Optional: 0.7–1.2, adjusts speech rate
)
)
```
Available ElevenLabs model IDs: `eleven_monolingual_v1`, `eleven_multilingual_v1`, `eleven_turbo_v2`, `eleven_turbo_v2_5`, `eleven_flash_v2_5`, and `eleven_v3`. The default is `eleven_turbo_v2_5`. See [ElevenLabs](https://elevenlabs.io/docs) for details on each model.
**`eleven_v3` limitations:**
* **Stability:** The `eleven_v3` model only supports discrete `stability` values: `0.0` (Creative), `0.5` (Natural), and `1.0` (Robust). Values between these are not supported and may produce unexpected results. This differs from earlier models where `stability` accepts a continuous range.
* **Streaming latency:** Do not set `optimize_streaming_latency` when using `eleven_v3` – this parameter is not supported by the v3 model and will cause an error.
### Example: Cartesia
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from polyai.voice import CartesiaVoice, Emotion, EmotionKind, EmotionIntensity
conv.set_voice(
CartesiaVoice(
provider_voice_id="a1b2c3d4",
speed=0.0, # -1.0 (slowest) to 1.0 (fastest)
emotions=[
Emotion(EmotionKind.POSITIVITY, EmotionIntensity.HIGH)
],
model_id="sonic-3" # or "sonic-3.5", "sonic-preview", or any Cartesia-compatible identifier e.g. "sonic-3-2025-10-27"
)
)
```
Some Cartesia voices are faster than expected at the default speed. Test your chosen voice at `speed=0.0` before deploying, and adjust toward `-1.0` if the output is too fast.
**Emotion options (legacy models):**
* `EmotionKind`: `ANGER`, `POSITIVITY`, `SURPRISE`
* `EmotionIntensity`: `LOWEST`, `LOW`, `HIGH`, `HIGHEST`
**Sonic 3 and Sonic 3.5 parameters**: When using a Sonic 3 or Sonic 3.5 model ID, the following additional parameters are supported:
* `volume` (float, optional) – controls output volume (e.g. 0.5–2.0).
* `emotion` (str, optional) – emotion string (e.g. `"happy"`).
* `language` (str, optional) – language code (e.g. `"en"`).
### Example: Rime
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from polyai.voice import RimeVoice
conv.set_voice(
RimeVoice(
provider_voice_id="voice_id",
speech_alpha=1.0, # <1.0 faster, >1.0 slower
model_id="mistv2" # or "mist"
)
)
```
### Example: Minimax
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from polyai.voice import MinimaxVoice
conv.set_voice(
MinimaxVoice(
model_id="speech-02-hd", # or speech-02-turbo, speech-01-hd, speech-01-turbo
voice_id="voice_id",
speed=1.0, # 0.5-2.0
vol=1.0, # 0-10
pitch=0, # -12 to 12
emotion="happy" # happy, sad, angry, fearful, disgusted, surprised, neutral
)
)
```
### Example: Hume
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from polyai.voice import HumeVoice
conv.set_voice(
HumeVoice(
provider_voice_id="voice_uuid_or_name",
voice_description="patient, empathetic counselor", # Optional
version="2", # "1" for octave-1, "2" for octave-2
instant_mode=False, # Ultra-low latency mode
provider="HUME_AI" # "CUSTOM_VOICE" or "HUME_AI"
)
)
```
### Example: Google TTS
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from polyai.voice import GoogleVoice
conv.set_voice(
GoogleVoice(
provider_voice_id="ja-JP-Neural2-B",
gender="male" # "male", "female", or "neutral"
)
)
```
### Example: Custom provider
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from polyai.voice import CustomVoice
conv.set_voice(
CustomVoice(
provider="MY_PROVIDER",
provider_voice_id="voice_id",
custom_param="value" # Any additional kwargs
)
)
```
## Voice randomization
Use `VoiceWeighting` to randomly select a voice based on weighted probabilities:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from polyai.voice import VoiceWeighting, ElevenLabsVoice
conv.randomize_voice([
VoiceWeighting(
voice=ElevenLabsVoice(provider_voice_id="voice1"),
weight=0.7
),
VoiceWeighting(
voice=ElevenLabsVoice(provider_voice_id="voice2"),
weight=0.3
),
])
```
* Weights must sum to 1.0.
* Voices without explicit weights share the remaining probability equally.
## Cache behavior
* Changing `model_id` does not automatically invalidate cached audio.
* To reset [cached audio](/voice-channel/audio-library):
* Go to **Voice > Audio library** and delete existing cache entries.
* Or, create a new voice entry with a different voice ID.
Prepend the model ID to the voice ID (e.g. `eleven_turbo_v2_5/a1b2c3...`) to isolate cache entries per model. This is the most reliable way to ensure the correct model is used after a switch.
## Language codes
When configuring a voice, make sure the language code in the `provider_voice_id` matches your deployment's locale. An incorrect language code (e.g. `en-GB` instead of `en-IE`) can cause the TTS provider to render a different accent or voice than expected, even when the correct voice ID is set.
## Additional options
* **stability** – controls tone variability across runs (ElevenLabs).
* **speed** – adjusts speech rate (ElevenLabs: `0.7`–`1.2`; PlayHT: `0.1`–`5.0`; other providers may differ).
* **randomize\_voice()** – supports external providers for weighted selection.
# Delay control
Source: https://docs.poly.ai/tools/delay-control
Play interim filler phrases while functions execute to keep callers engaged and avoid silence.
The **Delay control** panel plays transition utterances while a function is processing, preventing long silences and making interactions feel more natural.
## Key benefits
* **Keep users engaged:** Provide real-time feedback rather than leaving users waiting in silence.
* **Fully configurable timing:** Control when and how often delay responses play.
* **Supports multiple utterances:** Define a sequence of responses to use as interim messages.
* **Works across different function types:** Available for global and flow functions (not supported on start or end functions).
## Important timing behavior
Delay timing does **not** begin from when the user stops speaking. It starts when the **function begins executing** – which can happen seconds later due to LLM, ASR, or model routing latency. This means that if you set a delay of `1s`, filler utterances may not begin until several seconds after silence, depending on system load and model timing.
When setting delays, consider the **full turn latency** – not just the time your function takes to respond. For LLM-heavy flows, use shorter delays like `0–0.5s` or include immediate filler lines directly in the step prompt.
## Function timeout
If no delay responses are configured, the function times out after **10 seconds** by default. You can adjust this using the **Timeout after** field in the delay control panel to give longer-running functions more time to complete.
If a function exceeds the configured timeout, execution is terminated. Make sure the timeout value accounts for your function's expected response time under typical load.
## What happens if the caller speaks during delay playback
Delay scripts play on a fixed timer that starts when the function begins executing. If the caller speaks while delay responses are playing, the behavior depends on your [barge-in](/voice-channel/audio-library#barge-in) configuration:
* **Barge-in enabled:** The agent stops playing the current delay response and listens to the caller. The remaining delay scripts in the queue are discarded. When the function completes, the agent responds to whatever the caller said rather than resuming the delay sequence.
* **Barge-in disabled:** Delay responses continue playing on schedule regardless of caller speech. The caller's input is still captured by ASR but is only processed after the current agent utterance finishes.
Barge-in behavior cannot be fully tested in the chat panel — use a real phone call to verify how your delay responses interact with caller interruptions. See [Advanced voice settings](/voice-channel/advanced/call-settings#barge-in) for barge-in configuration.
### Functions with side effects
If your function performs external actions (booking an appointment, submitting a form, processing a payment), be aware that barge-in during delay playback can cause the caller to interrupt *after* the function has already executed. The caller may not hear the confirmation, but the action still completes.
For functions with important side effects, consider:
* Disabling barge-in on the specific flow or step using [flow overrides](/voice-channel/audio-library#barge-in)
* Returning a deterministic `utterance` in the function response to ensure the confirmation is spoken
## Delay control vs. filler utterances
Delay control and global filler utterances are two distinct systems that can run **simultaneously**. Understanding the difference is important to avoid unexpected interleaving of utterances during a call.
| | Delay control | Filler utterances |
| ------------ | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
| **Scope** | Function-scoped – only applies to the specific function where configured | Turn-scoped – catches latency anywhere in the model turn |
| **Trigger** | Fires based on the delay timing set on an individual function | Fires based on global filler utterance configuration |
| **Coverage** | Handles latency during a single function execution | Covers latency across the full turn, including multiple consecutive function executions |
Delay control and filler utterances are not mutually exclusive — if both are configured, both fire independently and may interleave. Check both if you see overlapping utterances on calls.
## How it works
1. **Define delay responses**
* Create a list of phrases the agent can say while waiting for a function to complete.
* These might include confirmations or status updates.
2. **Configure delay timing**
* Set the initial delay (in seconds) after function execution begins.
* Define the interval between each subsequent utterance.
3. **Specify utterance length**
* For sound-based responses (e.g., typing noises), specify the expected duration to pace playback accurately.
## Example scenario
An appointment booking function takes several seconds to confirm availability:
1. The user asks for an appointment.
2. The agent immediately says: *"Let me check availability for you."*
3. After 0.5s, it plays: *"Just a moment, I'm still checking..."*
4. After another 2s, it plays: *"Thanks for waiting!"*
5. Once complete, the agent returns: *"Your appointment is booked!"*
## Creating a new delay control phrase
* Go to **Tools** and select a function.
* Open the **Delay control** panel.
* Add one or more delay responses.
* Set the initial delay and interval.
* Optionally, specify the length of sound-based utterances.
* Save changes and test with an actual phone call.
The chat panel may not accurately replicate delay behavior. Since delay control is designed for voice/phone call latency, always test with a real call to verify timing and utterance ordering.
You can reference state variables inside delay responses using the `$` symbol. For example, `Still checking availability at $branch_name...` – the agent substitutes the value automatically.
For **multilingual agents**, avoid hardcoding delay utterances in a single language. Use state variables like `$DELAY_CHECKING` so the utterance resolves to the correct language at runtime. Hardcoded English text will be spoken in English even on non-English calls.
## Best practices
* Keep filler utterances brief and natural – phrases like "Still working on it..." or "Bear with me one moment" help maintain trust.
* Avoid overly repetitive or robotic phrasing.
* Use delay control sparingly: if your function is fast (under 1 second), it likely does not need filler responses. Adding delay control to near-instant functions can cause over-triggering, where utterances play unnecessarily.
* To prevent over-triggering on functions with variable latency, increase `interval_sec` or set a higher `initial_interval_sec` so utterances only fire when the function is genuinely slow.
* For very slow flows, consider adding initial speech directly in the step prompt and letting the delay control handle the fallback.
* Custom sound effects (e.g., typing sounds) require additional setup. Contact your PolyAI representative to enable custom audio for your project.
Delay control works best for **API-based functions** and **LLM utility functions** with irregular latency. It's **not** suitable for conversational steps where the function is near-instant or where multiple branches could be chosen. For near-instant functions, delay utterances fire unnecessarily – causing a worse user experience than silence.
## Timeline diagram
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant User
participant ASR
participant LLM
participant Function
participant Agent
User->>ASR: User speaks
ASR->>LLM: Transcript sent
LLM->>Function: Calls function (start timer here)
Note right of Function: Delay timer starts not at user silence
Function-->>Agent: Response pending...
Note right of Agent: Delay response triggered after set time
Function-->>Agent: Final result
Agent-->>User: Sends final reply
```
## Related pages
Full reference for return types, including utterance.
Configure interaction style and barge-in settings.
Delay control is not supported on start functions – plan accordingly.
# End tool
Source: https://docs.poly.ai/tools/end-tool
Run post-call processing asynchronously after a conversation ends.
**This page requires Python familiarity.** The end tool is a Python function named `end_function` that runs after every call.
The **End tool** (Python identifier: `end_function`) runs at the end of a conversation for final data processing, cleanup, and integration tasks. Because it runs asynchronously after the call has ended, you can perform complex operations – API calls, summaries, CRM writes – without affecting the caller experience.
`end_function` runs on every completed call. If it writes to external systems (CRM updates, ticket creation, data exports), errors or bugs can silently corrupt downstream data across all calls. Test changes in Sandbox before promoting.
Everything stored on `conv.state` during the call is fully available in `end_function`. This is the core mechanism that makes it useful: any data your tools collected or computed mid-call can be read, transformed, and sent to external systems after the conversation is over.
## Access the end tool in the UI
The end tool is a built-in tool that ships with every agent – you don't create it from scratch. Find it under **Tools**, in the **Start and end tool calls** section at the top of the page. This section contains two pre-built cards (**Start tool** and **End tool**) that are automatically invoked before and after every conversation.
To edit the end tool:
1. From your agent's main page, open **Tools** in the sidebar.
2. In the **Start and end tool calls** section, click **End tool**.
3. Write your Python in the **Function definition** field. The function is already named `end_function` and receives a single `conv: Conversation` argument – do not rename it.
4. Save your changes. The function runs at the end of every conversation in that environment from the next call onwards.
Unlike LLM-invoked tools, the end tool has no LLM parameters, description, or trigger configuration – it is invoked by the runtime, not the model. See [Create tool](/tools/how-to-setup) for the editor walkthrough used by regular LLM tools.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant Caller
participant PolyAI as PolyAI Agent
participant Ext as External Systems / APIs
Caller->>PolyAI: Conversation ends
PolyAI-->>Caller: Call terminated
activate PolyAI
Note over PolyAI: end_function() runs async
PolyAI->>Ext: Log conversation metadata
PolyAI->>Ext: Create support ticket (if needed)
PolyAI->>Ext: Schedule follow-up (if needed)
deactivate PolyAI
Note over PolyAI,Ext: Post-call processing complete
```
## Key features and functionality
1. **Asynchronous execution**: Runs after the conversation ends, so it does not delay the call.
2. **Post-conversation data handling**: Captures and processes important details from the conversation for reporting, logging, or integration.
3. **External integrations**: Call APIs, create tickets, or trigger follow-up workflows from the end tool.
## Use cases
The end tool:
### 1. Generates a structured call summary
The most common end-tool pattern is generating a structured summary from `conv.state` at the end of the conversation. This is especially valuable for voice agents where human agents need a written record of what was discussed.
* **Example use case**: Compile caller intent, topics discussed, and resolution status into a summary and push it to your CRM or ticketing system.
For LLM-based extraction of categorized reasons, sentiment, and other transcript-wide fields, call [`prompt_llm`](/tools/classes/conv-utils#prompt_llm) on the transcript.
### 2. Logs conversation metadata
* Save key conversation details, such as duration, topic, or sentiment analysis, to a database or CRM.
* **Example use case**: Track customer service interactions for reporting and performance analysis.
### 3. Triggers workflows
* Start processes like creating support tickets, sending confirmation emails, or updating account records.
* **Example use case**: Automatically notify the sales team about potential leads from the conversation.
### 4. Schedules follow-ups
* Prepare reminders, SMS notifications, or callbacks for unresolved queries.
* **Example use case**: Send a confirmation SMS after booking an appointment or a callback request.
### 5. Logs outbound call dispositions
The end tool is not limited to inbound voice. Outbound agents use it to log call dispositions, update lead records, and trigger follow-up sequences after each call.
* **Example use case**: After an outbound sales call, update the lead status in your CRM and queue a follow-up email if the prospect requested more information.
## Implementation example
Below is a Python implementation of the end tool. The function must be named `end_function`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def end_function(conv: Conversation):
try:
# Generate a structured call summary from conv.state
summary = {
"conversation_id": conv.id,
"topic": conv.state.get("last_topic", "Unknown"),
"sentiment": conv.state.get("sentiment", "Neutral"),
"resolution": conv.state.get("resolution_status", "Unresolved"),
}
log_to_crm(summary)
# Trigger external API
if conv.state.get("support_needed"):
create_support_ticket(conv.state.get("user_id"), summary)
# Schedule follow-up if necessary
if conv.state.get("follow_up_required"):
schedule_follow_up(
conv.state.get("user_id"),
conv.state.get("follow_up_time"),
)
except Exception as e:
# Errors are silent to the caller – log externally so they aren't lost
log_error_to_monitoring_service(e)
```
`end_function`'s return value is not used by the runtime. You do not need to return anything.
## Best practices for end-tool design
Errors in `end_function` can't surface to the caller but can silently break downstream workflows. Wrap logic in try/except, log externally, and test in Sandbox before production.
1. **Efficient execution**:
* Design `end_function` to complete quickly. Parallelize independent API calls where possible, avoid unnecessary data fetching, and keep heavy computation outside the critical path.
2. **Error handling**:
* Wrap the `end_function` body in a `try/except` block and log errors to an external monitoring service. Without this, failures are completely invisible.
3. **Data consistency**:
* Validate and sanitize data collected during the conversation before processing or logging it.
4. **Relevance**:
* Include only necessary post-conversation tasks to maintain efficiency and focus.
## Examples: Enhancing post-conversation workflows
### Data logging for analytics
Capture details like customer sentiment, topics discussed, and the resolution status for reporting and analytics.
**Example:**
* "Logged: Customer expressed interest in our premium plan and showed positive sentiment."
### Automatic follow-ups
Send reminders, confirmation messages, or escalation notices to keep the customer informed.
**Example:**
* "An email has been sent confirming your booking for January 10th."
### Task automation
Trigger external workflows or integrations.
**Example:**
* "Support ticket created: Issue with account login noted during the conversation."
### CRM updates
Ensure customer records are up to date with the latest interaction details.
## Related pages
Initialize conversation context before the greeting plays.
Trigger post-call satisfaction surveys from the end tool.
Access conv.state and other data in end\_function.
# Create tool
Source: https://docs.poly.ai/tools/how-to-setup
Write Python functions with parameters and return values to integrate APIs and business logic.
**This page requires Python familiarity.** Functions are written in Python and run inside your agent.
Create a function in **Tools**.
## Step-by-step guide
Set up a new function to integrate APIs, validate input, or add custom business logic to your agent.
### 1. **Find "Tools" in the sidebar**
* Go to the agent's main page and navigate to **Tools** in the sidebar.
### 2. **Click "Add Function"**
* Select **Add Function** to create a new function.
### 3. **Define the function name**
* At the top of the page, define the function name. Note that the function name can only contain alphanumeric characters and underscores.
Function names are processed by the LLM like any other text, so descriptive accuracy is key. Name functions to explicitly describe what they do and avoid "start" and "stop" language. For example, changing `start_package_upgrade` to `get_available_packages` prevents the LLM from assuming an unnecessary process is starting.
### 4. **Provide a description**
* Use the "Description" field to provide an accurate summary of what your function does. This helps the model understand when to call the function. Be descriptive and concise about its purpose.
### 5. **Define LLM parameters**
* In the "LLM Parameters" field, specify the parameters the LLM model will collect and use in the function.
* **Name**: Assign a clear and descriptive name to improve the accuracy of the LLM result.
* **Context Description**: Provide essential context to help the LLM accurately understand and extract the parameter from the caller.
* **Type**: Specify the parameter type. Options include "string," "number," "integer," or "boolean." Note that "number" supports decimals, while "integer" does not.
### 6. **Define the function**
* Use the "Function Definition" field to write the function in Python. You can retrieve secrets in your Python definition – see [Secrets](/secrets/introduction) for details.
* The function should return a string or dictionary. A string provides additional context for the LLM. A dictionary can control agent behavior (handoff, hangup, listen, etc.). See [Return values](/tools/return-values) for the full list of supported return types.
### 7. **Save the function**
* Click "Save" to finalize and create your function.
## Best practices
### Environment configuration
You can use the [`conv.env`](./classes/conv-object#env) property to define environment-specific functions and activate test features in sandbox or pre-release environments.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if conv.env == "sandbox":
# enable early feedback flow
pass
elif conv.env == "pre-release":
# activate staging tools
pass
elif conv.env == "live":
# run production features
pass
```
### Handling API response errors
When writing functions that call external APIs (e.g. to fetch user records or transactions), always handle non-`200` HTTP responses explicitly.
If `response.status_code != 200`, make sure to distinguish:
* A **true error** (e.g. invalid URL or broken integration)
* From a **valid request with no user data** (e.g. user has no transaction history)
Instead of returning a generic failure, you can hand off the call or give a clearer response depending on the cause.
Example:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
response = requests.get(url)
if response.status_code == 404:
return {
"handoff": {
"reason": "no_transaction_data",
"utterance": "It looks like there's no recent activity on your account. Let me transfer you to someone who can help."
}
}
elif response.status_code != 200:
return {
"utterance": "Sorry, we couldn't reach your account information right now. Please try again later."
}
```
## Related pages
Standard and non-standard libraries available in functions.
Control agent behavior with string and dictionary returns.
Full reference for conv.state, conv.log, and other properties.
# Libraries
Source: https://docs.poly.ai/tools/import-library
Standard library and pre-installed packages available to functions, including requests, jsonschema, and urllib3.
**Python required.** Functions run in a managed sandbox with the standard library plus the packages listed below — no additional installs. Contact your PolyAI representative if you need a library that isn't listed.
## Execution environment
Functions run in a **sandboxed cloud environment**. This means:
* **No local file system access** – you cannot read or write files from your machine (e.g., CSV files, text files, or databases stored locally).
* **No package installation** – only the standard library and the pre-installed packages listed below are available.
* **Network access** – you can make outbound HTTP requests using the `requests` library to call external APIs or fetch hosted data.
If you need to use data from a file, host it externally (e.g., on a web server or cloud storage) and fetch it at runtime using `requests`.
## Standard library
Within functions you have access to the full [Python standard library](https://docs.python.org/3/library/index.html), including modules like `datetime`, `json`, `re`, `urllib`, and `hashlib`.
## Notable language features
The runtime supports modern Python language features, including:
* **Structural pattern matching** (`match`/`case`, Python 3.10+) – a cleaner alternative to long `if`/`elif` chains for branching on values or structures. See the [Python docs on match statements](https://docs.python.org/3/tutorial/controlflow.html#match-statements).
* **Parenthesized context managers** (Python 3.10+) – you can use parentheses to split multiple context managers across lines:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
with (
open("input.json") as infile,
open("output.json", "w") as outfile,
):
...
```
* **Exception Groups and `except*`** (Python 3.11+) – handle multiple exceptions raised concurrently. Useful when your function makes parallel API calls. See [PEP 654](https://peps.python.org/pep-0654/).
## Pre-installed packages
The following packages are pre-installed and available for `import`:
| Library | Description |
| ---------------------------------------------------------------------- | ------------------------------------ |
| [requests](https://requests.readthedocs.io/en/stable/user/quickstart/) | HTTP library for making API requests |
| [urllib3](https://urllib3.readthedocs.io/en/stable/) | Low-level HTTP client |
| [jsonschema](https://python-jsonschema.readthedocs.io/en/stable/) | JSON schema validation |
The `requests` library is the recommended way to make external API calls from your functions. See the [requests quickstart guide](https://requests.readthedocs.io/en/stable/user/quickstart/) for usage examples.
If you encounter unexpected behavior with a library, contact your PolyAI representative.
# Tools
Source: https://docs.poly.ai/tools/introduction
Add custom logic and API integrations using Python functions.
**This section requires Python familiarity.** If you are a non-technical operator, share this page with your developer. For no-code alternatives, see [FAQs](/knowledge/faqs/introduction) (actions) or [no-code flows](/flows/no-code/introduction).
Functions let your agent perform actions during a conversation — looking up a booking, calling an API, validating input, or writing to a CRM.
If you are looking for a function that only exists inside a single [flow](/flows/introduction), visit the [transition functions](/flows/transition-functions) page.
Functions are Python scripts for deterministic logic and API integrations. Use [`conv.log`](./classes/conv-log) to write logs visible in **[Conversation review](/analytics/conversations/review) → [Diagnosis](/analytics/conversations/diagnosis)**.
For detailed guides, explore the subpages below:
## Guides
Set up a function with naming conventions, parameters, and Python code
Standard and non-standard libraries available in your functions
Initialize conversation context before the greeting plays
Run post-call processing after a conversation ends
Control agent behavior with string and dictionary returns
Define, update, and persist values across turns
Integrate functions into FAQs
Add filler phrases to avoid silence during slow functions
## Reference
Conversation states, flows, and telephony attributes
Structured diagnostics and PII-scoped logging
Built-in helpers for addresses, cities, and structured data
Call configured API integrations
Dynamic speech recognition biasing from functions
Access conversation turn history
Configure TTS voices programmatically
VoiceWeighting, TTSVoice, and provider classes
Persistent data across conversations for repeat callers
If you are looking to use functions to enable multi-site configuration – for example, handling enquiries for multiple store or branch locations – visit the [Variants](/knowledge/variants/introduction) feature page.
## Best practices
* **Use descriptive names** – Action-oriented names like `book_reservation` or `send_notification` help the LLM understand when to call the function.
* **Write clear descriptions** – Explain what the function does, its parameters, and when it should be triggered.
* **Use meaningful parameter names** – `reservation_date` is clearer than `r_date`.
* **Control triggering** – Define specific [rules](/behavior/general/rules) to prevent over-triggering or under-triggering.
* **Test thoroughly** – Use [Test Cases and Test Sets](/testing/simulation-tests) to validate function behavior across scenarios.
## Troubleshooting
* **Function not triggering** – Check **Conversation review → Diagnosis** to see if the function was considered. Refine your function description or add clearer rules.
* **Wrong parameters** – Review `conv.log` entries to see what the LLM extracted. Adjust parameter descriptions or add validation.
* **Unexpected behavior** – Simplify function logic and test in isolation before integrating with the agent.
# Return values
Source: https://docs.poly.ai/tools/return-values
Control agent behavior with strings, utterances, handoffs, and other return types from functions.
**This page requires Python familiarity.** It covers the return interface for functions written in Python.
Return a string or dictionary from your function to control the agent's next action, including what to say, whether to transfer the call, or how to configure listening behavior.
### String return
You can return a simple string, which will be used as the system prompt for the virtual agent:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return "Tell the user that you cannot assist with their request"
```
### Dictionary return
You can return a dictionary to specify more detailed and deterministic instructions. The following fields can be used individually or in combination.
#### `content`
Equivalent to returning a string, this field specifies the system prompt:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"content": "Tell the user that you cannot assist with their request"
}
```
#### `utterance`
Specifies the exact phrase the virtual agent will deliver after executing the function (spoken for voice, displayed for webchat):
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"utterance": "I cannot assist with your request."
}
```
**Note**: If both `content` and `utterance` are returned:
* The agent will stream the `utterance` to the user and end the turn.
* The `content` rules will apply to the next turn.
#### `utterance_channel`
Controls how the `utterance` is delivered. Defaults to `"SPEECH"`, which speaks the utterance through TTS. Set to `"DTMF"` to transmit the utterance as DTMF keypad tones instead of speech – useful when the agent needs to send digits to an IVR or other automated system during [outbound calling](/voice-channel/numbers/outbound-calling).
The `utterance` string must contain the digits to send (`0`–`9`, `*`, `#`). The agent does not speak them.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"utterance": "1234#",
"utterance_channel": "DTMF"
}
```
`utterance_channel` only takes effect when `utterance` is also set. The call must be running on a telephony channel that supports DTMF output – it is not available in webchat or with the realtime model.
#### `handoff`
Initiates a call handoff after the function executes. The `type` and `reason` fields match your configured [handoff destination](/voice-channel/handoffs). The nested object specifies the SIP method.
Transfers the call and drops PolyAI from it:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"handoff": {
"type": "CALL_CENTER",
"reason": "SPEAK_TO",
"refer": {
"phone_number": "12345"
}
}
}
```
Creates a bridged call where PolyAI stays on the line between both parties:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"handoff": {
"type": "CALL_CENTER",
"reason": "SPEAK_TO",
"invite": {
"phone_number": "2222222",
"outbound_caller_id": conv.caller_number,
"outbound_endpoint": "",
"sip_headers": {
"X-ANY_HEADER_NAME": "header"
}
}
}
}
```
Signals that the PolyAI call leg is over, returning control to the client's SBC:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"handoff": {
"type": "CALL_CENTER",
"reason": "CONTAINED",
"bye": {
"sip_headers": {
"X-Outcome": "contained"
}
}
}
}
```
You can also trigger handoffs using [`conv.call_handoff()`](/tools/classes/conv-object), which supports dynamic `destination`, `reason`, and `utterance` parameters. Use the dictionary return for fine-grained SIP control; use `conv.call_handoff()` for simpler routing.
#### `hangup`
Ends the conversation after the function executes. For voice, this disconnects the call; for webchat, this closes the session:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"hangup": True
}
```
#### `listen`
Configures the agent to listen on the next turn. Must be combined with an `utterance` for ASR timeout settings to take effect.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"utterance": "Can I help you with anything else?",
"listen": {
"asr": {
"timeout": 20
}
}
}
```
The `listen` object supports these configuration keys:
| Key | Type | Description |
| ------------------ | ---- | ---------------------------------------------------------------------------------------------------------------------------- |
| `listen` | bool | Whether to listen for input (default `True`) |
| `delayed_response` | bool | Enable delayed response mode – the client sends an empty input to retrieve the response |
| `asr` | dict | ASR configuration: `timeout`, `keywords`, `custom_biases`, `fields`, `corrections` |
| `dtmf` | dict | DTMF configuration: `num_digits`, `first_digit_timeout`, `inter_digit_timeout`, `finish_on_key`, `early_listening`, `is_pii` |
| `channel` | str | Input channel: `"SPEECH"` (default), `"DTMF"`, or `"SPEECH_AND_DTMF"` |
| `barge_in` | dict | Barge-in configuration: `is_enabled`, `interruption_window` |
| `smart_vad` | dict | Smart VAD configuration: `is_enabled`, `max_extensions`, `max_extension_duration` |
| `interjection` | dict | Interjection configuration: `enable`, `frequency` (seconds) |
**Example with DTMF:**
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"utterance": "Please enter your 6-digit code followed by the hash key.",
"listen": {
"channel": "SPEECH_AND_DTMF",
"dtmf": {
"num_digits": 6,
"finish_on_key": "#",
"first_digit_timeout": 5,
"inter_digit_timeout": 2,
"is_pii": True
}
}
}
```
ASR timeout doesn't apply to DTMF-only input. If using both channels, make sure you include an `utterance` so the ASR settings take effect.
#### `variant`
Switches the conversation to a different variant. The value must match an existing variant name configured in [Variants](/knowledge/variants/introduction). Use this to route callers to location-specific or segment-specific conversation flows mid-call.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"variant": "orleans"
}
```
You can combine `variant` with `utterance` to confirm the switch to the user:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"utterance": "Let me connect you with the Orleans location.",
"variant": "orleans"
}
```
## Combining return fields
You can return multiple fields together. Common combinations:
Say something before transferring – the most common handoff pattern:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"utterance": "Let me transfer you to a specialist who can help with that.",
"handoff": {
"type": "CALL_CENTER",
"reason": "SPEAK_TO",
"refer": { "phone_number": "12345" }
}
}
```
Say a closing message before ending the call:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"utterance": "Thanks for calling, have a great day!",
"hangup": True
}
```
Deliver a specific phrase now and set instructions for the next turn:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"utterance": "I've updated your reservation.",
"content": "The reservation has been changed. Ask if there's anything else you can help with."
}
```
## Related pages
Return values supported in the start function.
Add filler phrases while long-running functions execute.
Return values in the context of flow step routing.
# Start tool
Source: https://docs.poly.ai/tools/start-tool
Initialize conversation state, read SIP headers, and set variant routing before the greeting plays.
**This page requires Python familiarity.** The start tool is a Python function named `start_function` that runs before every call's greeting.
The **Start tool** (Python identifier: `start_function`) runs when a conversation begins, before the greeting plays. Use it to initialize conversation state, read SIP headers, or make API calls. A broken or slow `start_function` delays or prevents the greeting from playing, affecting every inbound call.
## Access the start tool in the UI
The start tool is a built-in tool that ships with every agent – you don't create it from scratch. Find it under **Tools**, in the **Start and end tool calls** section at the top of the page. This section contains two pre-built cards (**Start tool** and **End tool**) that are automatically invoked before and after every conversation.
To edit the start tool:
1. From your agent's main page, open **Tools** in the sidebar.
2. In the **Start and end tool calls** section, click **Start tool**.
3. Write your Python in the **Function definition** field. The function is already named `start_function` and receives a single `conv: Conversation` argument – do not rename it.
4. Save your changes. The function runs at the start of every conversation in that environment from the next call onwards.
Unlike LLM-invoked tools, the start tool has no LLM parameters, description, or trigger configuration – it is invoked by the runtime before the greeting plays. See [Create tool](/tools/how-to-setup) for the editor walkthrough used by regular LLM tools.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant Caller
participant PolyAI as PolyAI Agent
participant API as External API
Caller->>PolyAI: Inbound call
activate PolyAI
Note over PolyAI: start_function() executes
PolyAI->>PolyAI: Read SIP headers
PolyAI->>PolyAI: Initialize state variables
PolyAI->>API: Fetch customer data (optional)
API-->>PolyAI: Customer record
deactivate PolyAI
PolyAI->>Caller: Greeting plays
Note over Caller,PolyAI: Conversation begins
```
`start_function` is **synchronous** — it must complete before the greeting plays, so keep it fast. On timeout the greeting may not play and the conversation can land in an unexpected state.
**Outbound:** if the recipient hangs up before `start_function` completes, it may be skipped entirely. Handle this gracefully.
[Delay control](/tools/delay-control) (filler utterances) is **not supported** on the start tool. If `start_function` has variable latency, the only mitigation is to keep it fast or move slow operations into a flow step.
### When to use the start tool vs. a flow step
Not every API call belongs in the start tool. Use this decision framework:
| Scenario | Where to place it |
| ------------------------------------------------------------------------------------ | -------------------------------------------------------------- |
| Data needed **before** the greeting (e.g. caller lookup for a personalized greeting) | Start tool |
| Fast, reliable API (\<1s response time) | Start tool – reduces mid-conversation latency |
| Slow or unreliable API (external CRM, third-party lookup) | First [flow step](/flows/introduction) – protects the greeting |
| Data only needed mid-conversation | Flow step |
If an API call is not strictly needed before the greeting, move it to the first step of your flow to avoid timeout risk.
## Key features
* **Synchronous execution**: Completes before the greeting plays
* **Context preparation**: Stores data for use throughout the conversation
## Use cases
Use [`conv.channel_type`](/tools/classes/conv-object#channel_type) to determine whether the conversation is voice, webchat, or another channel – then branch accordingly. Disable call transfers for webchat, set a different persona, or inject channel-specific prompts.
Capture SIP headers (voice) or URL parameters (webchat) to determine the caller's origin – for example, mapping a dialled number to a business branch.
Initialize state with the current date, time, or day of the week for timestamping or scheduling logic.
Fetch external data such as user preferences, account information, or customer records to preload personalized context.
Use [`conv.set_variant()`](/tools/classes/conv-object#set_variant) to route the conversation to a specific [variant](/knowledge/variants/introduction) based on SIP headers, callee number, or other metadata. This is the standard way to configure multi-site agents.
Read a language header or parameter and configure the agent accordingly – set the variant, choose a language-specific TTS voice, or store language-specific prompt rules in state.
For outbound agents, read lead data or campaign metadata from SIP headers injected by the calling platform.
Access [`conv.integration_attributes`](/tools/classes/conv-object#integration_attributes) to read metadata passed from external integrations (e.g. DNIs pooling, Chat API).
`conv.integration_attributes` can only be read in `start_function`. Extract values and store them in `conv.state` for use later.
Set the TTS provider in `start_function`. Supported providers include [Cartesia](https://docs.cartesia.ai/api-reference/tts/tts), [PlayHT](https://docs.play.ht/reference/api-getting-started), and [Rime](https://docs.rime.ai/api-reference/voices). See [voice configuration](/voice-channel/advanced/call-settings) and [tool classes](/tools/classes).
## Implementation example
Below is a Python implementation of the start tool. The function must be named `start_function`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import datetime as dt
def start_function(conv: Conversation):
# Retrieve the current date and time
now = dt.datetime.now()
conv.state.current_date = now.strftime("%A %d-%m-%Y")
conv.state.current_weekday = now.strftime("%A")
conv.state.current_time = now.strftime("%H:%M")
# Initialize state variables
conv.state.available_times = None
conv.state.user_bookings = None
# Store the caller's phone number
conv.state.phone_number = conv.caller_number
# Detect channel type for multi-channel agents
conv.state.is_voice = conv.channel_type == "sip.polyai"
# Set variant based on dialled number (multi-site routing)
site_map = {
"+441234567890": "london",
"+442345678901": "new_york",
}
site = site_map.get(conv.callee_number, "default")
conv.set_variant(site)
# Store integration attributes (only available in start_function)
if conv.integration_attributes:
conv.state.shared_id = conv.integration_attributes.get("shared_id")
# Return an empty string to indicate successful execution
return str()
```
## Return values
The start tool supports the same [return values](/tools/return-values) as other tools. The most common patterns are:
### Empty string (default)
Return `str()` when `start_function` only needs to set up state. The agent greeting defined in the Agent settings will play as normal.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return str()
```
### Dynamic greeting
Return an `utterance` to override the default greeting with a dynamically generated message. This is useful when the greeting depends on data fetched during `start_function` (e.g. the caller's name or site-specific wording).
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"utterance": f"Welcome to {conv.variant.site_name}. How can I help you today?"
}
```
Returning an `utterance` from `start_function` overrides the configured channel greeting. Test carefully if you use both.
### Listen configuration
Return a `listen` object to configure ASR behavior for the first turn.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"utterance": "Welcome. Please say or enter your account number.",
"listen": {
"asr": {
"timeout": 15
}
}
}
```
See [return values](/tools/return-values) for the full list of supported return types.
## Best practices
1. **Keep it fast**: The start tool blocks the greeting. Target under 1 second total execution time. Move slow or unreliable API calls to a [flow step](/flows/introduction).
2. **Never hardcode credentials**: Use [`conv.utils.get_secret()`](/secrets/introduction) for all API keys, tokens, and passwords. Hardcoded credentials in tool code are a security risk and may be exposed in logs or version history.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
api_key = conv.utils.get_secret("my_api_key")
```
3. **Error handling**:
* Handle missing or malformed data and avoid runtime errors.
* Provide fallbacks for incomplete or invalid information (like missing SIP headers or unavailable APIs).
4. **State initialization**:
* Predefine and initialize all state variables needed for the conversation to avoid undefined behaviors. Reading an unset `conv.state` variable returns `None` (it does not raise), so initializing to `None` – or a sensible default like `""` or `0` – makes intent explicit and keeps [prompt templates](/tools/variables#prompt-templating) and `is None` checks predictable.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.state.booking_time = None # explicit "not yet collected" marker
conv.state.ooh_preface = "" # safe default for prompt templating
```
* Extract `conv.integration_attributes` here – they are only available in `start_function`.
5. **Contextual relevance**:
* Only include setup steps that are directly relevant to the conversation's purpose.
* Avoid overloading the start tool with unnecessary logic.
## Common patterns
The most common advanced use of the start tool is routing to a [variant](/knowledge/variants/introduction) based on the dialled number, SIP headers, or other metadata. This is how multi-site agents (hotels, restaurant chains, retail) determine which location's content to use.
**Hardcoded map** – use when the number-to-variant mapping is small and static:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start_function(conv: Conversation):
phone_numbers = {
"+441234567890": "London",
"+442345678901": "New York",
}
conv.set_variant(phone_numbers.get(conv.callee_number, "default"))
return str()
```
**Dynamic lookup from variant attributes** – use when you store the phone number (or any routing key) as an attribute in the variant table. For example, if your variant table has a `callee` column containing each variant's phone number:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start_function(conv: Conversation):
callee_map = {
variant.callee: variant_name
for variant_name, variant in conv.variants.items()
}
if conv.callee_number and conv.callee_number in callee_map:
conv.set_variant(callee_map[conv.callee_number])
return str()
```
Replace `variant.callee` with whatever attribute name you defined in **Knowledge > Variants** (e.g., `variant.phone_number`, `variant.routing_id`).
**Full article:** [Variants](/knowledge/variants/introduction)
Read a language code from SIP headers or integration data, set the appropriate variant, and configure a language-specific TTS voice.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start_function(conv: Conversation):
language = conv.sip_headers.get("X-Language", "en")
conv.set_variant(language)
if language == "es":
from polyai.voice import CartesiaVoice
conv.set_voice(CartesiaVoice(provider_voice_id="your-voice-id"))
return str()
```
**Force a language by dialled number (DNIS)** – use `conv.callee_number` and `conv.set_language()` when each market has its own phone number and you want to skip auto-detection. The language code must already be configured under **Behavior > Additional languages** (or be the main language); otherwise the agent falls back to the default language.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start_function(conv: Conversation):
spanish_dnis = {"+34911234567", "+34931234567"}
if conv.callee_number in spanish_dnis:
conv.set_language("es-ES")
else:
conv.set_language("en-US")
return str()
```
See [Multi-language](/behavior/language/multilingual) for the full setup.
Use `conv.channel_type` to disable voice-only features (like call transfers) for webchat, or to inject channel-specific prompts.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start_function(conv: Conversation):
conv.state.is_voice = conv.channel_type == "sip.polyai"
if not conv.state.is_voice:
conv.state.transfers_enabled = False
return str()
```
The start tool is where you configure which TTS voice the agent uses for a given call. This is required when running multiple voices across variants or channels, because the Voice page UI may not expose all available providers.
**Full article:** [Multi-voice](/voice-channel/multi-voice)
Use `conv.caller_number` or metadata from an API call to personalize the greeting or preload account context.
Make a fast API call to fetch scheduling information, past bookings, or account details so the agent has context from the first turn.
## Related pages
Run post-call processing after a conversation ends.
Control agent behavior with string and dictionary returns.
Route calls to specific variants from the start tool.
# Using tools
Source: https://docs.poly.ai/tools/using-tools-in-knowledge-base
Attach functions to FAQs actions to call APIs and perform logic when topics match.
Functions let your agent call APIs, look up data, or perform logic when a Managed Topic matches. This page shows how to add a function to a topic and test it.
## Adding a function
After creating and testing your function, integrate it into your agent's FAQs so it can be invoked during conversations.
You can add a function in three ways:
1. **Type `/`** in the Actions field of a Managed Topic card to open the function menu.
2. **Right-click** in the Actions field to access the function menu.
3. **Click the `+` icon** on the right side of the Actions field.
Each method lets you search for and select a function, or create a new one to populate later.
You can reuse the same function across multiple topics and actions.
Function references like `{"{{fn:order_lookup}}"}` are only valid in the **Actions** field of a Managed Topic. Placing them in the **Content** field will not trigger the function, and no error is shown. Always add function references in Actions.
The `{"{{fn:...}}"}` syntax references **global functions**, which can be used across topics, flows, and rules. This is different from **transition functions** (`{"{{ft:...}}"}` syntax), which are scoped to a single flow. See [Transition functions](/flows/transition-functions) for details.
## Best practices
Function invocation requires testing and iteration. Here is a recommended prompting pattern for the Actions field:
```plaintext theme={"theme":{"light":"github-light","dark":"github-dark"}}
When the user asks "where is my order," do not respond until you have called {{fn:order_lookup}} with an order number. If you don't have the order number, ask for it first, then call {{fn:order_lookup}}. Use the function's response to answer the caller.
```
The agent will always invoke the function before responding, and ground its answer in the function's output.
To control what the agent says or does after a function runs, use [return values](/tools/return-values). For example, you can return an exact `utterance` for the agent to speak, trigger a `handoff`, or end the call with `hangup`.
## Testing
Save your agent and click **Play** in the header to open the test chat panel.
Ask a test question like "Where is my order?" and observe how the agent handles the interaction.
In the test chat panel, enable the **tool calls** toggle in settings to inspect which functions were called and what parameters were passed. This is useful for confirming whether the function was actually triggered and whether the correct arguments were sent – especially when the agent is not behaving as expected.
## Related pages
Set up a function with naming conventions, parameters, and Python code.
Control agent behavior with function return types.
Configure actions that trigger alongside topic responses.
# Variables
Source: https://docs.poly.ai/tools/variables
Store and access data across conversation turns using the conv.state object in functions.
**This page requires Python familiarity.** Variables are set and read in Python functions.
Variables are defined by setting a property on the `conv.state` object within a function. You can choose a name for your variable and update its value to anything you want like so:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.state.my_variable = 1
conv.state.my_other_variable = {
"property": "value"
}
```
These variables will retain their state between turns of the conversation and can be referenced in subsequent tool calls like so:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if conv.state.my_variable == 10:
return "Well done you hit 10"
```
### Reading variables that haven't been set
Reading a `conv.state` variable that was never assigned returns Python `None` – it does **not** raise `AttributeError` or `KeyError`. Both attribute and dictionary access are supported and equivalent:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
conv.state.foo # None if never set
conv.state["foo"] # None if never set
conv.state.get("foo") # None if never set (with optional default)
```
This enables a few common patterns:
**Defensive defaults with `or`** – use when any falsy value should be replaced:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
attempts = conv.state.verification_attempts or 0
intent = conv.state.call_intent or "other"
```
Note: `or` cannot distinguish "never set" from a stored falsy value (`0`, `""`, `False`, `None`).
**Explicit unset check with `is None`** – use when the variable is intentionally initialized to `None` in the [start tool](/tools/start-tool) as a marker that no value has been collected yet:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start_function(conv: Conversation):
conv.state.booking_time = None # explicit "not yet collected" marker
conv.state.customer_id = None
# later, in a flow step
if conv.state.booking_time is None:
return "What time would you like to book?"
```
**Strict "key was assigned at all" check with `in`** – use when you must distinguish "never assigned" from a stored `None`:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if "booking_time" in conv.state:
...
```
Initializing variables to `None` (or a sensible default like `""` or `0`) in `start_function` is recommended whenever a variable is referenced in a [prompt template](#prompt-templating) or read in a flow step before it is guaranteed to be set, so template references and conditional checks always resolve predictably.
### Built-in state keys
The `conv.state` object comes pre-populated with system-managed keys. These are set automatically by the platform and available in every conversation:
| Key | Type | Description |
| ----------------------- | ----------- | --------------------------------------------------------------------------------------- |
| `from_` | str | Caller's phone number (note the trailing underscore – `from` is a Python reserved word) |
| `to` | str | Called number (callee) |
| `call_sid` | str | Unique call session identifier |
| `asr_lang_code` | str | Active ASR language code (e.g., `"en-US"`) |
| `tts_lang_code` | str | Active TTS language code (e.g., `"en-US"`) |
| `shared_id` | str | Shared identifier for correlating conversations across systems |
| `handoff` | object | Handoff configuration object (destination, reason, SIP method) |
| `disable_recordings` | bool | Whether call recording is disabled |
| `stop_recording` | bool | Whether recording has been stopped mid-call |
| `use_tts_for_responses` | bool | Whether to use TTS instead of pre-recorded audio |
| `asr_provider` | str or dict | Override the default ASR provider |
| `asr_config` | dict | Custom ASR configuration |
| `listen_for_sms` | bool | Whether SMS listening is enabled (default `False`) |
`handoff_reason` and `handoff_number` are deprecated. Use the `handoff` object instead, which supports structured SIP configurations (REFER, INVITE, BYE).
You can read these values in any function:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
log.info(f"Caller: {conv.state.from_}, Callee: {conv.state.to}")
log.info(f"ASR language: {conv.state.asr_lang_code}")
```
### Prompt templating
Variables can be used inside tool calls and injected dynamically into prompts shown to the LLM. To inject a variable's value
into your prompt, use the syntax `$variable_name`. The system will replace this placeholder with the variable's value wherever it matches.
**Example:**
In your start function, you can write:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from datetime import datetime
def start_function(conv: Conversation):
conv.state.current_date = datetime.now().strftime("%B %d, %Y")
```
**...then in your prompting:**
The current date is `$current_date`.
**...becomes:**
The current date is September 06, 2024.
When using variable templating, ensure the stored value is readable by the LLM. Complex objects like dictionaries or datetime
will be stringified automatically.
### Environment configuration
You can use the `conv.env` property to define environment-specific functions and activate test features in sandbox or
pre-release environments.
For example:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
if conv.env == "sandbox":
# enable early feedback flow
pass
elif conv.env == "pre-release":
# activate staging tools
pass
elif conv.env == "live":
# run production features
pass
```
### Dynamic updates
The value templated into the prompt is always kept up to date, so any updates will be reflected in the next turn sent to the LLM.
**Example:**
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from datetime import datetime
def set_fake_date(conv: Conversation):
conv.state.current_date = datetime(1995, 3, 22).date().strftime("%B %d, %Y")
```
After running this function, the resulting prompt would display:
The current date is March 22, 1995.
...in the next turn.
### Deleting a variable
To delete a variable, remove it from the state within a function:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
del conv.state['variable_name']
```
## Variables in the Conversations API
All `conv.state` variables – both built-in keys and custom variables your agent writes – are returned in the `state` field of the [Conversations API](/api-reference/conversations/introduction) response. This means you can retrieve any state variable programmatically after a call ends.
For details on the API response structure, see the [Conversations API overview](/api-reference/conversations/introduction#response-structure).
## Related pages
Full reference for conv.state and other conversation properties.
Initialize state variables before the greeting plays.
Use variables to pass data across flow steps.
# Frequently asked questions
Source: https://docs.poly.ai/troubleshoot/faq
Common questions and troubleshooting solutions for agent configuration, knowledge, rules, and actions.
Find answers to frequently asked questions about Knowledge (Managed Topics and Connected), rules, actions, and agent configuration. Start here if your agent isn't answering correctly, changes aren't live, or you need guidance on structuring topics and rules.
## Common issues
This is usually a **retrieval issue** – the agent has the right topic, but the retriever didn't match it to the user's message.
**How to debug:**
1. Open [conversation diagnosis](/analytics/conversations/diagnosis) for the affected conversation.
2. Check the **sources panel** – does the relevant topic appear in the retrieved results?
3. If the topic is **not retrieved**: improve the topic name and sample questions to better match how real users phrase the question.
4. If the topic **is retrieved** but the agent still gives a wrong answer: review the topic content for ambiguity, or check whether a conflicting topic is also being retrieved.
The retriever weights **topic name** and **sample questions** more heavily than content. If a topic isn't being found, rewriting these is the most effective fix.
**Saving a change does not make it live.** Changes go through a promotion pipeline (Draft → Sandbox → Pre-release → Live) before reaching production. Until you publish and promote, your changes stay in draft.
See the [deployment pipeline](/environments-and-versions/introduction) for full details, or the [environments FAQ](/troubleshoot/faq-environments) for common questions.
Tone and phrasing problems are best fixed through **specific, example-driven [global rules](/behavior/general/rules)** – not single-sentence personality instructions. Identify the problematic response in [conversation review](/analytics/conversations/review), add a rule with a concrete example of the correct behavior, and test in sandbox before promoting.
See the [rules FAQ](/troubleshoot/faq-rules) and [personality FAQ](/troubleshoot/faq-personality) for detailed guidance.
## Topic-specific FAQs
Topic structure, retrieval, sizing, and organization.
Differences from Managed Topics, when to use, and troubleshooting retrieval.
Behavioral rules, examples, channel filtering, and edge case handling.
Action types, combining actions, variables, and debugging failures.
Setting tone, personality, and role configuration.
Safe testing, deployment pipeline, and multi-site agents.
ASR/TTS independence, token limits, and context management.
***
Join the PolyAI community on Slack.
# Actions FAQ
Source: https://docs.poly.ai/troubleshoot/faq-actions
Common questions about configuring actions, combining them, using variables, and debugging failures.
Answers to questions about actions in Managed Topics including action types, combining multiple actions, using variables and state, and troubleshooting failures. See [Actions](/knowledge/faqs/actions/introduction) for full documentation.
Actions let the agent do things beyond responding with text. They are defined in Managed Topics under the **Actions field**. The main types are:
* **[SMS](/knowledge/faqs/actions/send-sms)**: Send a text message to the user (e.g., a link, confirmation, or follow-up details).
* **[Tool calls](/knowledge/faqs/actions/tool-call)**: Run a custom function to look up data, perform calculations, or update conversation state.
* **[Handoffs](/knowledge/faqs/actions/handoff)**: Transfer the user to a live agent. Handoffs have their own setup requirements, including logging reasons and configuring routing (e.g., SIP headers). See [call handoffs](/voice-channel/handoffs) for details.
**Example:** A user asks about a refund, and the agent sends an SMS with a link to the refund portal.
Yes. You can trigger multiple actions for a single topic – for example, sending an SMS and then initiating a handoff.
Yes. You can use [variables](/tools/variables) and state from [functions](/tools/introduction) to conditionally control what happens. For example, you can show different content or trigger different actions depending on whether the user is authenticated or which location they're calling from.
This is set up through functions that run earlier in the conversation. The state they set can then be referenced in your topic content and actions.
If an action does not work as expected:
1. Open the [conversation diagnosis](/analytics/conversations/diagnosis) tool for the affected conversation to see what happened.
2. Check that the action is correctly configured – correct function name, SMS template, or handoff destination.
3. For handoffs, verify that the target queue or SIP endpoint is reachable and correctly routed.
4. Reproduce the issue in [sandbox](/environments-and-versions/introduction) to test your fix before promoting.
Common causes: misconfigured function names, missing variables, and unreachable handoff endpoints.
# Connected knowledge FAQ
Source: https://docs.poly.ai/troubleshoot/faq-connected-knowledge
Learn when to use Connected Knowledge, how it differs from Managed Topics, and how to troubleshoot retrieval.
Answers to questions about Connected Knowledge including comparisons with Managed Topics, selection criteria, and troubleshooting retrieval. See [Connected Knowledge](/knowledge/sources/introduction) for full documentation.
Both **Connected** and **Managed Topics** live under the **Knowledge** area in Build, but they serve different purposes:
* **Connected knowledge** is a fast way to expose external content (websites, PDFs, Zendesk articles) to your agent. It is read-only, synced from external sources, and requires no prompting expertise. However, it **cannot** trigger actions, flows, SMS, or handoffs.
* **Managed Topics** are version-controlled, fully editable topics where you control sample questions, content, and actions. They support functions, flows, and all agent behaviors.
For a detailed comparison, see the [Connected knowledge introduction](/knowledge/sources/introduction#how-connected-knowledge-differs-from-managed-topics).
| Scenario | Recommendation |
| ------------------------------------------------------- | --------------------------------------------------------------------- |
| Large FAQ library from an existing help center | **Connected** – fast to set up, auto-syncs |
| Content that changes frequently in an external system | **Connected** – stays up to date through sync |
| Topics that need to trigger handoffs, SMS, or functions | **Managed Topics** – only option for actions |
| You need control over exactly what the agent says | **Managed Topics** – you write the utterances |
| Seasonal or toggleable content | **Managed Topics** – supports activation/deactivation per environment |
Both use RAG for retrieval. If there is a conflict, **Managed Topics content takes priority**.
If you're unsure, start with Connected knowledge for general FAQ content and use Managed Topics for anything that requires specific wording or triggers an action.
Several factors can affect retrieval:
* **Data structure**: Connected knowledge splits content into chunks. Very large or loosely structured documents may struggle with relevance. Restructure into smaller, tighter pieces.
* **Sync state**: Both the source and the agent must be up to date. Trigger a manual sync if needed.
* **Environment and variant**: Each source must be enabled in the correct environment and variant.
If a topic is critical, consider curating it as a Managed Topic for guaranteed retrieval.
# Environments and testing FAQ
Source: https://docs.poly.ai/troubleshoot/faq-environments
Guidance on safe testing, deployment pipelines, and managing multi-site agents.
Answers to questions about testing changes safely across environments (Draft, Sandbox, Pre-release, Live) and managing multi-location agents using variants.
Use the [deployment pipeline](/environments-and-versions/introduction) to test in isolated environments:
1. **Draft** – make changes in the editor.
2. **Sandbox** – publish your draft and test using [agent chat](/get-started/quickstart#test-your-agent) or a sandbox phone number.
3. **Pre-release** – promote for user acceptance testing (UAT).
4. **Live** – promote to live when ready.
You can [compare versions](/environments-and-versions/diffs) across environments before promoting, and [rollback](/environments-and-versions/introduction#rolling-back-to-a-previous-version) if issues arise.
Use [variant management](/knowledge/variants/introduction) to manage location-specific content in a single agent. Each variant stores attributes like phone numbers, addresses, and hours.
Variants are useful for:
* Hotel chains, restaurant groups, or retail chains with multiple branches
* Agents that need to respond differently based on which number was called
* Dynamically populating responses with location-specific data using `${variant_attribute}` syntax
See the [variant management guide](/knowledge/variants/introduction) and [CSV imports](/knowledge/variants/csv-imports) for bulk configuration.
# Managed topics FAQ
Source: https://docs.poly.ai/troubleshoot/faq-managed-topics
Guidance on structuring topics, optimizing retrieval, and scaling topic management.
Answers to common questions about Managed Topics including structure, retrieval (RAG), sizing, organization, and handling edge cases. See [Managed Topics](/knowledge/faqs/introduction) for full documentation.
Each topic should have:
* **A clear name**: Use short, descriptive titles like "Refund policy" or "Store hours." The topic name is heavily weighted during [retrieval](/knowledge/faqs/RAG/introduction), so make it specific.
* **Sample questions**: You can add up to **20** sample questions per topic. More sample questions help the retriever find the right topic.
* Example for *Refund policy*:
* "How do I get a refund?"
* "Can I return a product for a refund?"
* "What's the refund timeline?"
* **Content and actions**: Content defines what the agent says; actions define what it does (like triggering a handoff or sending an SMS). See the [actions overview](/knowledge/faqs/actions/introduction) for setup details.
Topic names and sample questions matter more than content for retrieval. The [RAG system](/knowledge/faqs/RAG/introduction) compares user input against all topics and returns the top matches – so well-written names and questions directly improve accuracy.
When a user sends a message, the agent does **not** see all topics at once. Instead, it uses [retrieval-augmented generation (RAG)](/knowledge/faqs/RAG/introduction):
1. The retriever compares the user's message against every topic's **name**, **sample questions**, and **content** – with higher weighting on the name and sample questions.
2. The top matching topics are returned to the LLM.
3. The LLM selects the best match and generates a response (and may trigger an action, function, or flow).
This is why topic naming and sample questions are so important – they are the primary signals the retriever uses to find the right content.
* **Larger topics**: Better for agents using newer LLM models (like Raven 3.5), because they can handle more context in a single turn.
* **Smaller topics**: Easier for reporting, analysis, and debugging. Also better for agents using older models with limited context windows.
Balance scope and specificity based on your use case. If you find the agent is confusing similar topics, consider splitting them.
If you have hundreds of topics, keeping them organized is important for maintenance:
* **Use consistent naming conventions**: Prefix topics by category (e.g., "Billing - refund policy", "Billing - payment methods") so they sort together.
* **Review regularly**: Deactivate topics that are no longer relevant rather than deleting them – you can reactivate later if needed. See [activating and deactivating topics](/knowledge/faqs/introduction#activating-and-deactivating-topics).
* **Use CSV import/export**: Managed Topics support CSV export and import for bulk updates – use the **Export CSV** button in the Managed Topics view to get started.
There is currently no folder or grouping structure for topics in the UI. Naming conventions are the best way to keep large topic sets navigable.
Create an "Out-of-scope" topic or add instructions in [global rules](/behavior/general/rules).
**Example response**: "I'm sorry, I can only help with questions about \[service]. For other inquiries, please contact our support team at \[number/email]."
Before UAT, clearly define what your agent handles and what it doesn't. This helps testers and customers set expectations, and reduces frustration when the agent declines a request.
Add a disambiguation prompt in the content.
**Example:**
* **Topic**: "Booking issues"
* **Content**: "Can you confirm if the booking was made online or over the phone?"
# Personality and role FAQ
Source: https://docs.poly.ai/troubleshoot/faq-personality
Learn how to configure your agent's personality, tone, and role using concrete examples.
Answers to questions about setting your agent's personality, tone, and role. This FAQ emphasizes using specific examples and rules rather than generic personality instructions. See [Agent](/behavior/general/agent) for full documentation.
Personality and role are configured in **Behavior > General**. The greeting is configured per-channel under **Voice > Advanced settings** (or **Messaging > Advanced > Chat configuration**), not on the Agent page.
The built-in personality tags (`Polite`, `Kind`, `Funny`, `Energetic`, `Calm`, `Thoughtful`) are simply inserted as adjectives into the system prompt – e.g. *"You are a polite, kind \[role]."*. Selecting **Other** replaces the joined adjectives with your free-form string. There's no hidden behavior tied to specific words – they're literal prompt content.
For detailed tone control, use [global rules](/behavior/general/rules) with **specific examples** of how the agent should respond. Single-sentence instructions like "Be professional" are less reliable than concrete examples:
**Instead of:** "Be friendly and helpful."
**Use a rule like:** "When greeting the customer, use their name if available. Example: 'Hi Sarah, thanks for calling \[Brand]. How can I help you today?'"
Always test tone changes in [sandbox](/environments-and-versions/introduction) with adversarial inputs before promoting.
It helps the agent stay focused. Add this in the [Agent](/behavior/general/agent) section or as a global rule.
**Examples:**
* "You are a virtual agent for \[Brand], focused on customer support."
* "You are a helpful hotel concierge, focused on resolving customer problems and managing reservations."
# Behavior FAQ
Source: https://docs.poly.ai/troubleshoot/faq-rules
Guidance on writing effective behavioral rules, channel filtering, and handling edge cases.
Answers to common questions about behavioral rules including best practices, examples, channel and language filtering, and planning for risky scenarios. See [Behavior](/behavior/general/rules) for full documentation.
Global rules set consistent agent behavior across all interactions. Use them for tone, scope, and task-specific instructions.
**Examples:**
* "Always remain professional and empathetic, even when the customer is frustrated."
* "Only answer questions about \[service]. For anything else, say: 'I can only help with \[service]-related questions.'"
Keep global rules **concise** so the model can follow them consistently.
**Best practices:**
* State the most important rules first.
* Combine overlapping rules into a single, clear instruction.
* Remove redundant or contradictory rules.
* Regularly audit your rules against actual agent behavior using [conversation review](/analytics/conversations/review).
Yes – specific examples are more reliable than general instructions. The more concrete you are about what the agent should say, the more consistently it will follow the rule.
**Instead of:** "Be empathetic."
**Use:** "When a customer expresses frustration, respond with empathy before problem-solving. Example: 'I completely understand your concern, and I want to make sure we get this sorted for you.'"
Yes. You can use channel and language tags to filter content and rules. **Each tag requires a matching closing tag** (`` or ``):
* `...` – applies only to voice calls
* `...` – applies only to webchat
* `...` – applies only to SMS
* `...` – applies only to English interactions
Closing tags are always plain `` or `` – never `` or ``. Tags can be nested, e.g. `Call 1-800-...`.
This is useful for multi-channel agents (where voice and chat may need different handling) and [multilingual agents](/behavior/language/multilingual) (where certain phrases or instructions only apply in specific languages).
These are common patterns that benefit from explicit global rules:
* **Small talk**: "If the user makes small talk, briefly acknowledge and redirect to the task."
* **Silence / no input**: "If the user does not respond, prompt them once, then offer to transfer to an agent."
* **Broken or unintelligible input**: "If you cannot understand the user's request after two attempts, offer to transfer to a human agent."
These rules help the agent handle real-world edge cases that are especially common in voice interactions.
Identify high-risk situations (like refunds, cancellations, or emergencies) and add clear rules or dedicated topics.
**Example for refunds:**
"Route all refund-related queries to a support specialist."
Always test risky scenario handling in [sandbox](/environments-and-versions/introduction) before deploying to production.
# Technical considerations FAQ
Source: https://docs.poly.ai/troubleshoot/faq-technical
Answers on ASR/TTS independence, token limits, and context management best practices.
Answers to technical questions about automatic speech recognition (ASR), text-to-speech (TTS), token limits, and context management.
No. Prompts do not influence automatic speech recognition (ASR) or text-to-speech (TTS). These systems are independent.
To customize speech recognition, use [ASR biasing](/flows/asr-biasing) and [keyphrase boosting](/voice-channel/advanced/call-settings#keyphrases). To customize voice output, see [voice settings](/voice-channel/introduction).
Your agent's context is made up of global rules, retrieved topics, conversation history, and system instructions. Keeping this focused helps the agent perform at its best.
**Tips for managing context:**
* Keep global rules concise and prioritized.
* Write shorter, focused topic content – this also retrieves better.
* If you have many topics, make sure they are clearly differentiated so the retriever returns only the most relevant ones.
* Use [conversation diagnosis](/analytics/conversations/diagnosis) to inspect what the agent actually received if behavior seems off.
# Role permissions
Source: https://docs.poly.ai/user-management/access-control-scope
Reference for account roles, permission levels, and every permission area in Agent Studio.
Agent Studio has two account roles and three permission levels. Admins set a permission level for each area, in each project, for each Member.
## Account roles
| Role | Access |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Admin** | Full access to every feature and every project in the account. Admins can add, edit, and delete users. Per-area permissions do not apply to Admins. |
| **Member** | Access to assigned projects only. In each assigned project, each area has a permission level of **None**, **Read**, or **Edit**. Members cannot open the **Users** page. |
[Wren](/wren/introduction) is available to everyone who can open a project. Wren follows your per-area permissions in that project:
* Wren can change only the areas where you have **Edit**.
* To create a branch or apply changes, you need **Edit** on at least one area.
* With **Read** on an area, you can ask Wren questions about it and explore it. Wren cannot change it.
* To analyze conversations with Wren, you need **Read** on **Conversations**.
* Wren cannot see areas set to **None**.
## Permission levels
| Level | What the user can do |
| -------- | ------------------------------------------------------------------------------- |
| **None** | The area is hidden. The user cannot view or open it. |
| **Read** | The user can view the area but cannot create, change, or delete anything in it. |
| **Edit** | The user can view, create, change, and delete content in the area. |
Permission levels are set **per project**. A Member can have **Edit** on **Knowledge** in one project and **None** on **Knowledge** in another.
## How the permission tree behaves
Areas are grouped in a tree. Each row has **None**, **Read**, and **Edit** options.
* **All** is the first row. Select a level in the **All** row to apply it to every area in the project.
* When you select a level on a top-level area, Agent Studio applies the same level to every item inside it.
* When you select a level on a single item, only that item changes. The top-level area and the other items do not change.
* A top-level area shows the level of its items when all items have the same level. When items have different levels, the top-level area shows **None**.
* **Conversations** is an exception. Selecting a level on the **Conversations** row does not change the items inside it. Set **PII**, **Call download**, **Intents**, **Entities**, and **Metrics** one by one.
Set the **All** row first, then change the top-level areas that must differ, then expand an area to change single items.
## Permission areas
The permission tree lists the areas below in this order. Each area has one line that says what it covers. If the area has items inside it, a table lists them. Select a level on an area to set every item inside it. Expand the area to set one item at a time. Some rows appear only when the related feature is enabled for the project.
### Analytics
Analytics dashboards, metrics, and analysis.
| Item | Controls |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Agent analytics** | Opens the **Analytics** page. With **None**, the user cannot open the page. |
| **Analytics homepage** | Builds dashboards on the **Analytics** page. With **Edit**, the user can create, edit, rename, duplicate, and delete dashboards. Every user who can open the page can view published dashboards. |
| **Metrics** | Custom metric definitions |
| **Agent analysis** | The **Agent analysis** pages. Agent analysis is a legacy feature that is enabled on some accounts only. |
### Conversations
The conversation list, transcripts, and recordings. Selecting a level on this area does not set the items inside it. Set each item one at a time.
| Item | Controls |
| ----------------- | --------------------------------------- |
| **PII** | Unmasked personal data in conversations |
| **Call download** | Download of call recordings |
| **Intents** | Intent data on conversations |
| **Entities** | Entity data on conversations |
| **Metrics** | Metric values on conversations |
### Custom dashboards
[Managed dashboards](/analytics/dashboards/custom) that PolyAI builds and maintains in QuickSight for your organisation. With **Edit**, your team can also build these dashboards if [QuickSight Authoring](/analytics/dashboards/custom#quicksight-authoring) is enabled.
### Behavior
The agent persona, rules, and settings.
| Item | Controls |
| ------------------------ | ---------------------------------------- |
| **Agent** → **Behavior** | The agent persona, rules, and guardrails |
| **General** | General agent settings |
| **Language hub** | Languages, coverage, and translations |
| **Agent config** | Agent configuration |
### Project context
Project documents that give the agent context.
### Knowledge
FAQs, knowledge sources, and variants.
| Item | Controls |
| ------------ | ---------------------------------------------- |
| **FAQs** | Topics, sample questions, answers, and actions |
| **Sources** | Connected knowledge sources |
| **Variants** | Multi-site variants |
### Flows
Conversation flows.
### Tools
Custom functions and tools.
### Test suite
Test cases, test sets, and test runs.
### Real-time configuration
Real-time configuration and branches.
| Item | Controls |
| --------------------------- | -------------------------------- |
| **Real-time configuration** | The real-time configuration view |
| **Configuration builder** | The configuration builder |
| **Branch management** | Branches |
### Voice
All voice channel settings.
| Item | Controls |
| ---------------------- | ---------------------------------------- |
| **Agent voice** | The agent voice and disclaimer |
| **General** | General voice channel settings |
| **Numbers** | Phone numbers |
| **Handoffs** | Call handoff destinations and routing |
| **In-call messages** | SMS messages sent during a call |
| **Response control** | Response control settings |
| **Audio management** | The audio library |
| **Speech recognition** | Speech recognition settings |
| **CSAT** | Customer satisfaction surveys |
| **PolyPhone** | PolyPhone settings for the voice channel |
| **Advanced settings** | Advanced voice settings |
### Web chat
All web chat settings.
| Item | Controls |
| --------------------- | ------------------------------- |
| **Settings** | Web chat configuration |
| **PolyPhone** | PolyPhone settings for web chat |
| **Advanced settings** | Advanced web chat settings |
### Integrations
App and API integrations.
| Item | Controls |
| -------- | ------------------------------------ |
| **Apps** | App integrations and MCP connections |
| **APIs** | API integrations |
### Deployments
Environments, releases, and widgets.
| Item | Controls |
| --------------------------------- | ------------------------------------------------------------------------------------- |
| **Deployment** | Environments, promotion, A/B tests, version comparison, and project history |
| **Widgets** → **Webchat widgets** | Web chat widget configuration. Appears only when widgets are enabled for the project. |
| **Widgets** → **Voice widgets** | Voice widget configuration. Appears only when widgets are enabled for the project. |
There is no permission row for **Home** or for Wren. Wren follows the per-area permissions above. There is no permission row for workspace settings. Only Admins can open the **Users**, **API keys**, and **Secrets** pages under **Manage workspace**.
## Set permissions
You set permissions when you [add a user](/user-management/invite-users) or when you [edit a user](/user-management/manage-users#edit-a-user). In both cases:
1. On the **Users** page, open the **Add users** or **Edit user** panel.
2. Select the **Member** role.
3. In **Projects**, select each project that the user can open.
4. Click a project row to expand it.
5. For each area, select **None**, **Read**, or **Edit**. Expand an area to set single items.
6. Click **Add** or **Save**.
To check the permissions of an existing user, click a project in the **Projects** column of the **Users** list.
## Common configurations
Apply these configurations to each project that the user can open. Set the **All** row first, then change the rows listed below it.
### Read-only reviewer
Views everything. Changes nothing. Wren answers questions but does not build.
```
All: Read
```
### Conversation analyst
Reviews conversations and builds dashboards. Cannot see build areas.
```
All: None
Analytics: Read
Conversations: Read
Conversations → PII: Read
Conversations → Call download: Read
Conversations → Intents: Read
Conversations → Entities: Read
Conversations → Metrics: Read
Custom dashboards: Edit
```
The **Conversations** row does not set its items, so set each item one by one.
### PII-restricted analyst
Same as the conversation analyst, but cannot see unmasked personal data or download recordings. Use this configuration for external teams or for users who do not need personal data.
```
All: None
Analytics: Read
Conversations: Read
Conversations → PII: None
Conversations → Call download: None
Conversations → Intents: Read
Conversations → Entities: Read
Conversations → Metrics: Read
Custom dashboards: Edit
```
### Knowledge editor
Edits FAQs, sources, and variants. Reads everything else.
```
All: Read
Knowledge: Edit
```
### Builder
Builds and tests the agent. Reads the Deployments page but does not manage environments there.
```
All: Edit
Deployments: Read
```
### Operations manager
Deploys, manages real-time configuration, and owns analytics. Does not change agent logic.
```
All: Read
Analytics: Edit
Custom dashboards: Edit
Real-time configuration: Edit
Deployments: Edit
```
## Related pages
Overview of roles, projects, and permission levels.
Add users, assign a role, and set project permissions.
Edit, delete, and export users. Re-send invitations.
# User management
Source: https://docs.poly.ai/user-management/introduction
Control who can access your account, which projects they can open, and what they can change.
Use user management to control three things:
* Who can sign in to your account.
* Which projects each user can open.
* What each user can view or change inside each project.
To open user management, click **Manage workspace** at the bottom of the project sidebar. Then click **Users** in the **Workspace** section. Only account **Admins** can see the **Users** page.
## Roles
Every user has one account-level role.
| Role | Access |
| ---------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Admin** | Full access to every feature and every project in the account. Admins can add, edit, and delete users. |
| **Member** | Access to the projects that an Admin assigns. For each assigned project, an Admin sets a permission level for each area. |
## Projects and permission levels
Member permissions are set **per project**. A Member cannot open a project that no Admin assigned to them.
Inside each assigned project, each area has one of three permission levels:
| Level | Description |
| -------- | ------------------------------------------------ |
| **None** | The user cannot see the area. |
| **Read** | The user can view the area but cannot change it. |
| **Edit** | The user can view and change the area. |
Areas are grouped in a tree. Set a top-level area to apply one level to everything inside it. Expand the area to set a different level on a single item. For example, give a QA team **Read** on **Conversations** and **None** on every build area.
See [Role permissions](/user-management/access-control-scope) for the full list of areas and how the tree behaves.
## Groups
A group is a label that you attach to a user. Use groups to find and organise users in the **Users** list. Groups do not change permissions.
## Sign-in
Users sign in at the Agent Studio URL for their region:
| Region | URL |
| ---------- | --------------------------- |
| Self-serve | `https://studio.poly.ai` |
| US | `https://studio.us.poly.ai` |
| EU | `https://studio.eu.poly.ai` |
| UK | `https://studio.uk.poly.ai` |
**Single sign-on (SSO) handles sign-in only.** SSO does not assign roles or permissions. You must manage roles and permissions on the **Users** page. If you see unexpected changes to user permissions, contact your PolyAI representative.
## Related pages
Add users, assign a role, and set project permissions.
Edit, delete, and export users. Re-send invitations.
Reference for every permission area and level.
# How to add users
Source: https://docs.poly.ai/user-management/invite-users
Add users to your account, assign a role, and set project permissions.
Add users to your account so that they can build and manage agents with you. Only account **Admins** can see the **Users** page and add users.
## Add users
Click **Manage workspace** at the bottom of the project sidebar. Then click **Users** in the **Workspace** section.
Click **+ User** in the top right corner. The **Add users** panel opens on the right.
In **User email**, type or paste one or more email addresses. Separate addresses with a comma, semicolon, space, or new line. Press **Enter** after each address to add it as a tag.
In **Role**, select **Admin** or **Member**.
* **Admin** has full access to every feature and every project. You do not select projects for an Admin.
* **Member** has access only to the projects that you select in the next steps.
In **Groups (Optional)**, type a group name and select it. To create a new group, type a new name and press **Enter**. Groups are labels only. Groups do not change permissions.
If the role is **Member**, type in **Projects** to find and select each project that the user can open.
Each selected project appears as a row. Click the row to expand it. For each area, select **None**, **Read**, or **Edit**. To apply one level to every area, use the **All** row at the top.
See [Role permissions](/user-management/access-control-scope) for a description of each area.
Click **Add**. Agent Studio shows a **User added** message and lists the new users with a **Pending** tag.
If you enter more than one email address, every user gets the same role, groups, projects, and permissions. To give users different permissions, add them separately or edit them after you add them.
## What the new user receives
Each new user receives an invitation email. The invitation link expires **48 hours** after Agent Studio sends it.
In the **Users** list, the tag next to the email address shows the invitation status:
| Tag | Meaning |
| ----------- | -------------------------------------------------------------------- |
| **Pending** | Agent Studio sent the invitation and the user did not accept it yet. |
| **Expired** | The 48-hour window passed. |
| No tag | The user accepted the invitation and is active. |
Hold the pointer over the tag to see when Agent Studio sent the invitation and when it expires. If an invitation expires, you can [re-send it](/user-management/manage-users#re-send-an-invitation).
After the user accepts the invitation, they sign in at the Agent Studio URL for your region:
| Region | URL |
| ---------- | --------------------------- |
| Self-serve | `https://studio.poly.ai` |
| US | `https://studio.us.poly.ai` |
| EU | `https://studio.eu.poly.ai` |
| UK | `https://studio.uk.poly.ai` |
A **Member** with no assigned project cannot open any project. If a Member sees a **Page not found** screen after sign-in, [edit the user](/user-management/manage-users#edit-a-user) and assign at least one project.
## Related pages
Edit, delete, and export users. Re-send invitations.
Reference for every permission area and level.
# How to manage users
Source: https://docs.poly.ai/user-management/manage-users
Find, edit, delete, and export users. Re-send invitations.
Use the **Users** page to change a user's role, groups, projects, and permissions after you add them. To open it, click **Manage workspace** at the bottom of the project sidebar, then click **Users**. Only account **Admins** can see the **Users** page.
## Find a user
* Type in the search field to filter the list by email address.
* Click the **User** or **Role** column header to sort the list.
* The **Projects** column shows each project that a Member can open. Click a project to see the permission level for each area in that project. Admins have access to every project, so the column is empty for Admins.
The list shows 50 users per page.
## Edit a user
Find the user in the list. Click the **⋮** menu at the end of the row, then click **Edit**. The **Edit user** panel opens on the right.
Change any of these fields:
* **Role**: select **Admin** or **Member**. If you change the role, Agent Studio clears the project permissions. You must set them again.
* **Groups (Optional)**: add or remove groups.
* **Projects**: add or remove projects. Click a project row to expand it and change the permission level for each area.
If you changed the role, a checkbox appears above the buttons. Select the checkbox to confirm that you want to update the role.
Click **Save**.
You cannot edit or delete your own user. Ask another Admin to make changes to your account.
## Edit multiple users
You can apply one set of role, groups, projects, and permissions to several users at once.
Select the checkbox at the start of each row. A bar appears at the bottom of the page with the number of selected users.
Click **Edit**. The **Edit users** panel opens on the right.
Set the role, groups, projects, and permissions. Hold the pointer over an email tag to see that user's current role and projects.
Click **Save**.
A bulk edit **replaces** the current permissions of every selected user. It does not add to them.
## Re-send an invitation
If a user has a **Pending** or **Expired** tag, they did not accept the invitation yet.
1. Click the **⋮** menu at the end of the user's row.
2. Click **Re-send invitation**.
Agent Studio sends a new invitation email. The new link expires 48 hours after Agent Studio sends it.
## Delete a user
Deleting a user removes them from the account. They lose access to every project in the account.
Click the **⋮** menu at the end of the user's row, then click **Delete**.
Type the user's email address in the field, then click **Delete**.
To delete several users at once, select the checkbox at the start of each row. Click **Delete** in the bar at the bottom of the page. Type `confirm` in the field, then click **Delete**.
## Export the user list
Click **Export** in the top right corner to download the user list as a CSV file. If you typed a search term, the file contains only the users that match the search.
## Related pages
Add users, assign a role, and set project permissions.
Reference for every permission area and level.
# Add a voice
Source: https://docs.poly.ai/voice-channel/add-a-new-voice
**This page requires Python familiarity.** It covers programmatic voice configuration using provider classes. For no-code voice selection, see the [Voice library](/voice-channel/voice-library).
The PolyAI platform supports flexible voice selection for external providers including Cartesia, ElevenLabs, Hume, Rime, Minimax, PlayHT, and Google TTS.
## Provider classes
When picking models, adjusting stability, or accessing third-party providers – use provider-specific `TTSVoice` classes. See the [Voice classes](/tools/classes/voice) reference for the full list of providers and parameters.
### Example: Cartesia
Cartesia is the recommended provider for new projects due to its low latency and natural output.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from polyai.voice import CartesiaVoice, Emotion, EmotionKind, EmotionIntensity
conv.set_voice(
CartesiaVoice(
provider_voice_id="a1b2c3d4",
speed=0.0, # -1.0 (slowest) to 1.0 (fastest)
emotions=[
Emotion(EmotionKind.POSITIVITY, EmotionIntensity.HIGH)
],
model_id="sonic-3" # or "sonic-3.5", "sonic-preview"
)
)
```
### Example: ElevenLabs
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from polyai.voice import ElevenLabsVoice
conv.set_voice(
ElevenLabsVoice(
provider_voice_id="gDnGxUcsitTxRiGHr904",
model_id="eleven_turbo_v2_5", # Recommended default
stability=1.0,
similarity_boost=0.7,
speed=1.0, # Optional: 0.7–1.2, adjusts speech rate
)
)
```
Available model IDs: `eleven_monolingual_v1`, `eleven_multilingual_v1`, `eleven_turbo_v2`, `eleven_turbo_v2_5`, `eleven_flash_v2_5`, and `eleven_v3`. The default is `eleven_turbo_v2_5`.
**`eleven_v3` limitations:**
* **Stability:** Only supports discrete values: `0.0` (Creative), `0.5` (Natural), and `1.0` (Robust). Values between these are not supported and may produce unexpected results.
* **Streaming latency:** Do not set `optimize_streaming_latency` when using `eleven_v3` – this parameter is not supported and will cause an error.
### Example: Hume
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from polyai.voice import HumeVoice
conv.set_voice(
HumeVoice(
provider_voice_id="voice_uuid_or_name",
voice_description="patient, empathetic counselor", # Optional
version="2", # "1" for octave-1, "2" for octave-2
instant_mode=False, # Ultra-low latency mode
)
)
```
## Cache behavior
* Changing `model_id` does not automatically invalidate cached audio.
* To reset cached audio after changing models:
* Go to **Voice > Audio library** and delete existing cache entries.
* Or, create a new voice entry with a different voice ID.
Prepend the model ID to the voice ID (e.g. `eleven_turbo_v2_5/a1b2c3...`) to isolate cache entries per model. This is the most reliable way to ensure the correct model is used after a switch.
## Related pages
Browse and select voices
Configure voice settings and fine-tuning
Use multiple voices in a single agent
Configure model selection and call settings
# Advanced voice settings
Source: https://docs.poly.ai/voice-channel/advanced/call-settings
Configure model selection, barge-in, filler phrases, safety filters, keyphrases, transcript corrections, and pronunciation for voice agents.
The **Advanced settings** page controls voice-specific configuration that goes beyond basic voice selection. Open it from the **Advanced settings** link in the top-right corner of the Voice page.
The page has three tabs: **Model**, **Call**, and **Speech**.
## Model
### Voice mode
Choose between two architectures for your voice agent:
| Mode | Description |
| --------------- | ------------------------------------------------------------------------------------------ |
| **Traditional** | Separate components handle speech recognition, language processing, and speech synthesis. |
| **End-to-end** | A unified model directly maps input (audio, text, or multimodal signals) to speech output. |
### Language model
Select the LLM that powers your voice agent.
Choose the LLM for voice conversations. PolyAI's **Raven** models are recommended for voice – they are designed for conversational AI and deliver the most natural, grounded responses.
Controls response variability. Lower values (toward **Deterministic**) produce more consistent, predictable responses. Higher values (toward **Creative**) produce more varied output. Default is 0.4.
**Recommended for voice:** PolyAI's [Raven 3.5](/behavior/models/raven) is designed for conversational AI. It produces concise, natural responses without needing example utterances in your prompts — just add raw information and Raven converts it into conversational speech. Raven 3.5 supports 24+ languages, delivers sub-300ms latency, and includes auto-reasoning, out-of-domain detection, and built-in safety.
For a full list of available models, see the [Model](/behavior/models/model-use) page.
### Speech recognition
Choose the primary transcription engine and target language for your voice agent.
Select the ASR model used to transcribe caller speech. The model you choose affects transcription accuracy, latency, and language support.
### Turn-taking
Control when the agent starts speaking after the caller stops.
How long to wait (in milliseconds) after the caller stops speaking before the agent begins its response. **Fast** (200ms) responds quickly but may cut off pauses. **Tolerant** (1000ms) waits longer through pauses but adds latency.
### AI-coustics
Removes background noise and enhances audio quality. Enable this for callers in noisy environments.
***
## Call
### Barge-in
Allow callers to interrupt the agent mid-utterance. When enabled, the agent stops speaking as soon as it detects caller speech, shortening [VAD](https://en.wikipedia.org/wiki/Voice_activity_detection) time and reducing response latency.
Toggle on to let callers interrupt the agent mid-sentence.
When enabled, barge-in is active from the very first agent utterance (including the greeting). When disabled, barge-in activates after the first turn completes.
Limit how many times a caller can interrupt per call. Set to 0 for unlimited.
#### How barge-in works
When barge-in is enabled:
1. The agent begins speaking its response.
2. If the caller starts talking, the agent stops its current utterance and begins listening.
3. The agent processes the caller's input and responds as a new turn.
This also applies to [delay control](/tools/delay-control) responses – if the caller speaks during a filler phrase, the delay sequence is interrupted.
#### When to use barge-in
Barge-in works well for:
* FAQ-heavy agents where callers may already know what they need
* Long agent responses where the caller wants to redirect the conversation
* Fast interaction styles where responsiveness is a priority
#### When to disable barge-in
Consider disabling barge-in (globally or per flow/step) when:
* The agent is executing a function with **external side effects** (bookings, payments, form submissions) – the caller may interrupt after the action completes but before hearing the confirmation
* The agent must deliver a **mandatory disclosure or disclaimer** that cannot be skipped
* The environment is **noisy**, causing false barge-in triggers from background sounds
#### Per-flow and per-step overrides
You can configure barge-in at a granular level using the experimental JSON config. This lets you enable barge-in globally while disabling it for specific flows or steps where interruption would be problematic.
Overrides follow a precedence order: **step > flow > global**. For example, you can have barge-in off globally, enabled for a specific flow, and disabled again for a sensitive step within that flow.
Barge-in behavior cannot be fully tested in the chat panel. Always verify with a real phone call.
### Filler phrases
Configure brief sounds or words the agent uses while processing the user's input — "One moment...", "Just a second...", etc. These fill the gap between the caller finishing and the agent responding, preventing awkward silence.
Toggle on to enable filler phrases during processing delays.
When enabled, the agent picks randomly from the configured phrases instead of cycling in order.
How many seconds to wait before the first filler phrase plays.
How many seconds between subsequent filler phrases if the agent is still processing.
### Customer satisfaction survey (CSAT)
Enable to collect customer feedback during voice calls. Additional options appear once enabled.
When enabled, the agent can route callers into a CSAT survey flow at the end of the conversation. For full configuration details — survey questions, scoring, and dashboard integration — see [CSAT surveys](/analytics/csat/introduction).
***
## Speech
### Safety filters
Safety filters are configured on a **per-channel basis**. The voice channel has its own safety filter settings, separate from the chat channel and the project-wide defaults. See [Safety filters](/behavior/guardrails/safety-filters) for the full reference on categories, severity levels, and how filters interact with [Guardrails](/behavior/guardrails/introduction).
When enabled, applies content filtering to voice responses. These filters apply to the voice channel only. Default settings can be managed on the **Behavior** page.
When safety filters are enabled, adjust the strictness for each category using the sliders:
| Category | Description |
| ------------------ | -------------------------------------------------------------------------- |
| **Violence** | Controls filtering of violent content (Lenient → Strict) |
| **Hate** | Controls filtering of hateful or discriminatory content (Lenient → Strict) |
| **Sexual content** | Controls filtering of sexually explicit content (Lenient → Strict) |
| **Self-harm** | Controls filtering of self-harm related content (Lenient → Strict) |
The jailbreak attack filter is always enabled and cannot be turned off.
More lenient settings allow a wider range of content through. Review your use case and compliance requirements when adjusting these settings.
### Keyphrases
Keyphrase boosting improves [ASR](https://en.wikipedia.org/wiki/Speech_recognition) recognition of domain-specific terms like product names, locations, or specialized vocabulary. It biases the ASR model toward recognizing specific words during transcription.
Biasing the ASR can cause unwanted side effects. Adding too many keyphrases or setting bias too high may cause the model to over-correct natural speech. For example, boosting the word `flimsy` at Maximum strength could cause unrelated words like `Lindsay` to be transcribed as `flimsy`. Always test thoroughly in sandbox before deploying changes to production.
#### Configuring keyphrases
1. Open the **Keyphrases** section on the Speech tab.
2. Add, edit, or remove keyphrases.
3. Adjust the **bias strength** for each keyphrase using the slider.
4. Save your changes. Updated keyphrases are applied immediately.
#### Bias strength levels
| Level | Behavior |
| ----------- | ------------------------------------------------------------------------------------------------------ |
| **Default** | Light bias. Balances recognition accuracy with overall ASR performance. |
| **Boosted** | Moderate bias. Increases recognition of the keyphrase without heavily impacting general transcription. |
| **Maximum** | Strong bias. Prioritizes the keyphrase but may interfere with natural speech patterns. |
Maximum bias does not always produce better results. In some cases it can cause the model to misrecognize unrelated words. Start with Default or Boosted and only escalate to Maximum after testing confirms it is needed.
#### Example keyphrases
| Keyphrase | Use case | Suggested strength |
| ---------------- | --------------------------------------------- | ------------------ |
| `flexi-access` | Financial product name | Boosted |
| `BlueStar` | Brand name frequently misheard as "blue star" | Maximum |
| `hablas español` | Spanish phrase in an English-language agent | Boosted |
| `isotretinoin` | Medical term | Maximum |
| `pension` | Domain-specific term | Default |
**Keyphrase boosting and ASR biasing are the same mechanism.** Keyphrase boosting is the *global* level of ASR biasing – it applies to every turn of the conversation. ASR biasing also has *per-step* and *dynamic* levels for more targeted control. See [ASR biasing in flows](/flows/asr-biasing) and [ASR biasing from functions](/tools/classes/asr-from-conv).
#### Global vs. per-step vs. dynamic biasing
The Speech tab configures **global** keyphrase boosting, which applies to every turn of the conversation. Two additional levels of biasing are available for more targeted control:
* **Per-step biasing** – configure ASR biasing on individual flow steps for contextual precision (e.g. biasing for doctor names only during a name-collection step). See [ASR biasing in flows](/flows/asr-biasing).
* **Dynamic biasing from functions** – set biasing at runtime using `conv.set_asr_biasing()` when you need to bias toward values retrieved from an API or database. See [ASR biasing from functions](/tools/classes/asr-from-conv).
**Precedence rules:** When multiple levels of biasing are active, they are merged with the following priority (highest first):
1. **Dynamic** – biasing set via `conv.set_asr_biasing()` in functions
2. **Per-step** – biasing configured on individual flow steps
3. **Global** – biasing configured on the Speech tab
If the same phrase appears at multiple levels, the highest-priority setting takes precedence.
### Transcript corrections
Fix common ASR misinterpretations using string matching and [regex](https://en.wikipedia.org/wiki/Regular_expression) patterns. Unlike keyphrases, transcript corrections run *after* the ASR model has produced a transcript – they replace text in the output rather than influencing what the model hears.
Transcript corrections can match broadly and introduce errors if the pattern is too wide. A correction like `blue star` → `BlueStar` could also fire on legitimate uses of "blue star" in other contexts. Use specific regex patterns and test corrections against a range of real transcripts before deploying.
#### Configuring transcript corrections
1. Open the **Transcript corrections** section on the Speech tab.
2. Click **Correction** to create a new rule.
3. Give the correction a **name** (must be unique) and optional **description**.
4. Add one or more regex rules within the correction:
* **Replacement type**: **Full transcript** (replaces the entire transcript if matched exactly) or **Partial transcript** (replaces only the matching portion).
* **Regex**: the regular expression to match the misinterpreted phrase.
* **Replacement**: the correct term or phrase (leave empty to delete the matched text).
5. Changes auto-save as you edit.
You can group multiple related regex rules under a single correction. For example, create a correction called "Brand names" containing rules for each brand your agent handles.
#### Example configurations
| Regex | Replacement | Type | Use case |
| --------------------------- | ------------------- | ------------------ | ----------------------------------- |
| `\bI\s?C\s?U\b` | `I see you` | Partial transcript | Medical abbreviation being misheard |
| `\b(blue star\|bluestar)\b` | `BlueStar` | Partial transcript | Brand name correction |
| `\bDr\.?\s?Amari\b` | `Dr. Amari` | Partial transcript | Proper noun / doctor name |
| `\bfull fund is cash\b` | `full fund as cash` | Partial transcript | Financial term correction |
#### Verifying corrections are applied
1. Open a conversation in [Conversation Review](/analytics/conversations/review).
2. Enable the **Transcript corrections** layer in [Conversation Diagnosis](/analytics/conversations/diagnosis).
3. Check each turn to see whether your correction was triggered.
If a correction is not firing as expected, compare the raw ASR transcript against your regex pattern. Common issues include unexpected whitespace, casing differences, or partial word boundaries.
### Pronunciation
Control how your agent pronounces specific terms using the [International Phonetic Alphabet (IPA)](https://pronunciationstudio.com/english-ipa-chart/) or SSML tags. Useful for brand names, proper nouns, and domain-specific vocabulary that TTS engines frequently mispronounce.
Pronunciation rules apply to **voice agents only**. For text-channel agents, this section can be skipped.
#### How it works
Pronunciation rules use regex patterns to match text in the agent's response and replace it with IPA notation or SSML markup before it reaches the TTS engine. You can also use SSML such as ``, ``, and `` in the replacement string.
#### Multilingual pronunciation rules
For multilingual agents, pronunciation rules are organized by language. Each language has its own set of rules displayed as separate collapsible cards. Rules within a language card only apply to responses in that language.
To add a rule for a specific language:
1. Expand the language card.
2. Add the regex pattern and replacement.
3. The rule automatically scopes to that language.
Rules with no language specified apply globally across all languages.
#### Rule evaluation order
Pronunciation rules are evaluated **from top to bottom**. Each rule runs on the text produced by the rule above it. Because rules are applied sequentially, later rules can modify or override earlier transformations.
#### Common pronunciation patterns
Replace a specific string with a spoken equivalent.
* **Regex:** `3\-5`
* **Replacement:** `three to five`
* **Regex:** `\b(\d)(\d)(\d)-(\d)(\d)(\d)-(\d)(\d)(\d)(\d)\b`
* **Replacement:** `\1 \2 \3, \4 \5 \6, \7 \8 \9 \10`
Produces "six five one, three five nine, two nine two three" for `651-359-2923`.
* **Regex:** `\b(\d)(\d)(\d)(\d)(\d)\b`
* **Replacement:** `\1, \2, \3, \4, \5`
* **Regex:** `www\.`
* **Replacement:** `W, W, W, dot`
* **Regex:** `\(?(\d{3})\)?[ -]?(\d{3})[ -]?(\d{4})`
* **Replacement:** `\1 \2 \3`
Handles multiple formats — `(651) 359-2923`, `651-359-2923`, or `6513592923` — and inserts half-second pauses. See [SSML breaks](https://cloud.google.com/text-to-speech/docs/ssml#break) for more timing options.
* **Regex:** `\bLouvre\b`
* **Replacement:** `/ˈluːvrə/`
* **Case sensitive:** `FALSE`
### Stop keywords
When a response matches a stop keyword pattern, the system halts the response and logs the occurrence. Use stop keywords to catch sensitive content, remove unnecessary preambles, or enforce brand adherence.
#### Stop keyword fields
| Field | Type | Description |
| ---------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Title** | String | Unique name for the stop keyword rule (max 100 characters) |
| **Description** | String | Optional note clarifying the keyword's purpose |
| **Regular Expression** | String | One or more regex patterns that identify phrases to control |
| **Say Phrases** | Boolean | `TRUE` – the agent speaks the text up to the matched phrase, then stops. `FALSE` – the agent is interrupted before speaking the phrase. |
| **Language** | String | Which language this rule applies to. Defaults to "All languages". Only visible for multilingual agents. |
| **Function** | Reference | Optional – a [function](/tools/introduction) to call when the phrase is detected |
When a function is triggered by a stop keyword, **caller input and LLM parameters are not passed** to the function. The function runs without conversation context, so it should return a static response or handle the absence of context gracefully.
#### Creating a stop keyword
Go to **[Tools](/tools/introduction)** to define a custom function for handling stop keywords.
**Example setup**:
* **Name**: restricted\_phrase
* **Description**: Respond when stop keywords are detected.
* **Function definition**:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def restricted_phrase(conv: Conversation):
return "I can only help with questions about our products and services."
```
In **Voice > Advanced settings > Speech**, add a new stop keyword.
**Setup details**:
* **ID**: restricted\_phrase
* **Description**: Restrict responses containing specific phrases.
* **Regular Expression**: `\brestricted_term\b`
* **Say Phrases**: `TRUE` (to halt responses when detected).
* **Action**: Link to the `restricted_phrase` function.
Go to the **Webchat** testing panel to validate your setup.
**Steps**:
1. Type a message containing the word "Ashmolean."
2. Observe the agent's response: "The detected phrase is restricted. Please adjust your input."
3. Confirm that the agent halts any further response generation.
#### Common use cases
Prevent offensive or harmful language in user interactions.
| Field | Value |
| --------------- | -------------------------------------- |
| **ID** | `stop_inappropriate` |
| **Regex** | `\b(offensiveWord1\|offensiveWord2)\b` |
| **Say Phrases** | `TRUE` |
LLMs often add unnecessary meta-explanations before executing an action. Stop keywords cut these off instantly.
**Before:** "Let's start by gathering some information. Please hold on while I check your cancellation options. Here are your cancellation options."
**After:** "Here are your cancellation options."
Add a stop keyword in **Voice > Advanced settings > Speech**:
| Field | Value |
| --------------- | ------------------------------------------------- |
| **ID** | `flow_redundancy_cutoff` |
| **Regex** | `\b(let's start\|please hold on\|let me check)\b` |
| **Say Phrases** | `TRUE` |
Block responses with phrases outside approved messaging. Log matches for review in the [Analytics Dashboard](/analytics/dashboards/introduction).
Use regex for complex patterns. See [regex101.com](https://regex101.com/) for a testing tool.
***
## Diacritics
If your agent operates in a language that uses [diacritics](https://www.sussex.ac.uk/informatics/punctuation/misc/diacritics) – such as **č, ć, š, ž, đ** – additional configuration is required before keyphrases and transcript corrections will work correctly.
Diacritics and multilingual ASR configuration cannot be self-served. **Contact your PolyAI representative before making changes** – they will configure the correct ASR language model, language codes, and any necessary preprocessing for your target language.
Transcript corrections can help with minor post-processing (e.g. fixing `Zeljko` → `Željko`) once the correct ASR model is in place, but they are not a substitute for proper language configuration.
## Environment behavior
Advanced voice settings follow the same [branching behavior](/environments-and-versions/introduction) as other channel settings:
* Make changes in **Sandbox** for testing
* Promote to **Pre-release** for UAT
* Deploy to **Live** for production
## Related pages
Configure voice selection, greeting, and disclaimer
Manage cached audio files
Configure per-step biasing for structured input collection
Full CSAT configuration, scoring, and dashboard integration
Project-wide safety filter reference
Configure the webchat channel equivalent
# Voice settings
Source: https://docs.poly.ai/voice-channel/agent
Configure your agent's voice, greeting, disclaimer, and style prompt.
The **Settings** tab on the Voice page is where you configure how your agent sounds — the TTS voice it uses, the greeting callers hear, the disclaimer, and the style prompt that shapes tone and delivery.
Open the Voice page and select the **Settings** tab.
## Voice & speech
Pick a voice for each language enabled on the agent. The main language card is tagged as "Main language". Each language gets its own voice and tuning, and the agent switches automatically when the conversation language changes.
### Selecting a voice
1. Click **Change** next to the voice you want to update.
2. The **[Voice library](/voice-channel/voice-library)** opens.
3. Browse voices using the **Explore** tab or access saved voices in **Favorites**.
4. Filter by **language**, **region**, and **gender**.
5. Preview voices with custom text before selecting.
6. Click **Select** to apply the voice.
Use the Voice library's preview feature with text that matches your agent's typical responses to ensure the voice fits your use case.
### Voice tuning
Fine-tune the selected voice's parameters by clicking the **Settings** icon next to the voice:
Controls how consistent the voice sounds over time. Higher values keep tone and delivery more uniform across responses.
**Recommended:** Set to 0.7 or above for reliable performance.
Adjusts how closely the generated voice matches the original recording. Higher values sound more realistic and detailed, but may be less resilient to unexpected prompts. Lower values smooth the voice slightly, making it better suited for dynamic or ASR-driven content.
Controls stylistic expressiveness of the voice (0.0–1.0). Higher values produce more expressive speech with varied intonation. Lower values keep delivery more neutral and consistent. Only supported by certain TTS providers and voice models.
### Voice greeting
The first thing your agent says when answering a call. Configure it in the **Voice greeting** field.
**Example:** "Thank you for calling. How can I help you today?"
Keep your greeting concise. Long greetings can frustrate callers who want to get to the point quickly.
## Style prompt
Tailor tone, format, and etiquette for your voice agent. The style prompt gives the LLM high-level guidance on how to respond — formal vs casual, verbose vs terse, and any brand-specific phrasing rules.
## Disclaimer
A message read once at the start of every call — typically a recording or AI-disclosure notice.
Toggle on to play a disclaimer before the greeting.
Use a separate voice for the disclaimer to distinguish it from the main agent voice. Each language can have its own disclaimer voice.
The text read aloud at the start of the call.
**Example:** "This call may be recorded for quality and training purposes."
## Ringing tone
A sound that plays after the disclaimer text and before the agent begins speaking. Gives the caller an audible signal that the call is about to start.
## Publishing changes
After updating voice settings:
1. Review your configuration on the Settings tab.
2. Click **Publish** in the top right corner to apply changes to your live agent.
Voice changes only take effect after publishing. Test your agent after publishing to confirm the new voice sounds as expected.
## Related pages
Browse and select voices
Guidelines for voice selection and matching
Configure model, barge-in, safety filters, and speech recognition
Configure custom voices programmatically
# Audio library
Source: https://docs.poly.ai/voice-channel/audio-library
Manage cached audio files to reduce voice latency.
Use the audio library to reduce voice latency and control how your agent sounds during key moments – greetings, transfer messages, and other frequently spoken phrases. Caching these responses means callers hear them faster and with consistent quality, instead of waiting for real-time TTS generation on every call.
Open the Voice page and select the **Audio library** tab.
## Managing cached audio
1. Open the **Audio library** tab.
2. Review all audio saved to the cache and monitor how often it has been used by the agent.
3. You can delete cached files and upload new ones to overwrite existing audio.
You can edit the **stability** and **clarity** of the agent's voice specifically for this utterance. The edit tab also includes sync and play buttons so you can test changes live in the edit panel.
You can add [IPA syntax](https://en.wikipedia.org/wiki/International_Phonetic_Alphabet) (International Phonetic Alphabet) to ensure your agent precisely pronounces industry-specific or non-traditionally pronounced terms, names, or domain-specific language.
**Why am I only seeing a few cached audios?**
The audio cache stores a file only if the same TTS is generated **at least twice within a 24-hour window**. This helps manage cache size and performance. If a particular utterance isn't used multiple times within that period, it won't persist in the cache and may appear missing.
To ensure key audios remain cached, consider generating them repeatedly or uploading static versions manually.
## Manage audio cache via API
You can also manage the audio cache programmatically with the [Agents API](/api-reference/agents/introduction). The audio cache endpoints let you list cached entries, download or replace audio files, synthesize previews, and bulk-delete entries.
Common use cases:
* **CI workflows** — verify that critical prompts are cached, and fail the build if they aren't.
* **Scripted refreshes** — regenerate cached audio after changing voice or tuning settings without going through the UI.
* **Migrating audio between agents** — list entries on a source agent, download the WAV files, and re-upload them under matching transcripts on a target agent.
* **Previewing tuning changes** — call [Synthesize audio preview](/api-reference/agents/endpoint/audio-cache/synthesize-audio-preview) to hear how new text or `VoiceTuningConfig` settings will sound before committing them to the cache.
Audio cache endpoints are gated by the `audio_cache` resource permission on the API key. `read` is required to list, download, or synthesize previews; `write` is required to delete, bulk-delete, replace an audio file, or update audio and settings. Entry IDs are numeric — requests with non-numeric IDs return `400 Invalid ID`. Audio files are limited to 6 MB.
## Related pages
Configure model, barge-in, and speech recognition settings
Select voices and configure greeting and disclaimer
Control how your agent pronounces specific terms
# Choosing a good voice
Source: https://docs.poly.ai/voice-channel/choosing-a-good-voice
Guidelines for selecting the right voice for your agent.
Use this guide when setting up a new agent or changing an existing voice. A well-chosen voice builds caller trust from the first second; a poor choice causes hang-ups and complaints regardless of how well the agent handles the conversation.
## Using the Voice library
The **[Voice library](/voice-channel/voice-library)** provides tools to find the right voice:
1. Go to **Voice > Settings**.
2. Click **Change** to open the Voice library.
3. Use filters for **language**, **region**, and **gender**.
4. Preview voices with **custom text** before selecting.
5. Save frequently-used voices to **Favorites** for quick access.
## Accent
Choose an accent that matches your audience.
Use the Voice library's **region filter** to find accents that match your audience:
* US English for North American customers
* UK English for British customers
* Regional variants for specific markets
## Personality and texture
Match the voice to your business.
| Industry/Audience | Voice Characteristics |
| --------------------- | ---------------------------- |
| Professional services | Confidence and reliability |
| Younger audience | Energy and enthusiasm |
| Older individuals | Maturity and trustworthiness |
| Healthcare | Calm and reassuring |
| Hospitality | Warm and welcoming |
Use the **style tags** shown on voice cards in the Voice library to identify voices with the right characteristics.
## Voice parameter settings
After selecting a voice, fine-tune Stability, Clarity, and Style to match your use case. See [Agent Voice – voice settings](/voice-channel/agent#voice-settings) for full parameter details.
For the most reliable experience, set **Stability** to 0.7 or above.
## Voice selection workflow
1. **Browse voices** in the [Voice library](/voice-channel/voice-library) using the **Explore** tab.
2. **Filter** by language, region, and gender to narrow options.
3. **Preview** voices using the play button and custom text input.
4. **Check style tags** on voice cards for characteristics like "warm", "professional", or "energetic".
5. **Save to Favorites** any voices you want to compare later.
6. **Select** the voice that best fits your needs.
7. **Configure settings** for stability and clarity.
8. **Publish** changes to apply to your live agent.
## Best practices
* Test voices with various sentence types your agent commonly uses
* Consider how the voice sounds when delivering both good news and handling complaints
* Match voice characteristics to your brand identity
* Use the Favorites feature to shortlist options before final selection
* Review voice performance in [Conversation Review](/analytics/conversations/review) after deployment
## Related pages
Browse and preview available voices
Configure voice settings and fine-tuning
Channel-specific settings and call handling
Use multiple voices to simulate a team
# Custom voice
Source: https://docs.poly.ai/voice-channel/custom-voice-request
Commission a brand-exclusive custom voice for enterprise accounts.
Enterprise customers can commission a custom voice built from recorded samples of a specific speaker, giving your agent a unique, brand-specific sound that only your organization uses.
## What custom voices offer
* **Brand-exclusive voice** – A synthetic voice trained on recordings of a specific speaker, available only to your organization.
* **Consistent identity** – The same voice across all calls, environments, and languages (where supported).
* **Fine-tuned characteristics** – Control over tone, pace, and delivery style during the cloning process.
## Requirements
* An active PolyAI enterprise account.
* High-quality audio recordings of the target speaker (PolyAI provides recording specifications).
* Legal clearance from the speaker to use their voice for synthesis.
## How to request
1. Contact your PolyAI account manager or representative.
2. PolyAI provides a recording brief with duration, format, and content requirements.
3. After recordings are submitted, the voice is trained and made available in your [Voice Library](/voice-channel/voice-library).
Custom voice creation typically takes 2-4 weeks depending on recording availability and review cycles.
## Related pages
Browse available voices
Configure voice selection and settings
Guidelines for voice selection
Complete voice configuration guide
# Handoff
Source: https://docs.poly.ai/voice-channel/handoffs
Set up handoff destinations to transfer users to human agents during conversations.
Use handoffs to transfer callers to human agents when the conversation requires human intervention — billing disputes, complaints, or requests outside the agent's scope. A correctly configured handoff routes the caller to the right team and preserves conversation context. A missing or misconfigured handoff fails the transfer.
**Prerequisites:** Understanding of SIP telephony or your contact center's routing setup. UI-based handoff configuration (adding destinations, setting SIP headers) does not require code. The `transfer_call` function path requires Python — see the [comparison table below](#comparison-call-handoff-and-the-transfer_call-function).
**Use Call Handoffs (UI-based)** for fixed transfer destinations with straightforward routing. **Use `transfer_call` (code-based)** for dynamic routing logic, custom SIP headers, or integrations like Zendesk. See the [comparison table](#comparison-call-handoff-and-the-transfer_call-function) for details.
The SIP-based handoff methods described below apply to voice interactions. Webchat handoffs use HTTP-based integrations with your live chat platform. To manage handoff states programmatically, visit the [Handoff API documentation](/api-reference/handoff/introduction).
Handoff is the primary [human-in-the-loop (HITL)](/glossary/introduction#hitl-human-in-the-loop) mechanism in Agent Studio: the agent runs autonomously by default and escalates to a human only when needed. Configure *when* the agent escalates with [Managed Topic actions](/knowledge/faqs/actions/handoff) and [flow actions](/flows/no-code/advanced-steps); configure *what context the human receives* with the fields documented in [Handoff context handover](#handoff-context-handover) below.
## Related handoff documentation
* **[Handoff actions in FAQs](/knowledge/faqs/actions/handoff)** - Add handoff triggers to Knowledge topics
* **[Handoff States API guide](/call-data/conversations-api/handoff-states)** - Monitor transitions between automated and live agents
* **[Handoff API reference](/api-reference/handoff/introduction)** - Retrieve handoff context for downstream systems
### Adding a handoff destination
To create a new handoff destination:
1. Go to **Voice > Handoffs** in the sidebar.
2. Click **Add Handoff**.
3. Fill in the following details:
* **Name**: Enter a descriptive name (e.g., "Front desk").
* **Description**: Add a note about when to use this handoff (e.g., "When the user needs to speak with an operator").
* **Method**: Choose the SIP method to use for call routing. Options include:
* **[SIP REFER](https://www.ietf.org/rfc/rfc3515.txt)** (default) – PolyAI specifies a transfer destination to the client Session Border Controller (SBC), then drops from the call.
* **[SIP INVITE](https://datatracker.ietf.org/doc/html/rfc3261#section-13.3.1)** – PolyAI creates a new call with the destination and acts as a bridge between the client SBC and the destination.
* **[SIP BYE](https://www.rfc-editor.org/rfc/rfc3261.html)** – PolyAI signals that its call leg is over, allowing the client SBC to take the call back over.
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
participant Caller
participant PolyAI
participant SBC as Client SBC
participant Dest as Destination
rect rgb(240, 248, 255)
Note over Caller,Dest: SIP REFER
Caller->>PolyAI: Active call
PolyAI->>SBC: REFER (transfer to Dest)
PolyAI--xCaller: Drops from call
SBC->>Dest: Routes call
Caller->>Dest: Connected
end
rect rgb(255, 248, 240)
Note over Caller,Dest: SIP INVITE
Caller->>PolyAI: Active call
PolyAI->>Dest: INVITE (new call)
PolyAI->>PolyAI: Bridges both legs
Caller->>PolyAI: Leg 1
PolyAI->>Dest: Leg 2
end
rect rgb(240, 255, 240)
Note over Caller,Dest: SIP BYE
Caller->>PolyAI: Active call
PolyAI->>SBC: BYE (end PolyAI leg)
SBC->>SBC: Takes over call
Caller->>SBC: Continues with SBC
end
```
* **Route**: Specify the destination SIP URI or extension (only applies to SIP INVITE and SIP REFER).
* **SIP headers**: Add optional [SIP headers](https://www.iana.org/assignments/sip-parameters/sip-parameters.xhtml) to include metadata or routing instructions.
4. Click **Add** to save the destination.
### Configuring SIP headers
SIP headers can be used to send additional metadata when making a handoff. To add SIP headers:
1. Click **Add SIP Header** in the handoff setup modal.
2. Enter a **Header Name** (e.g., `X-Customer-ID`).
* Custom headers should start with an `X-` prefix.
3. Enter a **Value** (e.g., `abc123`).
4. You can use variables prefixed with `$` in the SIP header values for dynamic data. Example:
`X-Caller-ID: $caller_id`
5. Repeat as needed for multiple headers.
SIP headers allow for custom integrations with external telephony systems and can help manage call behavior dynamically.
### Default handoff
One handoff destination can be marked as the **default**. This is the fallback destination used when no specific routing matches – for example, when the caller requests a transfer but no topic or flow step maps to a specific handoff.
* The first handoff you create is automatically set as the default.
* To change the default, open the actions menu on a handoff card and select **Set as default**.
* If the default handoff is deleted, you should assign a new default to avoid unrouted transfers.
### Managing handoffs
Once a handoff destination is created, it will appear in the list of destinations. From the actions menu on each card, you can:
* **Edit** – modify the name, description, method, route, or SIP headers
* **Duplicate** – create a copy of an existing handoff for a similar destination
* **Delete** – remove the handoff (requires typing the name to confirm)
* **Set as default** – mark as the fallback handoff destination
### Encryption options
When using **SIP INVITE**, you can choose the transport encryption:
| Option | Description |
| ---------------------- | ----------------------------------------------------------------------------------- |
| **TLS/SRTP** (default) | Encrypted transport. Use this unless your destination requires unencrypted traffic. |
| **UDP/RTP** | Unencrypted transport. Some legacy systems require this. |
Encryption only applies to SIP INVITE. SIP REFER and SIP BYE delegate transport to the carrier or SBC.
### Integration-level constraints
Your telephony integration (e.g. Twilio, SIP trunk) may impose constraints on handoff configuration. When constraints are active:
* The **SIP method dropdown may be locked** to a specific method (e.g. all handoffs must use SIP REFER)
* The **route/phone number field** may be hidden if routing is handled by the carrier
* The **SIP headers section** may be hidden if the integration does not support custom headers
If a handoff's method does not match the integration's required method, a warning appears on the handoff card. Update the handoff to match, or the transfer may not work as expected.
### Best practices for call handoffs
* **Use clear descriptions** – label handoffs with their intended use (e.g. "Billing disputes" not "Team A")
* **Always have a default** – ensure one handoff is marked as default so unmatched transfers have a destination
* **Test in sandbox** – verify handoff destinations route correctly before promoting to Live
* **Use SIP headers for context** – pass metadata like `X-Customer-ID` or `X-Reason` so the receiving system can route or display caller context
### Handoff reason and utterance
The built-in **handoff** template and the [`conv.call_handoff()`](/tools/classes/conv-object) helper accept two optional, structured fields:
| Field | Purpose | Example |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------- |
| `reason` | Machine-readable code explaining *why* the call is being escalated (e.g. `policy_violation`, `needs_human`, `no_availability`). Surfaces in Conversation Review and the Conversations API. | `policy_violation` |
| `utterance` | A short message the agent delivers *before* transfer begins (spoken for voice, displayed for webchat). Logged alongside the handoff for QA review. | "Let me transfer you to a specialist who can help." |
When using SIP REFER, the utterance may not play before the transfer completes because the REFER fires at the same time as function execution. If you need the utterance to be spoken reliably before transfer, use SIP INVITE instead.
**Where it shows up**
* **Flows & KB actions** – Selecting **builtin-handoff** displays *Reason* and *Pre-handoff utterance* fields.
* **Functions** – Call [`conv.call_handoff(destination="...", reason="...", utterance="...")`](/tools/classes/conv-object) to escalate programmatically.
* **Conversation Review** – Both fields appear in the metadata panel for quick troubleshooting.
* **Conversations API** – Returned inside the `handoff` object for BI dashboards or CRM routing.
**Benefits**
* Removes guesswork when diagnosing handoffs – no more relying on LLM summaries alone.
* Enables fine-grained routing rules in telephony or CRM systems.
* Gives QA teams full visibility into the exact wording customers heard.
### Handoff context handover
When the agent transfers a caller to a human, the human-side system needs context about what already happened. PolyAI exposes that context through three complementary channels — pick whichever your contact-center platform supports.
| Channel | Best for | What you get |
| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **SIP headers** | Lightweight metadata embedded in the transfer signal itself; supported by most SBCs and Twilio Flex. | Custom `X-` headers (e.g. `X-Customer-ID`, `X-Reason`, `X-Caller-ID`) populated from `conv.state` variables at transfer time. See [Configuring SIP headers](#configuring-sip-headers). |
| **[Handoff API](/api-reference/handoff/introduction)** | Larger structured payloads that don't fit in headers, or when the destination needs a "screen pop" with full context. | The `handoff` object stored in `conv.state.handoff` (or written via `conv.call_handoff()`), retrievable by `id` or `shared_id`. The `data` field is free-form JSON, so you control the shape. |
| **[Conversations API](/api-reference/conversations/introduction)** | Post-handoff CRM enrichment, BI, and QA workflows. | Full conversation including the `handoff` object with `destination`, `reason`, `utterance`, plus the entire transcript, [`conv.log`](/tools/classes/conv-log) entries, and any [custom metrics](/tools/classes/conv-object#write_metric) the agent recorded. |
The data the agent writes (and the human therefore sees) is whatever you choose to store. Common fields:
* **Identifiers** – `customer_id`, `account_number`, `shared_id` linking the PolyAI conversation to a record in your CRM
* **Verification status** – `successfully_identified`, `auth_method`, `verification_attempts` so the agent doesn't ask the caller to re-verify
* **Reason for escalation** – machine-readable `reason` (e.g. `policy_violation`, `complaint_escalation`, `customer_refund`); see [Handoff reason and utterance](#handoff-reason-and-utterance)
* **Pre-handoff utterance** – the exact wording the agent used before transferring, logged for QA
* **Caller intent and slots** – any entities collected during the AI portion of the call (order ID, refund amount, preferred callback time)
Choose **one** channel as the source of truth and keep the others consistent with it. Changes made in `conv.state.handoff` are reflected in both the Handoff API and the Conversations API automatically; SIP headers must be set explicitly on each handoff destination or via `conv.call_handoff(sip_headers={...})`.
#### Writing context from a function
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def transfer_to_billing(conv):
return conv.call_handoff(
destination="billing",
reason="refund_request",
utterance="Let me transfer you to a billing specialist who can help with that refund.",
sip_headers={
"X-Customer-ID": conv.state.customer_id,
"X-Verified": "true",
"X-Reason": "refund_request",
},
)
```
The `destination` parameter must match a handoff configured under **Voice > Handoffs**. The `reason` and `utterance` surface in [Conversation Review](/analytics/conversations/review) and the [Conversations API](/api-reference/conversations/introduction); the `sip_headers` are merged with any headers configured on the destination, with values passed here taking precedence.
#### Webchat and SMS handoffs
Webchat and SMS use the same conceptual model but signal the handoff over the [Chat API](/api-reference/chat/introduction): the `chat/respond` response includes a `handoff` object with `destination` and `reason`, and `end_conversation` is set to `true`. Your widget or SMS connector then routes the session to your live-chat platform and retrieves the full context via the Handoff API. See [Webchat and SMS handoff via the Chat API](/call-data/conversations-api/handoff-states#webchat-and-sms-handoff-via-the-chat-api) for the integration steps.
#### Feedback loops from the human side
Once a human has handled the escalation, you can close the loop in two ways:
1. **Tag the conversation in Conversation Review.** Live agents (or QA reviewers) can apply [annotations](/analytics/conversations/annotations) such as *Escalated unnecessarily* or *Wrong topic* directly on the PolyAI conversation. Tagged calls surface in [QA workflows](/learn/maintain/qa-analytics) and in the conversations Wren analyzes.
2. **Push outcomes back via the Conversations API.** If your CRM captures a resolution code on the human-handled portion, attach it back to the conversation by `shared_id`. This pairs the agent's `reason` field with the human's actual outcome, which is what [Wren](/wren/analyze) needs to answer questions like *"What share of complaint escalations were resolved on first contact?"*.
### Using your own Twilio number
If you're bringing your own Twilio phone number to route calls, follow these steps to integrate it as a handoff destination:
1. **Connect your Twilio account**:
* Ensure your [Twilio account](https://www.twilio.com/login) is set up and you have the necessary credentials ([Account SID](https://help.twilio.com/articles/14726256820123-What-is-a-Twilio-Account-SID-and-where-can-I-find-it-), [Auth Token](https://www.twilio.com/docs/iam/api/authtoken)).
* Go to **[Voice > Numbers](/voice-channel/numbers/introduction)** in the Agent Studio.
* Enter your Twilio credentials to connect your account securely.
2. **Assign a Twilio number**:
* Choose a number from your Twilio account to use for routing calls.
* If necessary, provision new numbers directly using the Twilio console.
3. **Set up routing in Twilio**:
* Configure your Twilio number to route calls to your PolyAI agent by setting the **Webhook URL** in your Twilio console. Example:
* **Voice Webhook URL**: `https://your-polyai-instance-url/voice/call`
* Make sure your webhook supports **POST** requests and uses the correct authentication methods.
4. **Add the Twilio number as a handoff destination**:
* In the **Call Handoffs** section, use the Twilio number as the "Extension / Number" field when creating a new destination.
* Add a description specifying its purpose (e.g., "Route to Twilio-based live agent team").
US-based Twilio SMS numbers must be [registered for A2P 10DLC](https://www.twilio.com/docs/messaging/compliance/a2p-10dlc#who-needs-to-register-for-a2p-10dlc). See the [Twilio handoff guide](/voice-channel/numbers/twilio/how-to-handoff) for details.
## Comparison: Call Handoff and the `transfer_call` function
These two methods serve similar purposes – routing the user to another endpoint – but are mutually exclusive and differ in flexibility and implementation.
In the table below, means the feature is supported and **–** means it is not supported.
| Feature | `Call Handoff` (UI-based) | `transfer_call` (code-based) |
| ------------------------------- | ----------------------------- | ------------------------------------ |
| Ease of setup | UI form | – Requires Python editing |
| Works in flow builder | | |
| Works in Function Editor | – | |
| Dynamic routing logic | – | Full control |
| Supports custom metrics | – | |
| Supports soft-handoff | – | |
| Best for static SIP integration | | – |
| Best for dynamic integrations | – | (e.g. Zendesk) |
The two methods can't be combined in one step — `transfer_call` overrides any UI-configured Call Handoff. If you use it, keep destination mappings in sync manually; UI changes don't propagate to function-based transfers.
The UI-based Call Handoff does not support custom metrics. Use `transfer_call` with [`conv.write_metric()`](/tools/classes/conv-object) if you need handoff reason codes for analytics.
### When to use each method
Use `Call Handoff` if:
* You want a quick setup through Agent Studio with minimal code.
* Your routing needs are straightforward and based on static values.
Use `transfer_call` if:
* You need to pass dynamic SIP headers (e.g., customer metadata).
* You want to use soft handoffs or log custom handoff metrics.
* You're integrating with a platform that does not support SIP REFER (e.g. Zendesk).
***
## Voicemail detection
PolyAI can detect voicemail in certain scenarios, but the behavior depends on the call direction and handoff method.
### Outbound calls
For outbound calls, PolyAI supports project-level voicemail detection. A detection flow at the start of the call classifies what the agent hears before the conversation begins. Common classifications include human, IVR, voicemail, and number not in service – though the exact categories depend on how the project is configured.
Based on the classification, the agent typically routes into the appropriate path. For example:
* **Human** – proceed to the greeting and main conversation flow
* **IVR** – enter an IVR traversal flow (DTMF navigation, hold loops)
* **Voicemail** – leave a message or hang up, depending on project requirements
* **Number not in service** – end the call
Detection uses barge-in on the first turn, since voicemail systems and IVRs do not wait for the agent to finish speaking. This is a project-level configuration – contact your PolyAI representative to set it up.
See [Outbound calling](/voice-channel/numbers/outbound-calling) for more on outbound call configuration.
### Inbound SIP handoffs
When PolyAI transfers an inbound call to an agent (via SIP REFER, SIP INVITE, or SIP BYE), voicemail detection on the *destination side* is **not available by default**. The behavior depends on the handoff method:
* **SIP REFER / SIP BYE** – PolyAI drops from the call after initiating the transfer, so it has no visibility into whether the destination answers or goes to voicemail.
* **SIP INVITE** – PolyAI bridges both call legs, which provides more visibility during the transfer. However, automated voicemail detection on the destination side is not a standard feature.
If you need voicemail detection as part of your inbound handoff workflow, contact your PolyAI representative to discuss what's possible for your specific setup.
***
## Related pages
Monitor transitions between automated and live agents.
Retrieve handoff context programmatically.
Twilio-specific handoff configuration.
# Voice
Source: https://docs.poly.ai/voice-channel/introduction
Configure how your agent sounds and how it listens.
This section is about how your agent speaks and listens – the TTS voice it uses, how it handles audio, what it does mid-conversation, and how it transcribes what callers say.
It is **not** about how customers reach your agent. For that, see [Phone](/voice-channel/introduction) (Numbers and Web Calling) and [Chat](/messaging-channel/introduction).
[Raven](/behavior/models/raven) produces responses that sound natural when spoken aloud. Recommended for any voice deployment.
## What lives here
The Voice page has five tabs:
Pick the TTS voice, greeting, disclaimer, style prompt, and ringing tone.
Manage cached audio to reduce latency and ensure consistent quality.
Model, call, and speech configuration — barge-in, keyphrases, pronunciation, safety filters, and more.
Supporting pages:
Browse and compare voices across providers (ElevenLabs, Cartesia, Hume, and more).
Match voice to brand, audience, and industry.
Use multiple voices in a single project.
## How to think about it
Start with [Choosing a good voice](/voice-channel/choosing-a-good-voice), then configure the voice in [Voice settings](/voice-channel/agent), and tune call-runtime behavior in [Advanced voice settings](/voice-channel/advanced/call-settings).
## Programmatic voice configuration
You can also configure voices programmatically using the [voice class](/tools/classes/voice) inside functions – for example, selecting a voice based on conversation context, caller preferences, or other runtime variables.
## Voice conversation style guide
These guidelines help your voice agent sound natural rather than robotic. They focus on the linguistic patterns that make spoken conversations feel human.
### Social presence markers
Natural conversation includes patterns that acknowledge conversational history and participants. These contribute to a sense of collaboration rather than rote routine-following.
**Use progressive tense for active collaboration:**
* "I'm not seeing any accounts under that phone number..." conveys active collaboration
* "I don't see any accounts" sounds too definitive
**Reference shared context implicitly** – don't restate what both parties already know:
* "How about Wednesday instead?" (not "How about Wednesday instead of Tuesday?")
* "In that case, how does Saturday at 2:30 sound?" (not "Since you said you prefer weekends...")
**Vary confirmationals** – use a mix of "Great," "Okay," "Perfect," and "Sure" rather than repeating the same one.
**Use conversational datives** for a collaborative feel:
* "Could you read **me** your account number?" rather than "Could you read your account number aloud?"
* "Can you log into your account **for me**?" rather than "Can you log into your account?"
**Use face-saving past tense** when referencing a user's request:
* "When **were** you trying to come in?" rather than "When are you trying to come in?"
### Avoid over-explaining
LLMs tend to justify every action in a way humans don't. Most of the time, the important information and the request can be formed into a single sentence:
* "No problem, what's your account number?" rather than "To check for outages, I'll need to look up your account. Could you tell me your account number?"
### Walkthrough conversations
When giving multi-turn walkthroughs, don't end every step with "let me know when you've done that." Provide the instruction and wait – the user will confirm on their own.
## Related
Voice transports – let customers reach your agent over the phone (Numbers) or from your website (Web Calling).
Add a text-based chat widget to your website.
# Message templates
Source: https://docs.poly.ai/voice-channel/message-templates
Send SMS messages from your agent using Twilio for confirmations, links, and follow-ups
**Prerequisites:** You need a Twilio account. See the [Twilio integration guide](/voice-channel/numbers/twilio/introduction).
Use SMS to send confirmation codes, appointment details, links, or follow-up information that callers can reference after the call ends.
Manage SMS message templates under the **Voice** channel's **Message templates** tab (**Voice > Message templates**). You can also create a template directly from **Knowledge** using the **Add SMS template** modal; once created, a template can be used in your knowledge base or rules.
## Setting up messaging
### Connect your Twilio account
1. Go to **Voice > Message templates**.
2. Click **Connect Twilio Account**.
3. In the pop-up form, fill in the following fields:
* [**Account SID**](https://help.twilio.com/articles/14726256820123-What-is-a-Twilio-Account-SID-and-where-can-I-find-it-): Find this in the "Account Info" section of your Twilio dashboard.
* [**Auth Token**](https://www.twilio.com/docs/iam/api/authtoken): Retrieve this from your Twilio account settings.
* **Twilio Phone Number**: Provide the number you wish to use for sending SMS messages.
4. Click **Connect** to link your Twilio account.
### A2P 10DLC registration (US and Canada)
If you are using a US or Canadian Twilio number, you must [register for A2P 10DLC](https://www.twilio.com/docs/messaging/compliance/a2p-10dlc#who-needs-to-register-for-a2p-10dlc) to comply with carrier regulations. Without registration, Twilio blocks SMS messages entirely.
A2P 10DLC registration can take **several weeks** and campaigns are frequently rejected. Start this process early – do not wait until launch.
#### Register
1. **Go to Twilio's A2P 10DLC registration page:**
[Twilio A2P 10DLC Registration Guide](https://help.twilio.com/articles/1260800720410-What-is-A2P-10DLC-)
2. **Complete brand and campaign registration** to comply with US and Canadian carrier regulations.
3. **Wait for approval.** This typically takes several weeks. Twilio has become increasingly strict with approvals.
4. **Once approved**, messages will send successfully.
Campaigns are often rejected for vague opt-in descriptions. Include detailed example conversation transcripts showing how callers consent to receiving SMS — linking a document with full sample transcripts significantly improves approval rates.
A2P 10DLC is specific to US and Canadian numbers. UK and other international numbers have separate regulatory requirements — see [Number availability & compliance](/voice-channel/number-availability) for country-by-country details, throughput limits, lead times, and regulation links.
**Troubleshooting:** If your messages fail to send, check the Twilio logs for these error codes:
* [**30034** - Unregistered Number](https://www.twilio.com/docs/api/errors/30034)
* [**30035** - Number Still Being Configured](https://www.twilio.com/docs/api/errors/30035)
### Add SMS templates
Once connected, follow these steps to create SMS templates:
1. Click **Add SMS template** (from the **Message templates** tab, or from **Knowledge**).
2. Fill in the form:
* **Title**: A descriptive name for the SMS template (e.g., `reservation_confirmation`). This is the name shown in the insert menu's **SMS** group and the name you reference when triggering the template in functions. Max 100 characters.
* **SMS Body**: The content of the message. Max 500 characters. Supports dynamic tokens (see below).
* **Phone Number**: Choose the Twilio phone number associated with this message.
3. Save the template to make it available during conversations.
Messages of 160 characters or fewer are sent as a single SMS segment. Longer messages are split across multiple segments, which may increase cost. Keep templates concise where possible.
### Per-environment phone numbers
Each SMS template can use a different Twilio phone number per environment, so you can test SMS in sandbox with a test number while using a production number in Live.
The phone number you select in the template form applies to the **Live** environment. Sandbox and pre-release environments can be configured separately. This prevents test messages from being sent from your production number during development.
### Messaging Service IDs
In addition to phone numbers, you can use a Twilio **Messaging Service ID** (format: `MG` followed by 32 hex characters) instead of a direct phone number. Messaging Services let Twilio manage number selection, compliance, and scaling automatically, which works well for high-volume deployments.
#### Dynamic tokens
You can insert dynamic values into SMS templates using the following syntax:
| Token type | Syntax | Example |
| ------------------------------------------------------ | ------------------------ | ---------------------------- |
| [Variant attributes](/knowledge/variants/introduction) | `${attribute_name}` | `${property_name}` |
| Entities | `{{entity:entity_name}}` | `{{entity:booking_date}}` |
| Variables | `{{vrbl:VARIABLE_ID}}` | `{{vrbl:CONFIRMATION_CODE}}` |
Example template body:
```plaintext theme={"theme":{"light":"github-light","dark":"github-dark"}}
Hi ${customer_name}, your reservation at ${property_name} is confirmed for {{entity:booking_date}}.
```
For multilingual projects, consider creating separate templates with language suffixes (e.g., `confirmation_en`, `confirmation_es`).
### Managing templates
All created SMS templates are listed under **Voice > Message templates**. You can:
* **Edit**: Modify the title, message content, or associated phone number.
* **Duplicate**: Quickly create a copy of an existing template for similar use cases.
* **Delete**: Remove unused or outdated templates.
### Using an SMS template
1. **Go to [Knowledge > FAQs tab](/knowledge/faqs/introduction)**
* Ensure you are on the FAQs tab.
2. **Add an action to a Managed Topic card**
* In any Managed Topic card, click ["Add Actions."](/knowledge/faqs/actions/send-sms)
3. **Set SMS action**
* Prompt something like "If someone asks for more details, send" in the action box.
* Open the insert menu (type `/`, or click the **+** button on the right-hand side of the box) and choose your template from the **SMS** group.
Here's an example of how to construct a prompt for your agent to send an SMS:
```plaintext theme={"theme":{"light":"github-light","dark":"github-dark"}}
After the user confirms that they would like to receive an SMS message with further details, call {{SMS_template}} to send the SMS out.
```
4. **Click 'Save' and 'Publish'**
## Compliance keywords (STOP, START, HELP)
PolyAI automatically handles the standard SMS compliance keywords required by [TCPA](https://www.fcc.gov/sites/default/files/tcpa-rules.pdf) and carrier rules. When a recipient replies with one of these keywords, PolyAI responds and updates their opt-out status without forwarding the message to your agent.
Opt-out status is tracked per (recipient number, Twilio number) pair, so a user opted out of one campaign can still receive messages from a different Twilio number.
### Supported keywords
Matching is case-insensitive and ignores leading or trailing whitespace and a single trailing punctuation character (`.`, `!`, `?`, `,`, `;`). For example, `stop`, `STOP.`, and ` Stop! ` are all treated as `STOP`.
| Keyword | Accepted variants | What happens |
| --------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **STOP** | `STOP`, `STOPALL`, `UNSUBSCRIBE`, `CANCEL`, `END`, `QUIT` | The sender is opted out. PolyAI replies with a confirmation and suppresses all future outbound SMS to that number from the same Twilio number until they opt back in. |
| **START** | `START`, `YES`, `UNSTOP` | The opt-out record is removed. PolyAI replies with a confirmation and resumes delivery of outbound SMS. |
| **HELP** | `HELP` | PolyAI replies with a static help message pointing the user to `STOP` and `START`. |
### Default reply messages
| Keyword | Reply sent to the user |
| ------- | ----------------------------------------------------------------------------------------------- |
| STOP | `You have been unsubscribed and will not receive further messages. Reply START to resubscribe.` |
| START | `You have been resubscribed and will receive messages again.` |
| HELP | `Reply STOP to unsubscribe or START to resubscribe.` |
### What this means for your agent
* **Inbound `STOP`/`START`/`HELP` replies never reach your flows or FAQs.** They are intercepted at the SMS handler, so you do not need to script keyword responses yourself.
* **Outbound SMS to opted-out recipients is silently dropped.** If a function or Managed Topic action calls `conv.send_sms` or `conv.send_sms_template` for an opted-out number, the request returns successfully but no SMS is delivered. Check the [Self-serve dashboards](/analytics/dashboards/introduction) SMS widget to monitor delivery.
* **Any other inbound message from an opted-out sender is also dropped** until they reply `START`.
* The opt-out record persists indefinitely until the user opts back in, satisfying TCPA record-keeping requirements.
A2P 10DLC campaigns in the US and Canada require you to disclose `STOP` and `HELP` behavior during opt-in. The replies above meet that requirement as-is.
## Best practices
* Keep messages short and relevant – 160 characters or fewer sends as a single segment.
* Use variant attributes or dynamic fields (e.g., customer name, booking details) to personalize messages.
* Disclose `STOP` and `HELP` keywords when collecting opt-in consent – PolyAI handles the replies automatically, but the disclosure is your responsibility.
## Handing off an SMS conversation to a live agent
To escalate an SMS conversation to a human agent in your CCaaS or CRM, use a [chat handoff integration](/integrations/chat/introduction). PolyAI proxies messages between the end user and the live agent for the rest of the session, so the user stays in the same SMS thread.
Supported chat handoff integrations include [Salesforce](/integrations/chat/salesforce), [Zendesk](/integrations/chat/zendesk), [NICE CXone](/integrations/chat/nice-cxone), [Amazon Connect](/integrations/chat/amazon-connect), [Genesys Cloud](/integrations/messaging/genesys), and [Webex by Cisco](/integrations/chat/webex).
## Example: Triggering an SMS alongside a voice handoff
Combine a [voice handoff](/voice-channel/handoffs) with an outbound SMS:
1. The agent transfers the caller to a specific agent or team using **Handoffs**.
2. At the same time, an SMS template is triggered, sending the user additional details or confirmation of the transfer.
## Integrating SMS into a function
SMS can be triggered as part of a [function](/tools/introduction) using [the `conv` object](/tools/classes/conv-object):
### `conv.send_sms`
Sends a free-form SMS message to a specified phone number.
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Send to the current caller
conv.send_sms(
to_number=conv.caller_number,
from_number="+441234567890", # your Twilio number
content="Your appointment is confirmed for tomorrow at 2pm."
)
```
### `conv.send_sms_template`
Sends a pre-configured SMS template by **template name** (the title you defined under **Voice > Message templates**).
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Send a template to the current caller
conv.send_sms_template(to_number=conv.caller_number, template="reservation_confirmation")
```
The second argument is the template **name** (the title from **Voice > Message templates**), not an ID. For example, if your template is titled `prescription_refill`, use `conv.send_sms_template(conv.caller_number, 'prescription_refill')`.
`conv.send_sms_template` queues the SMS for delivery rather than sending it synchronously. This means `try/except` blocks will **not** catch delivery failures – the call returns successfully even if the SMS later fails to send. To verify delivery, check conversation metadata or the [Self-serve dashboards](/analytics/dashboards/introduction) SMS widget.
## Session timeouts
If an SMS conversation goes idle and the session times out due to inactivity, PolyAI sends a final notification SMS to the user before closing the session:
```plaintext theme={"theme":{"light":"github-light","dark":"github-dark"}}
I am going to end conversation now
```
This only fires when the session ends because the user stopped responding (an *abandoned* session). Sessions that end because the user explicitly leaves or because the conversation reaches a natural conclusion do not trigger an additional SMS — the user already knows the conversation is ending in those cases.
The notification is sent automatically and does not require configuration. It uses the same Twilio number that the agent has been replying from for the rest of the conversation.
## Delivery and logging
* Successful SMS sends are logged in the conversation metadata.
* Failed sends increment the **API Failures** metric.
* Use the **SMS widget** on the [Self-serve dashboards](/analytics/dashboards/introduction) to monitor delivery rates.
## Related pages
Country-by-country Twilio number availability, throughput, lead times, and regulatory links.
Trigger SMS sends from Knowledge topic actions.
Connect your Twilio account to enable SMS.
Send SMS programmatically and open reply-enabled sessions.
Monitor SMS delivery rates on your own dashboard.
# Use more than one voice
Source: https://docs.poly.ai/voice-channel/multi-voice
Assign multiple voices to simulate a team of agents.
Use multi-voice when you want callers to hear different voices across interactions – simulating a team of agents or varying the experience for repeat callers. You can configure this through the UI or programmatically in Python.
Click **New voice** to add additional voices to your agent. You can select from the available languages, genders, and styles or [add a new voice](/voice-channel/add-a-new-voice).
Once added, all voices will appear in the list and can be configured independently.
After adding multiple voices, use the weight sliders to define how frequently each voice is selected during conversations. All weights must add up to 100%.
For example:
* 80% main voice
* 10% each for two alternates
For each voice, you can fine-tune the advanced settings:
* **Stability (%):** Control how consistent the voice sounds across generations.
* **Clarity and Similarity (%):** Balance naturalness and similarity to the target voice.
## Using Python in a code field
You can configure this in any function, though this guide assumes you're using the [start function](/tools/start-tool).
To use multiple voices, you need their voice IDs from your chosen TTS provider. Refer to the [function TTS provider configuration](/tools/classes) for full details on supported providers and their configuration options.
Here is an example of how to configure multiple voices, where the agent is randomly assigned a voice at the start of each interaction:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
def start_function(conv: Conversation):
conv.randomize_voice([
VoiceWeighting(
voice=ElevenLabsVoice(
voice_id="LcfCDJNUPlGQjkzn1xUU",
similarity_boost=0.2,
stability=0.4
),
weight=0.5
),
VoiceWeighting(
voice=RimeVoice(
provider_voice_id="s3://path-to-manifest.json",
style="neutral"
),
weight=0.5
)
])
```
* `conv.randomize_voice([...])`: Selects a voice at random based on the assigned weights.
* `VoiceWeighting`: Associates a voice with a probability of being selected.
* **Weights** define selection probability:
* The sum of all weights must equal 1.0.
## Understanding voice weights
You can add a maximum of **ten voices** to an AI agent.
In the UI, weights are displayed as percentages (must total 100%). In code, weights are decimal values (must total 1.0).
The `weight` parameter determines how often each voice is selected. The sum of all weights must equal **1.0** (or 100% in the UI).
Examples:
* One voice at `1.0`: Always uses that voice.
* Two voices at `0.5` each: Each is selected 50% of the time.
* Four voices at `0.25` each.
## Personalizing repeat caller experiences
Assigning different voices to repeat callers can help create a sense of interacting with different team members, which may reduce escalation requests. Use the `conv.randomize_voice()` method in your [start function](/tools/start-tool) to vary voice selection across interactions.
## Related pages
Browse and select voices
Guidelines for voice selection and matching
Configure custom voices programmatically
Configure voice settings and fine-tuning
# Number availability & compliance
Source: https://docs.poly.ai/voice-channel/number-availability
Country-by-country Twilio number availability, throughput limits, lead times, and regulatory links for 2-way SMS.
2-way SMS lets your PolyAI agent both send **and** receive text messages in a conversation — the caller can reply to an SMS and the agent continues the dialogue over text. This is different from outbound-only SMS (confirmations, links, follow-ups) where no reply is expected.
Not every country supports 2-way SMS, and the type of phone number you use affects throughput, cost, and lead time. This page helps you plan which countries you can cover, which number type to choose, and what compliance steps to complete before launch.
PolyAI uses [Twilio](https://www.twilio.com/) as the underlying SMS provider. The availability data, throughput figures, and regulation links below come from Twilio's published guidelines.
## Number types
There are two main types of SMS-capable phone number. Choosing between them depends on your expected message volume and how quickly you need to go live.
**MPS** (messages per second) is the maximum rate at which a number can send SMS. Higher MPS means you can handle more concurrent conversations without queuing delays.
| Type | Best for | Throughput (MPS) | Typical lead time |
| -------------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------- | -------------------------------------- |
| **Short code** (5–6 digits) | High-volume transactional messaging. Highest throughput and best carrier deliverability. | 1,000–3,000 (US/CA) | 6–16 weeks (carrier approval required) |
| **Long code** (standard phone number format) | Conversational 2-way SMS, moderate volume. | 10–225 depending on country | Immediate to several weeks |
For US and Canada, long codes are capped at **225 MPS total** across AT\&T, T-Mobile, and Verizon (75 MPS per carrier). If you need higher throughput, apply for a short code — but factor in the 6–10 week approval timeline.
## Availability by country
"Private Offering" means the number type is available but requires a direct arrangement with Twilio — contact your Twilio account manager to set it up.
### North America
| Country | 2-way SMS | Short code | SC MPS | SC lead time | Long code | LC MPS | LC lead time | Regulations |
| ------------- | --------- | ---------- | ----------- | ------------ | --------- | -------------------------- | ------------ | ------------------------------------------------------------------- |
| United States | Yes | Available | 1,000–3,000 | 6–10 weeks | Available | 225 (75 per major carrier) | Immediate | [US SMS guidelines](https://www.twilio.com/en-us/guidelines/us/sms) |
| Canada | Yes | Available | 1,000–3,000 | 6–10 weeks | Available | 225 (75 per major carrier) | Immediate | [CA SMS guidelines](https://www.twilio.com/en-us/guidelines/ca/sms) |
### Europe — 2-way SMS supported
| Country | Short code | SC MPS | SC lead time | Long code | LC MPS | LC lead time | Regulations |
| -------------- | ------------- | ------ | ---------------- | --------- | ------ | ---------------- | --------------------------------------------------------------- |
| United Kingdom | Available | 300+ | 10–16 weeks | Available | 10 | Immediate | [UK guidelines](https://www.twilio.com/en-us/guidelines/gb/sms) |
| Germany | Available | — | Private Offering | Available | 10 | Immediate | [DE guidelines](https://www.twilio.com/en-us/guidelines/de/sms) |
| France | Available | — | Private Offering | Available | 10 | Private Offering | [FR guidelines](https://www.twilio.com/en-us/guidelines/fr/sms) |
| Ireland | Not available | — | — | Available | 10 | — | [IE guidelines](https://www.twilio.com/en-us/guidelines/ie/sms) |
| Netherlands | Not available | — | — | Available | 10 | Immediate | [NL guidelines](https://www.twilio.com/en-us/guidelines/nl/sms) |
| Italy | Not available | — | — | Available | 10 | Private Offering | [IT guidelines](https://www.twilio.com/en-us/guidelines/it/sms) |
| Czech Republic | Not available | — | — | Available | 10 | Immediate | [CZ guidelines](https://www.twilio.com/en-us/guidelines/cz/sms) |
### Europe — 2-way SMS not supported
These countries do not currently support 2-way SMS through Twilio. Neither short codes nor long codes are available for bidirectional messaging.
| Country | Regulations |
| -------- | --------------------------------------------------------------- |
| Croatia | [HR guidelines](https://www.twilio.com/en-us/guidelines/hr/sms) |
| Bulgaria | [BG guidelines](https://www.twilio.com/en-us/guidelines/bg/sms) |
| Slovakia | [SK guidelines](https://www.twilio.com/en-us/guidelines/sk/sms) |
## Compliance requirements
Carrier regulations vary by country and must be met **before** you send any messages. Non-compliance can result in messages being silently dropped or your number being suspended.
### US and Canada — A2P 10DLC
US and Canadian long code SMS requires [A2P 10DLC registration](/voice-channel/message-templates#a2p-10dlc-registration-us-and-canada). Without it, carriers block messages entirely. See the [SMS overview](/voice-channel/message-templates) for the full registration walkthrough.
For short code campaigns:
* **US**: Follow the [CTIA Short Code Monitoring Handbook](https://api.ctia.org/wp-content/uploads/2024/01/CTIA-Short-Code-Monitoring-Handbook-v1.9-FINAL.pdf) and [Messaging Principles and Best Practices](https://api.ctia.org/wp-content/uploads/2023/05/230523-CTIA-Messaging-Principles-and-Best-Practices-FINAL.pdf).
* **Canada**: Follow the [Canadian Wireless Telecommunications Association (txt.ca)](https://txt.ca/) guidelines.
* Both US and CA short code applications require a screenshot or mock-up of the opt-in process.
### United Kingdom
* Follow the [UK Mobile Network Operator Code of Practice](https://www.twilio.com/en-us/guidelines/gb/sms).
* Short code provisioning takes **10–16 weeks** and goes through a separate carrier approval process.
* Long codes are available immediately through the Twilio Console.
### EU countries
Each EU country has its own regulatory framework — check the regulation links in the tables above. Key considerations:
* Some countries (France, Italy) require a **Private Offering** arrangement with Twilio for number provisioning.
* GDPR applies to all EU SMS communications — ensure you have appropriate consent and opt-out mechanisms.
## Pre-launch checklist
Before going live with 2-way SMS in any country:
1. **Confirm number availability** — check the tables above to verify your target country supports 2-way SMS and that the number type you need is available.
2. **Provision your number** — long codes can often be provisioned immediately through the Twilio Console; short codes require a carrier application.
3. **Complete registration** — for US/CA, complete [A2P 10DLC registration](/voice-channel/message-templates#a2p-10dlc-registration-us-and-canada). For other countries, follow the linked regulatory guidelines.
4. **Validate your campaign** — use the [A2P Campaign Pre-Scanner](https://www.a2pcheck.com/) to check your setup against carrier requirements before sending.
5. **Connect to PolyAI** — follow the [SMS setup guide](/voice-channel/message-templates#connect-your-twilio-account) to link your Twilio number to your PolyAI project.
## Industry resources
### Regulatory bodies
* [CTIA — The Wireless Association](https://www.ctia.org/the-wireless-industry/industry-commitments/messaging-interoperability-sms-mms) — US industry body governing short code campaigns and A2P messaging standards.
* [Canadian Wireless Telecommunications Association (CWTA / txt.ca)](https://txt.ca/) — Canadian equivalent; governs short code applications and carrier compliance.
* [Ofcom — Premium Rate Services](https://www.ofcom.org.uk/make-a-complaint/complain-about-premium-rate-services/quick-guide-to-premium-rate-services-prs) — UK regulator for premium-rate and short code services (took over from the PSA in 2025).
* [BEREC — Body of European Regulators for Electronic Communications](https://www.berec.europa.eu/) — EU-wide telecom regulatory coordination.
### Twilio documentation
* [Twilio Messaging Guidelines by Country](https://www.twilio.com/en-us/guidelines) — per-country SMS regulations, sender types, and carrier requirements.
* [A2P 10DLC Overview](https://www.twilio.com/docs/messaging/compliance/a2p-10dlc) — registration walkthrough for US and Canadian long code SMS.
* [Twilio Short Code Guide](https://www.twilio.com/docs/numbers-and-senders/short-codes) — how short codes work, provisioning, and compliance requirements.
* [Twilio Short Code Provisioning in Console](https://help.twilio.com/articles/30888460607003) — step-by-step application walkthrough.
* [Twilio Messaging Services](https://www.twilio.com/docs/messaging/services) — number pooling, compliance, and scaling for high-volume use cases.
* [Twilio Phone Number Regulatory Compliance](https://www.twilio.com/docs/phone-numbers/regulatory/getting-started) — identity and address verification requirements by country.
### Carrier compliance
* [CTIA Short Code Monitoring Handbook (v1.9, PDF)](https://api.ctia.org/wp-content/uploads/2024/01/CTIA-Short-Code-Monitoring-Handbook-v1.9-FINAL.pdf) — rules for US short code campaigns.
* [CTIA Messaging Principles and Best Practices (May 2023, PDF)](https://api.ctia.org/wp-content/uploads/2023/05/230523-CTIA-Messaging-Principles-and-Best-Practices-FINAL.pdf) — US carrier expectations for A2P messaging content and opt-in.
* [UK Code of Practice for Common Short Codes](https://www.short-codes.com/short-code-uk/uk-code-of-practice/) — UK MNO requirements for short code services.
### Compliance tools
* [A2P Campaign Pre-Scanner](https://www.a2pcheck.com/) — validate your campaign setup against carrier requirements before launch.
* [Twilio Trust Hub](https://www.twilio.com/docs/trust-hub) — manage brand and campaign registrations for A2P compliance.
## Related pages
Set up Twilio, create templates, and send SMS from your agent.
Send SMS programmatically and open reply-enabled sessions.
Connect your Twilio account for voice and SMS.
Purchase PolyAI-provisioned numbers for voice.
# Buy a number from PolyAI
Source: https://docs.poly.ai/voice-channel/numbers/how-to-buy-number
Purchase phone numbers directly in Agent Studio.
You can purchase a phone number directly from PolyAI without needing a third-party telephony provider like Twilio. PolyAI-provisioned numbers are preconfigured to route calls to your agent immediately.
Managing many numbers? The [Agents API](/api-reference/agents/introduction) exposes bulk [import](/api-reference/agents/endpoint/phone-numbers/import-phone-numbers-into-a-project), [get](/api-reference/agents/endpoint/phone-numbers/batch-get-phone-numbers), and [reassign](/api-reference/agents/endpoint/phone-numbers/reassign-a-phone-number-to-a-different-connector) operations.
## Purchase a number
Go to **Voice > Numbers** in the sidebar.
Click **Add Number**.
Choose **US**, **UK**, or **Canada** depending on where your callers are located.
For **US** and **Canada** numbers, you can optionally specify a **3-digit area code** to source the number from a specific region (for example, `415` for San Francisco or `416` for Toronto). Leave the field empty to let PolyAI auto-assign one from the available pool.
Both countries share the North American Numbering Plan (NANP), so area code selection works identically.
Click **Add Number**. The number provisions in seconds and is ready for use.
Once provisioned, you can assign the number to a specific [environment](/environments-and-versions/introduction) (Sandbox, Pre-release, or Live) to control which version of your agent receives calls.
Use separate numbers for each environment to avoid accidentally routing test calls to production.
## Next steps
Deploy numbers across Sandbox, Pre-release, and Live environments
Use existing Twilio numbers instead of purchasing new ones
Configure proactive outbound calls for reminders and follow-ups
Add, delete, and assign numbers across environments
# Numbers
Source: https://docs.poly.ai/voice-channel/numbers/introduction
Connect phone numbers to your agent using PolyAI, Twilio, or outbound calling
Numbers is how PolyAI connects your agent to the public phone network. Provision a number directly through PolyAI, bring your own Twilio number, or route through your existing contact-centre provider — every option terminates on the same agent runtime so the conversation logic is identical across channels. Manage everything in the **Numbers** tab on the Voice page, where you configure SIP addresses (Main SIP and Fallback SIP) and view the numbers assigned to each environment.
| Option | Best for | What you get |
| ----------------------------------------------- | --------------------- | ---------------------------------------------------------- |
| **[Buy a number](./how-to-buy-number)** | No existing provider | Purchase directly from PolyAI. |
| **[Twilio integration](./twilio/introduction)** | Existing Twilio setup | Connect your Twilio numbers. Voice, SMS, and Flex routing. |
| **[Outbound calling](./outbound-calling)** | Proactive outreach | Agent-initiated calls through API or SIP. |
You can also connect through other contact center platforms like [Genesys](/integrations/voice/sip/genesys), [Five9](/integrations/voice/sip/five9), or [NICE CXone](/integrations/voice/sip/NICECXone) with SIP. See [Integrations](/integrations/introduction) for the full list.
## Integration options
Purchase a number directly from PolyAI.
Voice, SMS, and contact center routing with Twilio.
Agent-initiated calls for reminders and follow-ups.
## Twilio integration guides
* **[Twilio overview](./twilio/introduction)** – Benefits and setup overview
* **[Phone number integration](./twilio/how-to-integrate-voice)** – Connect a Twilio-hosted number
* **[SMS integration](/voice-channel/message-templates)** – Set up SMS messaging with Twilio
* **[Twilio Flex integration](/integrations/voice/twilio)** – Advanced contact center routing
## Outbound calling
Agent-initiated calls for reminders, follow-ups, and notifications.
* **[Outbound calling overview](./outbound-calling)** – Methods and best practices
* **[Outbound Calling API](/api-reference/outbound/introduction)** – Programmatically trigger calls
* **[SIP integration](/integrations/voice/sip/custom-sip)** – Route through your SIP infrastructure
## Automate with the Agents API
Telephony plumbing is scriptable too — helpful for bulk provisioning and for reshaping routing as part of a broader deployment.
Connectors bind phone numbers to voice infrastructure; phone numbers are E.164 strings routed through a connector. The [Agents API](/api-reference/agents/introduction) has CRUD for both.
```bash curl theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Import a phone number onto an existing connector
curl -X POST https://api.us.poly.ai/v1/agents/AGENT_ID/telephony/phone-numbers/+442071234567 \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "connectorId": "conn_abc123" }'
# Reassign a number to a different connector
curl -X PATCH https://api.us.poly.ai/v1/agents/AGENT_ID/telephony/phone-numbers/+442071234567 \
-H "x-api-key: $POLYAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "connectorId": "conn_new456" }'
```
```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os, requests
BASE = "https://api.us.poly.ai"
HEADERS = {"x-api-key": os.environ["POLYAI_API_KEY"]}
# Look up which connector serves a given number
resp = requests.post(
f"{BASE}/v1/agents/{AGENT_ID}/telephony/connectors/lookup",
headers=HEADERS,
json={"phoneNumber": "+442071234567"},
)
connector = resp.json()
```
See [Connectors](/api-reference/agents/endpoint/connectors/list-all-connectors-for-a-project) and [Phone numbers](/api-reference/agents/endpoint/phone-numbers/list-all-phone-numbers-for-a-project) for the full endpoint list.
## Related pages
Configure how calls are routed to your agent
Connect to Genesys, Five9, NICE, and other platforms
Send text messages during voice conversations
Import and reassign numbers via the Agents API.
# Outbound calling
Source: https://docs.poly.ai/voice-channel/numbers/outbound-calling
Configure outbound calls for appointment reminders, follow-ups, and notifications
PolyAI supports outbound calling for appointment reminders, follow-ups, and automated notifications.
## Prerequisites
* An active PolyAI project
* Outbound calling enabled (contact your PolyAI representative)
* A phone number configured for outbound calls
**Outbound calling requires configuration by PolyAI.** Contact your account manager or PolyAI representative to enable this feature.
## Outbound calling methods
Programmatically trigger calls through the REST API
Route outbound calls through your SIP infrastructure
## Using the Outbound Calling API
The [Outbound Calling API](/api-reference/outbound/introduction) lets you programmatically trigger calls and monitor their status:
* **Appointment reminders** - Call customers before scheduled appointments
* **Follow-up calls** - Re-engage customers after specific events
* **Notifications** - Deliver time-sensitive information by voice
* **Campaigns** - Run proactive outreach at scale
### Quick start
1. Obtain your authentication token from your PolyAI representative
2. Use the base URL provided for your project by PolyAI:
* US: `https://api.us-1.platform.polyai.app`
* UK: `https://api.uk-1.platform.polyai.app`
* EUW: `https://api.euw-1.platform.polyai.app`
3. Trigger a call:
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api.us-1.platform.polyai.app/v1/outbound-calling \
-H "X-PolyAi-Auth-Token: YOUR_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to_number": "+14155551234",
"metadata": {
"customer_name": "John",
"appointment_time": "2:00 PM"
}
}'
```
4. Monitor call status using the returned `call_sid`:
Call status data is retained for approximately **2 hours** after the call ends. Poll and store status data before it expires if you need it longer.
```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "https://api.us-1.platform.polyai.app/v1/outbound-calling/{call_sid}/status" \
-H "X-PolyAi-Auth-Token: YOUR_AUTH_TOKEN"
```
For complete API documentation, see the [Outbound Calling API reference](/api-reference/outbound/introduction).
## SIP-based outbound calling
If your telephony setup uses SIP, you can route outbound calls through your existing infrastructure instead of using the API. This is a good option if you already have a SIP-based contact center and want to keep routing under your control.
SIP-based outbound calling supports:
* Custom SIP header injection for the outbound leg
* Integration with your contact center platform
* Routing through your preferred carrier
When using custom SIP handoffs, you can specify the outbound endpoint in your function:
```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
return {
"handoff": True,
"outbound_caller_id": conv.caller_number,
"outbound_endpoint": "YOUR_OUTBOUND_ENDPOINT_NAME"
}
```
For detailed SIP configuration, see the [Custom SIP integration guide](/integrations/voice/sip/custom-sip).
## Twilio-based outbound calling
**Twilio-based outbound calling requires configuration by PolyAI.** Contact your PolyAI representative to set this up for your project.
If you're integrated with Twilio, outbound calls can be routed through your Twilio account. This uses your existing Twilio infrastructure and phone numbers.
## Recipient detection
Outbound calls need to determine who – or what – answered before the conversation begins. This is handled by a **custom detection flow** that you build as the first step of your outbound project. It is not an out-of-the-box feature – it requires flow logic designed by your project team.
On the first turn, the agent classifies the recipient into one of several categories. Typical classifications include:
* **Human** – a live person answered; proceed to the greeting and main conversation
* **IVR** – an automated phone system answered; navigate menus with DTMF or hold
* **Voicemail** – a voicemail system answered; leave a message or hang up
* **Number not in service** – the number is disconnected; end the call
* **Operator** – a switchboard operator answered
The exact categories and how the agent decides between them are defined in the detection step's prompt and classification function – they are fully customizable per project.
### How recipient detection works
Detection runs on the first step of the outbound flow. A typical implementation involves:
1. **A classification function** – the step prompt instructs the agent to evaluate what the recipient says on the first turn and classify it (e.g. human greeting, IVR menu, voicemail recording, or out-of-service message). The agent calls a function with the detected category and routes to the appropriate path.
2. **Barge-in enabled** on the detection step – voicemail greetings and IVR prompts do not wait for the agent to finish speaking, so barge-in prevents the agent from talking over them.
3. **Speech recognition tuning** – detection steps often use specialized ASR settings to improve transcription accuracy for pre-recorded messages and low-quality automated voices. Your PolyAI team can configure these settings for your project.
Once the recipient is classified, the agent routes to the appropriate path – for example, entering an IVR traversal flow, leaving a voicemail, or starting the main conversation.
### Voicemail actions
Depending on project requirements, the agent can:
* **Leave a voicemail** – deliver a scripted or dynamic message after the beep
* **Hang up** – end the call immediately if voicemail is not in scope
* **Retry later** – exit with a status code so the calling system can schedule a retry
Recipient detection is a project-level flow pattern, not a toggle you can enable in settings. Work with your project team to design the detection step, classification function, and routing logic. See [Call handoffs](/voice-channel/handoffs#voicemail-detection) for how voicemail detection differs in inbound scenarios.
## IVR traversal
When the agent detects it has reached an IVR, it can navigate the phone menu to reach a human representative or collect information from the IVR itself (e.g. hours of operation from a pre-recorded message).
**IVR traversal is not a built-in platform feature.** It requires custom flow design specific to your use case. Work with your PolyAI team to implement IVR navigation for your project.
### How it works
A typical IVR traversal flow uses three capabilities:
* **DTMF output** – the agent sends keypad tones to select IVR menu options (e.g. "Press 1 for sales"). See [DTMF](/flows/dtmf) for configuration details.
* **Wait on hold** – after selecting an option, the agent waits for a human to answer. The flow includes a dedicated hold step where the agent stays silent until someone picks up.
* **Escape hatch** – if the agent gets stuck in the wrong branch of the IVR, it ends the call gracefully so the system can retry later.
The agent transcribes the IVR menu, determines which option to select, and sends the appropriate DTMF tone or spoken response. If the IVR transfers to a hold queue, the agent waits silently until a human answers.
### Design considerations
* **Looping IVRs** – some phone trees loop back to the main menu. Always include an escape path so the agent can end the call and retry rather than getting stuck in a loop.
* **Speech vs. DTMF** – some IVRs accept spoken commands ("say 'agent'") while others only accept keypresses. Your flow may need to handle both output channels depending on the menu instructions.
* **Long hold times** – the agent must stay silent while on hold and respond quickly when a human picks up. Your PolyAI team can tune speech recognition and silence settings for hold scenarios.
* **Low-quality audio** – IVRs often use automated voices that are harder to transcribe. Specialized speech recognition settings can improve accuracy on these steps.
## Best practices
* **Validate phone numbers** - Use E.164 format (e.g., `+14155551234`)
* **Respect time zones** - Schedule calls during appropriate hours for the recipient
* **Handle failures** - Implement retry logic with exponential backoff
* **Pass context through metadata** - Include customer information to personalize conversations
* **Monitor outcomes** - Track delivery status for optimization
## Call status tracking
When using the API, you can track call progress through these statuses:
| Status | Description |
| --------- | ------------------------------------------ |
| `queued` | Call has been queued for processing |
| `calling` | Call is being placed to the destination |
| `success` | Call completed successfully |
| `failure` | Call failed to connect or was not answered |
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
stateDiagram-v2
[*] --> queued: Trigger call
queued --> calling: Call initiated
calling --> success: Call completed
calling --> failure: Connection error / no answer / busy
success --> [*]
failure --> [*]
```
## Related pages
Complete API documentation and examples
API endpoint to initiate calls
Monitor call progress and retrieve results
Configure SIP-based calling through your infrastructure
# Phone numbers
Source: https://docs.poly.ai/voice-channel/numbers/route-management
Add, manage, and track phone numbers used by your agent.
Phone number management is available under **Voice > Numbers** once a telephony integration has been established between your telephony provider and PolyAI.
Use the Numbers page to add, delete, and assign phone numbers across your agent's environments. Numbers are grouped by environment – sandbox, pre-release, and live – so you can manage test and production numbers separately.
## Prerequisites
* A telephony integration must be configured between your provider and PolyAI (SIP peering, Twilio, etc.)
* Your account must have the appropriate [access permissions](/user-management/access-control-scope)
If no integration exists yet, the page displays a message asking you to contact the PolyAI team.
## Viewing numbers
Navigate to **Voice > Numbers** to see all phone numbers for your project, grouped into collapsible sections by environment:
* **Sandbox** – numbers for testing your latest saved version
* **Pre-release** – numbers for pre-production validation
* **Live** – numbers serving production traffic
Each section displays a table with:
| Column | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Number** | The phone number (with country code) |
| **Source** | The telephony integration the number belongs to (e.g., Twilio, PolyAI) |
| **Assigned variant** | Which [variant](/knowledge/variants/introduction) handles calls on this number (only shown if your project uses variants) |
Each environment section also shows **SIP addresses** (main and fallback) that you can copy for use in your telephony configuration.
## Adding a number
1. Select **Number** (the add button) in the environment section where you want the number
2. Choose the number type:
* **UK**, **US**, or **Canada (CA)** phone number
3. For **US** and **Canada** numbers, you can optionally specify a 3-digit **area code**. Both countries share the North American Numbering Plan (NANP), so area code selection works identically.
4. If your project uses [variants](/knowledge/variants/introduction), optionally assign the number to a variant
5. Select **Add**
Phone numbers are purchased through PolyAI's telephony infrastructure. You can also connect an existing Twilio number – select that option in the modal and follow the instructions.
## Assigning a variant
If your project uses [variants](/knowledge/variants/introduction), you can assign or reassign a variant to any number:
1. Open the actions menu (three-dot icon) on a number row
2. Select **Reassign variant**
3. Choose the variant from the list
4. Select **Assign**
Variants must be published to the target environment before they can be assigned. If no variants are available, publish the project first.
## Deleting a number
1. Open the actions menu on a number row
2. Select **Delete number**
3. Confirm the deletion
Deleting a live number removes it immediately. Calls to that number will stop reaching your agent.
## Variant-related callouts
The Numbers page displays contextual messages when variant changes affect your numbers:
* **New variant added** – publish the project so you can assign the new variant to numbers
* **Variant deleted** – some assigned variants were removed; publish to see the changes
* **Draft variants** – your project now uses variants; publish so you can assign them
## Fallback and routing configuration
Fallback routing and advanced telephony settings (language, SIP REFER/INVITE configuration, per-route fallback numbers) are not configured through the Numbers UI. These are managed at the infrastructure level by PolyAI. Contact your PolyAI account manager if you need to:
* Configure a fallback number for when the agent is unavailable
* Set up language-specific routing
* Configure SIP REFER or INVITE handoff behavior
## Related pages
Step-by-step guide for purchasing a phone number
Configure variants for multi-site deployments
Connect your existing Twilio account
Configure proactive outbound calls
# Handoff integration
Source: https://docs.poly.ai/voice-channel/numbers/twilio/how-to-handoff
Set up handoff destinations with Twilio
Configure handoff destinations so your agent can transfer calls to live agents or specific departments through your Twilio-connected number. When a handoff triggers, the call routes to the destination number you specify.
## Prerequisites
* Your agent must be [integrated with a Twilio phone number](/voice-channel/numbers/twilio/how-to-integrate-voice).
* You need the phone number or extension for each transfer destination.
## Setup
Go to **Voice > Handoffs**.
Click **Add Handoff** and fill in:
* **Handoff destination name** – A descriptive label (e.g., "Billing support", "Front desk").
* **Extension/Number** – The phone number to transfer to, in [E.164 format](https://www.twilio.com/docs/glossary/what-e164) (must start with `+`, e.g., `+14155551234`).
* **Description** – When this handoff should be used (e.g., "When the caller requests to speak with billing").
Click **Add**. The destination is now available for use in [FAQs actions](/knowledge/faqs/actions/handoff) and [functions](/tools/return-values).
## Test your handoff
1. Add the handoff as an [action on a Managed Topic](/knowledge/faqs/actions/handoff).
2. Test in **Sandbox** by triggering the topic that should initiate the transfer.
3. Verify the call routes to the correct destination.
## Next steps
Configure SIP methods and headers for transfers
Retrieve handoff context programmatically
Advanced contact center routing
Set up your Twilio number with PolyAI
# Phone number integration
Source: https://docs.poly.ai/voice-channel/numbers/twilio/how-to-integrate-voice
Integrate a Twilio phone number with PolyAI Agent Studio
Connect a phone number hosted in your [Twilio](https://www.twilio.com/) account to your PolyAI agent. This routes incoming calls from your Twilio number to your agent using [SIP](https://en.wikipedia.org/wiki/Session_Initiation_Protocol) (Session Initiation Protocol).
## Prerequisites
* A Twilio account with at least one phone number
* A PolyAI agent project
## Integration steps
1. Go to your agent page and click **Voice > Numbers** in the sidebar.
2. Scroll to **Connect Twilio Phone Number**.
3. Click **Generate SIP** to create a SIP address for your agent.
4. Copy the generated SIP address – you will need it in the next step.
1. Log in to your [Twilio console](https://console.twilio.com/).
2. Navigate to **Phone Numbers > Manage > Active Numbers**.
3. Select the phone number you want to connect.
4. Under **Voice configuration**, set the incoming call routing to use the SIP address you generated in Agent Studio.
5. Save your changes.
Call your Twilio number and confirm the call reaches your PolyAI agent. Review the conversation in **Conversations**, filtered to Voice, to verify the call was handled correctly.
## Next steps
Set up SMS messaging with your Twilio account
Configure call transfers to live agents
Advanced contact center routing setup
Explore other phone number integration options
# Twilio
Source: https://docs.poly.ai/voice-channel/numbers/twilio/introduction
Connect your Twilio account to use existing phone numbers and SMS with PolyAI
Use Twilio with PolyAI to use your existing phone numbers, SMS infrastructure, and contact center setup.
## Why use Twilio with PolyAI
If your organization already uses Twilio for telephony, you can connect your existing numbers rather than purchasing new ones. This lets you:
* **Keep your current numbers** – No need to port or change numbers your customers already know.
* **Use Twilio SMS** – Send SMS messages during or after calls for confirmations, links, or follow-ups.
* **Integrate with Twilio Flex** – Route calls between your PolyAI agent and live agents in your Flex contact center.
## Integration guides
Connect a Twilio-hosted phone number to your agent. Start here if you just need voice.
Set up SMS messaging so your agent can send texts during or after calls.
Route calls between PolyAI and your Flex contact center for live agent handoff.
## Related pages
Configure handoff destinations for live agent transfers
General telephony setup and phone number management
Configure proactive calls through Twilio
# Voice library
Source: https://docs.poly.ai/voice-channel/voice-library
Browse, preview, and select voices for your agent.
The **Voice library** lets you explore, preview, and select voices for your agent. Access it from **Voice > Settings**.
The Voice library includes filters, favorites, and custom text preview.
## Accessing the Voice library
1. Go to **Voice > Settings** in the sidebar.
2. Click **Change** next to either the **Agent** or **Disclaimer** voice section.
3. The Voice library opens with two tabs: **Explore** and **Favorites**.
## Explore tab
Browse all available voices with filtering options:
| Filter | Description |
| ------------ | --------------------------------------------------------------- |
| **Language** | Filter by supported language (e.g., English, Spanish, French) |
| **Region** | Filter by accent or regional variant (e.g., US, UK, Australian) |
| **Gender** | Filter by voice gender |
### Voice cards
Each voice displays:
* **Voice name** and provider
* **Language** and region flags
* **Style tags** describing the voice characteristics
* **Play button** to preview the voice
### Previewing voices
1. Click the **play button** on any voice card to hear a sample
2. Enter **custom text** in the preview field to hear your own content
3. Compare multiple voices before selecting
Preview voices with text that matches your agent's typical responses.
## Favorites tab
Save voices you frequently use or want to evaluate later.
* Click the **heart icon** on any voice card to add it to Favorites.
* Access your saved voices quickly from the **Favorites** tab.
* Remove voices from Favorites by clicking the heart icon again.
## Selecting a voice
1. Find a voice using **Explore** or **Favorites**.
2. Click the voice card to select it.
3. Click **Select** to apply the voice to your agent.
The selected voice is applied to either the **Agent** or **Disclaimer** section depending on which **Change** button you originally clicked.
## Voice settings
After selecting a voice, fine-tune its Stability, Clarity, and Style parameters. See [Agent Voice – voice settings](/voice-channel/agent#voice-settings) for full details on each slider.
## Separate voice management
The Voice library supports separate voice selection for **Agent** (main voice) and **Disclaimer** (legal/informational). See [Agent Voice](/voice-channel/agent) for details on each section.
## Publishing changes
After selecting and configuring voices, click **Publish** in the top right corner to apply changes. See [Agent Voice – publishing](/voice-channel/agent#publishing-changes) for details.
## Related pages
Configure voice settings and fine-tuning
Guidelines for voice selection and matching
Channel-specific voice and call handling settings
Use multiple voices in a single agent
# Configure widget
Source: https://docs.poly.ai/widgets/configure
Brand your widget, write the copy, set up consent. One editor for Phone and Chat.
Build a widget that matches your brand and ship it from a single editor in Agent Studio. The editor is the same whether you're configuring a Phone or Chat widget. Differences are called out inline.
## Creating a widget
From **Widgets**, click **Add widget** to open the **Add widget** dialog. (When no widgets exist yet, the page shows an empty state with the same button.)
The dialog asks you to pick a **Widget type** first (**Phone** for Web Calling, **Chat** for Webchat), then fill in:
A name to identify this widget (e.g., "Main website", "Support landing page").
The domain where this widget will load. The widget only renders on this origin. Embed attempts from other domains are ignored.
The [variant](/knowledge/variants/introduction) this widget connects to. Use variants for location-specific or A/B-tested deployments.
Sandbox, Pre-release, or Live. You can promote a widget through environments without re-embedding.
Click **Add** to create the widget. You'll land in the editor.
The **Chat** widget type is disabled until you've completed [Chat configuration](/messaging-channel/advanced/chat-configuration). **Phone** widgets are available immediately — no chat agent required.
## Widget editor
The editor has three tabs: **Styling**, **Content**, and **Embed**. A live preview panel on the right updates as you type.
### Header controls
* **Widget selector**: switch between widgets in this project.
* **Widget type indicator**: shows whether you're editing a Phone or Chat widget. Set at creation, can't be changed later.
* **Save and publish**: deploy changes to the selected environment.
* **Unpublished changes** indicator: appears when the widget has unsaved or unpublished edits.
## Styling tab
Match the widget to your brand. Some controls are shared, others are type-specific. The editor only shows the fields that apply to the widget you're editing.
### Shared
Display name for the agent (defaults to the variant's agent). Used in both the call card (Phone) and the chat header / message rows (Chat). 1 to 50 characters. Example: "Emily".
The launcher is a 60 px circle on desktop, 44 px on mobile. Check contrast against its white iconography to keep the call/chat button legible.
### Phone-only
Choose between **Dark** or **Light**. This controls the widget background, animation, and buttons. Stored as `theme_color` (`"dark"` or `"light"`) on the Web Calling configuration. Custom hex values aren't supported in v1.
### Chat-only
These fields appear only when the widget type is **Chat**:
Accent color for suggestion bubbles, the widget button, header, and user chat bubbles. Enter a hex code. Example: `#c3da28`. Stored as `color` on the Webchat configuration.
The title shown in the chat header. Use your brand or product name. Example: "Acme Support".
Optional. Logo or wordmark for the chat header. JPEG / JPG / PNG up to 2 MB. Min. 400 × 100 px (recommended 800 × 200 px). Choose a fit option of **Fit** (preserve aspect ratio) or **Fill** (crop to frame).
Optional. Avatar shown alongside agent messages. JPEG / JPG / PNG / static GIF up to 1 MB. Min. 128 × 128 px (recommended 256 × 256 px). Same Fit / Fill options.
Phone widgets don't render a header logo or agent avatar. The call card is intentionally minimal so the focus stays on the live audio. Use the agent name and primary color to brand it.
## Content tab
Write the copy your visitor sees when they open the widget: welcome message, CTAs, and your disclaimer.
### Welcome message
Shown on the widget's start screen. Phone widgets default to "Hi I'm \[agent name], how can I help today?". For Chat, the conversation greeting comes from the agent's [chat configuration](/messaging-channel/advanced/chat-configuration), not the widget. Up to 100 characters.
### Phone-only: call-to-action labels
Primary CTA the visitor clicks to begin a call. Defaults to "Start call". Up to 100 characters.
Label on the in-call hang-up button. Defaults to "End call". Up to 100 characters.
For Phone widgets, transient call-state strings (e.g. "Connecting…", "In call", "Mute") are not editable in v1.
### Disclaimer and consent
PolyAI gives you optional controls for privacy disclaimer and consent. They're off by default and easy to switch on when you need them.
You are responsible for meeting transparency, disclosure, and consent requirements in every jurisdiction you operate in (notably EU and some US states).
Toggle to render the disclaimer. When enabled, both policy URLs below become required.
Shown to visitors before they start. Up to 500 characters. Supports the inline link variables `\{{privacyPolicyUrl}}` and `\{{termsUrl}}`, which are replaced with the URLs you configure under **Company policies**.
Phone default: *"By starting this call, you agree to our Privacy Policy and Terms."*\
Chat default: *"This is an AI-powered system and responses should be verified. All chats are recorded in order to respond to your query and for training and quality control purposes."*
**Chat widgets only.** When enabled, users must click "I consent and start chat" before the conversation begins.
### Company policies
Required when the disclaimer is enabled. Must be `https://`. Example: `https://example.com/privacy`.
Required when the disclaimer is enabled. Must be `https://`. Example: `https://example.com/terms`.
### Chat-only: launcher and behavior
Chat widgets expose extra runtime controls that don't apply to Phone:
| Control | Where it's set | Effect |
| -------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Render mode | Editor + `data-render-mode` script attribute | `default` (floating launcher + panel) or `fullscreen` (chat fills the viewport, launcher hidden). |
| Auto-open | `data-auto-open="true"` on the script tag | Opens the chat as soon as the widget initializes. Useful for dedicated chat pages. |
| Show / hide launcher | `data-show-icon="false"` on the script tag, plus `WebchatAPI.showIcon()` / `hideIcon()` at runtime | Use to hook up a custom trigger button on your site. |
| Header visibility | `data-show-header` on the script tag, plus per-element flags in `visibilityConfig` | Show or hide the header, minimize button, close button, privacy link, terms link, and "Start new chat" button. |
| Prompt bubble | Default text + background color in the editor; runtime calls via `WebchatAPI.showBubble({...})` | Shows a speech bubble next to the launcher to draw attention or run a campaign. |
| Launcher animation | `WebchatAPI.animateIcon({ type, durationMs })` | One of `bounce`, `grow`, `pulse`. |
| iOS WebView | `data-platform="ios"` on the script tag | Forces fullscreen and hides the header so the chat looks native inside `WKWebView`. Sessions are reported as `ios-web` (use `android` for Android WebViews, reported as `android-web`). |
Phone widgets don't have these knobs. The launcher renders with a fixed size, the call card is fullscreen on mobile only, and behavior is driven by call state rather than host-page config. See [Install on your site](/widgets/install) for the full script-tag attribute reference.
## Embed tab
When you're happy with the widget, the **Embed** tab gives you a copyable script tag plus deployment guidance. Paste it into your site and you're live.
The Embed tab shows:
* **Status**: Live, Draft, or Archived.
* **Script tag**: paste before `
` on your site. See [Install on your site](/widgets/install).
* **Last published**: timestamp of the most recent publish to this environment.
* **Snippet changed** banner: appears when the published script tag differs from the current draft (your dev team needs to re-embed).
## Saving and publishing
The publish button lives in the top-right of the editor. Widgets follow a draft → published lifecycle:
1. Edits are saved locally as you type and reflected in the live preview immediately.
2. Click **Save and publish** to deploy to the selected environment.
3. A confirmation modal asks you to confirm. Click **Publish**.
4. A bottom-right toast confirms "Widget published".
5. The **Unpublished changes** indicator clears.
If you change the script tag (for example by switching variants), the Embed tab shows a snippet-changed warning so you can hand the new script to your dev team.
Permissions are scoped per widget type. Publishing a Phone widget requires write access on `polyphone_configurations`; publishing a Chat widget requires write access on `webchat_configurations`. Read access on the same resources controls whether the **Widget** entry appears in the sidebar. Ask your admin if you can't see the page.
## Live preview
The right rail shows a live iframe preview that updates as you edit. Confirm copy length, brand-color contrast, and disclaimer placement before publishing without leaving the editor.
For Phone widgets, click **Test widget** to open a hosted preview page in a new tab where you can place a real call. See [Test your widget](/widgets/test).
## Managing multiple widgets
Spin up separate widgets for different domains, variants, or environments. Each gets its own branding and policies. Use the widget selector in the header to switch between them. Common patterns:
* **Per domain**: different branding for `example.com` and `support.example.com`.
* **Per region**: a UK widget on a UK variant, a US widget on a US variant.
* **Per environment**: isolate a Sandbox widget for QA from your Live widget.
* **Per type**: a Phone widget on high-intent conversion pages, a Chat widget elsewhere.
This is how multi-brand and multi-site teams ship: one editor, many widget variants, no duplicated configuration.
## Next steps
Embed the script via direct HTML or Tag Manager.
Hosted preview for stakeholder review.
Mic, network, CSP, and rendering fixes.
# Install on your site
Source: https://docs.poly.ai/widgets/install
Embed your widget on your website. Single script tag, live in minutes.
Once your widget is configured, getting it live is a single script tag. Paste it on your site, publish, and your visitors can start using it. The install flow is identical for Phone and Chat widgets.
## Installation methods
Pick **one** route:
* **Option A: Direct HTML embed**, recommended for most sites.
* **Option B: Tag Manager**, when your organization manages scripts through Google Tag Manager or similar.
Not sure which route to take? Start with **Option A**. It's the fastest path to live.
## Step 1: Generate a widget tag
1. Go to **Widgets** in Agent Studio and open your widget.
2. Configure [Styling and Content](/widgets/configure).
3. Click **Save and publish**.
4. Open the **Embed** tab.
5. Copy the unique script tag. It looks like this:
```html theme={"theme":{"light":"github-light","dark":"github-dark"}}
```
The tag is bound to the widget's domain, variant, and environment. If any of those change, re-publish and re-embed.
## Option A: Direct HTML embed
1. Open the HTML template that loads on **every page** (often called your global layout, base template, or `index.html`).
2. Paste the script tag **just before the closing `` tag**.
3. Publish your website changes.
```html theme={"theme":{"light":"github-light","dark":"github-dark"}}