Signals Search
One bounded discovery workflow that finds companies, verifies signals, finds relevant people, and produces evidence-backed organization prospects without prematurely creating CRM records.
Signals Search
Signals Search is the canonical discovery workflow for Agentic CRM. It replaces the older split between Sonar company discovery and Headhunter people discovery.
The workflow starts from an ICP/query, a bounded company set, or a saved monitoring scope. It finds companies and evidence, verifies meaningful signals, finds the relevant people inside the strongest companies, and returns one reviewable Organization Prospect result.
It consumes the shared Runtime, Tools, Context, Trigger, Policy, Idempotency and Observability layers. It owns no private scheduler, queue, intelligence store, people store, CRM, runtime or policy engine.
Product boundary
Signals Search owns the discovery experience and its product state. The Agentic CRM design owns the three-state boundary:
Signals Search
│
▼
Global Intelligence
companies + people + evidence + signals
│
▼
Organization Prospect
fit + scores + relevant people + review state
│
├── watch / dismiss
│
└── promote ──> CRM
Discovery is not CRM creation. A company or person may be fully researched and reviewable before the organization chooses to manage that relationship in CRM.
Phase 7 capability composition
signals.search is the stable product ID. The capability contract owns its registry and version rules.
The three root source shapes and existing product outputs remain compatible.
The published node list below includes both Phase 7 composition lanes.
ENG-2282 replaces company mechanics with explicit company.search and company.enrich children.
ENG-2302 replaces people mechanics with explicit people.search and people.enrich children.
Published subworkflow nodes hold executor UUIDs resolved from tenant bindings. A running graph does not query the registry again.
Signals keeps bounded web and thesis research. It passes discovered company or person identities through the supplied child search path. Direct Company/People Search does not gain web sources from this composition. Company-set and saved-search starts retain their scope confinement, including their existing smaller limits. The larger direct-search limits never widen a Signals Run implicitly.
Scoring, persona-fit evaluation, signal evidence, smart-feed publication, saved-search diffs and prospect promotion remain Signals responsibilities. The compile steps adapt child results to the existing scoring and publication contracts. No capability child adds automatic CRM writes or outreach.
People Enrich replaces private profile/contact mechanics. Request only the fields required by the published Signals lane. If email is requested, profile results can appear early, but the root remains live until the email child ends. Pending email is distinct from no email found. The provider-job contract governs replay, cancellation and settlement. The design scenarios define the regression cases.
Company lane contract
The research Agent keeps web discovery, source verification, canonical identity writes and signal recording.
A signal needs a canonical company ID before the Agent can store its evidence.
The Agent and its Skills do not declare research.enrich_company.
Company Enrich owns the profile provider calls.
The company lane runs after research and before signal scoring:
- Compile at most ten researched companies into supplied Company Search references.
- Run the Company Search child through an empty-input wrapper.
- Match returned rows to the supplied canonical IDs and compile Company Enrich subjects.
- Run the Company Enrich child through an empty-input wrapper.
- Validate the child result and pass the admitted company and signal IDs to the existing scoring steps.
Each wrapper uses an existing branch and subworkflow node.
Its empty arm returns an explicit empty batch without starting a capability.
The wrapper returns the existing node-output map. The next compile step accepts only the expected empty or product result shape.
This avoids references to skipped nodes. It does not change the direct capability schemas or the workflow DSL.
Company Search uses sources: [supplied], no firmographic filters and a target of ten.
Signals keeps free-text criteria in its opportunity scorer. It does not translate thesis text into Explorium filters.
The compile step preserves each company's signal IDs, citations and gaps through both children.
It matches by canonical ID and validated input indexes, never by result order.
Conflicting, replaced or unknown IDs cannot reach enrichment or promotion.
For a company-set start, resolve each unnamed seed domain after research. Only the canonical ID for that exact domain can enter the child input. A missing or conflicting domain admits no company. It never grants a spare position to an unrelated company. Saved searches keep the five-company rotation and can discover new companies within the ten-company total.
Enrichment uses the Standard preset with an explicit field selection:
name, description, industry, business_model, country_code, location,
employee_count_exact, employee_count_band, annual_revenue, funding_round and funding_amount.
Use refresh: stale. The existing scorer reads canonical facts after the child completes.
Retained facts and partial field failures remain usable. A missing canonical identity does not.
For a warm row with only country_code, the scorer derives the country name with the shared country converter.
An existing country name takes precedence. The projection does not write to Intelligence.
Child inputs and complete wrapper results must fit the existing 32 KiB boundaries. Compile steps measure their complete payloads and refuse overflow before the next child starts. They reject a truncation marker, a missing product result and an invalid product contract. They do not report a malformed or incomplete child result as an empty search. Cancellation, budget stops and platform faults stop the lane. A partial Run cannot publish a Smart Feed result. An ordinary per-field provider failure can preserve other company results.
The runtime owns child keys, parent spans, root budgets, retries and usage accounting. Each child uses the existing parent Run ID and node ID key. Compile steps add no aggregate charge. Completed child results replay from durable state. Interrupted external calls retain the existing uncertain-charge limitation.
Publication validates active tenant child definitions and their exact V1 capability bindings. The bundle accepts resolved executor UUIDs. ENG-2275 owns registry resolution; ENG-2277 owns installation. The bundle publishes the two wrappers before the root definition and reports partial publication failures. ENG-2303 owns upgrades and rollback. Existing published Run snapshots remain unchanged. Root, wrapper and capability occupy all three supported workflow levels. A custom workflow cannot add another enclosing level. Publication reports the existing depth refusal.
People lane contract
Signals keeps its bounded Exa and Parallel research. The capability boundary starts after that research:
- Build one stable list of at most 75 valid LinkedIn person references from at most three companies.
- Use a root branch to run supplied People Search directly only for a nonempty list.
- Match returned rows by validated
input_indexes, then run one tool-less persona-fit agent. - Compile the selected rows into People Enrich subjects.
- Use a nested root branch to run People Enrich directly only for a nonempty selection.
- Persist selected Exa and Parallel observations in Intelligence, compile settled attachments and write one bounded prospect batch.
People Search already includes Company Search, and People Enrich already includes People Email. Both capability workflows must therefore run directly from the Signals root. A wrapper around either capability would create a fourth workflow level. Each branch arm uses a unique attachment node ID. The branches need no joined output because the final Smart Feed step reads durable prospect rows.
People Search uses sources: [supplied], the shared persona and no company scope.
Signals retains a stable side table from each supplied input index to one prospect and its provider evidence.
The Search result may reorder or merge rows, so the adapter never joins by result position.
Every returned row needs at least one owning input index.
Every returned input index must exist, appear under one result only and resolve to one prospect.
A merged identity that points to several prospects is ambiguous and is attached to none.
The adapter decides complete, partial and empty separately for each prospect from that prospect's provider calls and Search diagnostics.
A global partial outcome for another prospect does not change this decision.
The supplied Search contract returns identity evidence, not the complete Signals research record.
After Enrich resolves selected people, one workflow-only intelligence.upsert_people_batch writes their original Exa and Parallel observations to the canonical Intelligence people.
It accepts at most 75 people, reuses the existing Intelligence person service in stable order and refuses a canonical identity mismatch.
Each source write carries a UUIDv5 ProviderFetch.id from the frozen root Run ID, discover-people node ID, fan-out position and provider-result position.
The frozen provider payload and this ID do not change on retry, so a lost acknowledgement or partial batch retry cannot add a second provenance row.
The 75-person evidence writer can use the complete 120-second step budget.
Provider search completion and failure records stay in the durable tool journal and determine the prospect people state and reason.
People Search applies the deterministic persona gate first. Its unknown-value diagnostics remain visible to the persona-fit agent. The agent has no Tools or Skills and returns only selected Search result IDs, scores and evidence-based reasons. A deterministic compiler rejects unknown or repeated selections, invalid scores, missing reasons and an oversized result. The model cannot write Intelligence or Prospect rows and cannot call contact providers.
People Enrich uses the Standard preset, refresh: stale, and only these fields:
full_name, current_title, current_company_text, location, country_code,
avatar_url, email, email_source and email_score.
Use the canonical person reference when Search returns one. Otherwise use its normalized LinkedIn reference.
People Enrich resolves a valid canonical reference to its stored LinkedIn identity before email work.
The adapter validates input_indexes again, rejects a changed canonical ID and attaches only rows with a canonical person ID.
One identity-invalid row in a schema-valid Enrich envelope does not remove unrelated valid siblings.
A schema-invalid row invalidates the complete child envelope.
A newly created or still-pending prospect keeps people_state: pending while the Enrich child and its email child run.
A completed People Enrich result contains no pending selected field state.
The adapter rejects a final pending state instead of mapping it to a prospect contact state.
Map a safe, exact-address verified result to verified, a settled not-found email to unavailable, and every other settled result to unverified.
Do not copy or construct an email in Signals. People Enrich owns the stored value and verification lineage.
A complete search with no selected person can attach no_matching_people with complete search evidence.
A partial search with no usable person writes an exact reason only when the prospect is still pending; an existing found or no_matching_people state wins.
A partial search with valid people attaches those people and retains the incomplete search evidence.
The bounded attachment service publishes one result.item_ready event for each changed prospect, including complete-empty and partial-pending changes.
The lane measures each complete capability input, agent input, agent output, evidence input and attachment input before its next consumer.
It never trims persona criteria, identities or evidence silently.
Publication validates active tenant people.search and people.enrich V1 bindings before it writes a definition.
It also validates an active tenant persona-fit Agent with no Tools, Skills or context sources before it writes a company wrapper.
It writes only the root for the people lane; there is no people wrapper to publish or recover.
The runtime owns child keys, lineage, budgets, retries and usage once for the root tree.
The root duration is three hours. It holds the 4,800-second People Enrich child after the earlier children and leaves time for evidence, attachment and the final Smart Feed step.
Old published snapshots and admitted Run payloads remain unchanged.
Inputs
A Signals Search Run may start from one of three source shapes.
| Source | Example | Workflow behavior |
|---|---|---|
| Discovery brief | ICP, geography, company criteria, signal criteria, buyer persona | discover companies/signals first, then relevant people |
| Bounded company set | selected prospects, CRM list, selected CRM companies, normalized CSV/import | skip broad company discovery; refresh signals and find relevant people |
| Saved monitoring scope | saved search, watched prospects, target list | look for new/materially changed evidence and refresh only what changed |
The brief may contain:
- an ICP text that states the target-company and signal thesis;
- company criteria;
- buyer persona with
titles,departments,seniorityandcountry_codes.
V1 has no separate signal-theme or result-limit field. The workflow passes the ICP text and company criteria to opportunity scoring and signal research. A surface must not collect a separate value that the Run input would discard.
Every source shape carries a brief. The brief must contain an ICP or company criteria, because the opportunity score reads them, and it must contain a persona with one criterion, because the people plan reads it. A bounded company set names which companies to research; it does not answer how valuable acting on one is. Phase 4 does not read Knowledge as a fallback.
The Front Door or product UI may help construct the brief. The Signals Search workflow owns execution.
Shared persona contract
ENG-2324 owns this input change. ENG-2302 owns people capability composition.
Import People Search's PersonaCriteria; do not copy its schema or weaken its filters.
The persona contains titles, departments, seniority and country_codes.
At least one field is required. Each supplied list contains 1 to 20 unique values.
Omit unused fields. Reject null, empty lists, unknown keys and duplicates after whitespace and case normalization.
Text values contain 1 to 1,000 characters. Country codes use the existing ISO alpha-2 contract.
The full Run input must also fit the platform byte limit. Field limits do not replace that limit.
Use OR within each list and AND between fields. Match text without case differences. Use the person's country. Do not substitute the employer's country or infer a country from free-text location. Keep missing facts unknown. Any observed contradiction excludes the merged identity, including a later duplicate observation. Use the same deterministic gate as People Search before persona-fit scoring. Unknown department or seniority vocabulary remains unknown under the supplied-source rules. Explorium-specific vocabulary checks remain at the Explorium boundary; Signals web research does not select that source here. Keep unknown diagnostics and source evidence so the scorer cannot claim verified fit from missing facts. Persona-fit scores and evidence-based reasons remain separate from signal scores.
Validate the brief before company reads or paid work for discovery and company-set starts.
Saved-search create, corrected-brief patch, manual start and scheduled dispatch use the same reader.
A rejected scheduled dispatch records input_unbuildable and starts no Run. Keep the schedule unchanged.
Provider queries carry all persona alternatives explicitly. Do not select the first value, truncate a list, or expand related titles.
Keep the existing company/provider call count. Do not create a Cartesian product of criteria.
Before company work, preflight the full six-call people plan with bounded company fields and the complete persona.
Check the exact scope output and company research input before their first consumer. Refuse overflow before paid work.
Check the complete plan again before provider fan-out. Never let the platform truncate a call or a criterion.
These resource checks can refuse a schema-valid persona. Return an actionable size error.
Measure the complete persona child input, including Unicode bytes, before starting that child.
Reduce the existing candidate budget by the persona envelope size. Never trim persona criteria to fit.
Legacy persona correction
Inspect stored briefs before a write. Export a private dry-run report for each selected organization. Include the original brief, saved-search ID, update token, field errors and proposed correction. A valid shared persona needs no write. Do not guess an equivalent for title families, functions, regions or combined criteria. Even a legacy title can represent a broader search. Require a reviewed replacement when equivalent meaning cannot be proved. Keep the original brief in the migration report before applying a confirmed correction. Apply only the reviewed persona to that original brief through the existing organization boundary and update-token check. Reject a changed or deleted row. If the exact corrected brief is already stored, report it without another write. Keep saved-search IDs, watch membership, schedules and unrelated brief fields unchanged. An incompatible brief remains readable. Permit name-only edits; require a valid shared persona when the brief itself changes. A new execution returns actionable persona field errors until the brief is corrected.
Do not rewrite admitted Run inputs, snapshots, results or replay data. Before rollout, stop new legacy admissions and let admitted legacy Runs finish, or cancel them through the normal Run API. Do not deploy changed live tool contracts while such Runs can resume. Preserve historical reads after rollout. Capability installation and definition upgrades remain ENG-2303 responsibilities.
The Signals Search journey
owns direct launch, saved-search management, correction and diff review.
The saved-search route composes its private source fields and resolves the
stable signals.search capability. The frontend never accepts an executor UUID.
One Run input, three shapes
The Run input names its own source. Only the first node reads it.
{
"source": "discovery | company_set | saved_search",
"brief": {},
"companies": [],
"saved_search_id": null,
"baseline_run_id": null
}
companies belongs to a bounded company set and holds at most ten members. Each member names a company by canonical Intelligence id, prospect id, domain or organization LinkedIn URL. These references are top-level source fields, not brief fields. V1 accepts no person reference. A name is evidence and never an entity key. saved_search_id and baseline_run_id belong to a saved monitoring scope, and surfaces owns the route that composes them.
The Run freezes one as_of time. The first node freezes it, so no caller supplies it, and the workflow step memo keeps it stable across a retry. Scoring and prospect writes use this time as their source-observation time.
The workflow
input brief / company set / saved scope
│
▼
0. Resolve the source shape into one search scope
│
▼
1. Discover or resolve companies + signals
│
▼
2. Normalize entities + dedupe evidence
│
▼
3. Fill coverage gaps / verify strong candidates
│
▼
4. Grade signal evidence
│
▼
5. Grade opportunity from the brief
│
▼
6. Rank, prune, and upsert the kept prospects
│
▼
7. Find relevant people for bounded top companies
│
▼
8. Verify / dedupe / attach people
final Organization Prospect
company + signals + relevant people + reasons
│
▼
9. Freeze the saved-search publication projection
other source shapes -> not applicable
│
├── watch / dismiss
├── promote selected records to CRM
└── hand selected people to the email sequence workflow
This is one product workflow, not two chained product Runs. Internal units may be Agents, Skills, Tools or deterministic workflow steps depending on the job.
The three research rounds are not three workflow nodes. A workflow has no loops, so the rounds happen inside the company and signals agent step, which decides whether another bounded round is useful and stops on its own judgement inside the runtime ceilings. The workflow owns the order of the stages; the agent owns how many rounds a stage takes.
The published node list
The root sequence serves all three source shapes. The people branch contains its own selected child nodes.
| # | Node | Kind | Width |
|---|---|---|---|
| 1 | signals.compile_search_scope | tool, read | 1 |
| 2 | company-signals | agent | 1 |
| 3 | signals.compile_company_search | tool, read | 1 |
| 4 | company-search | subworkflow wrapper | 1 |
| 5 | signals.compile_company_enrich | tool, read | 1 |
| 6 | company-enrich | subworkflow wrapper | 1 |
| 7 | signals.compile_company_result | tool, read | 1 |
| 8 | signals.compile_signal_batch | tool, read | 1 |
| 9 | score-signals | agent | 1 |
| 10 | signals.compile_opportunity_batch | tool, read | 1 |
| 11 | score-opportunities | agent | 1 |
| 12 | prospect.promote_batch | tool, write | 1 |
| 13 | signals.plan_people_search | tool, read | 1 |
| 14 | research.discover_people | tool, read | fan out |
| 15 | signals.compile_people_search | tool, read | 1 |
| 16 | people-lane | root branch | 1 |
| 17 | people-search | direct subworkflow in the nonempty arm | 1 |
| 18 | signals.compile_search_persona | tool, read in the nonempty arm | 1 |
| 19 | has-persona-candidates | nested root branch | 1 |
| 20 | persona-fit | tool-less agent in the candidate arm | 1 |
| 21 | signals.compile_people_enrich | tool, read in the candidate arm | 1 |
| 22 | has-people-enrich | nested root branch | 1 |
| 23 | people-enrich | direct subworkflow in the selected arm | 1 |
| 24 | signals.compile_person_evidence | tool, read in the selected arm | 1 |
| 25 | persist-person-evidence | intelligence.upsert_people_batch, write in the selected arm | 1 |
| 26 | signals.compile_people_result | tool, read in the selected arm | 1 |
| 27 | attach-selected-people | prospect.attach_people_batch, write in the selected arm | 1 |
| 28 | compile-enrich-empty | signals.compile_empty_people, read in the Enrich-empty arm | 1 |
| 29 | attach-enrich-empty | prospect.attach_people_batch, write in the Enrich-empty arm | 1 |
| 30 | compile-persona-empty | signals.compile_empty_people, read in the persona-empty arm | 1 |
| 31 | attach-persona-empty | prospect.attach_people_batch, write in the persona-empty arm | 1 |
| 32 | compile-search-empty | signals.compile_empty_people, read in the Search-empty arm | 1 |
| 33 | attach-search-empty | prospect.attach_people_batch, write in the Search-empty arm | 1 |
| 34 | signals.compile_smart_feed_observations | tool, read | 1 |
The root reads one normalized scope. Each company wrapper branches on a validated nonempty batch and returns either the child output or an explicit empty result. The root therefore never refers to a skipped node.
Deterministic read tools carry data between reasoning steps. A node input
names input.* and steps.<id>.output.* only. The language has no expression,
array index or list join. Each reshape is therefore a tool.
| Tool | Reads | Answers |
|---|---|---|
signals.compile_search_scope | the whole Run input, Intelligence company identity, the prospects and the saved-search membership of this organization | the scope every later node reads |
signals.compile_company_search | researched companies and exact seed identities | confined canonical references for supplied Search |
signals.compile_company_enrich | complete Search output and research evidence | admitted companies and selected-field Enrich input |
signals.compile_company_result | complete Enrich output and research evidence | validated companies for scoring |
signals.empty_company_batch | no data | an explicit empty wrapper result |
signals.compile_signal_batch | admitted companies, Intelligence signals and sources, stored prospect grades | the signal scoring input, holding ungraded signals only |
signals.compile_opportunity_batch | the new grades, the stored grades, Intelligence company facts, the seed | the opportunity scoring input, with the resolved grades under each company |
signals.plan_people_search | the kept prospects and the buyer persona | one ordered research.discover_people argument list |
signals.compile_people_search | that argument list and the fan-out results | a branch flag, stable supplied Search input and index-to-prospect evidence map |
signals.compile_search_persona | the Search result and the index-to-prospect evidence map | a bounded persona-fit input grouped by prospect, with per-prospect completeness |
signals.compile_people_enrich | the persona-fit result and Search rows | a branch flag, selected Enrich subjects and selected source observations |
signals.compile_person_evidence | selected source observations and complete Enrich results | canonical-checked Intelligence person evidence writes |
signals.compile_people_result | selected evidence, evidence-write results and the complete Enrich result | one validated attachment argument for each attempted prospect |
signals.compile_empty_people | exactly one empty Search, persona or Enrich stage | one complete-empty or partial-pending attachment argument for each attempted prospect |
signals.compile_smart_feed_observations | the source, frozen time, promoted prospect ids, and their current signal grades and people | one frozen publication projection, or not_applicable, superseded or overflow |
No Agent declares one of them. They are workflow plumbing, and a model never calls them.
The scope step answers one key set
A node input that names an absent key fails, and a null fails the same way. So
the scope step answers the same nine fields whichever shape started the Run,
and variance lives inside a value rather than in the presence of a key. Every
later node reads that step and never input.*.
| Field | Holds |
|---|---|
source | the Run input source |
as_of | the frozen observation time |
brief | the ICP and the company criteria, and nothing else |
persona | the buyer persona |
seed_companies | the companies the Run starts from. It is empty for a discovery brief |
confine_to_seed | whether a company outside the seed may become a prospect |
unresolved_inputs | each supplied reference that names no company |
dropped | supplied references a company already in the seed had claimed |
watched_total | how many companies the saved search watches |
The step also reads the brief, so a Run with no ICP and no persona stops at the first node, before it pays a vendor.
The step writes nothing. A supplied domain that Global Intelligence does
not hold yet is research work: the seed keeps the domain and no id, and the
Agent writes the canonical row. A reference with no strong key becomes an
unresolved input, and no entity is created for it. The Run result carries
unresolved_inputs, so a reviewer sees which imported rows never became
companies.
A bounded company set is a promise about the answer. The Agent is told to
search inside the seed, and signals.compile_opportunity_batch proves it: a
company outside the seed never reaches the scorer, so it cannot become a
prospect row. A seed entry with no id admits one company the seed could not
name, because the Agent created that row during the Run. The kept set is never
wider than the seed.
A saved scope refreshes and discovers. It seeds the companies the search
already surfaced, oldest observation first, and it does not confine, because a
new company is one of the changes the Smart Feed reports. The Agent answers at
most ten companies, so the seed takes at most five and a wider watch list
rotates across Runs. watched_total records how many the search watches.
The final node freezes publication input
signals.compile_smart_feed_observations is always the last node. A discovery
or company-set Run returns not_applicable and reads no publication rows. A
saved-search Run reads all promoted prospects, their signal grades and their
attached people in one database statement. The same snapshot requires each
prospect last_seen_run_id to name this Run. Any mismatch returns
superseded.
The statement orders each embedded relation by its Intelligence id and asks
for at most 100 signal grades and 100 people per prospect. V1 supports 99 of
each. A relation that returns 100 makes the result overflow; the node hashes
nothing and cannot publish a partial relation. This explicit sentinel stays
inside the Data API ceiling at the maximum ten-prospect width and detects a
prospect that has outgrown the V1 projection bound.
A ready answer holds the scope's frozen as_of time and one versioned digest
envelope for each prospect. The immutable Run result therefore holds the
complete comparison input. A later Run can change mutable prospect state, but
it cannot change this projection. The node moves no saved-search pointer and
writes no publication row. Saved-search surfaces
own the durable publisher and the digest rules.
The workflow selects this node as its completed result under the single
smart_feed key. It does not carry the eleven earlier node outputs into the
completed Run result. A partial Run keeps the normal diagnostic map and cannot
publish.
This final node never prunes. The promoted set has at most ten prospects, and
each digest envelope has fixed-size identifiers, counts and hashes. The
handler measures the complete serialized answer before it returns. An answer
over the tool-output limit also returns overflow, not ready. The publisher
then writes nothing. Boundary tests prove that 99 components are ready, 100
are not, that ten maximum relations return every sentinel, and that ten
maximum digest envelopes fit the current output limit and remain intact after
the Run-result boundary.
intelligence.upsert_company and intelligence.record_signal resolve identity
and cluster evidence in their handlers. The Agent returns canonical IDs.
Company Search validates those IDs against current Intelligence before enrichment.
Company Enrich owns the single profile provider path.
Every compile tool bounds its own answer
A tool result over the tool output limit is bounded, and the call still succeeds. A compile tool that relies on that boundary reports success on a batch it silently cut. Each one therefore prunes to its own declared limit, in a stable order, and reports the count it dropped.
Company capability handoffs preserve the whole batch or refuse overflow.
They never trim signal evidence. The final Smart Feed projection also preserves
the whole bounded set or returns a non-ready overflow result.
The same rule holds at the two run boundaries. A Run input over the refusal
limit is refused, so the scoring and persona inputs must fit it. A default Run
result over the result limit is bounded and marked truncated. Signals Search
uses the workflow result selector to keep the final Smart Feed projection as
one top-level item. Its saved prospect rows and result.item_ready stream
remain the record for the other product surfaces.
Bounded research rounds
The research portion may perform up to three purposeful rounds. They are not three repeats of the same query.
| Round | Purpose | Typical work |
|---|---|---|
| 1. Discovery | Find candidate companies/signals with useful recall | focused ICP queries, category search, news/web search, existing-intelligence lookup |
| 2. Coverage | Fill meaningful gaps | missing signal categories, weak company attributes, stale evidence, missing geography |
| 3. Verification | Confirm strongest or ambiguous candidates | identity/domain validation, date verification, source agreement, conflicting evidence |
Stop after round 1 or 2 when evidence is sufficient. Stop searching when any one is true:
- required evidence is sufficient for the decision;
- another round is producing little new information;
- the result limit is reached;
- no candidates survive verification;
- maximum round count is reached.
The Runtime and Policy limits for tools, tokens, spend, turns, and duration are hard ceilings. If one stops the segment, the Run records its partial_reason. The Agent decides only whether another bounded round is useful inside those ceilings.
Company and signals agent result
The company/signals Agent has three Skills: discover companies, search signals, and verify evidence. Its tool set is:
research.search_webresearch.search_companyresearch.fetch_pageintelligence.upsert_companyintelligence.record_signal
Company enrichment runs through the company.enrich child.
The compile boundary preserves the following bounded Agent output for scoring.
The Agent returns one structured object. It contains:
- a
summaryof at most 100 characters; rounds_completed, from 1 through 3;stop_reason:evidence_sufficient,diminishing_returns,result_limit,max_rounds, orno_candidates;- at most ten
companies, each with one canonical company ID, one to ten unique canonical signal IDs, one to three source URLs, and zero to three evidence gaps; - at most three
unresolvedcandidates, each with one to three sources, one to threeevidencenotes, and the reasonunresolvedorconflict.
An unresolved or conflicting identity cannot enter companies. It stays in unresolved. A company or signal without source evidence cannot enter the result. The result uses no extra fields and stays inside the 32 KiB Run result limit.
A runtime or Policy ceiling can stop the segment before the Agent returns this object. That path reports the exact partial_reason from the runtime. It does not create an incomplete structured result.
Deterministic processing between rounds
Reasoning may be agentic. Identity and duplicate handling should not be.
Between batches use deterministic domain services/Tools for:
- canonical company/domain normalization;
- company and person entity resolution;
- deduplication across providers and queries;
- signal/evidence clustering and deduplication;
- freshness comparison and provenance retention;
- CRM lookup and existing-state projection;
- duplicate-safe Intelligence/Prospect writes.
Do not ask the model whether two rows with the same canonical domain or known person identity are separate entities when deterministic resolution can answer it.
Identity and cluster contract
Use conservative strong keys. A company first uses its exact normalized domain. One domain resolves the company. Different domains return conflict. When no domain exists, one organization LinkedIn key resolves the company. Different organization LinkedIn keys return conflict. A person uses one normalized LinkedIn profile key. Different profile keys return conflict.
Names, email addresses and brand roots are evidence. They are not entity keys. A missing strong key returns unresolved. The resolver does not select one key from a conflict.
Every resolved signal observation carries:
- one canonical subject key;
- one signal type;
- one source-independent event key;
- one source reference;
- the observed and fetched times when known;
- one or more claims.
The subject key, signal type and event key identify one cluster. A UTC date alone cannot identify an event. Two distinct events on the same date must stay separate. A missing subject stays unresolved. A conflicting subject stays conflict. Neither subject can form a resolved cluster.
The Intelligence write boundary keeps the same identity. It derives dedup_key from the canonical subject, signal type and event key. A repeated cluster returns the existing signal and adds only new source links. It never derives event identity from the observed date.
A cluster deduplicates source references and keeps each distinct claim. A newer claim does not remove an older conflicting claim. Freshness compares valid timestamps in UTC. Missing or invalid time returns unknown.
Resolution and clustering are pure batch operations. Reordering or repeating the same input must not change the result. The operations must not mutate their input.
Light scan, then deep scan
Use bounded signal verification on selected candidates. This section does not define a Deep enrichment preset. Company and People Enrich support only the Basic and Standard field sets. Do not fully enrich every candidate.
many candidate companies
│
▼
light evidence + basic fit
│
▼
rank / prune
│
▼
deep company verification on strongest / ambiguous candidates
│
▼
people discovery only for bounded top companies
│
▼
contact enrichment / verification only where useful
A broad search might cheaply scan dozens of companies, deeply research only the strongest subset, then search people inside those companies.
The limits are product configuration, and two of them are declared on the workflow rather than chosen at run time. The people discovery step is a fan out, so it carries max_fanout and fanout_concurrency: the first bounds the step against the Inngest step budget, and the second bounds how hard the step hits a search vendor at one instant. See runtime definitions.
They are configuration, not a new execution mechanism.
People discovery is part of the same prospect
Signals attaches relevant people to each opportunity. Standalone People Search also exists as a separate Phase 7 product capability.
The mechanics below describe the Phase 4 research baseline. Phase 7 keeps the web discovery and replaces private identity, enrichment, contact and attachment decisions with the child composition above. Identity gates, candidate bounds, verification meaning and prospect attachment remain required.
For each bounded company that survives company/signal qualification:
company + buyer persona
-> find candidate people
-> resolve/dedupe identity against Intelligence + CRM + current batch
-> cap the merged candidate observations
-> score/explain persona fit
-> verify available contact data for selected people
-> attach relevant people to the Organization Prospect
The persona uses the shared contract.
Legacy title, title_family, function and free-text location are not new Signals inputs.
research.discover_people runs one company and provider pair per call. The workflow owns the bounded fan out across companies and providers. This keeps each paid vendor call linked to one tool span and one usage row.
The workflow fans in every provider call before People Search. A fan-out item returns ok and data only. It does not echo its own arguments, so position is the only link back to the company and provider pair. One deterministic step reads the argument list and result list together, preserves each observation as one supplied input, and joins them by position. Each provider has a fixed position in the fan-out input, and each result keeps its provider and result positions. The earliest position pair ranks an observation, and the normalized profile key is the final sort value. People Search owns identity merging and the deterministic persona gate. The child input can contain at most 32 KB and at most 25 candidate observations for each company. A candidate without a valid profile key stays unresolved and cannot enter People Search in V1.
The persona-fit agent runs once for one bounded set of at most three companies. A workflow has no loop and an agent step has width one, so one Run covers every kept company. The deterministic input separates candidates by prospect and preserves stable Search result IDs. The agent returns structured judgments only. It does not call discovery, contact, Intelligence or Prospect Tools.
People Enrich owns profile writes, email lookup and safe verification. The deterministic attachment step maps only settled Enrich results into the three prospect contact states. It never constructs or stores an address.
A company with no matching person is still a valid company prospect; it carries a clear no_matching_people result rather than disappearing.
Two scores, separate meanings
Do not collapse evidence strength and customer fit into one number.
| Score | Scope | Answers |
|---|---|---|
| Signal score | Organization Prospect signal | How well does the linked evidence support this observed event at the frozen as_of time? |
| Lead / opportunity score | Organization Prospect | Given this organization's ICP and context, how valuable is acting on this company now? |
Two tool-less scoring agents enforce the boundary through their inputs:
- The signal agent receives the Intelligence signal, its linked sources and
as_of. It receives no ICP, persona, Knowledge or opportunity data. Relevance means direct support for the event. It does not mean customer fit. - The opportunity agent receives the brief criteria, company facts and resolved signal grades. A resolved grade is new output or a stored grade. The agent does not read Knowledge. Persona fit stays separate from both V1 scores.
Both agents return bounded structured output. Scores use one 0-100 scale: 0-39 weak, 40-69 moderate and 70-100 strong. An explicit hard ICP mismatch caps the opportunity score at 39. A strong-fit company with a moderate signal can therefore outrank a poor-fit company with a strong signal.
One scoring batch contains at most ten companies and ten signals for each company. Each compact signal keeps at most three sources and three evidence statements. A reason and a recommended action each contain at most 200 characters.
A scoring batch may be empty, and an empty batch answers an empty result. A workflow has no loop and no way to skip a node and still feed the node after it, so a retry that reuses every stored grade, and a search that resolves no company, both reach the scorer with nothing to grade.
The workflow scores only an ungraded signal. It reuses the stored grade when a retry or another search finds the same organization and intel_signal_id. This keeps the immutable grade stable when model output or the brief changes. The grade is a first-observation snapshot. The review shows current evidence freshness separately. A new Intelligence signal gets a new grade.
Missing linked source evidence refuses a signal score. Missing ICP and company criteria refuses an opportunity score. A scorer never substitutes a Knowledge source or an inferred default.
Every score keeps a short reason. Every opportunity keeps one short recommended next action. The action is a recommendation and causes no side effect.
Result shape
The workflow returns results grouped around the Organization Prospect, not separate company and people products.
Organization Prospect
company
name / domain / location / size
fit + opportunity score + reason
CRM state
signals
signal score + reason
evidence + provenance + freshness
relevant people
Person A: role, seniority, location, persona-fit reason,
evidence, contact verification state
Person B: ...
review state
new / watching / dismissed / promoted
recommended next action
The result should be useful without opening raw Run traces.
The prospect ref kind names one public.prospects row. The future prospect.person kind names one public.prospect_people row. A discovery Run reports ready company results with prospect refs. A selected person can move to outreach with a prospect.person ref before CRM promotion.
Incremental results
Signals Search streams useful stable updates while later work continues.
company discovered/resolved
-> preliminary evidence
-> signal/fit updated
-> company verified
-> relevant people discovered
-> contact/person evidence improved
-> final prospect ready
The same visible prospect card may improve during the Run. SSE is live transport; durable Intelligence, Prospect rows, Runs and spans remain the record.
Each successful prospect write Tool reads the current durable ProspectSummary and publishes it as result.item_ready. This includes prospect.upsert, prospect.attach_people, prospect.attach_people_batch and prospect.set_review_state. A batch publishes one event for each changed prospect. The event uses the prospect id as its stable item key and updated_at as its revision. A repeated, lost or out-of-order event cannot change the durable result.
Saved search and Smart Feed
The discovery surface keeps product state needed for repeat use:
- saved discovery query / ICP reference;
- Search History;
- companies already surfaced for that search;
- review state: new / watching / dismissed / promoted;
- evidence and signal references;
- latest signal score;
- latest opportunity score and reasons;
- relevant people snapshot/reference set;
- Smart Feed diff state.
The Smart Feed emphasizes diffs: new prospects, new signals, new relevant people, materially changed evidence or meaningful score changes. A scheduled Run must not replay the same unchanged list.
V1 serves the diff for the latest published successful Run. Each Run writes immutable observation rows and compares them with the published Run that its input names. Only one Run can publish from a given baseline. A successful overlapping Run whose baseline became stale remains in Run Explorer and does not replay the same delta. The API accepts no historical Run id, and the observation-row retention period is not a product promise. Run Explorer remains the product Run-history surface.
Scheduled monitoring
A saved search may own a Trigger. The Trigger starts the same published Signals Search workflow used by an active search.
saved search + schedule
-> Trigger
-> RunManager
-> Signals Search
-> compare with existing Intelligence / Prospect state
-> Smart Feed: new or materially changed opportunities
There is no product-specific scheduler.
The Trigger cannot carry the baseline in a static template. A Run publishes
a Smart Feed diff only when run.input['baseline_run_id'] equals the saved
search's current last_run_id, and that value changes on every run. A fixed
template therefore publishes once and answers stale for ever after.
The trigger row names the saved_search input builder instead. The builder
reads the saved search at dispatch and returns the same frozen input the
interactive start route builds.
{'source': 'saved_search',
'saved_search_id': str(search.id),
'baseline_run_id': str(search.last_run_id) if search.last_run_id else None,
'brief': search.brief}
The builder lives with this product and registers itself. The trigger layer imports the registry alone. See triggers.
Signal-driven refresh
Signals may arrive for watched prospects or existing CRM entities.
new evidence / signal
-> normalize + signal dedupe
-> update Intelligence
-> evaluate organization relevance
-> targeted verification when needed
-> refresh relevant people only when stale/missing/materially affected
-> update Prospect / Smart Feed
or recommend action on existing CRM entity
Do not rerun a broad discovery workflow when one targeted verification or people refresh is enough.
Agent / Workflow / Skill / Tool shape
A clean V1 shape is one published Signals Search Workflow Definition coordinating bounded deterministic stages, with reasoning inside focused Agents.
Example internal responsibilities:
| Unit | Responsibility |
|---|---|
| Company/signals Agent | search strategy, evidence gaps and canonical company/signal references |
| Signal scoring Agent | evidence strength and freshness for each ungraded signal |
| Opportunity scoring Agent | company value from the brief, company facts and resolved signal grades |
| People Agent | tool-less persona-fit judgment after supplied People Search |
| Deterministic steps | normalization, entity resolution, dedupe, limits, fan-out/fan-in |
| Skills | discover companies, search signals and verify evidence |
| Capability children | company/people search and enrichment, including requested email completion |
| Tools | signal research, scoped CRM reads, Intelligence/Prospect persistence and provenance; no CRM writes |
The workflow defines order and bounded fan-out. Agents reason inside steps. Tools own safe effects, credentials, policy checks, idempotency, vendor limits and result limits.
Do not create a separate Sonar runtime, Headhunter runtime, people database or discovery queue.
Prospect write contract
V1 has three model-facing prospect write tools and three workflow-only batch
writes. Each tool declares repeatable = False. A repeated call returns the
first result through the tool journal.
| Tool | Contract |
|---|---|
prospect.upsert | Resolve one (organization_id, intel_company_id) row. Upsert at most 25 signal grades. Preserve first_seen_at, CRM references and review state. |
prospect.attach_people | Add or improve at most 25 relevant people. An empty list requires a reason and is valid only when the prospect has no person row. |
prospect.set_review_state | Set new, watching or dismissed. Refuse promoted, because the promotion tool owns that state. |
prospect.promote_batch | Rank scored companies, prune them to the result limit, then apply the prospect.upsert contract to at most ten of them in one call. It returns one ordered row for each kept company. No Agent declares it. |
intelligence.upsert_people_batch | Persist original source observations for at most 75 selected people in stable order. Reuse the existing Intelligence person service, return canonical IDs and refuse identity changes. No Agent declares it. |
prospect.attach_people_batch | Apply a found, complete-empty or partial-pending people outcome to at most three prospects in stable order. It returns one row for each attempted prospect and emits one item-ready event for each changed row. No Agent declares it. |
Why batch writes, and not a write fan out. A wide step must be read
only, because a partial write has no recovery rule yet. A batch write inside
one step needs no new rule. Each row keys on (organization_id, intel_company_id) and is idempotent on its own, and the invoker releases the
idempotency claim when a handler raises. A retry therefore repeats the whole
batch and resolves the same rows. The handler writes each row independently. It
never opens one transaction over the batch.
prospect.promote_batch deduplicates by intel_company_id before it writes.
The company and signals Agent result marks its companies array
uniqueItems, and that rule compares whole objects. Two entries that name one
company with different sources both satisfy it.
The invocation supplies the organization and Run. No tool argument supplies
either value. A signal score requires its short reason. A people batch with
items sets people_state to found and clears the empty reason.
The workflow fans in before each batch write. Each prospect and person write
includes the time of its source observation. A newer observation can replace
its score, reason and contact state. An older observation cannot. Equal-time
writes must have the same values or the tool refuses the conflict. The write
keeps first_seen_at and advances last_seen_at only to the greater value.
A signal grade is immutable for one intel_signal_id, because that identity
names one observed event. A repeated grade must have the same values. A
correction creates a new Intelligence signal instead of changing the old
event. Before scoring, the workflow reads existing prospect signal grades and
passes only ungraded signals to the signal agent. A retry or another search
reuses the stored grade. These rules make the merge independent of Run
completion order.
People attachment is additive in V1. The workflow can write an empty result
only after every bounded provider call for that prospect succeeds and People
Search settles each owned input. A partial outcome with no usable person keeps
people_state: pending and writes the exact failure reason only while the
stored state is still pending. It cannot regress an existing found or
no_matching_people state. The batch carries
one result record for each provider search of that prospect. Its result count
records the candidates that the search found. It can be positive when no
candidate matches the buyer persona. people_state_reason is required plain
text, not a second state enum.
The empty path first confirms that the prospect has no person row. It then
updates only a pending or no_matching_people prospect. A non-empty path
writes found after its people rows. Therefore, found wins a concurrent
empty write. An empty result never deletes an earlier person result.
A partial-pending path also confirms that the prospect has no person row, then
updates only people_state: pending. If that compare-and-set changes no row,
it reads and preserves the current complete state.
The tool handlers and the product endpoints call one platform-local prospect
service. A product endpoint never passes through ToolInvoker. Prospect curation writes
product state and does not write an approval row by itself.
CRM promotion
Promotion is explicit and can happen after the combined prospect is reviewed.
Organization Prospect
+ selected relevant people
-> user / policy-approved promotion
-> CRM domain Tools
-> canonical company upsert / resolve
-> selected person upserts / relationships
-> optional list membership
-> CRM references returned
If company/person records already exist, promotion resolves to the canonical records. Discovery never silently populates CRM merely because a company or person was found.
Promotion uses recovery, not a business-logic database function. It writes
each canonical CRM reference after its idempotent upsert. It sets the prospect
state to promoted only after every selected reference is linked. A retry
resolves the same CRM rows and completes the missing links. A partial attempt
can leave canonical CRM rows present while the prospect keeps its earlier
review state.
Two callers share one promotion service: the prospect.promote tool, and
POST /api/v1/agentic/prospects/{id}/promote. A person promotes over the
route and an agent promotes over the tool, and the two cannot diverge. The
optional list receives the company. A selected person who already holds a
different CRM company link is a conflict that names that person; the reviewer
deselects that person and retries.
Existing CRM companies may use Signals Search without re-promoting the company; newly discovered people still require the normal explicit/domain write path when the organization chooses to manage them.
Outreach handoff
selected relevant people
+ canonical company/person context
+ current signals / prospect reasons
-> email sequence workflow
-> personalized draft(s) -> approval -> send
The handoff may use Intelligence/Prospect identities when records are not yet promoted, plus CRM references when available. It does not require premature CRM creation and does not copy prompt blobs into another product store.
Failure / edge cases
| Case | Behavior |
|---|---|
| Same company returned by multiple searches | one canonical entity; merge evidence/provenance |
| Same signal appears in several sources | one logical signal/evidence cluster with source references |
| Duplicate person found from several sources | merge/dedupe and preserve provenance |
| Company already exists in CRM | project CRM state; update Intelligence; never duplicate company |
| Person already exists in CRM | resolve canonical person; do not duplicate on promotion |
| Company has no matching people | keep company prospect with explicit empty people result |
| Contact data cannot be verified | keep person; mark verification state; never fabricate |
| One candidate/company fails | independent candidates continue unless workflow semantics require stop |
| CSV/imported company cannot be resolved | the scope step reports it in unresolved_inputs; no entity is created for it |
| Evidence conflicts | verification round or lower confidence; preserve disagreement |
| Scheduled Run finds nothing new | successful Run with empty Smart Feed diff |
| Search/persona too broad | enforce bounded result limits; return best set / ask user to narrow next Run |
| Budget exhausted | a Run that already produced node output succeeds with partial_reason and truncation metadata; a Run that produced none fails and still names the clock |
V1 rules
- One canonical Signals Search workflow replaces separate Sonar and Headhunter execution flows.
- Company discovery, signal research and relevant-people discovery produce one Organization Prospect result.
- Search is bounded to at most three purposeful research rounds.
- Use light signal research before selective verification. Company/People enrichment uses only Basic or Standard targets.
- Entity resolution, dedupe, provenance and CRM identity projection are deterministic.
- Signals are evidence, not leads; signal score and opportunity score remain separate.
- A discovered company/person is not automatically a CRM record.
- Scheduled work uses Trigger +
RunManager+ Inngest. - Stream useful partial prospect updates.
- CRM promotion is explicit and idempotent through CRM Tools.
- A separate explicit Phase 8 action can hand selected people to the email sequence workflow.
- No Sonar→Headhunter Run chaining, separate discovery scheduler, people database, queue or runtime.
Minimum scenarios
| Scenario | Expected behavior |
|---|---|
| Broad ICP search | bounded company/signal rounds → strongest companies → relevant people → combined prospect results; no CRM writes |
| Existing CRM target list | skip broad company discovery; refresh signals + relevant people against canonical CRM companies; no company outside the set becomes a prospect |
| Same company from five searches | one normalized entity with merged evidence, not five prospects |
| Strong funding signal on poor-fit company | high signal score may coexist with low opportunity score; people search may be skipped after pruning |
| Strong-fit company with no people found | prospect remains reviewable with no_matching_people state |
| Person found by three providers | one person result with merged provenance/contact state |
| New signal on existing CRM customer | update Intelligence and recommend action against existing CRM entity; no duplicate CRM record |
| Scheduled run finds no material change | success with no Smart Feed additions |
| Discovery or company-set Run reaches the final node | not_applicable; no publication rows are read or written |
| Another Run writes during the final saved-search read | one ready snapshot or superseded; never mixed prospect state |
| User promotes prospect + two people | idempotent CRM Tools resolve/create canonical company and selected people only |
| User selects person for outreach before CRM promotion | the email sequence workflow can use authorized Intelligence/Prospect context; CRM creation is not forced |