Company search

Implementation notes for company.search. Search resolves and gates company identities; enrichment is a separate capability.

1 min read Updated Sep 2, 2026

Company search

The Phase 7 capability contract owns the public schema and limits. This page preserves reusable workflow details. It does not define a second input or result contract.

Product behavior

Company Search answers which companies match supported account criteria. It returns selectable company references and observed display fields. It does not enrich, score or write CRM records. The Companies surface carries selected refs into Company Enrich or People Search through a separate user action.

The earlier enrich: True default is superseded. V1 has no enrich, enrich_fields or depth search parameter. Sonar remains a legacy surface during migration; it is not the new capability's product name.

Execution stages

TEXT
company.search
  -> validate source selection and bounded filters
  -> read supplied identities and/or query Explorium
  -> normalize identity and merge duplicate observations
  -> apply deterministic gates once
  -> persist tenant-owned Run output
  -> return stable selectable references

ENG-2278 owns the core and supplied source. ENG-2295 owns the Explorium source. The company.search tool uses the Explorium businesses endpoint for bounded discovery. The company.search_supplied tool retains the free supplied-only contract and grant. Publish a new workflow version through the standard lifecycle to use paid discovery. The default workflow budget is 400 cents. No provider enrichment call belongs to this path.

Sources

SourceV1 statusReusable detail
Supplied listSupported as typed CompanyRefsPreserve input order and report unresolved rows
ExploriumSupported V1 providerTranslate only the contract's supported firmographic filters
Exa / Parallel company discoveryDeferred for direct Company SearchExisting planners can inform a later adapter; do not invoke signal extraction here
Signals storeDeferred for direct Company SearchA future source reads stored signals; it must not start a new signal search

The caller names sources. No compiler silently enables another source after zero results or a provider error. Merge provider and supplied observations before gates. Keep evidence from both within the public result limit.

Identity resolution

Use the shared domain normalization rule. Use company LinkedIn when a valid company has no domain. A company name is evidence, never a deduplication key. Conflicting canonical keys require an explicit conflict result. An unresolved row stays visible in diagnostics. Do not silently drop a domainless company that has a valid LinkedIn identity. Search returns canonical IDs only when known. It does not create an Intelligence row merely to obtain a selectable ref.

The gate

The gate applies supported filters once. It has no refill loop. Keep a row when a field is unknown and report unknown_filter_value. Drop a row only when an observed value contradicts a filter. Record filter_mismatch and funnel counts. A row with unknown size does not become a verified size match. Unsupported criteria fail validation; they do not pass through to an undisclosed trailing judge.

Deduplication, gates and result limits use stable ordering. A replay cannot choose a different winning observation. Keep per-source status and unresolved counts when one source fails. Total source failure is not a successful empty roster. The design scenarios cover these cases.

Reuse from Signals

Signals owns signal research and scoring. It may pass discovered candidates through the supplied search source. It then calls Company Enrich explicitly when fields are needed. This composition preserves signal discovery without adding a web source or automatic enrichment to Company Search.

Legacy diagram

The diagram below records the superseded automatic-enrichment design. Its enrichment step, four-source V1 claim and trailing scoring path are not the Phase 7 contract.

Company search — find the companies, and nothing else
Company search — find the companies, and nothing else
Discovery picks the instrument that fits the brief. Enrichment is invoked, not embedded. A signal is read from the store, never searched again.
Discovery picks the instrument that fits the brief. Enrichment is invoked, not embedded. A signal is read from the store, never searched again.
workflow/company_search.requested
{ brief, sources[], target_count, depth, enrich }
workflow/company_search.requested{ brief, sources[], target_count, depth, enrich }
workflow.company_search  ·  Inngest orchestrator (thin step spine)
workflow.company_search · Inngest orchestrator (thin step spine)
1 · prepare
parse event · preflight · start run
1 · prepareparse event · preflight · start run
2 · compile the brief
request → CompanySearchBrief
2 · compile the briefrequest → CompanySearchBrief
3 · discover
ctx.group.parallel over the enabled sources
3 · discoverctx.group.parallel over the enabled sources
4 · resolve identity
candidate → one domain, then dedup
4 · resolve identitycandidate → one domain, then dedup
5 · invoke enrichment
ON by default — the roster comes back filled
5 · invoke enrichmentON by default — the roster comes back filled
6 · gate
hard filters · one serial pass · no refill
6 · gatehard filters · one serial pass · no refill
7 · persist + finalize
store the roster, then dispatch scoring
7 · persist + finalizestore the roster, then dispatch scoring
Pick the instrument the brief deserves
a firmographic brief, a thesis brief and an event brief are three different searches — the old spine ran only one of them
Pick the instrument the brief deservesa firmographic brief, a thesis brief and an event brief are three different searches — the old spine ran only one of them
Sources  ·  component.company.search.source.*  ·  each one self-gates on the brief
Sources · component.company.search.source.* · each one self-gates on the brief
.explorium  firmographic provider query
size · country · industry · revenue
deterministic, no agent, one billed call
.explorium firmographic provider querysize · country · industry · revenuedeterministic, no agent, one billed call
.signals_store  SQL read on intel_signals
the change: a stored signal costs one query. Sonar searched the web for the same fact on every run and then threw the evidence away.
.signals_store SQL read on intel_signalsthe change: a stored signal costs one query. Sonar searched the web for thesame fact on every run and then threw the evidence away.
.exa · .parallel  planner agent + web search
for a thesis no database holds. The planner now plans a COMPANY search — the signal extractor is gone from this lane.
.exa · .parallel planner agent + web searchfor a thesis no database holds. The planner now plans a COMPANY search —the signal extractor is gone from this lane.
.supplied  the caller's own list
a named list needs a gate, not a search. Import and the CRM enter here.
.supplied the caller's own lista named list needs a gate, not a search. Import and the CRM enter here.
Identity resolve + dedup  ·  deterministic
one domain per candidate · dedup on the domain · a candidate that resolves to no domain is dropped
it asserts identity only, and never a firmographic field
Identity resolve + dedup · deterministicone domain per candidate · dedup on the domain · a candidate that resolves to no domain is droppedit asserts identity only, and never a firmographic field
workflow.enrichment  ·  INVOKED, NOT EMBEDDED  ·  enrich=True IS THE DEFAULT
workflow.enrichment · INVOKED, NOT EMBEDDED · enrich=True IS THE DEFAULT
subject_type=company  refresh=stale
ownership moves out, the fields do not: the result carries dict[FieldName, FilledField] — value, source and filled_at
enrich=False is the opt-out, for a caller that needs identity alone
subject_type=company refresh=staleownership moves out, the fields do not: the result carries dict[FieldName, FilledField] — value, source and filled_atenrich=False is the opt-out, for a caller that needs identity alone
Gate  exclusions · country · industry · size · revenue · recency
keep on unknown. A filter drops a row only on a present, contradicting value. One serial pass has no refill, so an over-drop is permanent.
what no field can prove (funding bands · SEIS/EIS · semantic excludes) does not gate — it rides to the judge
Gate exclusions · country · industry · size · revenue · recencykeep on unknown. A filter drops a row only on a present, contradicting value. One serial pass has no refill, so an over-drop ispermanent.what no field can prove (funding bands · SEIS/EIS · semantic excludes) does not gate — it rides to the judge
Persist the ENRICHED roster — unscored
workflow_run_companies
results render before the judge runs
Persist the ENRICHED roster — unscoredworkflow_run_companiesresults render before the judge runs
lead_score_background
dispatched after finalize
patches badges in place
lead_score_backgrounddispatched after finalizepatches badges in place
What this workflow stopped doing

Sonar discovered companies by searching the web for signal news, then grouped the news into companies.

A firmographic brief has no news to find, so the best instrument for it never ran.

It also enriched inline, so the CRM could not reach the waterfall without a fake search.
What this workflow stopped doingSonar discovered companies by searchingthe web for signal news, then grouped thenews into companies.A firmographic brief has no news to find,so the best instrument for it never ran.It also enriched inline, so the CRM couldnot reach the waterfall without a fakesearch.
Signals are a store, not a search

workflow.signals_search fills intel_signals. This workflow reads it.

One agent run pays for the fact once. Every later brief that wants the same fact pays for one query.

That is the whole reason the two were split.
Signals are a store, not a searchworkflow.signals_search fillsintel_signals. This workflow reads it.One agent run pays for the fact once.Every later brief that wants the same factpays for one query.That is the whole reason the two weresplit.
Why the gate stays here

Only search holds the brief. Enrichment holds no opinion about a match, and it must not learn one — four other callers use it.

So enrichment fills the fields and hands them back. The gate is the next step, in this workflow.
Why the gate stays hereOnly search holds the brief. Enrichmentholds no opinion about a match, and itmust not learn one — four other callersuse it.So enrichment fills the fields and handsthem back. The gate is the next step, inthis workflow.
Legend
Legend
trigger event
trigger event
costs an LLM call
costs an LLM call
the branch point
the branch point
deterministic step
deterministic step
durable write
durable write
dashed = another workflow, or a trailing lane
dashed = another workflow, or a trailing lane
brief
brief
sources[]
sources[]
candidates
candidates
domains
domains
filled fields
filled fields
matched
matched
Text is not SVG - cannot display
Company search — find the companies, and nothing else
Company search — find the companies, and nothing else
Discovery picks the instrument that fits the brief. Enrichment is invoked, not embedded. A signal is read from the store, never searched again.
Discovery picks the instrument that fits the brief. Enrichment is invoked, not embedded. A signal is read from the store, never searched again.
workflow/company_search.requested
{ brief, sources[], target_count, depth, enrich }
workflow/company_search.requested{ brief, sources[], target_count, depth, enrich }
workflow.company_search  ·  Inngest orchestrator (thin step spine)
workflow.company_search · Inngest orchestrator (thin step spine)
1 · prepare
parse event · preflight · start run
1 · prepareparse event · preflight · start run
2 · compile the brief
request → CompanySearchBrief
2 · compile the briefrequest → CompanySearchBrief
3 · discover
ctx.group.parallel over the enabled sources
3 · discoverctx.group.parallel over the enabled sources
4 · resolve identity
candidate → one domain, then dedup
4 · resolve identitycandidate → one domain, then dedup
5 · invoke enrichment
ON by default — the roster comes back filled
5 · invoke enrichmentON by default — the roster comes back filled
6 · gate
hard filters · one serial pass · no refill
6 · gatehard filters · one serial pass · no refill
7 · persist + finalize
store the roster, then dispatch scoring
7 · persist + finalizestore the roster, then dispatch scoring
Pick the instrument the brief deserves
a firmographic brief, a thesis brief and an event brief are three different searches — the old spine ran only one of them
Pick the instrument the brief deservesa firmographic brief, a thesis brief and an event brief are three different searches — the old spine ran only one of them
Sources  ·  component.company.search.source.*  ·  each one self-gates on the brief
Sources · component.company.search.source.* · each one self-gates on the brief
.explorium  firmographic provider query
size · country · industry · revenue
deterministic, no agent, one billed call
.explorium firmographic provider querysize · country · industry · revenuedeterministic, no agent, one billed call
.signals_store  SQL read on intel_signals
the change: a stored signal costs one query. Sonar searched the web for the same fact on every run and then threw the evidence away.
.signals_store SQL read on intel_signalsthe change: a stored signal costs one query. Sonar searched the web for thesame fact on every run and then threw the evidence away.
.exa · .parallel  planner agent + web search
for a thesis no database holds. The planner now plans a COMPANY search — the signal extractor is gone from this lane.
.exa · .parallel planner agent + web searchfor a thesis no database holds. The planner now plans a COMPANY search —the signal extractor is gone from this lane.
.supplied  the caller's own list
a named list needs a gate, not a search. Import and the CRM enter here.
.supplied the caller's own lista named list needs a gate, not a search. Import and the CRM enter here.
Identity resolve + dedup  ·  deterministic
one domain per candidate · dedup on the domain · a candidate that resolves to no domain is dropped
it asserts identity only, and never a firmographic field
Identity resolve + dedup · deterministicone domain per candidate · dedup on the domain · a candidate that resolves to no domain is droppedit asserts identity only, and never a firmographic field
workflow.enrichment  ·  INVOKED, NOT EMBEDDED  ·  enrich=True IS THE DEFAULT
workflow.enrichment · INVOKED, NOT EMBEDDED · enrich=True IS THE DEFAULT
subject_type=company  refresh=stale
ownership moves out, the fields do not: the result carries dict[FieldName, FilledField] — value, source and filled_at
enrich=False is the opt-out, for a caller that needs identity alone
subject_type=company refresh=staleownership moves out, the fields do not: the result carries dict[FieldName, FilledField] — value, source and filled_atenrich=False is the opt-out, for a caller that needs identity alone
Gate  exclusions · country · industry · size · revenue · recency
keep on unknown. A filter drops a row only on a present, contradicting value. One serial pass has no refill, so an over-drop is permanent.
what no field can prove (funding bands · SEIS/EIS · semantic excludes) does not gate — it rides to the judge
Gate exclusions · country · industry · size · revenue · recencykeep on unknown. A filter drops a row only on a present, contradicting value. One serial pass has no refill, so an over-drop ispermanent.what no field can prove (funding bands · SEIS/EIS · semantic excludes) does not gate — it rides to the judge
Persist the ENRICHED roster — unscored
workflow_run_companies
results render before the judge runs
Persist the ENRICHED roster — unscoredworkflow_run_companiesresults render before the judge runs
lead_score_background
dispatched after finalize
patches badges in place
lead_score_backgrounddispatched after finalizepatches badges in place
What this workflow stopped doing

Sonar discovered companies by searching the web for signal news, then grouped the news into companies.

A firmographic brief has no news to find, so the best instrument for it never ran.

It also enriched inline, so the CRM could not reach the waterfall without a fake search.
What this workflow stopped doingSonar discovered companies by searchingthe web for signal news, then grouped thenews into companies.A firmographic brief has no news to find,so the best instrument for it never ran.It also enriched inline, so the CRM couldnot reach the waterfall without a fakesearch.
Signals are a store, not a search

workflow.signals_search fills intel_signals. This workflow reads it.

One agent run pays for the fact once. Every later brief that wants the same fact pays for one query.

That is the whole reason the two were split.
Signals are a store, not a searchworkflow.signals_search fillsintel_signals. This workflow reads it.One agent run pays for the fact once.Every later brief that wants the same factpays for one query.That is the whole reason the two weresplit.
Why the gate stays here

Only search holds the brief. Enrichment holds no opinion about a match, and it must not learn one — four other callers use it.

So enrichment fills the fields and hands them back. The gate is the next step, in this workflow.
Why the gate stays hereOnly search holds the brief. Enrichmentholds no opinion about a match, and itmust not learn one — four other callersuse it.So enrichment fills the fields and handsthem back. The gate is the next step, inthis workflow.
Legend
Legend
trigger event
trigger event
costs an LLM call
costs an LLM call
the branch point
the branch point
deterministic step
deterministic step
durable write
durable write
dashed = another workflow, or a trailing lane
dashed = another workflow, or a trailing lane
brief
brief
sources[]
sources[]
candidates
candidates
domains
domains
filled fields
filled fields
matched
matched
Text is not SVG - cannot display
Legacy reference only. Phase 7 removes automatic enrichment and limits direct search to supplied identities and Explorium.