Sales lead agent
This page explains how a sales lead agent uses WebSearch to collect public signals about target companies (news, funding, hiring, tech stack), scores them deterministically on the app side against a versioned ICP, and writes sourced lead summaries back to the CRM as draft records. After reading it, you will understand why collecting public signals only needs a silently issued runtime credential, why scoring lives on the app side rather than in the model, and why ICP profiles and interaction history stay canonical in the CRM.
Use case
Sales teams need to quickly understand target companies, contacts, recent news, technology signals, and potential entry points. These signals are scattered across company sites, press releases, and job pages; researching each account manually cannot keep pace with a growing lead list. The Agent's output is a lead summary with scoring evidence plus draft CRM records; outreach is always performed by the seller.
Typical triggers:
- A marketing campaign delivers a batch of new leads that must be enriched and prioritized before follow-up.
- A target account shows a buying signal (funding, expansion, relevant job postings) and the entry point must be refreshed promptly.
- A new quarter starts, and the existing lead pool must be re-ranked against the latest ICP criteria.
Engineering challenges
- Signal quality: press releases, retellings, and stale pages are noisy — "they're hiring DevOps" may be a year-old posting. Every signal that enters scoring needs a source URL and capture time, or the score is just storytelling.
- Scoring consistency: ICP criteria evolve with closed-won experience; unversioned criteria make batches incomparable — "80 points last quarter" and "80 points this quarter" no longer mean the same thing.
- CRM data sovereignty: the single source of truth for ICP profiles and interaction history is the CRM. Copying them into a second store drifts immediately — enrichment results may only go back as draft records, with official changes confirmed by the seller.
Module composition
| Module | Role | Notes |
|---|---|---|
| GenAuth | Core | Silent delegation issues the short-lived runtime credential every product call requires; interactive consent is only needed when login state or write actions enter the picture — see "When interactive consent is needed" below. |
| Web Agent | Core | WebSearch searches company sites, news, job pages, and public sources, keeping a source URL and capture time per signal. |
| GUMem | Not used | ICP profiles and interaction history stay canonical in the CRM, read directly by your app and injected by version into scoring; no second source of truth in Memory. |
When interactive consent is needed
Every product call requires a GenAuth delegate token; public read-only scenarios are covered by silent delegation. This scenario reads publicly visible pages only and needs no interactive confirmation from the seller in the Console:
- Public reads: issue a runtime credential silently with
delegateToken(products: ['webSearch']). The credential is short-lived and revocable at any time; every issuance and search enters the audit chain. - Signed-in targets or write actions: CRM reads and draft writes are done by your app directly with its own CRM API credentials, not through Qoni delegation. Only if enrichment needs the Web Agent to sign in to a third-party data source (for example, a paid database) do you switch to
mode: 'interactive'so the seller approves in Qoni Console. The example on this page includes no such target.
Note: the SDK example requests product-level authorization. Fine-grained boundaries — target domain lists and fetch frequency — are enforced by the GenAuth Agent Profile or your policy layer, not by the task prompt; that configuration is not shown on this page. See Delegate token and attenuation for the full semantics.
Workflow
The seller enters a target company or submits a lead list for enrichment.
Your app silently issues a runtime credential and reads the current ICP criteria version and the accounts' interaction history from the CRM.
Web Agent collects public signals from company sites, news, and job pages through WebSearch, keeping a source URL and capture time per signal.
Checkpoint: When a target page shows a login wall or CAPTCHA, skip the source and record it; escalate to the seller to decide on a manual look — never attempt a bypass.
Your app parses and validates the signals: anything missing a source URL or capture time is dropped and never enters scoring.
Your app scores the leads deterministically against the ICP criteria, keeping the rationale and sources per signal.
Checkpoint: Every signal that enters scoring must have a source; unsourced speculation never contributes to a score.
Your app writes the scored summary, outreach angles, and source list back to the CRM as draft records, which become official data only after seller confirmation.
Checkpoint: Changes to official CRM records require seller confirmation; the Agent never overwrites manually maintained fields.
The seller decides the follow-up order from the summary; for email outreach, a draft is sent only after the seller confirms it.
Example code
The example below wires this scenario in with the official Qoni SDK (@qoniai/qoni): silent delegation (webSearch only) → load the versioned ICP criteria from the CRM → webSearch.run() to collect public signals → app-side source validation and deterministic scoring → write back to the CRM as draft records.
import { Qoni } from '@qoniai/qoni'
const qoni = new Qoni({
accessKey: process.env.QONI_ACCESS_KEY!,
secretKey: process.env.QONI_SECRET_KEY!,
})
export async function enrichLeads(sellerId: string, targetCompanies: string[]) {
// 1. Silent delegation: public-signal collection only, no site sign-in;
// request the webSearch product only
const { data: grant } = await qoni.delegateToken({
user: { id: sellerId },
agent: 'sales-lead',
products: ['webSearch'],
})
// 2. Before scoring: read the current ICP criteria and interaction history
// from the CRM (the CRM is the canonical source, not Memory)
const icp = await crm.loadIcpCriteria() // e.g. { version: '2026-Q3', industries: [...], signals: [...] }
const history = await crm.loadInteractionHistory(targetCompanies)
// 3. WebSearch collects public signals from sites, news, and job pages
const search = await qoni.webSearch.run({
token: grant.token,
prompt: `
Recent news, funding, hiring and tech signals for these companies:
${targetCompanies.join(', ')}.
Return signals as a JSON array of
{ company, signal, sourceUrl, capturedAt } objects.
Public pages only — skip anything behind a login wall.
`,
maxResultsPerQuery: 5,
})
const result = await search.wait()
// 4. App-side validation: signals without a source URL or capture time
// never enter scoring
const signals = parseSignals(result.output).filter((s) => s.sourceUrl && s.capturedAt)
// 5. App-side deterministic scoring against the versioned ICP,
// keeping the rationale per signal — the model does not assign scores
const scored = scoreLeads(signals, icp, history)
// 6. Write enrichment results to the CRM as draft records (your app calls
// the CRM API directly); official changes require seller confirmation,
// and manually maintained fields are never overwritten
await crm.createDraftRecords(sellerId, scored, { icpVersion: icp.version })
return {
scored,
icpVersion: icp.version,
audit: { auditId: grant.auditId, permissionBoundary: grant.grantedScopes },
}
}The output structure is a contract set by the task prompt: here it is an array of { company, signal, sourceUrl, capturedAt }, parsed and validated by parseSignals on the app side, with signals missing a source or capture time dropped; scoring is done by the app-side scoreLeads against the ICP version, not by the model. The SDK itself returns the generic RunResult (runId, status, output, artifacts, and so on).
Data and memory boundaries
This scenario touches four kinds of data; none of them belongs in GUMem:
- Versioned rules: ICP criteria, industry preferences, and scoring weights — managed by version in the CRM or a config store, with every scoring batch referencing the ICP version; a criteria change is a version bump.
- Business state: public signals, scoring results, outreach angles, and draft records — written back to the CRM as drafts; ICP profiles and interaction history stay canonical in the CRM, with no second source of truth.
- Audit records: the behavior chain of credential issuance and every search — maintained by GenAuth.
- User Memory: this scenario does not use GUMem. Profiles, interactions, and scoring criteria are CRM data that needs team sharing and version reconciliation, not one user's session preferences.
Failure handling
| Situation | Recommended handling |
|---|---|
| A target page shows a login wall or CAPTCHA | Skip the source and record it; escalate to the seller to decide on a manual look — never attempt a bypass. |
| Public signals are too thin to support a score | State the insufficient evidence and lower the confidence; never fabricate a scoring rationale. |
| A signal lacks a source URL or capture time | App-side validation drops the signal and the summary notes how many were dropped. |
| A CRM draft conflicts with a manually maintained field | The draft stays in draft state with the conflict flagged; the seller arbitrates, and the Agent never overwrites manual fields. |
Production notes
Do not let the Agent send external email automatically; the timing and wording of outreach remain the responsibility of the seller who confirms the draft. CRM writes stay limited to draft records, and official customer data changes must be confirmed by the seller. Cap the fetch frequency against target sites; for a long-lived watch on a target account's site or job pages, see the Track shape in the Vendor monitor agent.
Next steps
- Read the Quickstart to run the shortest path for Agent identity and delegation.
- Read Authorization and browser sandbox for the security boundaries of controlled sessions.
- Continue with the Recruiting sourcing agent for an adjacent scenario.