# Outbound phone call, end to end

An outbound call starts as a reason — a follow-up falling due, an instruction, a person pressing dial — and then passes the same gates a message would: memory blocks, consent, autonomy, hours, budget. Only then is a dial handed to the provider. Everything after that is timing and evidence: did the leg join, did it ring, did anyone speak, and what does the record say when it did not.

- **Status:** Available
- **Audience:** both
- **Channels:** phone
- **In the app:** #/calls, #/follow-ups
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/workflows/outbound-phone-call/

## The chain, when Connect is the one calling

1. Trigger — a due follow-up, a standing instruction, a supervisor's decision, or a person dialling.
2. User or external event — the reason is already a record; nothing external has happened yet.
3. Authentication and workspace resolution — the placer is identified and the workspace is entered before any line setting is read.
4. Ingest — the request names the contact, the line and the purpose.
5. Canonical record — the intent to call exists before the dial does, so a failure has somewhere to be written.
6. Reasoning — the purpose selects the persona and the opening; the brief is rendered once.
7. Knowledge, memory and rules — contact facts, standing instructions and hard rules go into the brief under fixed character budgets, and `voice_region.plan` adds the language rule for this person.
8. Autonomy and approval — memory blocks, consent, suppression, autonomy mode and `budget_refusal` all gate the dial.
9. Action through a provider — `comms_providers.dispatch_call` places the leg with a `placer`, exactly as a carrier call would be.
10. Result — answered, `no_answer` (it rang, nobody picked up), or `not_reached` (it never rang).
11. Relationship, timeline and memory — the outcome lands on the person, whether or not there was a conversation.
12. Audit, usage and Needs You — the cost is booked, the attempt is on the timeline, and a call worth a person's attention is queued.

The one rule worth memorising: an outbound call from the realtime engine goes through the same dispatch as a carrier call, with a `placer` recorded. That is deliberate. It means switching engines cannot quietly bypass consent, autonomy or budget, because there is only one door.

An outbound call knows more about the person than an inbound one usually does, and the language rule uses it. If the record carries a `preferred_language` — saved on an earlier call that was allowed to use memory, latest value winning — Connect speaks it from its first reply after the opening line, and only a whole sentence in another language moves it. If it carries a city or state instead, that language is *likely*, not assumed: the call opens in the line's language and follows the person into the regional one as soon as they use two or three words of it. Where the number is all there is, the dialling code gives a country, and an Indian landline's STD code a city — a mobile gives neither, because portability made the series meaningless.

## The gates, in the order they refuse

| Gate | What you see | What changes | What can fail |
|---|---|---|---|
| Memory block or do-not-contact | The call is not attempted and the reason is shown | The refusal is recorded against the contact | A block set on one identity while the number belongs to another row |
| Consent and suppression | Same | Nothing is dialled | A number with two contact rows — `identify_caller` resolves to the oldest |
| Autonomy mode | Either it goes, or it waits in Needs You | An approval record, if a person must say yes | A mode set at the wrong scope; the narrowest scope wins |
| Line hours and permission | Refused with the line's own reason | Nothing dialled | A line without outbound permission on `channel_routes` |
| `budget_refusal` | Refused before the dial | Nothing dialled | The workspace's cap already spent by earlier calls |
| `ai_quota` breaker | Scheduled work is skipped | The breaker opens on a 429 or a revoked key | A caller already on the line is still attempted — the breaker is not a hard stop for live conversation |
| `ring` admission | `not_ready`, and the drain retries | No `Call` row yet | No worker checked in for 90 s, or a malformed or Vertex-path model id (`bad_model`) |

## The dial itself, and what a non-answer means

Once a dial is out, the interesting states are the ones that are not a conversation. Connect gives them separate words because they need separate responses, and because a lead invented out of a call that never connected is worse than no lead.

**`no_answer`** — It rang and nobody picked up. Try again later; the contact is real and reachable.
**`not_reached`** — It never rang. The dial, the dispatch or the worker failed. This is an operational problem, not a sales one.
**Dial timeout** — A carrier answer of 408, 503 or 504 is dialled once more, and once only.
**Leg answered, never joined** — The carrier says the leg was answered but it never appears in the room. It is written off after `DIAL_JOIN_TIMEOUT_S` rather than waited on indefinitely.

`_capture_lead` creates nothing from either of the two non-conversations. That single rule is what keeps a week of failed dials from turning into a pipeline of people who never spoke to anyone.

> **Careful** A retry rule that is easy to get backwards: a request a call cannot lose — `arrived`, `brief`, `end` — is retried through a deploy restart or a proxy 502. A 4xx is an answer, and is never retried. Retrying a refusal is how one refused call becomes twenty.

## What is written when nobody speaks

Every attempt leaves a record, which is the point of opening the intent before the dial. The row carries the outcome word, the attribution for the ending, and the cost — a call that reports no tokens is still charged from its own duration at the published per-minute rate, never at zero. Stale sessions are swept to the configured cap from the engine tick as well as the worker heartbeat, because a browser call has no worker and one such call once sat active for the better part of a day.

If the call was placed to keep a promise, the follow-up is not closed by the attempt alone. A promise with nothing behind it has its own finding, `promised_unbooked`, and it exists so that "I called them" and "they know" stay different facts.

## Questions

### Does an outbound call from the realtime engine skip any of the checks?

No. It goes through `comms_providers.dispatch_call` with a `placer`, so memory blocks, consent, autonomy and budget gate it exactly as they gate a carrier call. There is one door, and both engines use it.

### Why was a call refused with `not_ready` when the Phone screen looked fine?

`ring` refuses before any row exists when no worker has checked in for 90 seconds, or when every live worker is over the busy-load threshold. The refusal is deliberately retryable so a deploy drain picks the call up again rather than losing it.

### Can Connect send a text message instead when a call is not answered?

Not on the live carrier, which carries no SMS. The Phone screen says so rather than presenting a thread that cannot send. A follow-up on another channel, or a task for a person, is the honest alternative.

## Related

- [Inbound phone call, end to end](https://connectbyjbrh.com/docs/workflows/inbound-phone-call/)
- [A promised call-back, end to end](https://connectbyjbrh.com/docs/workflows/phone-follow-up-execution/)
- [The call never rang](https://connectbyjbrh.com/docs/troubleshooting/call-never-rang/)
- [What Connect may do](https://connectbyjbrh.com/docs/autonomy/)
- [SMS in Connect](https://connectbyjbrh.com/docs/sms/)

## What this page is based on

- Reconciled 2026-09-10 against `f5ea518` — `backend/app/voice_region.plan`, read from the code rather than its docstring
- AGENTS.md §9a — outbound dispatch, refusal and attribution
- `backend/app/voice_engine.py`, `comms_providers.py`, `ai_quota.py`
- Connect capability registry (docs-source/facts.py)
