curl --request POST \
--url https://api.goyappr.com/agent-eval/cases \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "Yes path — agreement on first ask",
"agent_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"persona_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"scenario": "<string>",
"description": "<string>",
"suite_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"success_criteria": [],
"max_turns": 20,
"pass_threshold": 80,
"agent_overrides": {},
"tool_policy": "mock",
"tool_allowlist": []
}
'import requests
url = "https://api.goyappr.com/agent-eval/cases"
payload = {
"name": "Yes path — agreement on first ask",
"agent_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"persona_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"scenario": "<string>",
"description": "<string>",
"suite_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"success_criteria": [],
"max_turns": 20,
"pass_threshold": 80,
"agent_overrides": {},
"tool_policy": "mock",
"tool_allowlist": []
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: 'Yes path — agreement on first ask',
agent_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
persona_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
scenario: '<string>',
description: '<string>',
suite_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
success_criteria: [],
max_turns: 20,
pass_threshold: 80,
agent_overrides: {},
tool_policy: 'mock',
tool_allowlist: []
})
};
fetch('https://api.goyappr.com/agent-eval/cases', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.goyappr.com/agent-eval/cases",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => 'Yes path — agreement on first ask',
'agent_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'persona_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'scenario' => '<string>',
'description' => '<string>',
'suite_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'success_criteria' => [
],
'max_turns' => 20,
'pass_threshold' => 80,
'agent_overrides' => [
],
'tool_policy' => 'mock',
'tool_allowlist' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.goyappr.com/agent-eval/cases"
payload := strings.NewReader("{\n \"name\": \"Yes path — agreement on first ask\",\n \"agent_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"persona_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scenario\": \"<string>\",\n \"description\": \"<string>\",\n \"suite_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"success_criteria\": [],\n \"max_turns\": 20,\n \"pass_threshold\": 80,\n \"agent_overrides\": {},\n \"tool_policy\": \"mock\",\n \"tool_allowlist\": []\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.goyappr.com/agent-eval/cases")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"Yes path — agreement on first ask\",\n \"agent_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"persona_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scenario\": \"<string>\",\n \"description\": \"<string>\",\n \"suite_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"success_criteria\": [],\n \"max_turns\": 20,\n \"pass_threshold\": 80,\n \"agent_overrides\": {},\n \"tool_policy\": \"mock\",\n \"tool_allowlist\": []\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.goyappr.com/agent-eval/cases")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"Yes path — agreement on first ask\",\n \"agent_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"persona_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scenario\": \"<string>\",\n \"description\": \"<string>\",\n \"suite_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"success_criteria\": [],\n \"max_turns\": 20,\n \"pass_threshold\": 80,\n \"agent_overrides\": {},\n \"tool_policy\": \"mock\",\n \"tool_allowlist\": []\n}"
response = http.request(request)
puts response.read_body{
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"company_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"agent_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"persona_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "Yes path — caller agrees on first ask",
"scenario": "The persona is responding to a missed call from your business about their recent inquiry. They have time to talk for 5 minutes.",
"success_criteria": [
{
"type": "must_say",
"kind": "must_say",
"weight": 1,
"description": "<string>",
"pattern": "<string>",
"phrase": "<string>",
"match_type": "substring",
"case_sensitive": false,
"tool_name": "<string>",
"args_match": {},
"node_id": "<string>",
"rubric": "<string>"
}
],
"max_turns": 20,
"pass_threshold": 80,
"tool_policy": "mock",
"created_at": "2023-11-07T05:31:56Z",
"agent": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"type": "prompt",
"flow_config": {
"nodes": [
{
"id": "<string>",
"type": "start",
"name": "<string>",
"position": {
"x": 123,
"y": 123
},
"agent_speaks_first": true,
"greeting": "<string>",
"is_literal": false,
"next_step_id": "<string>",
"auto_advance": true
}
],
"flow_config_version": "1",
"metadata": {
"custom_metadata_keys": [
"<string>"
]
}
},
"system_prompt": "<string>",
"description": "<string>",
"voice": "Michal",
"background_sound": "call_center",
"background_sound_volume": 0.3,
"language": "he",
"temperature": 1,
"greeting_message": "<string>",
"agent_speaks_first": true,
"vad_stop_secs": 0.5,
"vad_start_secs": 0.2,
"vad_confidence": 0.7,
"silence_timeout_secs": 60,
"max_continuous_speech_secs": 120,
"max_call_duration_secs": 600,
"lead_memory_enabled": true,
"is_active": true,
"webhook_url": "<string>",
"webhook_events": [
"call.started"
],
"webhook_headers": {
"Authorization": "Bearer sk_live_…",
"X-Source": "yappr"
},
"extraction_parameters": [
{
"name": "customerName",
"description": "The caller's full name as mentioned during the conversation"
}
],
"created_at": "2023-11-07T05:31:56Z",
"updated_at": "2023-11-07T05:31:56Z"
},
"persona": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"company_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "Frustrated tenant",
"identity_prompt": "You are a 38-year-old tenant calling about a leaking pipe in your kitchen. You're frustrated because this is the third time you've reported it.",
"language": "en",
"created_at": "2023-11-07T05:31:56Z",
"description": "<string>",
"behavior_traits": {
"patience": "low",
"verbosity": "chatty",
"cooperation": "cooperative",
"interruption_tendency": "occasional",
"goal": "Get a maintenance technician scheduled today"
},
"voice_config": {},
"updated_at": "2023-11-07T05:31:56Z",
"deleted_at": "2023-11-07T05:31:56Z"
},
"suite_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"description": "<string>",
"agent_overrides": {},
"tool_allowlist": [],
"updated_at": "2023-11-07T05:31:56Z",
"deleted_at": "2023-11-07T05:31:56Z"
}{
"error": "<string>",
"code": "<string>"
}Create Case
Required scope agent_eval:create. The agent_id must reference a non-deleted agent in this company; persona_id must reference a non-deleted persona. Suites are optional — you can create stand-alone ad-hoc cases.
curl --request POST \
--url https://api.goyappr.com/agent-eval/cases \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "Yes path — agreement on first ask",
"agent_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"persona_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"scenario": "<string>",
"description": "<string>",
"suite_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"success_criteria": [],
"max_turns": 20,
"pass_threshold": 80,
"agent_overrides": {},
"tool_policy": "mock",
"tool_allowlist": []
}
'import requests
url = "https://api.goyappr.com/agent-eval/cases"
payload = {
"name": "Yes path — agreement on first ask",
"agent_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"persona_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"scenario": "<string>",
"description": "<string>",
"suite_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"success_criteria": [],
"max_turns": 20,
"pass_threshold": 80,
"agent_overrides": {},
"tool_policy": "mock",
"tool_allowlist": []
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
name: 'Yes path — agreement on first ask',
agent_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
persona_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
scenario: '<string>',
description: '<string>',
suite_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
success_criteria: [],
max_turns: 20,
pass_threshold: 80,
agent_overrides: {},
tool_policy: 'mock',
tool_allowlist: []
})
};
fetch('https://api.goyappr.com/agent-eval/cases', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.goyappr.com/agent-eval/cases",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'name' => 'Yes path — agreement on first ask',
'agent_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'persona_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'scenario' => '<string>',
'description' => '<string>',
'suite_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
'success_criteria' => [
],
'max_turns' => 20,
'pass_threshold' => 80,
'agent_overrides' => [
],
'tool_policy' => 'mock',
'tool_allowlist' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.goyappr.com/agent-eval/cases"
payload := strings.NewReader("{\n \"name\": \"Yes path — agreement on first ask\",\n \"agent_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"persona_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scenario\": \"<string>\",\n \"description\": \"<string>\",\n \"suite_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"success_criteria\": [],\n \"max_turns\": 20,\n \"pass_threshold\": 80,\n \"agent_overrides\": {},\n \"tool_policy\": \"mock\",\n \"tool_allowlist\": []\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.goyappr.com/agent-eval/cases")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"Yes path — agreement on first ask\",\n \"agent_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"persona_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scenario\": \"<string>\",\n \"description\": \"<string>\",\n \"suite_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"success_criteria\": [],\n \"max_turns\": 20,\n \"pass_threshold\": 80,\n \"agent_overrides\": {},\n \"tool_policy\": \"mock\",\n \"tool_allowlist\": []\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.goyappr.com/agent-eval/cases")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"name\": \"Yes path — agreement on first ask\",\n \"agent_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"persona_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"scenario\": \"<string>\",\n \"description\": \"<string>\",\n \"suite_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n \"success_criteria\": [],\n \"max_turns\": 20,\n \"pass_threshold\": 80,\n \"agent_overrides\": {},\n \"tool_policy\": \"mock\",\n \"tool_allowlist\": []\n}"
response = http.request(request)
puts response.read_body{
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"company_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"agent_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"persona_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "Yes path — caller agrees on first ask",
"scenario": "The persona is responding to a missed call from your business about their recent inquiry. They have time to talk for 5 minutes.",
"success_criteria": [
{
"type": "must_say",
"kind": "must_say",
"weight": 1,
"description": "<string>",
"pattern": "<string>",
"phrase": "<string>",
"match_type": "substring",
"case_sensitive": false,
"tool_name": "<string>",
"args_match": {},
"node_id": "<string>",
"rubric": "<string>"
}
],
"max_turns": 20,
"pass_threshold": 80,
"tool_policy": "mock",
"created_at": "2023-11-07T05:31:56Z",
"agent": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "<string>",
"type": "prompt",
"flow_config": {
"nodes": [
{
"id": "<string>",
"type": "start",
"name": "<string>",
"position": {
"x": 123,
"y": 123
},
"agent_speaks_first": true,
"greeting": "<string>",
"is_literal": false,
"next_step_id": "<string>",
"auto_advance": true
}
],
"flow_config_version": "1",
"metadata": {
"custom_metadata_keys": [
"<string>"
]
}
},
"system_prompt": "<string>",
"description": "<string>",
"voice": "Michal",
"background_sound": "call_center",
"background_sound_volume": 0.3,
"language": "he",
"temperature": 1,
"greeting_message": "<string>",
"agent_speaks_first": true,
"vad_stop_secs": 0.5,
"vad_start_secs": 0.2,
"vad_confidence": 0.7,
"silence_timeout_secs": 60,
"max_continuous_speech_secs": 120,
"max_call_duration_secs": 600,
"lead_memory_enabled": true,
"is_active": true,
"webhook_url": "<string>",
"webhook_events": [
"call.started"
],
"webhook_headers": {
"Authorization": "Bearer sk_live_…",
"X-Source": "yappr"
},
"extraction_parameters": [
{
"name": "customerName",
"description": "The caller's full name as mentioned during the conversation"
}
],
"created_at": "2023-11-07T05:31:56Z",
"updated_at": "2023-11-07T05:31:56Z"
},
"persona": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"company_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"name": "Frustrated tenant",
"identity_prompt": "You are a 38-year-old tenant calling about a leaking pipe in your kitchen. You're frustrated because this is the third time you've reported it.",
"language": "en",
"created_at": "2023-11-07T05:31:56Z",
"description": "<string>",
"behavior_traits": {
"patience": "low",
"verbosity": "chatty",
"cooperation": "cooperative",
"interruption_tendency": "occasional",
"goal": "Get a maintenance technician scheduled today"
},
"voice_config": {},
"updated_at": "2023-11-07T05:31:56Z",
"deleted_at": "2023-11-07T05:31:56Z"
},
"suite_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"description": "<string>",
"agent_overrides": {},
"tool_allowlist": [],
"updated_at": "2023-11-07T05:31:56Z",
"deleted_at": "2023-11-07T05:31:56Z"
}{
"error": "<string>",
"code": "<string>"
}suite_id: null is an ad-hoc case you can run on its own.
Worked example
{
"name": "Yes path — caller agrees on first ask",
"agent_id": "...",
"persona_id": "...",
"scenario": "The persona is responding to a missed call from your business about their recent inquiry. They have 5 minutes to talk.",
"success_criteria": [
{
"type": "must_say",
"pattern": "would Tuesday at 3pm work for you",
"weight": 1
},
{
"type": "must_call_tool",
"tool_name": "bookAppointment",
"weight": 2
},
{
"type": "must_not_say",
"pattern": "guarantee",
"weight": 1
}
],
"max_turns": 20,
"pass_threshold": 80,
"tool_policy": "mock"
}
Assertion shape
Each entry insuccess_criteria is one assertion with these fields:
| Field | Required for | Notes |
|---|---|---|
type | all | One of must_say, must_not_say, must_call_tool, must_reach_node, custom_llm_judge. The alias kind is accepted and behaves identically; if you send both, type wins. |
weight | optional | Relative weight in the case score (defaults to 1). |
phrase | must_say / must_not_say | Text to look for in the agent transcript, matched as a plain substring — regex characters are not special, so 12.30 matches only 12.30. No escaping needed. |
pattern | must_say / must_not_say | Same role as phrase but compiled as a regex. Escape literal punctuation (e.g. goodbye\\?). Supply phrase or pattern; pattern wins if both are present. |
match_type | optional | substring or regex. Overrides the default (substring for phrase, regex for pattern). |
case_sensitive | optional | Defaults to false. |
tool_name | must_call_tool | The tool name the agent must invoke at least once during the run. |
args_match | must_call_tool (optional) | Object whose keys must equal the values in the tool’s invocation args. Use "$present" to assert “key exists with any non-null value”. |
node_id | must_reach_node | Flow node id the run must enter, matched against the node_entered events in GET /agent-eval/runs/{id}/turns. Flow agents only — on a prompt-mode agent it fails saying no flow nodes were entered. When it fails, the reason lists the nodes the flow did reach. |
rubric | custom_llm_judge | Natural-language description of what a successful run looks like. An LLM grades the run against it, seeing the full transcript, the business tool calls with their arguments, and the flow nodes entered. Internal routing calls are excluded, so “must not call any tool” rubrics aren’t tripped by the flow engine’s own bookkeeping. Grading is strict: what the run doesn’t positively demonstrate fails. |
description | optional | Human-readable note shown in the dashboard alongside this assertion. |
type/kind, or a must_say / must_not_say missing
both phrase and pattern, fails with an explicit reason naming the missing
field — it is never treated as a silent pass.Choosing tool_policy
| Policy | When to use |
|---|---|
mock (default) | Your CI suite. Webhook tools never fire — every call returns the fixed synthetic result {"success": true, "status_code": 200}. The request body is still assembled in full, so argument-resolution bugs still surface. Free, deterministic. |
real | One-off pre-prod check that the full integration works. Hits real systems; charges real third-party costs. |
allowlist | Hybrid — list specific tools in tool_allowlist. The listed tools fire for real, the rest return a synthetic success. Useful when you want to validate one new tool but not regenerate calendar holds for the whole suite. |
must_call_tool passes on a
tool that was held back — a mocked run still proves your agent tried. The
assembled request body lands on the run’s turn as tool_calls.args, which is
what must_call_tool’s args_match compares against:
{ "type": "must_call_tool", "tool_name": "bookAppointment",
"args_match": { "topic": "annual checkup" } }
tool_name is the tool’s name — not the flow step’s name, which can differ,
and not a reformatted variant of it. GET /agent-eval/runs/{id}/turns shows the
tool name, the arguments and whether the call dispatched: under mock or
allowlist each recorded call carries dispatched — false for a request that
was assembled but never sent, true for one that really fired — for webhook
tools and connected-app steps alike. Under real the field is absent, because
nothing could have held the call back.
tool_allowlist entries match a tool’s id or its name, exactly and
case-sensitively. Prefer ids: tool names are not unique within a workspace, so
allowlisting a name arms every tool sharing it — possibly pointing at a
different endpoint than the one you meant. An entry that matches nothing is
mocked rather than fired, so a typo fails safe (and shows in the trace as
dispatched: false).
allowlist currently differentiates webhook tools only. Steps that call a
connected app (Google Calendar, Gmail) still run for real under allowlist,
because app connections are gated separately and have no per-tool name to match
against. Use mock if you need those held back too.mock does and does not prove. It proves your agent decides to call
the tool, and passes the right arguments. It does not prove your endpoint is
reachable or that it accepts the request — target validation only happens on a
real dispatch, so a tool pointed at an unreachable or rejected URL passes a
mocked run and fails on the first real call.Because the synthetic response body is fixed ({"success": true}), a step whose
transitions branch on the content of the response can never match under
mock — a mocked step always takes its success edge. Run those with real (or
allowlist) to exercise the response-conditional branch.tool_policy applies to both agent types. A flow agent’s tool steps and
connected-app steps, and the tools attached to a single-prompt agent, all follow
the table above — a prompt agent’s tool sends the same request a live call sends
under real, and returns the same synthetic success under mock.Two behaviours are policy-independent, because they make no request to hold
back: a hang-up tool ends the test run, and a transfer tool ends it
without ever placing a call to the destination.Authorizations
Your Yappr API key (e.g. ypr_live_...). Generate one in the dashboard under Settings → API Keys.
Body
"Yes path — agreement on first ask"
Show child attributes
Show child attributes
1 <= x <= 1000 <= x <= 100mock, real, allowlist Response
Case created
A specific eval scenario — persona + target agent + scenario + success criteria.
Agent under test. Full agent record is expanded inline as agent in API responses.
"Yes path — caller agrees on first ask"
Free-form one-paragraph framing the persona LLM is given on top of its identity. Describe the situation that prompted the call.
"The persona is responding to a missed call from your business about their recent inquiry. They have time to talk for 5 minutes."
Array of assertions evaluated after the run completes.
Show child attributes
Show child attributes
Hard cap on conversation turns. Hitting this terminates the run with termination_reason='max_turns'.
1 <= x <= 100Weighted-score threshold (0-100) for pass_fail=true.
0 <= x <= 100How the agent's tools behave during the run. Applies to BOTH agent types — a flow agent's tool_call steps and the tools attached to a single-prompt agent, which the model calls directly. mock (default): webhook tools make no request — each returns the fixed synthetic result {"success": true, "status_code": 200}. The canonical request body is still assembled (so argument-resolution bugs still surface, and must_call_tool + args_match still assert against it), but target validation and the network hop are skipped, so mock does not prove the endpoint is reachable or acceptable. Because the synthetic body is fixed, a step whose transitions branch on the response CONTENT always takes its success edge under mock. real: tools fire for real (charges real money, hits real systems). allowlist: webhook tools matching an entry in tool_allowlist fire for real, the rest return the same synthetic success. Note that allowlist differentiates webhook tools ONLY — steps calling a connected app (Google Calendar, Gmail) dispatch for real under both real and allowlist, because those are gated separately and have no per-tool name to match on. Use mock to hold those back too. Hang-up and transfer tools are policy-independent: they make no request, so in a run they simply end it (a transfer never places a call to its destination).
mock, real, allowlist Show child attributes
Show child attributes
Reusable caller archetype consumed by eval cases. The identity_prompt plus behavior_traits shape how the persona LLM responds; the same persona can be reused across many cases.
Show child attributes
Show child attributes
Optional parent suite. When null, the case is ad-hoc — runnable on its own but not part of a regression sweep.
Optional per-case overrides applied to the agent's saved config at run time (e.g. a different system_prompt or flow_config for A/B testing). Same shape as the agent record. The agent on disk is never mutated. Configuration only: the identity fields id, company_id, company_timezone, tools and flow_tools are stored but ignored at run time — a run always executes as the agent's own workspace, with the tools that workspace resolved.
Used only when tool_policy='allowlist'. Entries match a tool's id or its name, exactly and case-sensitively. Prefer ids: tool names are not unique within a company, so a name arms every tool that shares it. An entry that matches nothing is mocked rather than fired — a typo fails safe, and shows in the run trace as dispatched: false.