Evalgent
Back to Blog
Voice AI Testing

Voice Agent Post-Call Write-Back Testing: Record vs Call

Deepesh Jayal
14 min read
Voice Agent Post-Call Write-Back Testing: Record vs Call
On this page

The call ended well. The caller heard "you're all set." Three days later a technician shows up at the wrong hour, or a customer gets two confirmation texts, or a note in the CRM holds a full card number. The conversation was fine. The write-back was not.

If your team built its own voice agent inside a larger product, the call is the front door. The record is the thing your customers, your support team and your regulators actually see. Voice agent record accuracy is the metric most teams skip. Most test suites stop at the transcript or the tool call. This post on voice agent post call write back testing covers the last hop: the record that exists after hangup.

It does not re-teach tool-call checks. For those, see tool call accuracy for voice agents and voice agent tool-calling test cases. It also assumes you know the basics of silent tool failures. Here the question is narrower: does the stored record match the call?

Why the record can differ from the call

A voice agent CRM write back has five steps. The agent captures values from speech. Code normalises them into fields. A request goes to your CRM, scheduling table or ticket system. The system answers. A later job may add a summary or status.

Each step can change the value without raising an error. Speech recognition may hear "fifteen" for "fifty." A parser may read "3" as 3 AM. A retry may create a second row. A timeout may leave half the fields written. The agent still says "done," because it only saw a success code.

That is why transcript scoring cannot catch these bugs. The transcript can be perfect while the record is wrong. The reverse also happens. Evalgent's thesis is that judging from text alone misses audio-level failures. The record check is the other half: verify the outcome, not the wording.

Write path from caller speech to stored record, with six numbered points where the value can change silently: capture, normalise, request, response, retry and post-call summary

The write-back failure taxonomy

Use this grid to plan voice agent post call write back testing coverage. Each row is a class of bug, how it shows up, and the test that catches it.

Failure classWhat the record showsTest that catches it
Wrong valueField holds a near miss of what was saidField-level diff against golden truth
Wrong objectUpdate lands on a different contact or jobRead back by record ID, check the owner
Silent success200 or 202 returned, nothing storedRead-after-write with a timeout
DuplicateTwo rows for one callFault-injected retry, count rows per call
Partial writeSome fields set, others emptyKill the connection mid-write
Race after hangupSummary or status missing or staleHang up at each call stage
Time driftRight clock time, wrong zone or dayZone and DST cases
OverwriteOld good data replaced by new guessSeed existing values, then call
PII in notesCard, SSN or health detail in free textPattern scan of every text field
Summary conflictNote says one thing, fields say anotherCompare summary claims to fields

Trade posts show these classes with real data. The pharmacy refill line shows how a one-digit slip lands in a prescription record. The utility outage post shows an address that reads right on the call and wrong on the ticket. The pizza order status post shows a lookup that finds the wrong ticket. Capture errors upstream of the write are covered in alphanumeric capture and STT entity accuracy.

Silent success: a 200 with the wrong object

HTTP success tells you the server accepted a request. It does not tell you the right row changed. A lookup by phone number is the classic trap. Two contacts share a household line, the agent grabs the first match, and the update lands on the wrong person. Every status code in the chain is green.

The 202 code is worse. MDN describes `202 Accepted` as a request accepted for processing where "processing has not been completed or may not have started," and notes that "there is no way to later send an asynchronous HTTP response to indicate the outcome" (MDN, 202 Accepted). If your CRM returns 202, the agent cannot know the write worked. It has to poll or you have to reconcile.

Two rules follow.

1. Never let the agent say "done" on a status code alone. Confirm by reading the record back, by ID, and checking owner and key fields.

2. Test the lookup, not just the write. Seed near-duplicate contacts: same phone, same surname, same address with a different unit. Then check which row changed.

Retries and duplicates

Retries are where voice agent duplicate records come from. Networks drop responses, not just requests. The client sees a timeout, tries again, and the server now has two copies.

Method semantics matter here. An HTTP method is idempotent when "the intended effect on the server of making a single request is the same as the effect of making several identical requests," and POST is "not guaranteed to be idempotent" (MDN, Idempotent). Creating a record with POST can therefore duplicate on retry.

There is a standard fix in progress. The IETF HTTPAPI working group's `Idempotency-Key` draft (version 08, dated October 22, 2026, still an Internet-Draft as of November 2026) says a key "MUST be unique and MUST NOT be reused with another request with a different request payload" (IETF datatracker). It suggests a 409 if the first request is still running and a 422 for key reuse with a different payload. Because it is a draft, check whether your target system honours any such key before you rely on it.

Inbound events duplicate too. LiveKit's webhook docs say "there are no guarantees around delivery," that it retries on transient failure, and that each event carries a UUID `id` (LiveKit webhooks). Retries mean the same event can arrive more than once, and because delivery is not guaranteed it can also be missed, so your post-call job may run twice for one call or not at all. Dedupe on that event ID and reconcile for gaps.

The strongest fix lives in your own store. Make the call ID plus the operation a unique key, and let the database refuse the second write. PostgreSQL documents `ON CONFLICT DO NOTHING` as skipping the insert, and `DO UPDATE` as guaranteeing "an atomic INSERT or UPDATE outcome" even under high concurrency (PostgreSQL INSERT).

CREATE UNIQUE INDEX crm_records_call_op
  ON crm_records (source_call_id, op);

INSERT INTO crm_records (source_call_id, op, payload)
VALUES ($1, 'create_appointment', $2)
ON CONFLICT (source_call_id, op) DO NOTHING;
Timeline of one write: attempt one times out after the server commits, the retry without an idempotency key creates a second row, and the retry with a key returns the original row

The test is a fault injection. Let the first write commit, drop the response, and let the client retry. Then count rows for that call. The pass condition is exactly one.

Partial writes and overwrites

A record is rarely one field. An appointment has a time, a service, a contact, a location and a note. If the write is several calls, a failure in the middle leaves a record that looks valid but is incomplete. Worse, the downstream system may start acting on it.

Test this by failing the second request of a multi-step write. Then check what a human or downstream job would see. Is the half-written record visible? Is it marked incomplete? Does the agent tell the caller the truth?

Overwrites are the opposite problem. The agent writes a field that already held good data. A caller says a new phone number for this call only, and the profile's primary number changes for good. Seed each scenario with existing values and assert which ones must survive. Decide per field whether the agent may replace, append or must ask.

The race after hangup

Many teams run a post-call job that summarises the call and sets a status. It fires on a hangup or room-closed event. The final transcript and tool results may not be complete yet.

LiveKit lists separate events for `participant_left`, `room_finished` and `egress_ended`, and says it sequences queued events, delivering newer ones only after older ones are delivered or abandoned. Those are different moments. A job keyed to the first one can read a transcript that is still short. That behaviour depends on your stack, so treat this as a pattern to test, not a fact about yours.

Test it by hanging up at different points: mid-confirmation, right after the write, and during the read-back. Then check three things. Did the record exist? Was it complete? Was the summary written once, from the final transcript? A cheap guard: wait for a "finalised" marker, and make the write safe to repeat. The deeper logging checklist is in what to log in voice agent calls.

Time, format and normalisation

Speech gives you "Thursday at three." Your record needs an absolute timestamp with a zone. Every step in between is a chance to be wrong by hours.

RFC 3339 defines a widely used internet timestamp profile: a date, a `T`, a time and a mandatory offset that is either `Z` or a numeric value. Offsets are "local time minus UTC," so `18:50:00-04:00` equals `22:50:00Z` (RFC 3339). The same standard notes that `-00:00` means the local offset is unknown, which is different from `Z`. A timestamp stored without an offset is a latent bug.

Daylight saving adds a second trap. Python's `zoneinfo` docs show that an ambiguous local time, such as 1:00 AM on the November fall-back date, takes the earlier offset when `fold=0` and the later one when `fold=1` (Python zoneinfo). The docs cover ambiguous times only, so test skipped spring-forward times in your own code.

Build these cases:

  • The caller is in a different zone from the business.
  • "Tomorrow" said at 11:50 PM.
  • "Next Friday" said on a Wednesday and on a Friday.
  • A booking on the DST change weekend.
  • Phone numbers said with and without a country code.
  • Names with apostrophes, hyphens and suffixes.

Store the zone as an IANA key, not a bare offset, wherever the future date matters. The appointment moves with the clock; a fixed offset does not.

Free-text notes and summaries

Free text is where PII leaks and where facts drift. NIST SP 800-122 says PII "should be protected from inappropriate access, use, and disclosure" and asks organisations to identify it by context (NIST SP 800-122). A call note that copies a card number or a date of birth is PII in the wrong place. Scan every text field for patterns after every test call. See voice agent PII leakage detection for the detection side.

Summaries create a second record. A generated note can say "customer asked to cancel" while the structured status says "rescheduled." Pick one source of truth. Compare every claim in the summary against the fields, and flag any disagreement.

How to run a golden-call and record-diff test

A golden call is a scripted call with a known expected record. The method has six steps.

1. Write the expected record. For each scenario, list every field and its correct value, as the caller meant it. Include fields that must stay unchanged.

2. Seed the system. Create the contacts, jobs and existing values the scenario needs. Use a fixed test tenant so you never touch live data.

3. Place the call. Use a synthetic caller with a fixed script, accent, pace and noise level. Record the audio and the call ID.

4. Wait for settle. Poll until the record reaches its final state or a timeout hits. A timeout is a failure, not a skip.

5. Read the record back. Use the same read path your customers' tools use. Fetch by ID, then fetch by the key a user would search.

6. Diff and score. Compare field by field with per-field rules. Log missing, wrong, extra, duplicate and overwritten values separately.

Test harness: golden script, synthetic call, system under test, read-back through the customer path, field diff against expected record, and a verdict with five counters

Here is a simplified record diff in Python. It normalises before it compares, so it flags real errors, not formatting noise.

from datetime import datetime
from zoneinfo import ZoneInfo

def same_instant(a, b):
    return datetime.fromisoformat(a) == datetime.fromisoformat(b)

RULES = {
    "start_at": same_instant,
    "phone": lambda a, b: digits(a) == digits(b),
    "service": lambda a, b: a.strip().lower() == b.strip().lower(),
}

def diff(expected, stored, preserve):
    out = {"wrong": [], "missing": [], "overwritten": []}
    for f, want in expected.items():
        got = stored.get(f)
        if got is None:
            out["missing"].append(f)
        elif not RULES.get(f, lambda a, b: a == b)(want, got):
            out["wrong"].append((f, want, got))
    for f, old in preserve.items():
        if stored.get(f) != old:
            out["overwritten"].append(f)
    return out

The `digits` helper strips everything but digits. Keep rules strict. A loose comparison hides the bugs you came to find.

Scenario list

Keep scenarios as data, so any engineer can add one after an incident.

- id: wb-001-wrong-contact
  seed: [{contact: A, phone: "+15550100"}, {contact: B, phone: "+15550100"}]
  caller: {says_name: B, pace: fast}
  expect: {updated_contact: B, untouched: [A]}
- id: wb-002-retry-duplicate
  fault: {drop_response_after_commit: true}
  expect: {rows_for_call: 1}
- id: wb-003-partial-write
  fault: {fail_request: 2}
  expect: {record_status: incomplete, agent_says_failed: true}
- id: wb-004-hangup-before-summary
  hangup_at: after_write
  expect: {record_exists: true, summary_count: 1}
- id: wb-005-timezone
  caller: {zone: America/Los_Angeles, says: "tomorrow at 9"}
  business_zone: America/New_York
  expect: {start_at: "2026-11-06T09:00:00-08:00"}
- id: wb-006-overwrite
  seed: [{contact: A, email: "a@example.com"}]
  caller: {gives_second_email_for_this_call: true}
  expect: {preserve: {email: "a@example.com"}}
- id: wb-007-pii-in-notes
  caller: {reads_card_number: true}
  expect: {note_matches_card_pattern: false}

The scorecard

Score each run on counters, not one pass rate. This is the voice agent post call write back testing scorecard. The thresholds below are illustrative starting points, not published benchmarks. Tune them to what a wrong record costs you.

MetricDefinitionIllustrative gate
Field exactnessFields matching expected / fields checked99% on critical fields
Wrong-object rateWrites to the wrong record / writes0 in the golden set
Duplicate rateCalls with more than one row / calls0
Missing-write rateSuccess calls with no record / success calls0
Overwrite countPreserved fields changed0
Settle time p95Hangup to final record stateSet from your own data
PII-in-notes hitsPattern matches in free text0

Critical fields cause a wrong action: time, address, contact, amount and status. Weight them above cosmetic ones. For gating, see voice agent metric thresholds.

How many golden calls you need

Newman's 1995 JAMA letter, building on Hanley and Lippman-Hand, gives a quick rule: with zero events in N trials, the 95% upper bound on the event rate is about 3/N. His examples are 6% for 50 patients and 0.3% for 1,000 (JAMA).

Here is a worked example for write-back. If 100 golden calls produce zero wrong records, the true error rate could still be near 3%. Run 300 and the bound is about 1%. For a 0.3% ceiling you need about 1,000 runs. These are arithmetic from the rule, not data from any system. The rule assumes independent, identically distributed trials, so treat it as a floor for scripted calls. Use the rule to size your set, and to explain to a customer why "we tested 20 calls" is not proof.

Reconciliation in production

Voice agent reconciliation is the daily check that every call has exactly one correct record. Post-call data sync jobs and idempotency for a voice agent both depend on it. Golden calls prove the code path, and voice agent post call write back testing should not stop there. Reconciliation proves what happened to real callers. Run each reconciliation query daily and alert on any rows. They assume you store a call ID on every record and keep what the agent captured.

-- 1. Successful calls with no record
SELECT c.call_id FROM calls c
LEFT JOIN crm_records r ON r.source_call_id = c.call_id
WHERE c.outcome = 'booked'
  AND c.ended_at < now() - interval '15 minutes'
  AND r.id IS NULL;

-- 2. Duplicates
SELECT source_call_id, count(*) FROM crm_records
GROUP BY 1 HAVING count(*) > 1;

-- 3. Stored value differs from what the agent captured
SELECT c.call_id, c.captured->>'start_at' AS captured, r.start_at AS stored
FROM calls c JOIN crm_records r ON r.source_call_id = c.call_id
WHERE (c.captured->>'start_at')::timestamptz IS DISTINCT FROM r.start_at;

Query 3 has a limit. It compares the agent's captured value with the record. It cannot tell you the capture itself was wrong. For that you need the audio, so sample calls for human review. Evalgent's production monitoring takes pushed call recordings, transcribes them, maps each to a scenario and judges it, and it flags tool-call failures and policy failures. That covers the call side of the join. Your reconciliation queries cover the record side. See monitoring voice agents in production for the wider setup.

Mapping status codes to agent behaviour

Decide in advance what the agent says for each outcome, and test each path.

Write outcomeAgent shouldRecord should
2xx, read-back matchesConfirm to the callerFinal state set
2xx, read-back differsNot confirm; retry or hand offFlagged for review
202 acceptedSay it is being processedPending until confirmed
409 or duplicate keyTreat as already done, verifySingle row
4xx validationRe-ask the field or hand offNothing written
5xx or timeoutRetry safely, then hand offNo partial row

Honesty to the caller is part of the test. An agent that says "you're booked" over a failed write has made a promise your system does not keep. See also why voice agents fail in production.

Evalgent can generate scenarios from an agent's prompt and flows, covering states, branches and hard constraints, and runs simulated callers with chosen accents, pace and background noise. The record-diff step above is the part your team wires to its own system, since only you know the read path.

Frequently asked questions

What is post-call write-back in a voice agent?

Post-call write-back is the step where a voice agent, or a job after the call, stores results in a CRM, scheduler or ticket system. It covers fields, status and notes. Testing it means checking that the stored record matches what the caller said and meant, not just that the call sounded right.

How do you test that a voice agent wrote the right record?

Run a scripted golden call against a seeded test tenant, then read the record back by ID. Compare each field with the expected value using per-field rules. Also check for duplicates, missing writes and overwritten data. A pass needs every critical field to match and no extra rows.

Why do voice agents create duplicate CRM records?

Retries cause most duplicates. A write commits, the response is lost, and the client sends it again. HTTP POST is not guaranteed to be idempotent, so the server may create two rows. Use an idempotency key or a unique index on call ID plus operation, and test with a dropped response.

What is a silent failure in write-back?

A silent failure is a write that returns success but stores the wrong thing or nothing at all. Examples are a 200 on the wrong contact and a 202 that never completes. The agent tells the caller it worked. Only a read-back or reconciliation job reveals it.

How do you handle time zones when a voice agent books a time?

Resolve the caller's words into an absolute timestamp with an explicit offset, and store an IANA zone key when the date is in the future. Test cross-zone callers, late-night "tomorrow," and DST change weekends. A timestamp without an offset is a bug waiting to surface.

Should the agent write the call summary or should a separate job?

Either works if the summary never contradicts the structured fields. A separate job must wait for the final transcript and must be safe to run twice. Treat the structured fields as the source of truth, and test that every claim in the summary matches them.

How many test calls prove the write-back is reliable?

No count proves it. With zero failures in N runs, the 95% upper bound on the error rate is roughly 3/N, so 100 clean runs still allow about 3%. Use 300 for about 1% and 1,000 for about 0.3%. Keep sampling live calls after launch.

How do you reconcile call records with the CRM in production?

Store a call ID on every record, then run daily queries for calls with no record, calls with several records, and fields that differ from what the agent captured. Add human review of audio samples, because queries cannot tell whether the capture itself was wrong.

The bottom line

The record, not the recording, is what your customers and downstream systems act on, so test it as its own product. Replay golden calls, diff every field, inject retries and hangups, and reconcile live calls against records every day.

Related Articles