Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions fern/composer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,16 @@ Composer understands voice agent architecture and Vapi's capabilities. It can cr
- Configure integrations and webhooks
- Update agent prompts and settings
- Troubleshoot technical issues
- Create and run [simulations](/observability/simulations-overview), and [generate a test suite](#generate-tests-for-an-assistant) for an assistant
- Answer questions about Vapi features
- Recommend best practices for your use case

### Limitations

- **Cannot delete resources**: Composer cannot delete assistants, tools, phone numbers, or any other resources. This is an intentional safety measure. Use the dashboard sidebar to delete resources manually.
- **Cannot delete resources**: Composer cannot delete assistants, tools, phone numbers, or any other resources. This is an intentional safety measure. Use the dashboard sidebar to delete resources manually. Composer can discard your own unpublished assistant or tool draft, which never changes the live resource.
- **Cannot access external systems**: Composer operates within Vapi and cannot reach your CRM, database, or other third-party systems directly.
- **Cannot make business logic decisions**: You provide the domain knowledge; Composer handles the technical configuration.
- **Cannot test your agent**: You need to test the agent yourself and provide feedback.
- **Cannot test every behavior**: Suites Composer generates run in chat, so they can't check voice-only behavior such as pronunciation, and [some handoffs and transfers](/observability/simulations-advanced#handoffs-and-transfers-in-simulations) aren't tested. Make test calls yourself for these.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three ideas are chained with "so" and "and". Splitting them up:

Suggested change
- **Cannot test every behavior**: Suites Composer generates run in chat, so they can't check voice-only behavior such as pronunciation, and [some handoffs and transfers](/observability/simulations-advanced#handoffs-and-transfers-in-simulations) aren't tested. Make test calls yourself for these.
- **Cannot test every behavior**: Generated suites run in chat, so they can't check voice-only behavior such as pronunciation. They also skip [some handoffs and transfers](/observability/simulations-advanced#handoffs-and-transfers-in-simulations). Test these with real calls.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Suites Composer generates run" makes the reader backtrack to find the verb.

Suggested change
- **Cannot test every behavior**: Suites Composer generates run in chat, so they can't check voice-only behavior such as pronunciation, and [some handoffs and transfers](/observability/simulations-advanced#handoffs-and-transfers-in-simulations) aren't tested. Make test calls yourself for these.
- **Cannot test every behavior**: Composer-generated suites run in chat, so they can't check voice-only behavior such as pronunciation, and [some handoffs and transfers](/observability/simulations-advanced#handoffs-and-transfers-in-simulations) aren't tested. Make test calls yourself for these.

- **Cannot deploy to production automatically**: You control when and how changes go live.

## Common use cases
Expand Down Expand Up @@ -171,6 +172,37 @@ Composer understands voice agent architecture and Vapi's capabilities. It can cr
</Accordion>
</AccordionGroup>

## Generate tests for an assistant

Ask Composer to generate tests for an assistant, for example "Generate a test suite for my dental office agent." Composer reads the assistant's prompt and tools, agrees a list of testable requirements with you, then builds a [simulation suite](/observability/simulations-overview) that covers them.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"agrees a list of testable requirements with you" is hard to parse.

Suggested change
Ask Composer to generate tests for an assistant, for example "Generate a test suite for my dental office agent." Composer reads the assistant's prompt and tools, agrees a list of testable requirements with you, then builds a [simulation suite](/observability/simulations-overview) that covers them.
Ask Composer to generate tests for an assistant, for example "Generate a test suite for my dental office agent." Composer reads the assistant's prompt and tools and drafts a list of testable requirements. Once you agree on them, it builds a [simulation suite](/observability/simulations-overview) that covers them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Agrees on" is the US form.

Suggested change
Ask Composer to generate tests for an assistant, for example "Generate a test suite for my dental office agent." Composer reads the assistant's prompt and tools, agrees a list of testable requirements with you, then builds a [simulation suite](/observability/simulations-overview) that covers them.
Ask Composer to generate tests for an assistant, for example "Generate a test suite for my dental office agent." Composer reads the assistant's prompt and tools, agrees on a list of testable requirements with you, then builds a [simulation suite](/observability/simulations-overview) that covers them.


<Steps>
<Step title="Review the requirements">
Composer drafts a numbered list of **Test requirements**. Each one is a behavior a simulated caller could trigger. Composer tags a requirement **Assumption** when the prompt implies it without stating it, and **Can't be simulated** when a chat simulation can't check it, such as pronunciation or the exact wording of the first message. It also asks up to three open questions when an answer would change a requirement.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little rough to read for me, I feel like:

Requirements are intended to outline simulated caller behaviors. We tag requirements with "assumption" when the prompt implies it without stating it, or "can't be simulated" when simulations can't test the behavior (e.g. pronunciation or exact wording). It will ask clarifying questions about the requirements. 

is maybe more readable? Not sure

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Splitting the two tags into separate sentences keeps each one short and easy to scan.

Suggested change
Composer drafts a numbered list of **Test requirements**. Each one is a behavior a simulated caller could trigger. Composer tags a requirement **Assumption** when the prompt implies it without stating it, and **Can't be simulated** when a chat simulation can't check it, such as pronunciation or the exact wording of the first message. It also asks up to three open questions when an answer would change a requirement.
Composer drafts a numbered list of **Test requirements**. Each one is a behavior a simulated caller could trigger. Composer tags a requirement **Assumption** when the prompt implies it without stating it. It tags a requirement **Can't be simulated** when a chat simulation can't check it, such as pronunciation or the exact wording of the first message. It also asks up to three open questions when an answer would change a requirement.

</Step>
<Step title="Accept or change them">
Select **Generate coverage** to accept the draft, or type the changes you want in the chat. Composer revises the draft and asks you to review it again. It never generates a suite from requirements you haven't agreed to.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Select **Generate coverage** to accept the draft, or type the changes you want in the chat. Composer revises the draft and asks you to review it again. It never generates a suite from requirements you haven't agreed to.
Select **Generate coverage** to accept the draft, or iterate on them with Composer in the chat. Composer will revise the draft and ask you to review it again. It never generates a suite from requirements you haven't agreed to.

</Step>
<Step title="Review the suite">
Composer generates the suite and shows it on a card. Each simulation covers one caller goal, and each requirement it covers becomes one of its success criteria. The card shows how many requirements are covered and why any aren't, and lists the handoffs and transfers the suite doesn't test as **Not tested**, with the reason. Every tool in the assistant's tool list that would reach an external system is [mocked by default](/observability/simulations-advanced#mock-tool-responses), and each simulation lists the tools it mocks. Tools that only a hook calls aren't mocked.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence carries five facts. A list makes the card easy to scan.

Suggested change
Composer generates the suite and shows it on a card. Each simulation covers one caller goal, and each requirement it covers becomes one of its success criteria. The card shows how many requirements are covered and why any aren't, and lists the handoffs and transfers the suite doesn't test as **Not tested**, with the reason. Every tool in the assistant's tool list that would reach an external system is [mocked by default](/observability/simulations-advanced#mock-tool-responses), and each simulation lists the tools it mocks. Tools that only a hook calls aren't mocked.
Composer generates the suite and shows it on a card. The card shows:
- Each simulation. A simulation covers one caller goal, and the requirements it covers become its success criteria.
- How many requirements are covered, and why any aren't.
- Handoffs and transfers the suite doesn't test, marked **Not tested**, with the reason.
- The tools each simulation mocks.
Composer [mocks](/observability/simulations-advanced#mock-tool-responses) every tool that would reach an external system. Tools that only a hook calls aren't mocked.

</Step>
<Step title="Run the suite">
Select **Run suite** on the card, or ask Composer to run it. The suite runs once, in chat, against the assistant it was generated for. Composer doesn't wait for the run: progress and results appear at the run's dashboard URL. When the run ends, ask Composer about the results.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Select **Run suite** on the card, or ask Composer to run it. The suite runs once, in chat, against the assistant it was generated for. Composer doesn't wait for the run: progress and results appear at the run's dashboard URL. When the run ends, ask Composer about the results.
Select **Run suite** on the card, or ask Composer to run it. The suite runs once, in chat, against the assistant it was generated for. Composer doesn't wait for the run: progress and results appear on the assistant's test tab or the [runs page](https://dashboard.vapi.ai/simulations/runs). When the run ends, ask Composer about the results.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capitalize after a colon when a full sentence follows.

Suggested change
Select **Run suite** on the card, or ask Composer to run it. The suite runs once, in chat, against the assistant it was generated for. Composer doesn't wait for the run: progress and results appear at the run's dashboard URL. When the run ends, ask Composer about the results.
Select **Run suite** on the card, or ask Composer to run it. The suite runs once, in chat, against the assistant it was generated for. Composer doesn't wait for the run: Progress and results appear at the run's dashboard URL. When the run ends, ask Composer about the results.

</Step>
</Steps>

After generating a suite, ask Composer to change it in the same conversation:

- **Remove simulations**: Removed simulations are unlinked from the suite, not deleted.
- **Rewrite a simulation**: Describe the change, for example "make the caller refuse to give a date of birth."
- **Retry failed simulations**: When some simulations couldn't be generated, Composer offers to try them again.

The requirements you agreed to are saved with the suite, and edits always work from that saved set. Revising the requirements later doesn't change an existing suite: Composer generates a new suite from the new requirements.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passive voice plus a colon; simpler:

Suggested change
The requirements you agreed to are saved with the suite, and edits always work from that saved set. Revising the requirements later doesn't change an existing suite: Composer generates a new suite from the new requirements.
Composer saves the agreed requirements with the suite, and every edit works from them. If you change the requirements later, Composer builds a new suite. The existing suite doesn't change.


<Tip>
One passing run shows the assistant **can** succeed, not that it does so reliably. Run five or more iterations before you trust a pass rate: ask Composer, or [rerun the suite](/observability/simulations-manage#rerun-a-suite) with more **Iterations**. See [testing best practices](/test/simulations-best-practices).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional tightening:

Suggested change
One passing run shows the assistant **can** succeed, not that it does so reliably. Run five or more iterations before you trust a pass rate: ask Composer, or [rerun the suite](/observability/simulations-manage#rerun-a-suite) with more **Iterations**. See [testing best practices](/test/simulations-best-practices).
One passing run shows the assistant **can** succeed, not that it will every time. Run five or more iterations before you trust the pass rate. Ask Composer to run more, or [rerun the suite](/observability/simulations-manage#rerun-a-suite) with more **Iterations**. See [testing best practices](/test/simulations-best-practices).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same colon capitalization.

Suggested change
One passing run shows the assistant **can** succeed, not that it does so reliably. Run five or more iterations before you trust a pass rate: ask Composer, or [rerun the suite](/observability/simulations-manage#rerun-a-suite) with more **Iterations**. See [testing best practices](/test/simulations-best-practices).
One passing run shows the assistant **can** succeed, not that it does so reliably. Run five or more iterations before you trust a pass rate: Ask Composer, or [rerun the suite](/observability/simulations-manage#rerun-a-suite) with more **Iterations**. See [testing best practices](/test/simulations-best-practices).

</Tip>

## Safety features

Composer includes safeguards to prevent accidental or irreversible changes to your account. You control how much Composer can do on its own with per-conversation approval modes.
Expand All @@ -188,7 +220,7 @@ Approval enforcement always happens on the server, and read operations never req

### Deletion

Composer can delete resources such as assistants, tools, phone numbers, squads, and files. Deletion follows your approval mode: unless you have selected Bypass, Composer asks for your explicit approval before it deletes anything. Because deletion is permanent, keep an approval mode on if you want a confirmation step before a resource is removed.
Composer does not delete resources. Delete assistants, tools, phone numbers, squads, and files from the dashboard yourself. The approval settings include a delete category, so a policy you set now also applies if Composer gains deletion later. When Composer removes a simulation from a suite it generated, the simulation is unlinked from the suite, not deleted.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean we're gonna have a bunch of orphan suites?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"The approval settings include a delete category, so a policy you set now also applies if Composer gains deletion later" talks about a future feature, and readers will wonder why it's there. I'd drop this sentence.


### How the approval flow works

Expand Down
36 changes: 36 additions & 0 deletions fern/observability/simulations-advanced.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ Open a suite and select **Edit**, then **Next**. The review step contains the **

During a simulation, the [**assistant**](/assistants) or [**squad**](/squads) under test runs its real [**tools**](/tools). Mock a tool to return fixed text instead, so that tool never calls its live service or API. A mock intercepts only the tool it names; every other tool the assistant or squad calls still runs for real.

<Note>
Suites that [Composer generates](/composer#generate-tests-for-an-assistant) come with a mock already set for every tool that would reach an external system, including handoffs and transfers that can't run safely. Tools that only end or route the conversation, such as end call, voicemail, DTMF, and static handoffs, run as they would on a real call. To review or change a generated mock, edit the suite and open **Tool mocks & webhooks**: change the **Mock result**, or turn off **Enabled** to run that tool live. Suites you build yourself start with no mocks, so every tool runs for real until you add one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This note is one dense paragraph. Shorter sentences:

Suggested change
Suites that [Composer generates](/composer#generate-tests-for-an-assistant) come with a mock already set for every tool that would reach an external system, including handoffs and transfers that can't run safely. Tools that only end or route the conversation, such as end call, voicemail, DTMF, and static handoffs, run as they would on a real call. To review or change a generated mock, edit the suite and open **Tool mocks & webhooks**: change the **Mock result**, or turn off **Enabled** to run that tool live. Suites you build yourself start with no mocks, so every tool runs for real until you add one.
Suites that [Composer generates](/composer#generate-tests-for-an-assistant) come with mocks already set. Composer mocks every tool that would reach an external system, including handoffs and transfers that can't run safely. Tools that only end or route the call run normally, such as end call, voicemail, DTMF, and static handoffs.
To change a generated mock, edit the suite and open **Tool mocks & webhooks**. Change the **Mock result**, or turn off **Enabled** to run the tool live. Suites you build yourself start with no mocks, so every tool runs for real until you add one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same colon capitalization.

Suggested change
Suites that [Composer generates](/composer#generate-tests-for-an-assistant) come with a mock already set for every tool that would reach an external system, including handoffs and transfers that can't run safely. Tools that only end or route the conversation, such as end call, voicemail, DTMF, and static handoffs, run as they would on a real call. To review or change a generated mock, edit the suite and open **Tool mocks & webhooks**: change the **Mock result**, or turn off **Enabled** to run that tool live. Suites you build yourself start with no mocks, so every tool runs for real until you add one.
Suites that [Composer generates](/composer#generate-tests-for-an-assistant) come with a mock already set for every tool that would reach an external system, including handoffs and transfers that can't run safely. Tools that only end or route the conversation, such as end call, voicemail, DTMF, and static handoffs, run as they would on a real call. To review or change a generated mock, edit the suite and open **Tool mocks & webhooks**: Change the **Mock result**, or turn off **Enabled** to run that tool live. Suites you build yourself start with no mocks, so every tool runs for real until you add one.


Generated mocks cover the tools in the assistant's tool list and its handoff destinations. A tool that only an assistant [**hook**](/assistants/assistant-hooks) calls isn't mocked, and the assistant's [server messages](/server-url), such as the end-of-call report, still go to its server URL. Check both before you run a generated suite against production systems.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the real safety risk, and it's buried at the end of a Note. Consider making it a <Warning>.

Suggested change
Generated mocks cover the tools in the assistant's tool list and its handoff destinations. A tool that only an assistant [**hook**](/assistants/assistant-hooks) calls isn't mocked, and the assistant's [server messages](/server-url), such as the end-of-call report, still go to its server URL. Check both before you run a generated suite against production systems.
Generated mocks don't cover everything. Tools that only an assistant [**hook**](/assistants/assistant-hooks) calls aren't mocked. [Server messages](/server-url), such as the end-of-call report, still go to the assistant's server URL. Check both before you run a generated suite against production systems.

</Note>

<Tabs>
<Tab title="Dashboard">

Expand Down Expand Up @@ -126,6 +132,32 @@ Return plain text to test how the assistant handles a service that is unavailabl

These strings replace the selected tool's output. They do not produce an HTTP error, add network latency, or cause a network timeout.

## Handoffs and transfers in simulations

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The heading covers transfers, but apart from the generated-suite gaps table, the section doesn't say what a transferCall does in a suite you build yourself. Does it actually transfer the call? A sentence after the dynamic-handoff paragraph would close that gap.


During any simulation, a [**handoff**](/squads/handoff) that lists its destinations, none of them dynamic, runs only when the model names one of those destinations. If the model gives no destination, asks for `dynamic`, or names an assistant the handoff doesn't list, the handoff is refused: the assistant doesn't switch, and the model is asked to call the handoff again with one of its destinations. A refused handoff never asks your server for a destination.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"a handoff that lists its destinations, none of them dynamic, runs only when…" takes several reads.

Suggested change
During any simulation, a [**handoff**](/squads/handoff) that lists its destinations, none of them dynamic, runs only when the model names one of those destinations. If the model gives no destination, asks for `dynamic`, or names an assistant the handoff doesn't list, the handoff is refused: the assistant doesn't switch, and the model is asked to call the handoff again with one of its destinations. A refused handoff never asks your server for a destination.
In a simulation, a [**handoff**](/squads/handoff) with a fixed list of destinations runs only when the model picks one of them. If the model names no destination, asks for `dynamic`, or names an assistant that isn't on the list, Vapi refuses the handoff. The assistant doesn't switch, and the model is asked to try again with a listed destination. Vapi doesn't ask your server for a destination.

Also: what happens on a live call in the same case? Saying it in one clause would show the reader why this is a simulation difference.


A [**dynamic handoff**](/squads/handoff/destinations#dynamic-handoffs) runs as it does on a live call, so it asks your server for a destination unless you [mock it](#mock-tool-responses).

### Gaps in generated suites

When [Composer generates a suite](/composer#generate-tests-for-an-assistant), it lists each handoff or transfer the suite doesn't exercise as **Not tested**, with the reason:

| Reason | What the suite does |
| --- | --- |
| Your server chooses the destination (a dynamic handoff) | Mocks the handoff. |
| It transfers to a phone number or your server | Mocks the transfer. |
| The destination assistant couldn't be checked safely: it uses MCP tools or pinned versions, couldn't be loaded, or was past the number of assistants Composer reads | Mocks the handoff into it. |
| It hands off by assistant name outside a [**squad**](/squads) | Leaves the handoff live, but the path isn't reached. |
| It shares its name with a mocked tool | Mocks it too. |
Comment on lines +149 to +151

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few cells here are hard to follow:

  • Row 149 fits three reasons into one cell, and "was past the number of assistants Composer reads" is especially hard. Consider one row per reason, or: "Composer couldn't check the destination assistant. It uses MCP tools or a pinned version, it didn't load, or Composer hit its limit on how many assistants it reads."
  • Row 150: "Leaves the handoff live, but the path isn't reached" is confusing. Maybe: "Doesn't mock it. A name-based handoff can't resolve outside a squad, so the simulation never reaches it."
  • Row 151: in "It shares its name with a mocked tool | Mocks it too", it's unclear what "it" refers to. Maybe: "A built-in tool (such as endCall) has the same name as a mocked tool | The mock answers both."


Other handoffs and transfers run as they would on a real call. Cover the paths above with test calls against sandbox systems.

## Hooks and call endings

Simulations keep the [**hooks**](/assistants/assistant-hooks) on the assistant or squad under test, so in a voice simulation a `call.ending` hook runs as it would on a real call. The exception is `customer.speech.timeout`: Vapi removes those hooks from the assistant and every squad member, so the assistant doesn't prompt the AI tester when it pauses. If a scenario sets `targetOverrides.hooks`, Vapi uses those hooks exactly as set, which lets you test re-engagement behavior.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Four ideas run through "so", ":" and "which". Split up:

Suggested change
Simulations keep the [**hooks**](/assistants/assistant-hooks) on the assistant or squad under test, so in a voice simulation a `call.ending` hook runs as it would on a real call. The exception is `customer.speech.timeout`: Vapi removes those hooks from the assistant and every squad member, so the assistant doesn't prompt the AI tester when it pauses. If a scenario sets `targetOverrides.hooks`, Vapi uses those hooks exactly as set, which lets you test re-engagement behavior.
Simulations run the [**hooks**](/assistants/assistant-hooks) on the assistant or squad under test, just like a real call. For example, a `call.ending` hook runs in a voice simulation. The one exception is `customer.speech.timeout`. Vapi removes these hooks from the assistant and every squad member, so the assistant doesn't nudge the AI tester when it pauses. To test re-engagement, set `targetOverrides.hooks` on the scenario. Vapi uses those hooks exactly as you set them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our docs avoid naming Vapi as the actor when the platform is doing the work. This also clears up "when it pauses," which could read as the assistant pausing.

Suggested change
Simulations keep the [**hooks**](/assistants/assistant-hooks) on the assistant or squad under test, so in a voice simulation a `call.ending` hook runs as it would on a real call. The exception is `customer.speech.timeout`: Vapi removes those hooks from the assistant and every squad member, so the assistant doesn't prompt the AI tester when it pauses. If a scenario sets `targetOverrides.hooks`, Vapi uses those hooks exactly as set, which lets you test re-engagement behavior.
Simulations keep the [**hooks**](/assistants/assistant-hooks) on the assistant or squad under test, so in a voice simulation a `call.ending` hook runs as it would on a real call. The exception is `customer.speech.timeout` hooks, which are removed from the assistant and every squad member so the assistant doesn't prompt the AI tester when the tester pauses. If a scenario sets `targetOverrides.hooks`, those hooks are used exactly as set, which lets you test re-engagement behavior.


When the AI tester hangs up first in a voice simulation, for example after saying goodbye, the call to the assistant under test ends as `customer-ended-call`, the same as when a real caller hangs up. Hooks and logic that depend on a caller hang-up run as they would on a real call.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second sentence repeats the first.

Suggested change
When the AI tester hangs up first in a voice simulation, for example after saying goodbye, the call to the assistant under test ends as `customer-ended-call`, the same as when a real caller hangs up. Hooks and logic that depend on a caller hang-up run as they would on a real call.
If the AI tester hangs up first in a voice simulation, the call ends with `customer-ended-call`, just like a real caller hanging up. Anything that depends on a caller hang-up runs normally.


## Send webhooks on run start and end

Turn on webhooks to notify your server when a simulation iteration starts or ends. Each webhook sends a `POST` request to the URL you configure.
Expand Down Expand Up @@ -351,6 +383,10 @@ For a transient scenario, set the same `targetOverrides` on the inline `scenario
| There is no audio or recording | Check whether the run used chat mode. Use voice mode to test or record audio. |
| Start or end webhooks do not trigger | Confirm the corresponding hook and its URL are configured on the scenario. |
| A tool mock does not apply | Confirm that `toolName` matches the configured tool name exactly and that `enabled` is `true`. |
| A handoff is refused in the transcript | The model gave no destination, `dynamic`, or an assistant the handoff doesn't list. Check the handoff's destinations and the prompt's routing instructions. See [**Handoffs and transfers in simulations**](#handoffs-and-transfers-in-simulations). |
| A generated suite lists a handoff or transfer as **Not tested** | Composer couldn't run that path safely, so it mocked it or couldn't reach it. Cover it with a test call. See [**Gaps in generated suites**](#gaps-in-generated-suites). |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In "so it mocked it or couldn't reach it", it's unclear what each "it" refers to.

Suggested change
| A generated suite lists a handoff or transfer as **Not tested** | Composer couldn't run that path safely, so it mocked it or couldn't reach it. Cover it with a test call. See [**Gaps in generated suites**](#gaps-in-generated-suites). |
| A generated suite lists a handoff or transfer as **Not tested** | That path couldn't run safely, so Composer mocked it, or the simulation can't reach it. Cover it with a test call. See [**Gaps in generated suites**](#gaps-in-generated-suites). |

| The assistant never asks whether the AI tester is still there | Simulations remove `customer.speech.timeout` hooks. To test them, set `targetOverrides.hooks` on the scenario. See [**Hooks and call endings**](#hooks-and-call-endings). |
| A voice simulation's assistant call ends as `customer-ended-call` | The AI tester hung up first, which matches a real caller hanging up. |
| A tool looks up the wrong caller | Set `customer.number` in the scenario's variable values. See [**Simulate a call from a specific phone number**](#simulate-a-call-from-a-specific-phone-number). |
| A latency limit fails without a measured value | No turn in the call measured that metric. Confirm the run used voice mode and that the conversation completed at least one exchange. |
| Latency limits show as skipped | Check whether the run used chat mode or the assistant under test uses GPT-Live. Both skip latency limits. |
Expand Down
42 changes: 41 additions & 1 deletion fern/observability/simulations-manage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,25 @@ curl -X GET "https://api.vapi.ai/eval/simulation/run/<run-id>" \

The run includes `status` and `itemCounts` (`total`, `passed`, `failed`, and so on). Fetch the result for each simulation iteration with `GET /eval/simulation/run/<run-id>/item`. Each item's `results` holds its `evaluations` and, when the scenario sets latency limits, its `latencyEvaluations`. See [**Review latency results**](/observability/simulations-advanced#review-latency-results).

Filter the lists with query parameters:

| Endpoint | Filters |
| --- | --- |
| `GET /eval/simulation/run` | `suiteId` (repeat for up to 100 suites), `targetType` (`assistant` or `squad`), `targetId`, and `status` (`queued`, `running`, or `ended`). `targetType` and `targetId` each work alone. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"targetType and targetId each work alone" makes the reader guess. Maybe: "You can send targetType or targetId without the other."

| `GET /eval/simulation/run/<run-id>/item` | `status` (`queued`, `running`, `evaluating`, `passed`, `failed`, or `canceled`) and `simulationId`. |
| `GET /eval/simulation/suite` | `assistantId` and `squadId` (repeat each for up to 100 IDs; sending both requires both to match) and `name`, which matches part of the suite name. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe replace "sending both requires both to match" with "if you send both, a suite must match both".


```bash
curl -X GET "https://api.vapi.ai/eval/simulation/run?suiteId=<suite-id>&status=ended" \
-H "Authorization: Bearer $VAPI_API_KEY"
```

Each list also accepts `limit` (default 100, maximum 1,000) and `page`.

<Note>
The response shape depends on the query. With no query parameters, these endpoints return a plain array. With any query parameter, the run and run item lists return a paginated object, `{ "metadata": { ... }, "results": [ ... ] }`, with at most `limit` records. The suite list does the same when you send `limit`, `page`, `name`, `assistantId`, or `squadId`. If your integration parses the response as an array, read `results` whenever you send a query parameter.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dense note; a short lead plus bullets:

Suggested change
The response shape depends on the query. With no query parameters, these endpoints return a plain array. With any query parameter, the run and run item lists return a paginated object, `{ "metadata": { ... }, "results": [ ... ] }`, with at most `limit` records. The suite list does the same when you send `limit`, `page`, `name`, `assistantId`, or `squadId`. If your integration parses the response as an array, read `results` whenever you send a query parameter.
Adding query parameters changes the response shape:
- With no query parameters, these endpoints return a plain array.
- The run and run item lists return `{ "metadata": { ... }, "results": [ ... ] }` when you send any query parameter.
- The suite list returns the same object when you send `limit`, `page`, `name`, `assistantId`, or `squadId`.
If your code expects an array, read `results` whenever you send a query parameter.

</Note>

</Tab>
</Tabs>

Expand Down Expand Up @@ -121,6 +140,27 @@ curl -X POST "https://api.vapi.ai/eval/simulation/run" \

Use `"provider": "vapi.websocket"` for voice.

To retry a run request without starting a second run, send an `Idempotency-Key` header with a value you generate, such as a UUID:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idempotency-Key applies to every POST /eval/simulation/run, not only reruns, but it's documented only in the Rerun tab. Is this the place you want it, or should it also be mentioned where a run is first created?


```bash
curl -X POST "https://api.vapi.ai/eval/simulation/run" \
-H "Authorization: Bearer $VAPI_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: <unique-key>" \
-d '{
"simulations": [{ "type": "simulationSuite", "simulationSuiteId": "<suite-id>" }],
"target": { "type": "assistant", "assistantId": "<assistant-id>" },
"transport": { "provider": "vapi.webchat" },
"iterations": 1
}'
```

- A retry with the same key and the same body returns the run the first request created, with `reused: true`. It doesn't start or bill the run again.
- Reusing a key with a different body returns `409`. Use a new key for a different run.
- If a request returns `503` with `code: SIMULATION_WORK_DISABLED`, simulations are paused across Vapi. Retry later. Any other `503` can be retried with the same key.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Active voice for the last sentence:

Suggested change
- If a request returns `503` with `code: SIMULATION_WORK_DISABLED`, simulations are paused across Vapi. Retry later. Any other `503` can be retried with the same key.
- If a request returns `503` with `code: SIMULATION_WORK_DISABLED`, simulations are paused across Vapi. Retry later. Retry any other `503` with the same key.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code: SIMULATION_WORK_DISABLED reads like a literal but isn't the response's actual shape, so just the value in backticks is safer. Also a direct instruction for the last sentence.

Suggested change
- If a request returns `503` with `code: SIMULATION_WORK_DISABLED`, simulations are paused across Vapi. Retry later. Any other `503` can be retried with the same key.
- If a request returns `503` with the code `SIMULATION_WORK_DISABLED`, simulations are paused across Vapi. Retry later. For any other `503`, retry with the same key.

- A key is up to 255 printable ASCII characters and is scoped to your organization. An empty or invalid key returns `400`.
- A key stays bound to its run for as long as the run exists. It does not expire after a set time.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- A key stays bound to its run for as long as the run exists. It does not expire after a set time.
- Keys don't expire. A key stays tied to its run for as long as the run exists.


</Tab>
</Tabs>

Expand All @@ -138,7 +178,7 @@ Create a regression test when you fix a defect or find an unexpected response. R

### Edge-case tests

Test realistic variations such as an ambiguous request, an impatient customer, an unavailable appointment, a tool error, an interruption, or a handoff. Base AI tester personalities on the customer types the [**assistant**](/assistants) or [**squad**](/squads) handles. Synthetic callers don't reliably reproduce every interruption, silence, or background-noise condition. Keep controlled real-call checks for problems the simulation can't reproduce.
Test realistic variations such as an ambiguous request, an impatient customer, an unavailable appointment, a tool error, an interruption, or a handoff. Some handoffs and transfers [behave differently in simulations](/observability/simulations-advanced#handoffs-and-transfers-in-simulations). Base AI tester personalities on the customer types the [**assistant**](/assistants) or [**squad**](/squads) handles. Synthetic callers don't reliably reproduce every interruption, silence, or background-noise condition. Keep controlled real-call checks for problems the simulation can't reproduce.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inserted sentence about handoffs breaks the paragraph's flow. Consider moving it to the end of the paragraph.


### Design evaluations

Expand Down
2 changes: 1 addition & 1 deletion fern/observability/simulations-overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Simulations test live interactions. An AI tester with a defined personality and

Choose Evals when you want to lock down a specific response or verify a tool call's arguments with fast, rerunnable checks.

Choose Simulations when you want to see how your assistant or squad performs against an AI tester with different personalities, multi-turn flows, squad handoffs, or voice.
Choose Simulations when you want to see how your assistant or squad performs against an AI tester with different personalities, multi-turn flows, squad handoffs, or voice. Some handoffs and transfers [behave differently in simulations](/observability/simulations-advanced#handoffs-and-transfers-in-simulations).

See [testing voice agents](/test/voice-testing) for guidance on combining both
tools into one coverage plan.
Expand Down
2 changes: 1 addition & 1 deletion fern/observability/simulations-quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ A reusable **simulation suite** that asks an AI tester to book an appointment wi
You create Riley and the structured output during this quickstart.

<Warning>
Simulations run unmocked tools for real. Before running, inspect Riley's tools and use sandbox integrations or [mock every tool](/observability/simulations-advanced#mock-tool-responses) that could create a booking, send a message, or affect a real customer.
In a suite you build yourself, every tool without a [mock](/observability/simulations-advanced#mock-tool-responses) runs for real. Before running, inspect Riley's tools and use sandbox integrations or mock every tool that could create a booking, send a message, or affect a real customer. Suites that [Composer generates](/composer#generate-tests-for-an-assistant) mock tools that reach external systems by default.
</Warning>

## Build and run a simulation
Expand Down
1 change: 1 addition & 0 deletions fern/test/run-and-maintain-tests.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Rerun affected tests whenever you change prompts, tools, models, voices, transcr
AI conversations can take different paths from the same starting point. One passing Simulation shows that one path worked, not that the journey is reliable.

- Use one iteration while you build or debug a Simulation.
- Run five or more iterations before you trust a pass rate.
- Repeat release checks based on the impact of failure and how much the result has varied before.
- Use more iterations for critical journeys and intermittent failures when the extra evidence justifies the time and cost.

Expand Down
2 changes: 1 addition & 1 deletion fern/test/simulations-best-practices.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ Build and debug a scenario in chat mode, then run important paths in voice mode

## Repeat critical tests and review the conversation

Agent behavior can vary between runs. One pass shows that the agent **can** succeed; repeated passes provide stronger evidence that it succeeds consistently. Use more iterations for outcomes where a failure would harm a customer or the business.
Agent behavior can vary between runs. One pass shows that the agent **can** succeed; repeated passes provide stronger evidence that it succeeds consistently. Use one iteration while you set up a test, then run five or more iterations before you trust a pass rate. Use more iterations for outcomes where a failure would harm a customer or the business.

Read every failed transcript and sample passed transcripts. Ask:

Expand Down
Loading
Loading