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.

1 min read Updated Sep 3, 2026

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:

TEXT
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:

  1. Compile at most ten researched companies into supplied Company Search references.
  2. Run the Company Search child through an empty-input wrapper.
  3. Match returned rows to the supplied canonical IDs and compile Company Enrich subjects.
  4. Run the Company Enrich child through an empty-input wrapper.
  5. 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:

  1. Build one stable list of at most 75 valid LinkedIn person references from at most three companies.
  2. Use a root branch to run supplied People Search directly only for a nonempty list.
  3. Match returned rows by validated input_indexes, then run one tool-less persona-fit agent.
  4. Compile the selected rows into People Enrich subjects.
  5. Use a nested root branch to run People Enrich directly only for a nonempty selection.
  6. 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.

SourceExampleWorkflow behavior
Discovery briefICP, geography, company criteria, signal criteria, buyer personadiscover companies/signals first, then relevant people
Bounded company setselected prospects, CRM list, selected CRM companies, normalized CSV/importskip broad company discovery; refresh signals and find relevant people
Saved monitoring scopesaved search, watched prospects, target listlook 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, seniority and country_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.

JSON
{
  "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

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

#NodeKindWidth
1signals.compile_search_scopetool, read1
2company-signalsagent1
3signals.compile_company_searchtool, read1
4company-searchsubworkflow wrapper1
5signals.compile_company_enrichtool, read1
6company-enrichsubworkflow wrapper1
7signals.compile_company_resulttool, read1
8signals.compile_signal_batchtool, read1
9score-signalsagent1
10signals.compile_opportunity_batchtool, read1
11score-opportunitiesagent1
12prospect.promote_batchtool, write1
13signals.plan_people_searchtool, read1
14research.discover_peopletool, readfan out
15signals.compile_people_searchtool, read1
16people-laneroot branch1
17people-searchdirect subworkflow in the nonempty arm1
18signals.compile_search_personatool, read in the nonempty arm1
19has-persona-candidatesnested root branch1
20persona-fittool-less agent in the candidate arm1
21signals.compile_people_enrichtool, read in the candidate arm1
22has-people-enrichnested root branch1
23people-enrichdirect subworkflow in the selected arm1
24signals.compile_person_evidencetool, read in the selected arm1
25persist-person-evidenceintelligence.upsert_people_batch, write in the selected arm1
26signals.compile_people_resulttool, read in the selected arm1
27attach-selected-peopleprospect.attach_people_batch, write in the selected arm1
28compile-enrich-emptysignals.compile_empty_people, read in the Enrich-empty arm1
29attach-enrich-emptyprospect.attach_people_batch, write in the Enrich-empty arm1
30compile-persona-emptysignals.compile_empty_people, read in the persona-empty arm1
31attach-persona-emptyprospect.attach_people_batch, write in the persona-empty arm1
32compile-search-emptysignals.compile_empty_people, read in the Search-empty arm1
33attach-search-emptyprospect.attach_people_batch, write in the Search-empty arm1
34signals.compile_smart_feed_observationstool, read1

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.

ToolReadsAnswers
signals.compile_search_scopethe whole Run input, Intelligence company identity, the prospects and the saved-search membership of this organizationthe scope every later node reads
signals.compile_company_searchresearched companies and exact seed identitiesconfined canonical references for supplied Search
signals.compile_company_enrichcomplete Search output and research evidenceadmitted companies and selected-field Enrich input
signals.compile_company_resultcomplete Enrich output and research evidencevalidated companies for scoring
signals.empty_company_batchno dataan explicit empty wrapper result
signals.compile_signal_batchadmitted companies, Intelligence signals and sources, stored prospect gradesthe signal scoring input, holding ungraded signals only
signals.compile_opportunity_batchthe new grades, the stored grades, Intelligence company facts, the seedthe opportunity scoring input, with the resolved grades under each company
signals.plan_people_searchthe kept prospects and the buyer personaone ordered research.discover_people argument list
signals.compile_people_searchthat argument list and the fan-out resultsa branch flag, stable supplied Search input and index-to-prospect evidence map
signals.compile_search_personathe Search result and the index-to-prospect evidence mapa bounded persona-fit input grouped by prospect, with per-prospect completeness
signals.compile_people_enrichthe persona-fit result and Search rowsa branch flag, selected Enrich subjects and selected source observations
signals.compile_person_evidenceselected source observations and complete Enrich resultscanonical-checked Intelligence person evidence writes
signals.compile_people_resultselected evidence, evidence-write results and the complete Enrich resultone validated attachment argument for each attempted prospect
signals.compile_empty_peopleexactly one empty Search, persona or Enrich stageone complete-empty or partial-pending attachment argument for each attempted prospect
signals.compile_smart_feed_observationsthe source, frozen time, promoted prospect ids, and their current signal grades and peopleone 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.*.

FieldHolds
sourcethe Run input source
as_ofthe frozen observation time
briefthe ICP and the company criteria, and nothing else
personathe buyer persona
seed_companiesthe companies the Run starts from. It is empty for a discovery brief
confine_to_seedwhether a company outside the seed may become a prospect
unresolved_inputseach supplied reference that names no company
droppedsupplied references a company already in the seed had claimed
watched_totalhow 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.

RoundPurposeTypical work
1. DiscoveryFind candidate companies/signals with useful recallfocused ICP queries, category search, news/web search, existing-intelligence lookup
2. CoverageFill meaningful gapsmissing signal categories, weak company attributes, stale evidence, missing geography
3. VerificationConfirm strongest or ambiguous candidatesidentity/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_web
  • research.search_company
  • research.fetch_page
  • intelligence.upsert_company
  • intelligence.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 summary of at most 100 characters;
  • rounds_completed, from 1 through 3;
  • stop_reason: evidence_sufficient, diminishing_returns, result_limit, max_rounds, or no_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 unresolved candidates, each with one to three sources, one to three evidence notes, and the reason unresolved or conflict.

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.

TEXT
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:

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

ScoreScopeAnswers
Signal scoreOrganization Prospect signalHow well does the linked evidence support this observed event at the frozen as_of time?
Lead / opportunity scoreOrganization ProspectGiven 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.

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

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

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

PYTHON
{'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.

TEXT
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:

UnitResponsibility
Company/signals Agentsearch strategy, evidence gaps and canonical company/signal references
Signal scoring Agentevidence strength and freshness for each ungraded signal
Opportunity scoring Agentcompany value from the brief, company facts and resolved signal grades
People Agenttool-less persona-fit judgment after supplied People Search
Deterministic stepsnormalization, entity resolution, dedupe, limits, fan-out/fan-in
Skillsdiscover companies, search signals and verify evidence
Capability childrencompany/people search and enrichment, including requested email completion
Toolssignal 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.

ToolContract
prospect.upsertResolve one (organization_id, intel_company_id) row. Upsert at most 25 signal grades. Preserve first_seen_at, CRM references and review state.
prospect.attach_peopleAdd 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_stateSet new, watching or dismissed. Refuse promoted, because the promotion tool owns that state.
prospect.promote_batchRank 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_batchPersist 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_batchApply 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.

TEXT
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

TEXT
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

CaseBehavior
Same company returned by multiple searchesone canonical entity; merge evidence/provenance
Same signal appears in several sourcesone logical signal/evidence cluster with source references
Duplicate person found from several sourcesmerge/dedupe and preserve provenance
Company already exists in CRMproject CRM state; update Intelligence; never duplicate company
Person already exists in CRMresolve canonical person; do not duplicate on promotion
Company has no matching peoplekeep company prospect with explicit empty people result
Contact data cannot be verifiedkeep person; mark verification state; never fabricate
One candidate/company failsindependent candidates continue unless workflow semantics require stop
CSV/imported company cannot be resolvedthe scope step reports it in unresolved_inputs; no entity is created for it
Evidence conflictsverification round or lower confidence; preserve disagreement
Scheduled Run finds nothing newsuccessful Run with empty Smart Feed diff
Search/persona too broadenforce bounded result limits; return best set / ask user to narrow next Run
Budget exhausteda 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

ScenarioExpected behavior
Broad ICP searchbounded company/signal rounds → strongest companies → relevant people → combined prospect results; no CRM writes
Existing CRM target listskip broad company discovery; refresh signals + relevant people against canonical CRM companies; no company outside the set becomes a prospect
Same company from five searchesone normalized entity with merged evidence, not five prospects
Strong funding signal on poor-fit companyhigh signal score may coexist with low opportunity score; people search may be skipped after pruning
Strong-fit company with no people foundprospect remains reviewable with no_matching_people state
Person found by three providersone person result with merged provenance/contact state
New signal on existing CRM customerupdate Intelligence and recommend action against existing CRM entity; no duplicate CRM record
Scheduled run finds no material changesuccess with no Smart Feed additions
Discovery or company-set Run reaches the final nodenot_applicable; no publication rows are read or written
Another Run writes during the final saved-search readone ready snapshot or superseded; never mixed prospect state
User promotes prospect + two peopleidempotent CRM Tools resolve/create canonical company and selected people only
User selects person for outreach before CRM promotionthe email sequence workflow can use authorized Intelligence/Prospect context; CRM creation is not forced