---
title: Troubleshoot empty windows and missing Manifests
category: troubleshoot
summary: A step-by-step ladder that tells you whether an empty result is a quiet source, a not-yet-available day, a too-narrow filter or a real problem, before you tell your user anything.
order: 28
updated: 2026-10-08
next: troubleshoot-channels-stale-crawls-and-quota
related: freshness-and-available-through, read-the-response-envelope
reference: /agent-guide.md, /docs/api
---

An empty result is an answer only when you know why it is empty. Most are one of five ordinary things. Work down this ladder in order. Each rung is free.

## When to use this

Use it when a pull returns nothing, returns less than you expected, or when your user says "I saw an item that is not here". Do it **before** you reply, so your reply says something true.

## Smallest working call

Count the scope and ask for diagnostics. Both are free:

```mcp
synorb-manifests
{"stream_ids": ["STREAM_ID"], "days": 3, "mode": "count", "include_diagnostics": true}
```

```bash
curl -sS -X POST https://api.synorb.com/manifests/query \
  -H "Authorization: Bearer $SYNORB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"stream_ids": ["STREAM_ID"], "days": 3, "mode": "count", "include_diagnostics": true}'
```

## What you get back

### The ladder

**Rung 1. Did the call run?** Check the HTTP status, then `executed` and `execution_status`. A `4xx` such as `400 empty_stream_ids` is a refused request. A `200` with an `execution_status` of `planning_failed`, `needs_scope` or `invalid_request` is also a refusal. Neither case bills any Manifest.

A `mode: "count"` call is a plan, so a good count has `executed: false` and `execution_status: "planned"`. That is normal. Do not wait for `executed: true` on a count.

`tag_names_unresolved` is different: it arrives as `coverage.reason` on an executed, zero-usage response. It means a name did not resolve, not that nothing happened. Fix the name before you report an empty result.

**Rung 2. Is the answer authoritative?** Read `coverage.status` and `coverage.reason`. A blocked, unresolved or unproven scope is not an exact zero. A `pending_scan` is not "no matches".

Only a clean, authoritative empty is an empty. Also read `did_you_mean` for suggested corrections.

**Rung 3. Is the window past the ceiling?** Compare your window with `available_through` from `GET /account`. On a Daily Batch plan the newest day is yesterday. With explicit dates past `available_through`, a pull returns `403 content_not_yet_available`, which carries the latest available date.

A count over a window that lies wholly past the ceiling returns an exact zero, but with `execution_status: needs_scope` and `coverage.status: impossible`. That zero is a refusal, not an empty. Relative `days` and `lookback_hours` windows already end at `available_through`.

With `relative_to: "wall_clock"`, an empty result early in a UTC day can mean the window is only a few hours long. Look for `temporal.window_note`. Dates are whole UTC days, so `lookback_hours: 24` right after 00:00 UTC covers little. Retry with `lookback_hours: 48` or explicit dates before you call the scope empty.

**Rung 4. Is the scope or a filter too narrow?** Count each filter on its own. Remove `source_class`, then `significance`, then `tag_names`, then the source channel filter, until the count turns positive. The filter you removed last is the cause.

Filters narrow each other. `tag_logic: "and"` is strict.

**Rung 5. Is the Stream or source channel quiet?** Read `freshness_state`, `days_since_last_manifest` and `volume`, and each source channel's `avg_items_30d`, `crawl_frequency` and `is_active`. A source that averages a few items a month will often be empty for a day.

**Rung 6. Widen the window.** Count the previous 30 days. If that is positive, the source is quiet or slow, and the narrow window was empty.

The first function reads one count and decides whether it is proven. The second runs rungs 1, 2, 5 and 6 with free calls, and a one-step version of rung 4 when you pass filters. It reads the ceiling for rung 3. It says "quiet" or "empty" only when every count it used is proven:

```python
import requests

API = "https://api.synorb.com"
REFUSED = {"planning_failed", "needs_scope", "invalid_request"}


def read_count(status, body):
    """Say whether one count reply is proven. Never read a bare total_count."""
    body = body or {}
    data = body.get("data") or {}
    page = data.get("pagination") or {}
    cover = data.get("coverage") or {}
    if status != 200:
        return {"verified": False, "count": None, "why": "HTTP " + str(status)}
    if body.get("execution_status") in REFUSED:
        return {"verified": False, "count": None, "why": "refused: " + str(body["execution_status"])}
    if cover.get("status") != "covered" or not cover.get("authoritative"):
        return {"verified": False, "count": None, "why": "not authoritative: " + str(cover.get("reason"))}
    if page.get("total_count") is None or not page.get("total_count_exact"):
        return {"verified": False, "count": None, "why": "the count is not exact"}
    return {"verified": True, "count": page["total_count"], "why": "authoritative exact count"}


def _json(resp):
    try:
        return resp.json() if resp.status_code == 200 else {}
    except ValueError:
        return {}


def _count(key, stream_id, days, filters=None):
    body = dict(filters or {}, stream_ids=[stream_id], days=days, mode="count")
    resp = requests.post(API + "/manifests/query", headers={"Authorization": "Bearer " + key}, json=body, timeout=30)
    try:
        payload = resp.json()
    except ValueError:
        payload = {}
    return read_count(resp.status_code, payload)


def diagnose_empty(key, stream_id, days=3, filters=None):
    head = {"Authorization": "Bearer " + key}
    narrow = _count(key, stream_id, days, filters)
    wide = _count(key, stream_id, 30, filters)
    account = _json(requests.get(API + "/account", headers=head, timeout=30))
    stream = _json(requests.get(API + "/streams/" + stream_id, headers=head, timeout=30))
    ceiling = ((account.get("data") or {}).get("temporal") or {}).get("available_through")
    state = (stream.get("data") or {}).get("freshness_state")
    out = {"narrow": narrow, "wide": wide, "available_through": ceiling, "freshness_state": state}
    unproven = [c for c in (narrow, wide) if not c["verified"]]
    if unproven:
        out["verdict"] = "unproven: " + unproven[0]["why"] + ". Do not say nothing happened."
    elif narrow["count"] > 0:
        out["verdict"] = "not empty: " + str(narrow["count"]) + " item(s) in the last " + str(days) + " day(s)"
    elif wide["count"] > 0:
        out["verdict"] = "quiet: nothing in " + str(days) + " day(s), but " + str(wide["count"]) + " in 30 days"
    else:
        out["verdict"] = "empty for 30 days with these filters: check the filters and the Stream (freshness_state " + str(state) + ")"
        if filters:
            bare = _count(key, stream_id, 30)
            out["without_filters"] = bare
            if bare["verified"] and bare["count"] > 0:
                out["verdict"] = "your filters are too narrow: " + str(bare["count"]) + " item(s) in 30 days without them"
    return out
```

A verdict that starts with "unproven" is not an answer. Fix the cause in `why`, or tell your user you cannot confirm.

### A Manifest your user saw is missing

| Reason | How to check |
| --- | --- |
| It is outside your window or past `available_through` | Compare its date with `temporal` |
| It lives in a different Stream than the one you queried | Search for it. One source can route to several Streams, but not every Stream |
| A filter excluded it | Count with each filter removed |
| It is no longer published | A direct read by ID returns `404`. First check that you passed the `manifest_id` to `/manifests/by-id/`. Items can be held after a correction. Find the currently published replacement through its Stream |
| You are reading the delivery-delta population | `delivery_delta` is narrower than an ordinary query. A zero there is not "nothing matched" |
| It was published but not yet ingested | Check the source channel's `crawl_frequency` and wait for the next crawl |

Two other things look like a gap and are not. A default pull across several Streams can mix sources, so it may include older items from other sources, still inside your window. Synorb does not bill a Manifest again if you already received it, so it does not look "new" in your usage.

## Failure modes

| Symptom | Cause | What to do |
| --- | --- | --- |
| You told your user "nothing happened" and were wrong | You read an empty list, not `coverage` | Check rungs 1 and 2 first, every time. |
| The count is positive but the pull is empty | The window or filters differ between the calls | Send identical bodies, changing only `mode`. |
| Empty for "today" on Daily Batch plans (demo, Starter, Individual) | The ceiling is yesterday | Say the latest available date. |
| `404` on a by-ID read | No longer eligible for publication | Do not assume the old ID points to the replacement. |
| Everything is empty at once | Credentials or plan state | Call `GET /account` and check the key and usage. |
| The body has an `agent_hint` | Synorb knows why this scope failed and does not want a blind retry | Read `agent_hint.code`, `message` and `what_to_change`. Send `retry_with` when it is not `null`. When `operator_action_needed` is `true`, tell your user Synorb must act. |
| `agent_hint.code` is `stream_not_yet_queryable` | The Stream exists but is not in the query index yet | Query related Streams or Tags. Do not retry. |
| `agent_hint.code` is `stream_tag_intersection_unprovable` | A Stream plus `tag_names` cannot be proven to overlap | Use `tag_ids` from the Tag search, or widen `days`. The count may be 0 but is not asserted. |
| `agent_hint.code` is `semantic_count_not_exact` | A semantic `query` count is a relevance estimate, never exact | Use `mode: "plan"`, `tag_ids`, or `default` with a modest `target_count`. |

## Make it good for your user

Say what you checked, and what you concluded, in two lines. "Nothing new in the last three days. This source normally posts a few times a month, and the last item was nine days ago." Or: "I cannot confirm the last day yet. Your plan's latest available date is October 6." That turns an unhelpful empty into useful information. It also protects your user from acting on a gap that is not real.

## Next guide

[Troubleshoot source channels, stale crawls and allowance](/agent-guides/troubleshoot-channels-stale-crawls-and-quota): unresolved source channel names, sources that stopped publishing, and limit errors.
