---
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: 25
updated: 2026-10-07
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 `executed: false` and an `execution_status` of `planning_failed`, `needs_scope` or `invalid_request` is also a refusal. Neither case bills any Manifest.

`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 count returns zero and a pull returns `403 content_not_yet_available`, which carries the latest available date. Relative `days` and `lookback_hours` windows already end at `available_through`.

**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.

This function runs the whole ladder with free calls and returns a verdict you can act on:

```python
import requests

API = "https://api.synorb.com"


def _post(path, key, body):
    resp = requests.post(API + path, headers={"Authorization": "Bearer " + key}, json=body, timeout=30)
    return resp.json() if resp.status_code == 200 else {}


def _count(key, stream_id, days):
    body = {"stream_ids": [stream_id], "days": days, "mode": "count"}
    data = (_post("/manifests/query", key, body).get("data")) or {}
    return (data.get("pagination") or {}).get("total_count")


def diagnose_empty(key, stream_id, days=3):
    narrow = _count(key, stream_id, days)
    wide = _count(key, stream_id, 30)
    account = requests.get(API + "/account", headers={"Authorization": "Bearer " + key}, timeout=30).json()
    ceiling = ((account.get("data") or {}).get("temporal") or {}).get("available_through")
    if narrow is None or wide is None:
        verdict = "unproven: the count was not exact, so do not say nothing happened"
    elif narrow > 0:
        verdict = "not empty: " + str(narrow) + " item(s) in the last " + str(days) + " day(s)"
    elif wide > 0:
        verdict = "quiet: nothing in " + str(days) + " day(s), but " + str(wide) + " in 30 days"
    else:
        verdict = "empty for 30 days: check the Stream's freshness_state and the scope"
    return {"verdict": verdict, "narrow_count": narrow, "wide_count": wide, "available_through": ceiling}
```

### 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. |

## 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-resources/troubleshoot-channels-stale-crawls-and-quota): unresolved source channel names, sources that stopped publishing, and limit errors.
