// Definition — timeline anti-join, ONE definition every surface
isUnresponded(customer) =
latestInbound(customer) EXISTS // sms / email / MISSED call
AND NO dealerOutbound AFTER latestInbound
WHERE (aiIncluded OR actor == human) // AI counts only when Included
AND customer.optedOut == false
AND customer.status NOT IN ("closed", "bad")
AND age(latestInbound) BETWEEN 1h AND 48h // longest-wait-first
// CANNOT be computed on customernotifications.respondedAt: patch-responded-at.ts
// CLEARS that column on automation, scheduled, AI and manual-log sends, retroactively
// across the whole backlog with no time bound. Must be the anti-join above.
// Count metric id: Unresponded · Drilldown entity = Customer
{ "metric": "Unresponded", "value": 6, "drilldown": "customers" }
One definition everywhere — stat, drilldown, and smart view never diverge.
Automation and queued sends never count as a reply — only a human reply clears it (AI replies count only when AI is Included).
Window: no reply for over 30 minutes, scoped to the selected date range, longest-wait-first. Opt-outs and Closed/Bad excluded.
UNVERIFIED: whether opt-outs and Closed/Bad customers are actually excluded by the shipped filter.
Value red-01 when >0 · em-dash gray-04 at zero · invertColors (down = good).
Timeline-derived, never respondedAt — patch-responded-at.ts clears that column on automation, scheduled, AI and manual-log sends, so it must be a latest-inbound-vs-later-outbound anti-join with actorId + automation flag available for filtering.
Drilldown rows are READ-ONLY (R0 grammar) — the fix happens on the customer, not in the list.