<summary>Query the agent's entity row store (`agent_search_results`) — the people and
companies recorded for the agent from any source (discovery search, CRM reads,
enrichment, user-pasted lists).
Columns: id, agent_id, entity_type, list_name, source, identifier,
display_name, verdict, verdict_reason, data (JSONB), created_at, person_id,
company_profile_id, linkedin_slug, linkedin_provider_id, email. Filter by list
with "list_name = 'oil-gas'". `identifier` is the key the row was saved under —
a company's domain; for a person, whichever LinkedIn handle or email its source
had. A person's LinkedIn profile slug, provider id and email are in
`linkedin_slug`, `linkedin_provider_id` and `email`, read from the linked person
('' when the row isn't linked or the person has none). `source` is the
discovery tool that surfaced the row ('agent' when no discovery tool did;
'legacy' for rows older than the column). `verdict` is the curation call
recorded on the row with `record_search_results`, or by the check a search
tool, a post monitor's engager capture or a Websets list runs on the rows it
saves — 'qualified', 'rejected', 'unclear' for a row
screened but too thin to judge, or NULL for a row not yet screened — with
`verdict_reason` saying why. Rejected rows are left out unless you pass
`verdict='rejected'`, `include_rejected` or `run_id`, the same as the user's default
view, so a `where_clause` on `verdict` alone never reaches them.
`person_id` / `company_profile_id` are the canonical person / company FK ids
(NULL until the row is linked). A person's profile (title, company) and a
company's firmographics (industry, location, employee count, description) — the
values the agent's People and Companies tabs show — live on the linked record, not
in the row's own `data`: read them with `query_people` / `query_companies` on
those ids, and filter on them through the id, e.g. "company_profile_id IN (SELECT
id FROM research_app_company_profiles WHERE industry ILIKE '%oil%')".
For a person's LinkedIn degree and warm-intro connectors, or the agent's people
counted by list, source, degree or connector, use `list_people` instead. For
the agent's companies as its Companies tab shows them, one row per company with its
lists, verdict and signals rolled up across rows, use `list_companies`.
`data` has no single schema — its shape follows whichever source wrote the
row (it is the agent-authored blob). Most person rows nest under `person`
(data->'person'->>'position',
data->'person'->'company'->>'name') and most company rows under `company`
(data->'company'->>'name'), but other sources
write those fields flat. A path that doesn't exist yields NULL rather than
an error, and a NULL comparison drops the row — so a filter aimed at the
wrong shape comes back empty or short and reads as a genuine miss. Read a
page of rows without a `data` filter first, then filter against the shape
you see.
Person rows are annotated with the team connection overlay (mirrors the Find
UI): a matched row's `data` gains `connection_status: {you: bool, teammates:
[{email, name, first_name}]}` naming the viewer/teammates who are 1st-degree
to that prospect; unmatched rows and company rows carry no such key. A
top-level `connections_coverage: {you: 'none'|'partial'|'csv_uploaded',
team_missing: int}` reports the owner's own connection coverage and how many
active teammates still lack a CSV upload. The viewer is the agent owner.</summary>
<returns>
<description>In row mode (group_by omitted) — a dict with count, truncated, and items
array (each row {id, agent_id, entity_type, list_name, source, identifier,
display_name, verdict, verdict_reason, data, person_id, company_profile_id,
linkedin_slug, linkedin_provider_id, email, columns, created_at}),
plus connections_coverage {you, team_missing}. `columns` is the normalized
[label, value] projection of the row's agent-researched `data.columns`.
In aggregate mode (group_by set) — a dict {group_by, groups} where groups
is a list of {key, count, qualified, rejected, unclear, unscreened} ordered by
count descending. `count` is every row the call counts, and the other four split
it by verdict (`unscreened`: no verdict yet); rejected rows are ones the screen
ruled out, not matches. With group_by "list",
each group also carries `criteria`: the search criteria a Websets list screens
its rows against, '' for any other list.</description>
</returns>