leadbay_daily_check_in — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited leadbay_daily_check_in (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You keep your own memory of how this user likes to work — tone, naming, formatting, what they ask you to skip. Leadbay does not store that and does not need to.
What Leadbay does need is anything that changes who it should find. When the user states targeting criteria in conversation ("I target fleets over 100 vehicles", "carriers are a bad fit unless they do last-mile delivery", "climate engineering is also my market"), call leadbay_refine_lead_targeting so it changes what Leadbay surfaces for the whole org and on every future refresh — not just this conversation. When they say a specific lead is wrong for them, record the dislike with their words as its reason rather than noting it.
Run the Leadbay daily check-in for me. Treat this prompt the same way for any equivalent ask focused on NEW leads from the Discover wishlist: "get me leadbay leads", "best NEW leads to prospect today", "what's new today", "show me my batch", "let's prospect", "run my morning check-in", "my daily routine", "I do this every day", "every morning". For follow-up phrasings ("what should I follow up on", "leads I've already worked", "before my trip"), this is the wrong prompt — route to leadbay_followup_check_in instead. Recurrence language ("I do this every day", "every morning", "my routine") always means this prompt — it is a daily batch check-in, not a follow-up. If the user's intent is ambiguous ("what should I work on?"), ASK once before picking an entry point.
These rules apply to every Leadbay workflow that calls leadbay_pull_leads, leadbay_bulk_qualify_leads, leadbay_research_lead_by_id, leadbay_import_and_qualify, or leadbay_enrich_titles. Treat timeouts and stream-closed errors as transient, not as signals to replan.
After your first leadbay_pull_leads call, capture response.lens.id into your working memory and pass it explicitly as the `lensId` argument to every subsequent call in this session — including any re-pulls, bulk qualifies, or research calls that accept it. (Field-name caveat: the response nests it as lens.id; the parameter on subsequent calls is lensId.) The active lens can shift between calls (5-minute client cache + backend last_requested_lens can change if the user touches the web UI). A lens shift mid-workflow throws away your top-10 work.
leadbay_bulk_qualify_leads and leadbay_import_and_qualify accept wait_for_completion:false and return immediately. They hand back different ids: bulk_qualify_leads returns {status:'running', notification_id, lead_ids, lens_id} — poll leadbay_qualify_status with those. import_and_qualify returns {status:'running', import_ids} and no notification_id at all — poll leadbay_import_status({importIds, dry_run}) with those. Check it until it completes, per A LAUNCHED JOB below. Use the async pattern by default — the blocking default can exceed the MCP client's per-call timeout on large batches and produce a misleading "Request timed out" even though the server is still working.
leadbay_research_lead_by_id fan-outleadbay_research_lead_by_id is composite and reads many sub-resources. Calling it on 10 leads in parallel can saturate the transport and produce "Tool permission stream closed" errors that look like permission failures but are really backpressure. Call it sequentially, or at most 3 in parallel. If one call fails with a stream/timeout error, retry that one call once before moving on; on a second failure, note the lead and continue — do not abandon the remaining leads.
If a Leadbay tool returns "Request timed out", "stream closed", or any other transport-level error (distinct from a Leadbay-issued error payload), the work may still be running server-side. Do this in order:
wait_for_completion:false and poll the status tool with the returned id. Don't re-pull leads; that can shift the lens.If pull_leads itself fails and you have no prior batch, then yes — retry it, explicitly pass the lensId you captured (if any), and continue.
Leadbay has no cancel. Once leadbay_enrich_titles, leadbay_bulk_qualify_leads, leadbay_import_leads or leadbay_import_and_qualify has returned a launched or running result, that work is queued on Leadbay and runs to completion, and the quota it uses is already committed. A discovery, preview or dry_run result launched nothing and is not covered here.
The user cancelling in the chat, a request timeout, or a closed stream stops YOUR waiting, never the job. cancelled: true means we stopped watching, not that the work stopped. What to do next depends on what you are holding:
handle covers a second time — that uses the quota again on the same rows. leadbay_import_status takes importIds, so pass the values of import_ids under that name. A qualification started by leadbay_import_and_qualify has no notification of its own: resume it with leadbay_qualify_status({lead_ids, lens_id}).
failed[] entrieswith error:"not_queued", or a rows_pending_upload count. Poll the handle for what was launched and re-run for that subset only, never for the whole batch.
returned. Check leadbay_account_status first: the launch may have landed and finished. Calling the same tool again with the same arguments will usually hand back the job already launched rather than starting a second one, but that guard is in-memory, five minutes, and per process, so it is best-effort — say what you are about to re-run before you use the user's quota on it.
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:
does not look broken.
finished: wait_seconds: 45 where accepted, else every 15–30 s. A stop_reason or an early flat count is not finished.
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.
If you're resuming an interrupted session (you see a previous Phase already completed in your task list, or the user says "continue" / "continue from where you left off"), do NOT restart from Phase 1. Re-read the active lensId and your last completed phase from prior context, then resume from the next phase. If you genuinely have no state, restart from Phase 1.
Call leadbay_account_status to see what quota I have left and which lens is active. Note the remaining ai_rescore_remaining and web_fetch_remaining budgets — Phase 4 enrichment depends on them. This is for your planning: don't show me the quota unless a window is exhausted.
Call leadbay_pull_leads to get today's fresh batch. Capture response.lens.id (the response nests it under lens). Use it as an explicit `lensId` argument on every subsequent Leadbay call this session — including any re-pulls, bulk qualifies, or research calls that accept it. (See Rule 1 above — a mid-session lens shift discards your top-10 work.)
Render the top 10 leads using the canonical leadbay_pull_leads layout:
GATE — DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim — score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.
If the prompt's body and the tool's RENDERING appear to conflict, the tool's RENDERING wins for the structural layout; the prompt's voice wins for the commentary that surrounds it.
Present the response as a markdown table in the exact order the tool returned the leads — this is the Discover-tab order (the backend orders by new-today first, then status, then score). Do not re-sort the rows (in particular, do NOT re-order by score); render them top-to-bottom as received so the list matches what the user sees in the Leadbay UI. Exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
Wrap a 10-glyph bar in a SINGLE inline-code span (backticks). The inline-code styling is what gives the bar contrast in most chat renderers — HTML <span> is stripped inside table cells.
Glyphs (use these exact characters; do not substitute):
▰ — firmographic-only fill❖ — AI-booster cap (placed at the RIGHT END of the filled run, never the front)▱ — emptyComputation:
total_filled = round(score / 10), clamped to 0..10
ai_segments = round(qualification_summary.avg_qualification_boost / 3.3),
clamped to [0, total_filled]
normal_filled = total_filled − ai_segments
bar = "▰" × normal_filled
+ "❖" × ai_segments
+ "▱" × (10 − total_filled)If qualification_summary.answered == 0 or avg_qualification_boost is null, set ai_segments = 0 (no ❖). Always wrap the bar in backticks. Print the legend ` ▰ firmographic · ❖ AI booster cap · ▱ unfilled ` once below the table.
Column 1 — Company
<br> between lines. · + short location + · + compact size.website (prefix https:// if it's a bare hostname). Don't synthesize an app deep-link."Xk+" when size.min >= 1000, "min–max" otherwise.Column 2 — Why it fits
short_description, top 2 tags[].display_name, and the gist of qualification_summary.best_response_excerpt. The trim payload does NOT carry the longer description field — for that, agent must call leadbay_research_lead_by_id or leadbay_research_lead_by_name_fuzzy.(boost N) — the ❖ cap in column 1 already carries that signal.Column 3 — Contact
[Contact name](LINK) · short job title. The [Contact name](LINK) markdown link wrapping is mandatory — never render the name as plain text. See linking/contact-linkedin for the URL priority (real profile → constructed people-search) and the °-flag fallback.
Hide from the user (never include in any cell): id, location.pos, location.country (unless city/state both missing), sector_id, is_hq, web_fetch_in_progress, enrichment_in_progress, highlighted_fields, custom_fields, contacts_count when 0, notes_count / epilogue_actions_count / prospecting_actions_count when 0, stale_at, deal_insights, social_presence booleans (except as the °-flag signal), need_attention flags, any field whose value is the string "null".
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):
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).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.
Use the lead's website as the company-name link target — prefix https:// if the value is a bare hostname. (The MCP does NOT synthesize a Leadbay-app deep-link URL; the team has not standardized one. Linking to website is always real data.)
When the response carries social_urls (the post-fix multi-platform URL block on rich-lead responses), render every non-null platform as a pill chip in the company-info row. Iterate over social_urls's keys — never hardcode a fixed list — and emit each as [<platform-label>](<url>). Skip platforms whose URL is null.
social_presence carries booleans for the same 6 platforms (crunchbase, facebook, instagram, linkedin, tiktok, twitter) — useful when you only care that the company has a profile somewhere. Use it as the °-flag signal in the contact people-search fallback (see linking/contact-linkedin).
ABOVE the table, add a 2–4 sentence "Today's nudges" paragraph for the 3 most-promising rows. The nudges speak to urgency / opportunity / freshness — what makes acting on these RIGHT NOW the right call. Do NOT repeat the "why it fits" column from the table; the nudges should add fresh framing the table doesn't carry (e.g., recent news from the qualification_summary excerpt, a window closing, a competitor activity the user mentioned earlier in the session). One sentence per nudge, salesperson voice, not coachspeak.
If the batch returns fewer than 10 qualified leads, top it up: call leadbay_bulk_qualify_leads with lensId:<captured>, count:<1.5x deficit, capped at 25>, and `wait_for_completion:false`. Capture notification_id from the response and poll leadbay_qualify_status every ~10s until status:'done'. Then re-pull with the same lensId to pick up the newly qualified leads. Never re-pull without `lensId` — you will lose your batch to a lens shift. (The leadbay_qualify_top_n slash-prompt wraps this same tool with a friendlier surface for users; agents should call the underlying tool directly here.)
Skip Phase 4 if the user's request is primarily to view the batch (e.g., "show me today's leads", "run my morning check-in", "what's in my inbox") — proceed directly to NEXT STEPS. Run Phase 4 when the user explicitly asks to research leads, names a specific company, or says "and then research" / "deep dive" / "tell me more about".
Call leadbay_research_lead_by_id on every lead from your top 10 that the user might realistically prospect today (filter out clearly weak fits if any). Don't pick just one. Call it sequentially — one at a time, or batches of at most 3 in parallel. Do not fire 10 in parallel — it triggers transport backpressure that surfaces as "Tool permission stream closed" errors (see Rule 3 above). If a call fails, retry that single lead once; if the retry also fails, note the lead id and continue. Report Phase 4 results even if 1–2 leads were unresearchable.
For each researched lead surface:
Contact enrichment is offered in the NEXT STEPS widget below — do NOT emit a separate prose question here. The widget handles the enrichment offer as one of the selectable options. If the user selects enrichment, call leadbay_enrich_contacts({leadId, contactId}) ONCE PER CONTACT — the tool takes a single leadId + contactId, never a list. (For bulk title/seniority enrichment across many leads at once, use leadbay_enrich_titles({leadIds: [...]}) instead.) This consumes enrichment quota.
Sequential request gate: If the user's original message contained the literal phrase "and then" (e.g., "show me X and then do Y"), and all stated actions have been completed this turn, skip the NEXT STEPS widget entirely and emit STOP directly. The user stated their full plan; they do not need a "what next?" prompt.
REQUIRED OPTIONS — triggers and position rules:
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.
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 2–3 items below based on what was actually observed in the response. The table is the source of truth for which moves are valid.
| Observation | Suggest | Calls |
|---|---|---|
| ≥ 1 lead returned — offer FIRST | "Build an interactive lead triage board" | leadbay_get_artifact_runtime → its CANONICAL triage-board recipe, data in hand (do NOT re-call pull_leads) |
| ≥ 1 lead returned (any batch) | "Enrich top leads" (reveal decision-maker email/phone on the top leads) | leadbay_enrich_titles({ leadIds: shown leads[].id, lensId }) — scope to the leads JUST shown; OMIT titles so it runs the no-spend discovery preview. Confirm titles + channels, then re-call with titles + confirm to launch |
has_more == true | "Pull the next page (page N+1 of M)" | leadbay_pull_leads(page = current + 1, lensId = pinned) |
≥ 3 rows have qualification_summary.answered == 0 | "Deepen AI qualification on the rows without ❖ caps" | leadbay_bulk_qualify_leads(leadIds=[…]) |
| User points at a single row | "Research [Company] in depth" | leadbay_research_lead_by_id(leadId) |
| User only has a name (no leadId in context) | "Look up [Company] by name" | leadbay_research_lead_by_name_fuzzy(companyName) |
| Top row has phone AND email | "Prepare an outreach for [Contact] — call + email" | leadbay_prepare_outreach(leadId) |
| Top row has email but no phone | "Draft an outreach email for [Contact]" | leadbay_prepare_outreach(leadId) |
| Top row has phone but no email | "Show [Contact]'s call details + a 60-second opener" | leadbay_prepare_outreach(leadId) |
| Top row has contacts but no phone/email | "Order contact enrichment to surface email/phone first" | leadbay_enrich_titles(...) or leadbay_prepare_outreach(leadId, enrich:true) |
computing_scores == true or computing_wishlist == true | "Scores are still being computed — re-pull in ~30s" | leadbay_pull_leads (retry with same lensId) |
Batch is EMPTY and computing_wishlist/computing_scores == true (e.g. a just-created lens) | render the next_steps widget — it carries "Re-pull in ~30s" (first) + "Refine audience". Do NOT report "no leads": the lens is warming up, not empty | leadbay_pull_leads (retry with same lensId after ~30s) |
| User wants a narrower / wider audience | "Adjust the lens filters (sector / size)" | leadbay_adjust_audience(...) |
Phase 4 research was run (research_lead_by_id called) AND top contacts lack direct email/phone | "Enrich contacts on [Lead1], [Lead2] to get direct emails and phone numbers" | leadbay_enrich_contacts(leadId, contactId) — ONE call per contact (the tool takes a single leadId + contactId, never a list) |
If nothing in the menu applies cleanly, suggest only "pull next page" and "research a specific lead in depth" — never invent a tool that doesn't exist.
Final ordering check (do this before rendering): Recurring offer → option 1; triage board → option 1 (or 2 if scheduling is also required). A poor-fit / mismatched batch does NOT change this — triage board stays first, refine-audience goes later in the list. Swap if needed.
IRON LAW — DO NOT TAKE OUTBOUND ACTION. Do not call leadbay_report_outreach. Do not draft an outreach message into a tool argument. Outreach is the user's call after they've reviewed your research.
Render this acknowledgment VERBATIM as the last line of your message:
STOP — awaiting user decision. I will not take any further action until you tell me what to do next.Do not propose a next action. Do not call any more tools. Hand control back to the user.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.