curl -X POST "https://api.signalmd.dev/v1/agents/6f1d2c3b-4a5e-4f60-8b7a-9c0d1e2f3a4b/runs" \
-H "Authorization: Bearer $SIGNAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"version":8,"testSetId":"c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f","personas":["@source","hurried-returning-patient"]}'import requests
url = "https://api.signalmd.dev/v1/agents/{agentId}/runs"
payload = {
"version": 8,
"testSetId": "c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f",
"personas": ["@source", "hurried-returning-patient"]
}
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({
version: 8,
testSetId: 'c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f',
personas: ['@source', 'hurried-returning-patient']
})
};
fetch('https://api.signalmd.dev/v1/agents/{agentId}/runs', 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.signalmd.dev/v1/agents/{agentId}/runs",
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([
'version' => 8,
'testSetId' => 'c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f',
'personas' => [
'@source',
'hurried-returning-patient'
]
]),
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.signalmd.dev/v1/agents/{agentId}/runs"
payload := strings.NewReader("{\n \"version\": 8,\n \"testSetId\": \"c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f\",\n \"personas\": [\n \"@source\",\n \"hurried-returning-patient\"\n ]\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.signalmd.dev/v1/agents/{agentId}/runs")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"version\": 8,\n \"testSetId\": \"c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f\",\n \"personas\": [\n \"@source\",\n \"hurried-returning-patient\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.signalmd.dev/v1/agents/{agentId}/runs")
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 \"version\": 8,\n \"testSetId\": \"c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f\",\n \"personas\": [\n \"@source\",\n \"hurried-returning-patient\"\n ]\n}"
response = http.request(request)
puts response.read_body{
"object": "run",
"id": "d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f5a",
"status": "complete",
"message": null,
"kind": "run",
"trigger": "ci",
"version": 8,
"testSetId": "c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f",
"testIds": [
"3f1c2d4e-5a6b-4c7d-8e9f-0a1b2c3d4e5f"
],
"personas": null,
"repeats": null,
"shadowAgentId": null,
"phoneCalls": false,
"progress": {
"completed": 12,
"total": 12
},
"cells": {
"requested": 12,
"runnable": 12
},
"summary": {
"cells": 12,
"scored": 12,
"counts": {
"passed": 9,
"failed": 2,
"flaky": 1,
"error": 0,
"drift": 0,
"notRun": 0
},
"passRate": 0.75
},
"cost": {
"retellUsd": 1.84,
"signalUsd": 0.21
},
"goalId": null,
"draftId": null,
"draftScope": null,
"spend": {
"retellUsd": 1.84,
"signalUsd": 0.21,
"totalUsd": 2.05,
"goalId": null
},
"phoneMinutes": null,
"approvedAt": null,
"createdBy": "key:00000000-0000-4000-8000-00000000000a",
"createdAt": "2026-10-02T11:00:03.000Z",
"updatedAt": "2026-10-02T11:07:41.000Z"
}{
"error": {
"type": "not_found_error",
"code": "agent_not_found",
"message": "Agent not found.",
"param": null,
"requestId": "req_8c1f0e2b4a6d9e3f7a5c1b0d"
}
}{
"error": {
"type": "not_found_error",
"code": "agent_not_found",
"message": "Agent not found.",
"param": null,
"requestId": "req_8c1f0e2b4a6d9e3f7a5c1b0d"
}
}{
"error": {
"type": "not_found_error",
"code": "agent_not_found",
"message": "Agent not found.",
"param": null,
"requestId": "req_8c1f0e2b4a6d9e3f7a5c1b0d"
}
}{
"error": {
"type": "not_found_error",
"code": "agent_not_found",
"message": "Agent not found.",
"param": null,
"requestId": "req_8c1f0e2b4a6d9e3f7a5c1b0d"
}
}{
"error": {
"type": "not_found_error",
"code": "agent_not_found",
"message": "Agent not found.",
"param": null,
"requestId": "req_8c1f0e2b4a6d9e3f7a5c1b0d"
}
}{
"error": {
"type": "not_found_error",
"code": "agent_not_found",
"message": "Agent not found.",
"param": null,
"requestId": "req_8c1f0e2b4a6d9e3f7a5c1b0d"
}
}{
"error": {
"type": "not_found_error",
"code": "agent_not_found",
"message": "Agent not found.",
"param": null,
"requestId": "req_8c1f0e2b4a6d9e3f7a5c1b0d"
}
}{
"error": {
"type": "not_found_error",
"code": "agent_not_found",
"message": "Agent not found.",
"param": null,
"requestId": "req_8c1f0e2b4a6d9e3f7a5c1b0d"
}
}Start a run
Plays tests (a set, or a list) against one agent version as simulated calls, optionally once per persona. This spends: the customer’s Retell budget, and for a key, its monthly budget too. A run neither covers is refused with 402 budget_error before anything is spent; one they cover in part starts and plays the covered cells (estimate.partial). Returns at once with the queued run: poll GET …/runs/, then read …/result. Not idempotent.
curl -X POST "https://api.signalmd.dev/v1/agents/6f1d2c3b-4a5e-4f60-8b7a-9c0d1e2f3a4b/runs" \
-H "Authorization: Bearer $SIGNAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"version":8,"testSetId":"c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f","personas":["@source","hurried-returning-patient"]}'import requests
url = "https://api.signalmd.dev/v1/agents/{agentId}/runs"
payload = {
"version": 8,
"testSetId": "c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f",
"personas": ["@source", "hurried-returning-patient"]
}
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({
version: 8,
testSetId: 'c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f',
personas: ['@source', 'hurried-returning-patient']
})
};
fetch('https://api.signalmd.dev/v1/agents/{agentId}/runs', 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.signalmd.dev/v1/agents/{agentId}/runs",
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([
'version' => 8,
'testSetId' => 'c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f',
'personas' => [
'@source',
'hurried-returning-patient'
]
]),
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.signalmd.dev/v1/agents/{agentId}/runs"
payload := strings.NewReader("{\n \"version\": 8,\n \"testSetId\": \"c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f\",\n \"personas\": [\n \"@source\",\n \"hurried-returning-patient\"\n ]\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.signalmd.dev/v1/agents/{agentId}/runs")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"version\": 8,\n \"testSetId\": \"c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f\",\n \"personas\": [\n \"@source\",\n \"hurried-returning-patient\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.signalmd.dev/v1/agents/{agentId}/runs")
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 \"version\": 8,\n \"testSetId\": \"c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f\",\n \"personas\": [\n \"@source\",\n \"hurried-returning-patient\"\n ]\n}"
response = http.request(request)
puts response.read_body{
"object": "run",
"id": "d1e2f3a4-b5c6-4d7e-8f9a-0b1c2d3e4f5a",
"status": "complete",
"message": null,
"kind": "run",
"trigger": "ci",
"version": 8,
"testSetId": "c7d8e9f0-a1b2-4c3d-8e4f-5a6b7c8d9e0f",
"testIds": [
"3f1c2d4e-5a6b-4c7d-8e9f-0a1b2c3d4e5f"
],
"personas": null,
"repeats": null,
"shadowAgentId": null,
"phoneCalls": false,
"progress": {
"completed": 12,
"total": 12
},
"cells": {
"requested": 12,
"runnable": 12
},
"summary": {
"cells": 12,
"scored": 12,
"counts": {
"passed": 9,
"failed": 2,
"flaky": 1,
"error": 0,
"drift": 0,
"notRun": 0
},
"passRate": 0.75
},
"cost": {
"retellUsd": 1.84,
"signalUsd": 0.21
},
"goalId": null,
"draftId": null,
"draftScope": null,
"spend": {
"retellUsd": 1.84,
"signalUsd": 0.21,
"totalUsd": 2.05,
"goalId": null
},
"phoneMinutes": null,
"approvedAt": null,
"createdBy": "key:00000000-0000-4000-8000-00000000000a",
"createdAt": "2026-10-02T11:00:03.000Z",
"updatedAt": "2026-10-02T11:07:41.000Z"
}{
"error": {
"type": "not_found_error",
"code": "agent_not_found",
"message": "Agent not found.",
"param": null,
"requestId": "req_8c1f0e2b4a6d9e3f7a5c1b0d"
}
}{
"error": {
"type": "not_found_error",
"code": "agent_not_found",
"message": "Agent not found.",
"param": null,
"requestId": "req_8c1f0e2b4a6d9e3f7a5c1b0d"
}
}{
"error": {
"type": "not_found_error",
"code": "agent_not_found",
"message": "Agent not found.",
"param": null,
"requestId": "req_8c1f0e2b4a6d9e3f7a5c1b0d"
}
}{
"error": {
"type": "not_found_error",
"code": "agent_not_found",
"message": "Agent not found.",
"param": null,
"requestId": "req_8c1f0e2b4a6d9e3f7a5c1b0d"
}
}{
"error": {
"type": "not_found_error",
"code": "agent_not_found",
"message": "Agent not found.",
"param": null,
"requestId": "req_8c1f0e2b4a6d9e3f7a5c1b0d"
}
}{
"error": {
"type": "not_found_error",
"code": "agent_not_found",
"message": "Agent not found.",
"param": null,
"requestId": "req_8c1f0e2b4a6d9e3f7a5c1b0d"
}
}{
"error": {
"type": "not_found_error",
"code": "agent_not_found",
"message": "Agent not found.",
"param": null,
"requestId": "req_8c1f0e2b4a6d9e3f7a5c1b0d"
}
}{
"error": {
"type": "not_found_error",
"code": "agent_not_found",
"message": "Agent not found.",
"param": null,
"requestId": "req_8c1f0e2b4a6d9e3f7a5c1b0d"
}
}Authorizations
An API key (sig_...), or a session token from the web app.
Path Parameters
The agent's id.
^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"3f1c2d4e-5a6b-4c7d-8e9f-0a1b2c3d4e5f"
Body
The agent version to play. Its configuration must be stored, so its tools can be mocked.
x >= 0Run every test in this set. Send this or testIds.
^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"3f1c2d4e-5a6b-4c7d-8e9f-0a1b2c3d4e5f"
Run these tests, in this order. Send this or testSetId. Every id must be one of this agent's tests: an unknown id is 404 tests_not_found and an archived one 400 tests_archived, both naming the ids, and nothing runs.
1^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$Persona slugs (GET .../personas, runnable ones); each test runs once per persona. @source is each test's own caller. Absent: each test's own.
113: every cell runs three times and reports how many of them passed. Absent: once, and again on a failure; guard tests always run three times.
3 Play this shadow copy (POST .../shadows) instead of the version itself; the version must be the one it was cloned from.
^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"3f1c2d4e-5a6b-4c7d-8e9f-0a1b2c3d4e5f"
The goal this run works toward; its cost is counted on the goal. The in-app copilot must send the goal of a plan a person approved.
^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"3f1c2d4e-5a6b-4c7d-8e9f-0a1b2c3d4e5f"
The draft this run is for, recorded only. A draft's test through POST .../drafts/{draftId}/test is what counts toward publishing.
^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"3f1c2d4e-5a6b-4c7d-8e9f-0a1b2c3d4e5f"
true: tests that need a phone call (hearing or speaking issues) are played as real calls on the test's agent copy, at most 5 minutes each and never retried, instead of being skipped. Each is a call on your Retell account. Cannot be combined with repeats. Each call holds 30 credits while it runs and is charged 6 credits at least, then 1 per 10 seconds. When the calls' holds and the run's other tests would not fit the workspace's credits left, the run is refused with quota_exceeded (metric credits) and nothing is queued.
Response
The run, queued.
run ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"3f1c2d4e-5a6b-4c7d-8e9f-0a1b2c3d4e5f"
queued, running, then complete, refused (budget) or failed. An automatic run over the budget waits in awaiting_approval for a person.
queued, awaiting_approval, running, complete, refused, failed The latest status message: what the budget covered, or why it refused or failed.
A calibration run plays each test several times to decide whether it gates.
run, calibration Who started it: a person, SignalMD on a new version, or an API key.
manual, auto, ci, null The agent version played.
^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"3f1c2d4e-5a6b-4c7d-8e9f-0a1b2c3d4e5f"
The tests requested, in order.
^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$The personas requested; null means each test's own caller.
3: every cell runs three times and reports how many of them passed.
3 Set when the run played a shadow copy of the version.
^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"3f1c2d4e-5a6b-4c7d-8e9f-0a1b2c3d4e5f"
The goal this run worked toward.
^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"3f1c2d4e-5a6b-4c7d-8e9f-0a1b2c3d4e5f"
The draft whose shadow copy this run played.
^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"3f1c2d4e-5a6b-4c7d-8e9f-0a1b2c3d4e5f"
Set on a draft's test: an attempt, or the full accepted suite (what a publish through the API needs).
attempt, full_suite, null True when tests that need a phone call were played as real calls rather than skipped.
Cells finished of cells to play.
Show child attributes
Show child attributes
Fewer runnable than requested means the budget covered part of the run; the rest are not run.
Show child attributes
Show child attributes
Show child attributes
Show child attributes
{ "cells": 12, "scored": 12, "counts": { "passed": 9, "failed": 2, "flaky": 1, "error": 0, "drift": 0, "notRun": 0 }, "passRate": 0.75 }
Estimated spend.
Show child attributes
Show child attributes
What the run spent in one place; a goal's model spend is on GET .../goals/{goalId}.
Show child attributes
Show child attributes
Test minutes this run's phone calls count against the plan, from the same allowance its simulated tests use. Null when the run plays no phone calls.
Show child attributes
Show child attributes
When a person approved an automatic run that was over the budget.
"2026-10-02T14:03:07.120Z"
Who started it: a user id, or key: for an API key.
"2026-10-02T14:03:07.120Z"
"2026-10-02T14:03:07.120Z"
What was checked when the run was accepted.
Show child attributes
Show child attributes
Was this page helpful?