# Run Accent Testing

> End-to-end Coval accent testing workflow. Creates one persona per accent (each using a distinct accent voice and mirroring your Standard Customer behavior), launches one run per accent against the same voice agent + test set + metrics, polls for completion, builds a per-persona comparison table from the results, and creates the saved multi-run report (grouped by Persona) via the public API. Use when a user wants to follow the Testing Across Accents cookbook (https://docs.coval.dev/guides/testing-across-accents) without doing each step by hand.

- Skill: `coval-ai/run-accent-testing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add coval-ai/run-accent-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/coval-ai/run-accent-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: coval-ai (https://skillmd.com/u/coval-ai)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/coval-ai/run-accent-testing

---


# Run Accent Testing

Set up and launch a Coval accent testing sweep end-to-end: one voice agent, one
test set, an accent persona pack, shared metrics, a per-persona comparison table
built from the results, and a saved multi-run report grouped by **Persona**
(created for you via the API). Mirrors the public cookbook at
https://docs.coval.dev/guides/testing-across-accents.

Unlike the audio-quality pack, **the accent personas are not built-in**. This
skill creates them in the user's organization, one per accent voice, and makes
each one mirror the org's **Standard Customer** behavior so the only variable
between runs is the speaker's accent.

When the runs complete this skill hands off to
[analyze-accent-report](../../reports/analyze-accent-report/) for the
recommended-fixes write-up.

## When To Use

- The user wants to follow the "Testing Across Accents" cookbook but does not
  want to hand-create personas and hand-launch each run.
- The user has a voice agent (PSTN, SIP, WebSocket, LiveKit, Vapi, OpenAI
  Realtime, Gemini Realtime, etc.) and a test set they trust.
- The user wants a saved multi-run report grouped by Persona (created via the
  API) that they can open, share, or pass to the analysis skill.

Do **not** use this skill for chat-only agents. Accent testing exercises speech
recognition (STT) and audio timing, none of which a chat agent runs.

## Prerequisites

```bash
coval --version    # CLI must be installed (brew install coval-ai/tap/coval)
coval whoami       # must be authenticated to the right org
```

If `coval whoami` fails, run `coval login` first.

## Accent Persona Pack

The cookbook compares one neutral baseline against six accent personas. Each
accent persona uses a distinct accent voice; every other setting mirrors the
baseline so differences come from the accent, not behavior.

| Persona Name | `--voice` | Accent |
|---|---|---|
| Standard Customer | (built-in baseline) | Neutral baseline |
| Indian Accent (Vidya) | `vidya` | Indian |
| German Accent (Marshal) | `marshal` | German-accented |
| Chinese Accent (Ziyu) | `ziyu` | Chinese-accented |
| US Southern Accent (Cletus) | `cletus` | US Southern |
| Nigerian Accent (Kehinde) | `kehinde` | Nigerian |
| Malaysian Accent (Darryl) | `darryl` | Malaysian |

> **Low concurrency is expected.** All six accent voices are lower-concurrency
> ElevenLabs voices, and ElevenLabs voice simulations are globally capped per
> organization. Launch these runs at a **low `--concurrency` (2 is a safe
> default)** and expect simulations to queue and finish more slowly than a
> standard sweep. This is normal for this suite — it is not a failure.

## Workflow

### Step 1: Confirm The Voice Agent

If `$ARGUMENTS` includes an agent name or ID, look it up. Otherwise list voice
agents and ask the user to pick one:

```bash
coval agents list --format json | jq '.[] | select(.model_type | test("VOICE|REALTIME|WEBSOCKET")) | {id, display_name, model_type}'
```

Pick exactly one. The cookbook is explicit: **keep the agent configuration
fixed across all runs**. Confirm with the user before continuing.

If the agent emits OpenTelemetry traces, recommend including trace-based STT and
timing metrics in step 3. If not, recommend setting up traces (see
[Coval traces docs](https://docs.coval.dev/concepts/simulations/traces/opentelemetry))
or proceeding with transcript-and-audio-only metrics such as Transcription
Error.

### Step 2: Confirm The Test Set

```bash
coval test-sets list
```

Pick one test set. The same test set must be used across every persona run.
Subsampling is allowed as long as the **same sampled cases** run across all
seven personas — use a fixed `--sub-sample-seed` (e.g. `42`) and the same
`--sub-sample-size` on every run.

### Step 3: Confirm Metrics

Accent testing is primarily a **speech-recognition** stress test: does the agent
correctly understand accented speech, and does any mis-recognition change the
task outcome? Lead with STT, then task outcome and call shape. Reuse any metrics
the agent already has attached when they fit.

| Goal | Useful Metrics |
|---|---|
| Speech recognition (headline) | Transcription Error (audio-derived, needs no traces) is the default; add STT Word Error Rate only after confirming the agent emits STT trace spans and the metric actually scores — otherwise it silently fails |
| Task outcome | Composite evaluation, task-completion LLM judges, or scenario-specific pass/fail metrics |
| Responsiveness | Latency, time to first audio, trace TTFB, provider response-time |
| Conversation flow | Turn count, repeated confirmation/clarification loops, early termination, silence, interruption rate |

Do **not** include `manager_audio_frequency` — it measures signal
characteristics, not accent comprehension.

Do **not** recommend `STT Word Error Rate (Audio Upload)` for this general
workflow. It only applies when the test set uses uploaded audio with reference
transcripts, which is a different setup from the normal voice-simulation sweep.

> **STT Word Error Rate needs the right trace spans, or it silently fails.**
> The trace-based `STT Word Error Rate` metric only computes when the agent's
> OpenTelemetry traces carry STT spans with both the recognized hypothesis and a
> reference value. Many voice stacks — Vapi/PSTN agents in particular — do not
> emit those spans, in which case this metric comes back `FAILED`/empty on
> nearly every simulation. That is a **trace-setup gap, not an accent finding**.
> Always include **Transcription Error** (audio-derived word error rate, needs no
> traces) as the reliable recognition headline, and only rely on `STT Word Error
> Rate` after you have confirmed it scores on a real run for this agent.

Recognition that works **without agent traces** (the common case — most agents,
Vapi/PSTN/Pipecat included, emit no STT spans) is **Transcription Error**: a
Coval built-in that re-transcribes the recording with its own STT and scores word
error rate, so it needs no traces. It is a built-in (`organization_id=NULL`) and
can be **paged out of a plain `coval metrics list`**, so resolve it by name —
note the metrics filter uses `=`, not the `:` personas use:

```bash
# Recognition metric that needs no traces — always attach this.
TRANSCRIPTION_ERROR_ID=$(coval metrics list --include-builtin --page-size 100 \
  --filter 'metric_name="Transcription Error"' --format json \
  | jq -r '.[] | select(.metric_name == "Transcription Error") | .id' | head -1)

# Browse task-outcome / responsiveness / flow metrics; reuse the agent's
# attached metrics when they fit.
coval metrics list --include-builtin
```

Build `METRIC_IDS` as a comma-separated string with **Transcription Error first**,
then the task-outcome and call-shape metric IDs you chose. **Always attach a
working recognition metric — measuring recognition is the entire point of an
accent sweep.** Do **not** just omit `--metric-ids` to fall back on the agent's
defaults: many agents have no metrics attached (or none that measure
recognition), so the sweep would score everything *except* the thing it exists to
test. Add `STT Word Error Rate` only *in addition* (never instead), and only
after confirming the agent emits STT trace spans and it actually scores.

### Step 4: Ensure The Accent Personas Exist

The accent personas are not built-in, so create any that are missing. Make each
one mirror the org's **Standard Customer** so the only variable is the accent
voice.

First resolve Standard Customer and read its behavior prompt and wait time so the
accent personas can mirror them:

```bash
STD_ID=$(coval personas list --filter 'name:"Standard Customer"' --format json \
  | jq -r '.[] | select(.name == "Standard Customer") | .id' | head -1)

STD_PROMPT=""
STD_WAIT=""
if [ -n "$STD_ID" ]; then
  STD=$(coval personas get "$STD_ID" --format json)
  STD_PROMPT=$(echo "$STD" | jq -r '.persona_prompt // ""')
  STD_WAIT=$(echo "$STD"   | jq -r '.wait_seconds // ""')
fi

# Fallbacks if Standard Customer is missing or has no prompt/wait time.
if [ -z "$STD_PROMPT" ] || [ "$STD_PROMPT" = "null" ]; then
  STD_PROMPT="You are a standard customer calling in with a routine request. Speak naturally and conversationally. Answer the agent's questions, provide the information it asks for, and work toward completing your task."
fi
if [ -z "$STD_WAIT" ] || [ "$STD_WAIT" = "null" ]; then
  STD_WAIT="0.5"
fi

# IMPORTANT: do NOT reuse Standard Customer's language code for the accent
# personas. Accent voices are locale-bound and most do NOT accept en-US, so
# `coval personas create` rejects the pair with
# "Voice 'X' does not support language 'Y'". Supported languages per voice:
#   vidya  en, en-IN     marshal en, en-GB    ziyu   en, en-GB
#   cletus en, en-US     kehinde en (only)    darryl en (only)
# The one language all six accent voices share is the base "en", so use "en" for
# every accent persona. A single shared language keeps the accent voice the only
# variable. (If you specifically want an accent-native locale, map each voice to
# a code it supports above instead — but then language co-varies with accent.)
ACCENT_LANG="en"
```

Then create each accent persona only if an exact-name match does not already
exist (idempotent — never create duplicates):

```bash
# "Persona Name|voice" — the voice is the Coval catalog name.
ACCENTS=(
  "Indian Accent (Vidya)|vidya"
  "German Accent (Marshal)|marshal"
  "Chinese Accent (Ziyu)|ziyu"
  "US Southern Accent (Cletus)|cletus"
  "Nigerian Accent (Kehinde)|kehinde"
  "Malaysian Accent (Darryl)|darryl"
)

for entry in "${ACCENTS[@]}"; do
  name="${entry%%|*}"
  voice="${entry##*|}"

  existing=$(coval personas list --filter "name:\"$name\"" --format json \
    | jq -r --arg n "$name" '.[] | select(.name == $n) | .id' | head -1)

  if [ -n "$existing" ]; then
    echo "exists: $name ($existing)"
    continue
  fi

  echo "creating: $name (voice: $voice)"
  coval personas create \
    --name "$name" \
    --voice "$voice" \
    --language "$ACCENT_LANG" \
    --prompt "$STD_PROMPT" \
    --wait-seconds "$STD_WAIT" \
    --format json
done
```

Notes:

- The `--voice` value is the lowercase Coval catalog name (`vidya`, `marshal`,
  `ziyu`, `cletus`, `kehinde`, `darryl`), not the display label.
- Use the **same** `--prompt`, the same `--wait-seconds`, and the shared `en`
  language for every accent persona so the accent voice is the only thing that
  changes between runs. Do not pass Standard Customer's `en-US` to an accent
  voice — most accent voices reject it (see the language note above).
- `coval personas create` does not expose `conversation_initiation` or
  `background_sound`, so new personas take the defaults. If your Standard
  Customer overrides either, the accent personas will differ on that secondary
  setting — note it, or set those fields in the app for a pixel-perfect mirror.
- Do not overwrite an existing persona. If one already exists with a different
  voice or prompt, tell the user and let them decide — do not silently recreate.

### Step 5: Resolve Persona IDs

Resolve all seven persona IDs by exact-name match (Standard Customer plus the
six accent personas) into ordered arrays the launch step reuses. Keep Standard
Customer first so it is the baseline column in the report:

```bash
# Canonical persona order — Standard Customer first (baseline).
PERSONA_NAMES=(
  "Standard Customer"
  "Indian Accent (Vidya)"
  "German Accent (Marshal)"
  "Chinese Accent (Ziyu)"
  "US Southern Accent (Cletus)"
  "Nigerian Accent (Kehinde)"
  "Malaysian Accent (Darryl)"
)

PERSONA_IDS=()
for name in "${PERSONA_NAMES[@]}"; do
  id=$(coval personas list --filter "name:\"$name\"" --format json \
    | jq -r --arg n "$name" '.[] | select(.name == $n) | .id' | head -1)
  if [ -z "$id" ]; then
    echo "MISSING persona: $name — resolve Step 4 before launching" >&2
    exit 1
  fi
  PERSONA_IDS+=("$id")
  echo "$id  $name"
done
```

If any persona is still missing after Step 4, the loop stops — fix it before
launching. Do not skip an accent.

### Step 6: Launch The Runs

Launch seven runs — one per persona — with the same agent, test set, metrics,
and (if subsampling) the same `--sub-sample-seed`. Keep `--concurrency` low
because these are lower-concurrency voices. Use shared tags so the runs are easy
to find later.

```bash
SUB_SAMPLE_SEED=42
SUB_SAMPLE_SIZE=0   # 0 = all cases; set to a positive integer to subsample
CONCURRENCY=2       # low on purpose — accent voices are lower-concurrency
TAGS="accent-testing,cookbook"
metric_args=()
if [ -n "${METRIC_IDS:-}" ]; then
  metric_args=(--metric-ids "$METRIC_IDS")
fi

# Uses PERSONA_NAMES / PERSONA_IDS from Step 5; captures each run_id into RUN_IDS.
RUN_IDS=()
for i in "${!PERSONA_IDS[@]}"; do
  persona_id="${PERSONA_IDS[$i]}"
  persona_name="${PERSONA_NAMES[$i]}"
  resp=$(coval runs launch \
    --agent-id "$AGENT_ID" \
    --persona-id "$persona_id" \
    --test-set-id "$TEST_SET_ID" \
    "${metric_args[@]}" \
    --concurrency "$CONCURRENCY" \
    --sub-sample-size "$SUB_SAMPLE_SIZE" \
    --sub-sample-seed "$SUB_SAMPLE_SEED" \
    --tags "$TAGS" \
    --name "Accent — $persona_name" \
    --format json)
  run_id=$(echo "$resp" | jq -r '.run_id // .id')
  RUN_IDS+=("$run_id")
  echo "launched $persona_name -> $run_id"
done
```

The loop appends each launched `run_id` to `RUN_IDS`, which Step 7 watches and
Step 8 turns into the report URL.

**Tip:** the public API also supports a batch launch (`POST /v1/runs/batch`) if
you prefer one HTTP call. The CLI does not expose batch launch today.

### Step 7: Watch For Completion

```bash
for run_id in "${RUN_IDS[@]}"; do
  coval runs watch "$run_id"
done
```

`coval runs watch` blocks until the run reaches a terminal status (COMPLETED,
FAILED, or CANCELLED). Because the accent voices are lower-concurrency,
simulations may queue and the sweep may take longer than a standard run — that
is expected. Run these in parallel shells or background jobs if you want all
seven watched at once.

If any run finishes as **FAILED** with `total_test_cases=0`, the run-setup
worker crashed before sub-sampling. Re-launch that single run with
`--sub-sample-size 1` (or omit subsampling) and report the failure as a Coval
issue — this has been observed in some orgs.

### Step 8: Build The Persona Comparison Table

The report URL in Step 9 opens Coval's report builder, but the grouping is a
manual UI step, so it does not by itself hand the user a comparison. **The user
asked for a report comparing the personas — produce that comparison here**,
directly from the API, so they get a ready-to-read artifact without touching the
app. For every run, list its simulations, pull each simulation's metric values,
and aggregate per persona:

```bash
# Map metric_id -> name once (include built-ins like Transcription Error).
coval metrics list --include-builtin --page-size 100 --format json \
  | jq -r '.[] | "\(.id)\t\(.metric_name // .name)"' > /tmp/accent_metric_names.tsv

# Gather every simulation's metric values across all runs.
: > /tmp/accent_metrics.jsonl
for i in "${!RUN_IDS[@]}"; do
  run_id="${RUN_IDS[$i]}"
  persona="${PERSONA_NAMES[$i]}"
  coval simulations list --run-id "$run_id" --format json \
    | jq -r '.[].simulation_id' \
    | while read -r sid; do
        coval simulations metrics "$sid" --format json \
          | jq -c --arg p "$persona" --arg s "$sid" \
              '.[] | {persona:$p, sim:$s, metric_id:.metric_id, status:.status, value:.value}' \
          >> /tmp/accent_metrics.jsonl
      done
done
```

Then aggregate per persona per metric and print one Markdown table the user can
read directly — **pass rate** (YES / total) for binary judges, **mean** for
numeric metrics — ordered Standard Customer first and **leading with the
recognition metrics** (Transcription Error, then STT Word Error Rate only if it
actually scored), then task-outcome judges, then call-shape metrics:

| Metric (direction) | Standard | Indian | German | Chinese | Southern | Nigerian | Malaysian |
|---|---|---|---|---|---|---|---|
| Transcription Error (lower=better) | 0.04 | 0.03 | 0.02 | 0.03 | 0.04 | 0.03 | 0.03 |
| Conversation Success (higher=better) | 100% | 80% | 80% | 100% | 100% | 80% | 80% |
| … | | | | | | | |

Rules for the table:

- Mark any metric that is `FAILED`/missing on most simulations as a **measurement
  gap, not a result** — flag it and exclude it from the regression call-outs.
  `STT Word Error Rate` is commonly all-`FAILED` when the agent emits no STT
  trace spans (see Step 3); say so rather than reporting it as 0 or as an accent
  signal.
- Call out the largest per-accent deltas vs Standard Customer, but **do not
  over-read thin samples** — with a small test set each accent run has few
  simulations, so treat single-case swings as hypotheses to confirm in Step 10,
  not conclusions.
- Before blaming the accent, check whether Standard Customer regressed on the
  same metric/case too. A failure the neutral baseline also hits is an agent,
  tool, or judge issue, not an accent issue.

### Step 9: Create The Saved Report (grouped by Persona)

Create the saved multi-run report directly through the public API so it lands in
the org's Reports section **already grouped by Persona** — no manual builder
step. The CLI has no `reports` command yet, so call the API directly (a `coval
reports create` command would be a good follow-up). Use the same API key your
`coval` CLI is authenticated with.

```bash
# Resolve ORG_SLUG from the agent/test-set app URL the user gave you, e.g.
# https://app.coval.dev/acme-co/agents/AbC123 -> ORG_SLUG=acme-co.
ORG_SLUG="${ORG_SLUG:?set ORG_SLUG to the org slug from the Coval app URL}"

# Build the run-id JSON array from the RUN_IDS bash array (Step 6).
RUN_IDS_JSON=$(printf '%s\n' "${RUN_IDS[@]}" | jq -R . | jq -s -c .)

resp=$(curl -s -X POST "https://api.coval.dev/v1/reports" \
  -H "x-api-key: ${COVAL_API_KEY:?use the API key your coval CLI is authenticated with}" \
  -H "content-type: application/json" \
  -d "$(jq -nc --arg name "Accent sweep — $(date +%F)" --argjson run_ids "$RUN_IDS_JSON" \
        '{name: $name, run_ids: $run_ids, compare_by: "persona"}')")

REPORT_ID=$(echo "$resp" | jq -r '.report.id // empty')
if [ -n "$REPORT_ID" ]; then
  echo "Saved report: https://app.coval.dev/${ORG_SLUG}/reports/${REPORT_ID}"
else
  echo "Report not created (response: $resp)" >&2
fi
```

`compare_by: "persona"` persists `view_config.compareBy=persona`, so the saved
report **opens already grouped by Persona** at
`https://app.coval.dev/<org>/reports/<id>`. The default is `PRIVATE`; pass
`"permissions": "PUBLIC"` only if the user wants a login-free shareable link
(that also marks the included runs public).

**Fallback — if `POST /v1/reports` returns 404** (an older Coval without the
reports API), emit the report **builder** URL and have the user group + save by
hand instead:

```bash
RUN_IDS_CSV=$(IFS=,; echo "${RUN_IDS[*]}")
echo "https://app.coval.dev/${ORG_SLUG}/reports/new?run_ids=${RUN_IDS_CSV}"
# Open it, set Compare by -> Persona (default is None), then Save.
```

The Step 8 table already gives the user the persona comparison inline; this saved
report is the interactive, shareable version of the same data.

### Step 10: Hand Off Analysis

Once the comparison table and report exist, point the user at
[analyze-accent-report](../../reports/analyze-accent-report/) for a structured
next-fix write-up. The analysis skill turns the grouped report into prompt,
STT/confirmation, routing, or coverage recommendations.

Suggest pasting the saved report URL into:

```text
Use the Coval `analyze-accent-report` skill on this report:
<paste saved report URL>
```

## Output Format

When the skill finishes, return a short, actionable summary:

```markdown
## Accent Testing — Launch Summary

**Agent:** <display_name> (<id>)
**Test set:** <display_name> (<id>)
**Subsample:** <sub_sample_size> cases (seed <sub_sample_seed>) — or "full test set"
**Metrics:** <comma-separated metric names>
**Personas created this run:** <list, or "none — all already existed">

**Runs:**
| Persona | Voice | Run ID | Status | Run link |
|---|---|---|---|---|
| Standard Customer | (baseline) | … | … | https://app.coval.dev/<org>/runs/<id> |
| Indian Accent (Vidya) | vidya | … | … | … |
| German Accent (Marshal) | marshal | … | … | … |
| Chinese Accent (Ziyu) | ziyu | … | … | … |
| US Southern Accent (Cletus) | cletus | … | … | … |
| Nigerian Accent (Kehinde) | kehinde | … | … | … |
| Malaysian Accent (Darryl) | darryl | … | … | … |

**Persona comparison (from Step 8 — recognition metrics first, baseline first):**
| Metric (direction) | Standard | Indian | German | Chinese | Southern | Nigerian | Malaysian |
|---|---|---|---|---|---|---|---|
| Transcription Error (↓) | … | … | … | … | … | … | … |
| <task-outcome judge> (↑) | … | … | … | … | … | … | … |
| … | | | | | | | |

**Largest deltas vs Standard:** <one line per notable accent delta, or "none — all accents within noise of baseline">. Note any `FAILED`/missing metrics as measurement gaps, and flag small-sample (few-sims-per-accent) conclusions as tentative.

**Saved report (grouped by Persona):**
https://app.coval.dev/<org>/reports/<id> — created via `POST /v1/reports` (`compare_by: persona`); opens already grouped. (Fallback if the reports API was unavailable: the builder URL `…/reports/new?run_ids=<csv>` + a manual Compare-by-Persona save.)

**Next step:** run the `analyze-accent-report` skill on the saved report.
```

## Guardrails

- One agent, one test set, same metrics, same subsample seed across all seven
  runs. Any divergence breaks the grouped comparison the report depends on.
- Make every accent persona mirror Standard Customer's prompt and wait time, and
  give them all the same shared `en` language, so the accent voice is the only
  variable. Do not copy Standard Customer's `en-US` onto an accent voice — most
  accent voices reject it; `en` is the one code all six accept.
- STT Word Error Rate only scores when the agent emits STT trace spans. If it
  comes back `FAILED`/empty across the sweep, report it as a trace-setup gap and
  lean on Transcription Error — never present an unscored metric as a result.
- Always hand the user the Step 8 persona-comparison table. A bare report-builder
  URL is not the comparison they asked for; the grouped view is a manual UI step.
- Create accent personas only when an exact-name match is missing. Never create
  duplicates, and never silently overwrite an existing persona.
- Keep `--concurrency` low and expect queueing — these are lower-concurrency
  voices. Slow completion is expected, not a failure.
- Do not change the agent's configuration mid-sweep. If you must, launch a fresh
  sweep so the comparison stays apples-to-apples.
- Do not use chat-only agents. The cookbook explicitly does not apply.
- Do not skip an accent because the org "doesn't serve" that population. The full
  pack is what makes the comparison interpretable.
- Do not invent persona, agent, test set, or metric IDs — always resolve them
  from the user's org.

