---
name: a-failed-look-is-not-a-quiet-week
description: Make a scheduled send tell an empty period from a failed look, so a broken source does not mark everybody served and defer them to the next run.
runbook:
  schema_version: v1
  semver: 1.0.0
  profile: P1
  min_runtime_profile: R1
  domain: data-pipelines
  targets:
    - postgres
  trigger: alert
  execution: human-with-agent
  risk: reversible-write
  blast_radius: tenant
  duration: 5-30m
  capabilities:
    - cli:psql
  publisher: std
  lang: en
  license: CC-BY-4.0
  inputs:
    run_id:
      type: string
      required: true
      description: the scheduled run being examined
  steps:
    - id: s1
      kind: check
      title: Did the source answer at all?
      tool: cli:psql
      command: psql -Atc "select coalesce(source_available::text, 'unknown') from runs where id = '{{inputs.run_id}}'"
      expect: a recorded answer — the run knows whether it could look, not only what it found
      assert:
        stdout_not_matches: ^unknown$
      on_fail: escalate:s8
      next: s2
    - id: s2
      kind: decision
      title: Empty, or unable to look?
      question: Did the source answer and have nothing, or did it fail and return nothing?
      branches:
        unavailable: s3
        default: s6
    - id: s3
      kind: check
      title: Nobody was marked as served
      tool: cli:psql
      command: psql -Atc "select count(*) from subscriptions where last_sent_run = '{{inputs.run_id}}'"
      expect: zero — a failed look must leave the cadence untouched
      assert:
        stdout_matches: ^0$
      on_fail: s4
      next: s5
    - id: s4
      kind: action
      title: Put back the ones it marked
      risk: reversible-write
      requires_approval: true
      expect: their cadence is as it was, so the next run includes them
      on_fail: escalate:s8
      next: s5
    - id: s5
      kind: check
      title: The run reported itself as failed
      expect: it did not answer success; something alarmed
      on_fail: escalate:s8
      next: end:aborted
    - id: s6
      kind: check
      title: The count is a count, not a ceiling
      expect: where the walk hit its page limit, the number is written as "more than N"
      on_fail: escalate:s8
      next: s7
    - id: s7
      kind: action
      title: Record the period as genuinely empty
      risk: reversible-write
      expect: the run is marked served with a zero that means nothing matched
      on_fail: escalate:s8
      next: end:success
    - id: s8
      kind: escalate
      title: Escalate before the next run
      to: team:platform
  source:
    url: https://runbooks.directory/r/std/a-failed-look-is-not-a-quiet-week/source.md
    fetched_at: 2026-09-04
    upstream_state: current
---

## When to reach for this

Something goes out on a schedule — a digest, a report, a reminder — and it is assembled from
a source that can fail. Reach for this when a run went out empty, when a run did not go out
at all, or when designing the thing in the first place, because the failure below is designed
in rather than introduced.

## The failure this exists to prevent

A fetch layer that swallows errors and returns an empty list is **correct for a page**. A
listing whose source is down should render without content rather than break, and every
front end does this.

The same layer is catastrophic for a scheduled job. The job reads the emptiness as *nothing
happened this period*, marks every recipient as served, and the cadence rule — do not send
again within six days — defers them to the next run. Nobody is told. The alarm does not fire,
because as far as the job is concerned it succeeded. A week disappears, and the only evidence
is an absence.

## Availability travels with the data

The fix is one field and one exception. The fetch returns *whether it could look* alongside
what it found; the layer that collects raises rather than returning an empty list.

From there everything follows: the send is not marked, the run counts itself as failed, and
it answers with a failure — loudly enough that whatever watches the schedule notices. A
scheduled job that reports success on a failed look is a job nobody will ever investigate,
because there is nothing to investigate it with.

## On the page too: empty is not zero

The same distinction belongs wherever the number is shown. Where the source could not be
read, show *nothing* rather than a zero.

Zero means "nothing matches this selection", and it says that most confidently at the exact
moment everything does. An empty value is honest and a reader can act on it; a wrong zero is
indistinguishable from a right one.

## Numbers you may look at, and numbers you may not

Three rules that keep a report from lying by arithmetic.

**A count capped by a walk is not a count.** Where the collector stops after a fixed number
of pages, say "more than N". Publishing the ceiling as though it were exact turns a limit of
the tool into a fact about the world.

**A zero from a successful look is a fact; a zero from a failed one is not.** They must be
distinguishable in whatever the run records, or the difference is unrecoverable afterwards.

**An open rate from a tracking pixel is always understated, by an unknown fraction.** Mail
clients block images and nobody knows how many. It can be compared with itself from period to
period and with nothing else. It must never be a denominator: a conversion rate computed from
it is a ratio of a real number to an unknown one.
