# Executing phone follow-ups

A follow-up on the `phone` channel is executed by a drain in the runner: when one comes due, it places the call through the same outbound path any other call uses. The gates are the ordinary ones — memory blocks, consent, autonomy, budget, the line's hours — plus two the drain adds: a voice worker that is not ready, and a cut-off once a follow-up is a day late.

- **Status:** Available
- **Audience:** both
- **Channels:** phone
- **In the app:** #/follow-ups, #/calls
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/phone/phone-followup-drain/

## What happens when a call-back comes due

1. The drain picks up follow-ups whose due time has passed on the `phone` channel.
   - Result: Nothing is placed early. A call-back promised for tomorrow morning is not made tonight because the queue happened to be quiet.
2. The call is placed through `comms_providers.dispatch_call` with a `placer` — the same call that the Assistant, a person or the engine would make.
   - Result: There is one outbound path, so every rule written once applies everywhere. A follow-up cannot route around consent by being a follow-up.
3. The line rings the person, the conversation happens, and the call ends like any other.
   - Result: A summary, an outcome, facts, a lead and possibly another follow-up come out of it. A call-back that produces a second call-back is ordinary business, not a loop.
4. The follow-up is closed against the call that fulfilled it.
   - Result: The commitment and the evidence it was kept sit together, which is what makes the queue trustworthy enough to work from.

## The gates, in the order they refuse

| Gate | Refusal | Retried? |
|---|---|---|
| A memory block or do-not-contact on the person | The call is not placed | No — the block is the answer |
| Consent for this channel | The call is not placed | No |
| The autonomy rule for the phone channel | Held for a person to approve, or not made at all | Not by the drain — a person decides |
| `budget_refusal` before an outbound | The call is not placed | When budget is available again |
| The line's hours, or the line switched off | The call is not placed now | Yes, when the line is open |
| No voice worker ready (`not_ready`) | Refused before a `Call` row exists | Yes — this is the case the drain retries |
| A malformed model id (`bad_model`) | Refused before a `Call` row exists | No — retrying a bad configuration only rings the owner again |

The distinction in the last two rows is the useful one. `not_ready` is a statement about this moment — a worker is deploying, the fleet is saturated — and the right response is to try again shortly. `bad_model` is a statement about the configuration, and trying again changes nothing; one Lab call rang the owner and then died with a session error because a model id pointed somewhere that could not serve it.

## Why "not ready" is refused before a row exists

`ring` refuses when no worker has checked in for 90 seconds, and refuses when every live worker reports a one-minute load per core over 0.85 — the same threshold the worker itself gives its dispatcher, so admission control and the gate agree rather than arguing.

Refusing *before* a `Call` row is created is what makes the retry clean. There is no half-created call to reconcile, no row that has to be swept later, and nothing that shows up in the call list as an attempt that never rang. The follow-up simply stays due.

> **Note** A worker that reports no load at all reads as free rather than as busy. Refusing calls on an old worker's silence would take a fleet offline over a missing field, which is worse than the saturation the threshold guards against.

## The point at which a late call-back is abandoned

A phone follow-up that is more than **24 hours** late is not placed. That number is a judgement about customers, not about queues: somebody promised a call back this afternoon, and a phone ringing at this time tomorrow is not the promise being kept late, it is a different and worse experience. The commitment stays visible so a person can decide what to do about it.

The practical consequence is that a fleet outage lasting a day does not resolve itself into a burst of stale calls when it ends. That failure mode — every customer who was owed a call yesterday rung at once, out of context — is the one the cut-off exists to prevent.

## Questions

### The follow-up is overdue and no call was made. Where do I look first?

At the line before the queue. Most non-placements are the line being closed or switched off, the fleet having no ready worker, or the budget being spent — all of which are visible on the Phone screen's health signals rather than on the follow-up itself.

### Can a call-back be placed outside business hours?

Not by the drain. The line's hours gate an automated outbound the same way they gate answering an inbound. If a commitment genuinely needs a call outside hours, a person places it from the browser line, where the same `may_place_call` check runs with a person behind it.

### What happens to a follow-up that is refused for budget?

It stays due and is attempted again while it is still inside the 24-hour window. The refusal is recorded, so a queue that stalls on budget looks like a budget problem rather than an unexplained silence.

## Related

- [Follow-ups from a call](https://connectbyjbrh.com/docs/phone/call-follow-up/)
- [The budget that stops a call](https://connectbyjbrh.com/docs/phone/voice-budget/)
- [Capacity and admission control](https://connectbyjbrh.com/docs/phone/worker-capacity/)
- [Phone line health](https://connectbyjbrh.com/docs/phone/phone-health/)
- [Follow-ups in Connect](https://connectbyjbrh.com/docs/follow-ups/)
- [Placing outbound calls](https://connectbyjbrh.com/docs/phone/outbound-calls/)

## What this page is based on

- Connect phone source pack — `docs-source/sources/PHONE.md` §5 and §9, capacity and refusal
- Connect phone source pack — `docs-source/sources/PHONE.md` §3 and §4, the outbound path and the budget
- Connect capability registry — `docs-source/facts.py` (`phone_followup_drain`, `MEASURED.phone_followup_max_late_hours`)
