# The call record

`calls` is the canonical record of every call, on either engine. One row holds the ring and answer times, the provider's own status, the cost in minor units, the recording and transfer fields, the participants, the resolved voice settings under `quality`, the engine's `outcome` and a person's `disposition` — separate columns, so a webhook arriving late cannot overwrite what somebody decided.

- **Status:** Available
- **Audience:** both
- **Channels:** phone
- **In the app:** #/calls, #/data
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/phone/call-record/

## What one row holds

| Group | Fields | Written by |
|---|---|---|
| Timing | Ring time, answer time, end time, duration | The provider's events, normalised |
| Provider | The carrier's own status string for the call | The adapter, stored rather than interpreted |
| Money | Cost in minor units, accrued as the call runs | `meter_call`, split by modality |
| Participants | Who was on the call, and which line it used | Resolution at answer time |
| Recording and transfers | Whether either happened and where | Provider capability, not intent — see the caution |
| Quality | The resolved voice settings, snapshotted | `voice_style.resolve` at the start of the call |
| Outcome | The engine's verdict on what the call was | The engine, at the end |
| Disposition | A person's own verdict | A human, whenever they say so |
| Triage | `priority`, `starred`, `deleted_at` | A person, on the Phone screen |

> **Careful** The recording fields exist because the model is provider-independent, not because audio is being captured. Call recording is `foundation` and is not enabled on the live carrier: there is no file to play back.

## Why outcome and disposition are two columns

The engine decides an outcome when the call ends. A person may decide something different afterwards — that a call the engine recorded as answered was actually a wrong number, or that a short call was the most valuable one that week. If those shared a column, the later of the two writes would win, and telephony's later writes are frequently the machine's: carriers deliver status callbacks after the conversation is over, sometimes minutes after.

Keeping them apart means a late webhook can update everything it legitimately owns — duration, provider status, cost — without touching a judgement a colleague made. It also makes the pair readable: outcome tells you what happened on the wire, disposition tells you what it meant to the business, and disagreement between them is information rather than corruption.

## The row that was never written

The most instructive defect in this area produced no row at all. `carrier_incoming` read the voice settings before entering the workspace, saw `enabled=False` for a line that was switched on, answered with the goodbye message and created no `Call`. The carrier's own log showed a completed, charged six-second call. Connect showed nothing, and "nothing" reads as a quiet day.

That is why the standard audit for the phone channel is comparative rather than internal: put the carrier's call log next to the `calls` list for the same window. A `Completed` on their side with no row on this one is a bug every time, and it is the only check that catches a failure whose symptom is an absence.

> **Note** A second absence-shaped failure is a row that never closes. `sweep_stale` holds every provider to its configured cap and now runs from the engine tick as well as the worker heartbeat, because a browser call has no worker to heartbeat — one such call sat `active` for 24.78 hours before the sweep covered that path. Whatever the sweep closes, it charges.

## Reading calls in bulk

Calls appear on the Phone screen and as one of the thirteen sheets in the `#/data` grid, where they can be sorted, filtered, grouped and exported like any other record. Edits made there go through the service that owns the call rather than writing to the table, which is what keeps a bulk change subject to the same rules as a single one.

A person's own triage lives on the row itself — `priority`, `starred` and `deleted_at`, the same three fields conversations carry. Priority is not decoration: the engine reads it when it picks work, so re-prioritising a call changes what Connect does next rather than only what you see.

## Questions

### Can I get the audio of a call?

No. Call recording is `foundation`: the capability is asked of the provider rather than assumed, and it is not enabled on the live carrier. The transcript and the summary are the record of what was said, and both are searchable in a way audio is not.

### Why is a call's cost not zero when the model reported no tokens?

Because a call that reports no usage is still charged from its own duration at the published per-minute rate. Billing a real call at zero because a usage field was missing would make the ledger quietly wrong, which is worse than being approximately right.

### Does deleting a call remove it?

`deleted_at` marks it deleted for the people using the screen; it is the same triage field a conversation carries. The record itself stays, because the audit trail and the cost ledger both refer to it.

## Related

- [Call outcomes and dispositions](https://connectbyjbrh.com/docs/phone/call-outcome/)
- [Call transcripts](https://connectbyjbrh.com/docs/phone/transcript/)
- [Who ended the call](https://connectbyjbrh.com/docs/phone/hangup-attribution/)
- [What a call costs](https://connectbyjbrh.com/docs/phone/voice-cost/)
- [Stuck calls and the sweep](https://connectbyjbrh.com/docs/phone/stuck-calls/)
- [Files and data in Connect](https://connectbyjbrh.com/docs/files-data/)

## What this page is based on

- AGENTS.md §9a — the `calls` record (via docs-source/sources/PHONE.md)
- PROJECT-STATE.md — telephony defect 4 and the stale sweep (via docs-source/sources/PHONE.md §2, §4)
- `backend/app/voice.py` — call creation and closure
- `backend/app/voice_cost.py` — metering and per-minute fallback
