Visibility
Fan-out
What each engine actually searched for behind your prompt, and the eight categories those searches fall into.

What this capture is
When an engine expands your prompt into its own sub-queries before composing an answer, that expansion is called fan-out; see Engines and measurement for what it means and which engines do it. This page covers the panel underneath it, on a prompt's own page: what DiscoveredBy actually captures about those sub-queries, the eight categories each one can be filed under, and what it means when a run gives us less than the full picture, including when it gives us nothing at all.
Why the category lives in a table of its own
Each query an engine searched for is written once, at collection, as its own
row. What that query was about is not a column on it. It is a separate table,
fan_out_classifications, joined back to the query it describes
(api/models/fan_out_classification.py). The model's own docstring explains
why: the query table already has the property that matters, written once and
never updated, and keeping the category off it means a bug in classification
can never corrupt the record of what the engine actually searched for. A
future classifier version writes its own row beside an older one instead of
overwriting it. The category is decoration on a list a human reads; nothing
in the product computes a score, a metric or a threshold from it.
The eight categories, and the order they are judged in
The model is given the parent prompt and one query at a time, and told to
take the FIRST category that applies, checked in this fixed order, because
that is what makes the eight mutually exclusive (CATEGORY_DEFINITIONS,
api/services/fan_out_classifier.py):
- shopping: seeks a purchase artifact: price, cost, deal, coupon, or where to buy.
- comparative: sets named things against each other or asks for a ranking, such as vs, alternatives to, or best X.
- entity_expansion: introduces a named brand, product or company that is absent from the parent prompt.
- personalized: adds a constraint about the asker that is absent from the parent prompt, such as a place, a year or a segment.
- reformulation: restates the parent prompt with no new constraint.
- implicit: answers a sub-question the parent prompt assumes but does not ask.
- related: topically adjacent, neither a restatement nor an implied sub-question.
- synthetic: not a plausible human search: scaffolding, an instruction fragment, or tooling.
If nothing on that list fits, or the evidence is too thin to be confident,
the model returns a ninth value, unclear, instead of guessing.
The order is not a display convenience, it is the decision itself: because
entity_expansion is checked before related, a query that both names a
competitor and drifts topically is filed under the more specific read, not
the vaguer one. An earlier version of this list carried a ninth candidate
category, "search", describing a query's form rather than its relation to
the parent prompt; it was dropped because a set mixing two different axes
cannot be classified consistently.
The text above is not a paraphrase written for this page. It is
CATEGORY_DEFINITIONS itself, the same dictionary the classification prompt
is built from and the same text shipped to the product as category_note on
each query, so the sentence you would read in the app's tooltip is the
sentence the model was actually given, not a second copy that could drift
from it.
How much we actually captured: the fidelity states
Not every run gives the same picture of what an engine searched for, and
DiscoveredBy does not store a separate status saying which picture this run
got. That would be one more field that could fall out of sync with the
queries and sources actually saved. Instead it is worked out fresh each time
from what is on hand (derive_fan_out_state, api/services/fan_out.py).
There are seven possible answers, checked in this order, and three of them
mean DiscoveredBy does not know what the engine searched for:
- not_applicable: this engine's answer never passes through a general-purpose model deciding whether to search. DiscoveredBy calls Perplexity's search endpoint directly, so there is no engine-issued sub-query for it to report. Checked before every other state, specifically so a Perplexity run is never misread as an engine that searched and told us nothing.
- not_recorded: fan-out capture did not run for this execution at all. There are no queries and no sources to show, and this page has nothing to tell you about what the engine searched for.
- no_search: capture ran, and found nothing, the engine answered without issuing any sub-queries this time. This is a real claim about the engine's behaviour.
- searches_not_recorded: at least one retrieved source exists for this run, but zero queries were recorded for it. This is a gap in our own records, not a claim about the engine: we cannot say the engine did not search, only that we did not write down a search for it.
- queries_only: the queries the engine issued are recorded, but nothing links a retrieved source back to a specific one of them, so they read as a flat list rather than query-to-source pairs.
- grouped: sources are attributed to a search call, but not cleanly, either a source links to a call we never recorded, or the call it links to ran more than one query, so the honest claim is "one of these searches returned this", not which one.
- exact: every retrieved source resolves to a single call that ran exactly one query, so a specific source can be shown beside the specific query that produced it.
not_recorded, no_search and searches_not_recorded are the three states
where there is genuinely nothing, or next to nothing, to show: no captured
queries, and in the first and last of those, possibly no sources either.
no_search and searches_not_recorded read almost the same on the page and
mean different things on purpose: one is a fact about what the engine did,
the other is a fact about what we wrote down, and collapsing them into one
sentence would make a claim about the engine's behaviour that our own records
do not support.
A query only gets a category once it exists as a captured row, so on
not_applicable, not_recorded, no_search and searches_not_recorded
there is nothing to classify regardless of plan; the eight categories above
only ever apply to a query that was actually captured under queries_only,
grouped or exact.
Being captured is necessary but not sufficient. One classification pass
judges at most 40 of an execution's queries, in query order
(MAX_QUERIES_PER_CALL, api/services/fan_out_classifier.py). A pass skips
an execution outright only when every one of its classifiable queries
already carries a label at the current classifier version, counted as a
comparison of two counts rather than as "does any label exist"
(already_classified, same file), and a pass that does run fills its batch
from the queries that are still unlabelled at that version
(unlabelled_query_ids, same file). So a second pass over the same
execution resumes at the tail the first one stopped short of, instead of
re-reading the same leading queries.
What triggers that second pass is narrow. Classification is enqueued when a
run finishes, and again only if the same prompt, engine and day is run a
second time (api/tasks.py#_enqueue_fan_out_classification,
#_resume_completed_execution); nothing else in the product schedules a
repeat pass over an execution. An execution with more than 40 classifiable
queries can therefore still sit with part of its list unlabelled. The
difference is that the tail is no longer unreachable: any later pass over
that execution picks it up rather than skipping the whole execution.
A new classifier version does start from the front again, because nothing is
labelled at that version yet, so it lands on the same leading queries the
first version did. When a run has some but not all of its labelable queries
labelled, the panel says so directly, "We labelled the first N searches for
this run" (labels_capped_at, api/routers/pages.py).
The category is a model's read, and it is versioned
Classifying a query is a model call, not a lookup: the parent prompt and
query text both come from an engine's own output, so the classification
prompt treats that text as data, never as instructions to follow. A
classification below a confidence floor (MIN_DISPLAY_CONFIDENCE,
api/services/fan_out.py) still gets stored, but displays as unclear
rather than the model's guess.
A query with no chip at all is a third, different case: it has no stored classification, either because it fell past the batch bound above, or because this run's classification simply has not happened yet. Neither is permanent, but neither resolves on a schedule either: both wait on another pass over that execution.
Every stored classification carries the version of the classifier that
produced it, CLASSIFIER_VERSION (currently fanout-v1,
api/services/fan_out_classifier.py); a newer version writes its own row
beside an older one rather than editing it. Classification is enqueued
automatically once a run finishes and is best effort: a failure to
classify never fails the run itself, since the category is metadata
about the measurement, not the measurement. Category labels need a plan
that includes them; without one, every captured query and source still
shows, just without the category chip. See Pricing for what
each plan includes.
From a captured query to a tracked prompt
Because this is a real record of what an engine searched for, any query here can become its own prompt candidate for review, gated the same way category labels are. Tracking it this way does not retroactively classify it; it queues a fresh candidate through the same review flow as any other suggestion. See Suggestions and opportunities for what happens next.
Related
- Prompts: where this panel sits on a prompt's own page
- Engines and measurement: what fan-out is, which engines report it, and why Perplexity does not
- Suggestions and opportunities: what happens after a captured query is tracked as a prompt
Last verified 2026-08-11