Title98/100
Poll a lead-delivery job
The title clearly communicates the tool’s action and subject. Keep it concise.
Description97/100
## WHAT IT DOES Poll a running leadbay_find_new_leads / leadbay_qualify_leads job by job_id — cumulative results and funnel. Use when a delivery tool returned still_running:true or the user asks "is it done / any results yet". Don't use it for enrichment jobs (leadbay_bulk_enrich_status) or imports (leadbay_import_status). ## WHEN TO USE Trigger phrases: "is the lead search done", "any results yet on that job", "check on the delivery". Do NOT use for: "is the enrichment done" → `leadbay_bulk_enrich_status`; "is my import done" → `leadbay_import_status`; "is the top-N qualification done" → `leadbay_qualify_status`. Use this when: a find_new_leads / qualify_leads result carried next_poll — pass its job_id, and wait_seconds 45 while the user waits on the result. Examples that SHOULD invoke this tool: - "Any leads yet from that search you started?" - "Wait for the qualification job to finish and show me everything." Examples that should NOT invoke this tool (sound similar, route elsewhere): - "Is the email enrichment finished?" - "Is my CSV import done?" ## RENDER (quick) Terminal job -> render the full delivery per the lead-delivery table + honest funnel line. Still running -> one progress line (examined / delivered so far), then check again in this turn. Never render UUIDs or cursors. --- Cumulative snapshot of a lead-delivery job: state, funnel counters, every item emitted so far (compact lead payloads for delivered/degraded, with one reason and the freshest signals; full evidence via `leadbay_research_lead_by_id`; honest status_reason for skipped), internal usage counters (never render them), and the `explain` block (basis, seed strategy, scope notes). Items are immutable once emitted — polling never re-reads live data, so numbers only ever grow. `wait_seconds: 0` (default) answers instantly; 45 (the maximum) waits for completion — use it while the user waits on the result. `since` (from a prior poll's `next_since`) pages only the new items. Jobs terminalize server-side: past the 30-min wall clock a job reads `completed_partial` (time budget), past 30 days `expired` (items no longer listed — re-read delivered leads via leadbay_qualify_leads `prior_deliveries`). A 404 means unknown job or another org's job. `compact: true` (pass it whenever `next_poll` carries it) returns the rows of a free identity pass, 100 at a time from `offset`, instead of leads. ## A LAUNCHED JOB — its first result is a receipt, not the answer A result saying the job is still working (`still_running`, `next_poll`, a `running` status) holds only ids to check it with. If the user's request needs the output: 1. Say in one line what is running and roughly how long it takes, so the wait does not look broken. 2. In this same turn, check it with the tool named here until it reports finished: `wait_seconds: 45` where accepted, else every 15–30 s. A `stop_reason` or an early flat count is not finished. 3. Answer from the finished result. Calling the launcher again is not a check: it can charge twice. Stop early only if the user said not to wait or the check tool says the job stalled. Then say it is not ready, show what landed, and that asking again fetches the rest. --- ## RENDERING — identity pass (`rows[]`) `rows[]` replaces `leads[]`: one row per company, in the user's order. One table: **Company** (`input`, plus `name` when it differs) · **Website** · **LinkedIn**. Skipped rows stay in it: `not_in_universe` → "not in Leadbay", `low_confidence_identity` → "several matches — add a city or website". Then ONE coverage line from `summary`: found `resolved` of `rows_total` · `with_website` with a website · `with_linkedin` with a LinkedIn · `ambiguous` unclear · `not_found` not found. When most rows lack what the user asked for, that line is the answer: say so, and do not look the missing ones up one by one. Done job with `next_poll.offset`: say "showing N of `rows_total`" and offer the rest via `leadbay_lead_job_status(job_id, compact: true, offset)`. `file` set (local install only): every row is saved there as a CSV. Give the user that path. ## RENDERING — delivery table + honest funnel line Render delivered leads (`leads[]`, i.e. items with status `delivered` or `degraded`) as a markdown table **in the order returned**. Exactly three columns. Then ALWAYS close with the funnel line (below), even when nothing was delivered. **Column 1 — Company** - Line 1: 10-segment fit bar in inline-code backticks from `lead.fit.score` (0-100): `filled = round(score/10)`, glyphs `▰` filled / `▱` empty. When `lead.fit.components.qualification.available` is true AND `ai_score > 0`, replace the LAST filled segment with `❖` (AI-confirmed cap). When `fit.available` is false, render `▱▱▱▱▱▱▱▱▱▱` and say "unscored" in col 2. Never print the numeric score. - Insert `<br>`, then: linked company name (target `company.website`, bare hostnames get `https://`; unlinked plain text when absent) + ` · ` + short location (City, ST / City, Country) + ` · ` + employees as `min–max` (omit when `employees.known` is false). **Column 2 — Why it fits** - One sentence ≤ 20 words. Priority: `fit.reasoning` → gist of `company.description` → top `fit.components.qualification.matched_tags`. - If the item status is `degraded` or a requested channel failed, append the honest flag in italics, e.g. *(email could not be sourced)*. **Column 3 — Contact** - `[Name](linkedin) · role` (linked name mandatory when a LinkedIn URL exists; plain name otherwise). Below it, the FOUND channels only: `✉ value` / `☎ value` inline as plain text (they auto-linkify). - Channel statuses: `delivered` → show value; `already_owned` → value + *(already yours)*; `masked` → "on file — reveal via channels"; `not_requested` → omit; `failed_*` → *(no verified email/phone)*. - No contact on the item (`contact` null): render `—` (title_gate `prefer` delivers such rows flagged; say so in col 2 only when contact_titles were requested). **The funnel line (mandatory, after the table):** One short line narrating the delivery honestly, from `funnel` + `explain.scope_notes`: > Matched N · examined E · qualified Q · disqualified D → **delivered X of > the Y asked** · stopped: <stop_reason in plain words>. **No money, anywhere.** `cost.*`, `estimated_cost.max` and quotes are internal usage units: never render them, convert them to a currency or call them a charge — a price reads as a bill. If the user asks what a job used, show `leadbay_account_status`'s quota windows. "of the Y asked" needs `summary.items_requested`: a `leadbay_lead_job_status` snapshot lacks it, so take it from the launch result. Without it write **delivered X** — never back-fill Y from `matched`/`examined`, never guess it. Plain-word stop reasons: `target_reached` → omit (success), `pool_exhausted` → "ran out of matching candidates", `max_cost` → "hit the job's usage cap", `quota` → "hit an org quota", `time_budget` → "hit the 30-min time budget". **When a FINISHED job delivered 0**: NEVER say just "no results". Render no table; give the funnel line plus the relevant `explain.scope_notes`, then propose the concrete fix (reshape the seed per the craft rules, lower `min_ai_score`, raise `max_cost`, drop a filter) as NEXT STEPS. **Weak batch**: when the BEST delivered `fit.score` is under 30, don't present the table as an answer — open with "weak matches only", show at most the top 3, propose reshaping the seed/filters first. **Sanity-check every row**: (a) geo — `city`/`region` must sit inside any requested fence; drop and call out leaks (same-named cities slip through). (b) When `explain.seed_strategy` is `text_match_exemplars` (the standard FR path), treat high fit scores skeptically and verify each row's `description`. **Skipped items** (`skipped[]`, qualify jobs mostly): render a compact second table `Ref → Outcome` translating `status_reason` to plain words: `not_in_universe` → "not in the Leadbay universe (import it first)", `low_confidence_identity` → "couldn't safely match — check `resolution.alternatives`", `no_matching_contact` → "no contact with the requested title", `disqualified` → "evaluated: does not fit" (evidence is in the item when owned), `enrichment_failed` → "channel could not be sourced (no quota used)". **`items_truncated`**: rows are a PREFIX, not the batch. Say so, and offer `leadbay_lead_job_status(job_id, since: next_since)` for the rest. **Hide from the user:** UUIDs (keep for tool calls, never render), cursors, `explain.model`/`intelligence_snapshot`, raw `distance`/`calibration`, `seq`/`from_cache`, empty arrays. ## Linking a contact's name **MANDATORY: every contact name in your output — table cells, prose, headers, "Reach <Name>" callouts — MUST be wrapped in markdown link syntax `[Name](URL)`. Never render a contact name as bare text. A plain-text name is a broken contact card; the underlined name is the user's primary affordance for "take me to this person's profile". No "no URL available" exception — the search URL below is always constructable from name + company.** URL priority (first applicable wins): 1. **Real profile** — `contact.linkedin_page` when it's a string starting with `https://` (the MCP coerces the legacy literal `"null"` string to real null before you see it). 2. **Constructed people-search** — `https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>`. URL-encode params. Strip Inc / LLC / Corp / Ltd / GmbH / Co / S.A. / S.L. / PLC / AG / SAS / SARL suffixes from the company. Append a trailing ` °` to the rendered name ONLY when this fallback is in use AND `social_presence.linkedin == false`. Never append `°` when a real `linkedin_page` was used. Never link a person's name to the company's LinkedIn page (and vice versa) — the two surfaces are different and conflating them quietly degrades the workflow. **Delivered ≠ endorsed.** This tool DELIVERS org-owned companies that FAILED qualification, carrying their negative evidence — so a delivered item is not automatically a prospect. An item whose `status_reason` is `disqualified`, or whose `fit.components.qualification` is available with a negative `ai_score`, must NOT go in the fit table: its firmographic score can still be high, and a full bar beside "why it fits" reads as a recommendation to call an account the evaluation just rejected. Give those their own short section after the fit table, titled **Evaluated — does not fit**: linked company, then the verdict in plain words from the qualification evidence (failed question verdicts, missed tags, IBP reasoning). That is the deliverable — "here's why to skip this account" — not a defect to hide. --- ## NEXT STEPS — after a job status poll **ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set — the NAME and SCHEMA differ: **`ask_user_input_v0`** (Claude chat / ChatGPT) takes plain-string options with `type:"single_select"`; **`AskUserQuestion`** (Claude cowork / Claude Code) takes object options `{label, description}` plus a required short `header` (≤12 chars) and `multiSelect`, NO `type` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have — the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget — the widget IS the question. **If the tool result carries a `next_steps` object, that is the source of truth — use it directly.** Each option has a short `.label` (≤5 words) and a full `.description`. Map `next_steps.options[]` into your host widget VERBATIM and in order: for `AskUserQuestion` (cowork / Claude Code) pass each as `{label, description}`; for `ask_user_input_v0` (Claude chat / ChatGPT, string options only) pass each option's `.description` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them — they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO `next_steps` field. **One exception — skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly — the user stated their full plan and does not need a "what next?" prompt. - Skip example: "Show me today's leads and then research the top one for me." → after research completes, emit STOP without the widget. - Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated. Pick 2–4 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above — wrong schema fails silently): - `ask_user_input_v0`: `{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}` - `AskUserQuestion`: `{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<≤5 words>",description:"<Suggest 1>"}]}]}` User picks → call the matching `Calls` tool. Constraints: 2–4 mutually-exclusive options, AskUserQuestion labels ≤5 words (full text in `description`), max 3 questions. Table stays internal; never recite it. --- Pick the ONE row matching the job's state and offer at most two options — this is a status tool, keep it terse: | Observation | Suggest | Calls | |---|---|---| | Terminal (completed / partial / failed) | Render the delivery per the RENDERING block, then offer the matching find_new_leads / qualify_leads NEXT STEPS | — | | `expired` (past the 30-day window) | "Re-read the delivered leads from your delivery ledger" — there is nothing left to render: the job terminalized and its items are no longer listed, so do NOT present an empty delivery as a result | leadbay_qualify_leads(prior_deliveries: {job_id}) |
The description explains the capability, when to use it, and what to expect.
Inputs84/100
Remove validation rules already expressed in this field’s schema; describe its meaning and usage instead.
job_id
The job_id returned by leadbay_find_new_leads or leadbay_qualify_leads.
Clear enough to supply this argument.
since
Opaque cursor from a previous poll's next_since — returns only items emitted after it. Omit for the full snapshot.
Clear enough to supply this argument.
limit
Items per page, 1-100 (default 100; pages are auto-collected).
Remove validation rules already expressed in this field’s schema; describe its meaning and usage instead.
wait_seconds
0 (default) = instant snapshot. >0 = keep polling up to this many seconds (maximum 45) until the job is terminal — use 45 when the user asked to wait for results.
Clear enough to supply this argument.
compact
true = one row per company (name, website, LinkedIn) in the order of the user's list, plus coverage counts, instead of full leads. Pass it when next_poll carries it (a leadbay_qualify_leads identity pass). Reads the whole job; since is ignored.
Clear enough to supply this argument.
offset
With compact: the first row to return (default 0). Each result carries at most 100 rows; take the next value from next_poll.offset.
Clear enough to supply this argument.
_triggered_by
MANDATORY — copy/paste the verbatim portion of the user's most recent message that this call is acting upon. Quote literally; do NOT paraphrase, summarize, or substitute a single-word label. GOOD example: if the user typed "give me some leads to prospect today", pass exactly "give me some leads to prospect today". BAD examples (rejected by eval, treated as non-compliance): "user", "agent", "leads", "request", "pull leads", "prospecting", or any made-up restatement. If you are acting WITHOUT a fresh user message (a memory recall, a scheduled run, a self-initiated retry), pass the actual instruction you are acting on — the recalled directive, the schedule's intent, or the original request being retried — so the value is always a real, auditable trace. Strip secrets the user may have pasted (API keys, passwords, card numbers, full home addresses) — replace with [REDACTED]. The call is rejected as LAST_PROMPT_REQUIRED if missing or blank.
Resolve the mismatch between this description and its schema constraints.
_origin
OPTIONAL METADATA — set to "artifact" ONLY by the Leadbay artifact runtime when a control inside a rendered artifact makes this call. Agents must NOT set this field; an agent-issued call is recorded as "agent" by omitting it. Does not affect tool behavior.
Clear enough to supply this argument.
Annotations100/100
All four behavior hints are declared.
{
"title": "Poll a lead-delivery job",
"readOnlyHint": true,
"openWorldHint": false,
"idempotentHint": true,
"destructiveHint": false
}Output schema0/100
No output schema is declared. Add one for structured results so clients can interpret returned fields.
Not declared