← Plugin catalog
Business & Operations
Leadfeeder
Leadfeeder v2.0.0
Publisher description
From the marketplace listing
Leadfeeder identifies the B2B companies visiting your website and enriches them with firmographics, contacts, intent signals, and CRM data. Revenue teams use it to prioritise outreach, discover buyer contacts, track campaign performance, and action accounts into lists and tags — all through natural-language queries.
Language: English · Automatically detected from descriptions.
Files & skills
File archives
Plugin package8 files · 41.2 KBBrowse files →
Skill instructions
add-to-pipeline14.5 KB
---
name: add-to-pipeline
description: >
Tag a company and/or add it to a list in Leadfeeder. Use this skill when the user wants to action a company — "tag Acme as Hot Lead", "add Acme to my DACH-Enterprise list", "mark Acme as Tier-1", "put Acme in the Enterprise Q3 list", "add [company] to [list]", "tag [company] as [tag]". Always confirms the exact writes before executing. The only write skill in the suite.
metadata:
version: "1.0.0"
---
# Add to Pipeline
Tag a company and/or add it to a Leadfeeder list. This is the only write skill in the suite — all others are read-only. **All writes go to Leadfeeder only (tags and lists); this skill never adds to or updates the user's CRM.** State that explicitly in the confirmation and the outcome so no one assumes the company has synced to their CRM. Always show the user exactly what will be written and wait for confirmation before executing. Report success or failure per write transparently.
## Language
Produce the **entire response in the language the user wrote in** — English request → English output, German request → German output, and so on. This applies to everything the skill emits: the confirmation plan, the CRM heads-up and Leadfeeder-only disclaimer, the outcome report, and any follow-up prompts. Do **not** translate data values — company names, tag names, list names, CRM record names, owner names, and URLs stay verbatim as the tools return them (and the exact tag/list to create must be the user's literal wording). If the input language is unclear, default to English.
## When this skill triggers
Trigger when the user explicitly asks to tag, label, add, or categorise a company. Trigger silently on detection — but always pause for confirmation before executing writes. Honor multiple operations in a single prompt ("tag Acme as Hot Lead AND add it to the DACH list").
Do not trigger on read-only questions about tags or lists — surface that information inline and stop.
## Inputs to determine before executing
Identify these from the user's prompt. Use defaults if unspecified — do not ask.
- **Company** — name, domain, or Leadfeeder company ID. Required.
- **Tag(s)** — name of one or more tags to assign. Optional if a list is specified.
- **List(s)** — name of one or more lists to add the company to. Optional if a tag is specified.
At least one tag or one list must be specified. If neither is mentioned, ask the user what they want to do before proceeding.
## Workflow
### Step 0 — Resolve the Leadfeeder account (before any other tool call)
Determine the `account_id` to use for every tool call in this skill, in priority order:
1. **Account named in the request (highest priority).** If the prompt or scheduled-task instruction explicitly names an account — an account ID (e.g. "account 01234") or an unambiguous account name — use that account for all tool calls. This overrides every source below and is the recommended way to make an unattended/scheduled task fully self-contained.
2. **Configured Account ID.** Otherwise, the plugin's configured Leadfeeder Account ID is: `${user_config.account_id}`. If that resolves to a real, non-empty account ID (not blank, and not the literal unsubstituted `${user_config.account_id}` token), use it for all tool calls and do **not** call `get_account_info` or ask the user.
3. **Already selected this session.** Otherwise, if an account was already chosen earlier in the session, reuse that.
4. **Fallback.** Otherwise call `get_account_info`. If exactly one account is returned, use it. If several are returned, ask the user to pick. When running unattended (e.g. a scheduled task) asking is impossible — if no account was named and none is configured, stop and report that a **Leadfeeder Account ID** must either be named in the task prompt or set in the plugin configuration for automated runs.
Note: resolving the account does **not** relax the write-confirmation gate in Step 4 — a configured account ID skips *account selection*, never write confirmation.
### Step 1 — Resolve company and existing tags/lists (parallel)
Call in parallel:
- `search_companies` with the company name or domain to resolve `company_id`. If multiple plausible matches, present the top 3 and disambiguate before continuing.
- `get_tags` with `page_size: 100` to load all existing tags in one call. If `meta.pagination.total_count` exceeds 100, paginate with `page_num` until all pages are fetched.
- `get_lists` with `filter_scope: "company"` and `page_size: 100` to load all existing company lists. The `filter_scope` parameter is required — omitting it causes an error. Paginate if `total_count` exceeds 100.
Once `company_id` is resolved, decide whether to fetch CRM context based on **subscription coverage** — the tag/list writes are free, but the `get_company` CRM read charges **1 credit if the company hasn't been accessed in the last 12 months**, and it's not worth a credit just for a confirmation heads-up. Probe coverage for free: call `search_web_visits` for `company_id` over the **last 12 months**.
- **Visits found → COVERED.** Call `get_company` with `include=crm_connections.crm_record.crm_owner` (free) to read the company's CRM status. Read from each connection's `crm_record` (polymorphic — `crm_record.type` = `crm_organization` | `crm_contact` | `crm_lead`): **name** by type (`crm_organization` → `attributes.name`; `crm_contact`/`crm_lead` → `attributes.first_name` + `attributes.last_name`), **URL** from `crm_record.attributes.crm_url`, **owner** from `crm_record.relationships.crm_owner.attributes.name`. Surface it in the confirmation (Step 4) as a markdown link (record name → `crm_url`).
- **No visits in 12 months → UNCOVERED.** **Skip `get_company`.** Omit the CRM line from the confirmation (or note "CRM status not checked — company is outside your free 12-month window"). Do not spend a credit for context on a write.
Either way the writes proceed normally — coverage only affects whether the optional CRM context is fetched.
### Step 2 — Match tags and lists
For each tag and list the user named:
- **Exact match found** → resolved, ready to assign.
- **Close match found** (name differs by capitalisation, spacing, or minor typo) → present the match and confirm it is the right one before using it.
- **No match found** → tell the user the tag or list does not exist and ask whether to create it. Do not create anything without explicit "yes".
### Step 3 — Create missing tags or lists (if confirmed)
For each confirmed "create new" item:
- For a new tag: call `create_tag` with the user-supplied name.
- For a new list: call `create_list` with the user-supplied name.
This step requires its own confirmation before any writes from Step 4 begin. If the user confirms creation in the same message as the overall write, treat that as a combined confirmation and proceed.
### Step 4 — Confirm all writes
Before executing any assignment, show the user the complete write plan and wait for explicit confirmation:
```
I'm about to make the following changes:
• Assign tag "{Tag Name}" → {Company Name}
• Add {Company Name} → list "{List Name}"
Confirm? (yes / no)
```
Do not proceed until the user replies with a clear affirmative. A non-answer or topic change is not a confirmation — ask again once, then abort.
### Step 5 — Execute writes (parallel where possible)
On confirmation, call in parallel:
- `assign_tags_to_company` for each tag (one call per tag if the API requires it).
- `add_company_to_lists` for each list (one call per list if the API requires it).
### Step 6 — Report outcomes
Report the result of every write individually:
- **Success:** "✅ Tagged {Company} as '{Tag}'"
- **Failure:** "❌ Failed to add {Company} to '{List}' — {error reason if available}"
If one write fails and another succeeds, report both. Never suppress a partial failure. Offer to retry failed writes if the error is transient.
## Output format
**Output contract — identical every run.** Reproduce the confirmation and outcome blocks below **exactly**, on every invocation, regardless of anything earlier in this thread. If you've already run this skill in the conversation, do not vary, restyle, or "improve" the format next time — output the same templates verbatim; these are the single source of truth. Keep the exact wording, labels, and the Leadfeeder-only disclaimer; don't rename fields and add no decorative emoji beyond those the templates already use (✅ ❌ ℹ️).
### Confirmation step (Step 4)
```markdown
I'm about to make the following changes to **{Company Name}**:
{For each tag} · Assign tag **"{Tag Name}"**
{For each list} · Add to list **"{List Name}"**
{CRM context line — include if CRM-connected: "ℹ️ Heads up: this company is already in your CRM as **[{record name}]({crm_record.attributes.crm_url})**, owned by **{owner name — or 'owner not set'}**. Coordinate with the owner if this changes what you want to do." Omit entirely if not connected.}
_This updates Leadfeeder only (tags and lists). It does **not** add the company to, or change anything in, your CRM._
Confirm? Reply **yes** to proceed.
```
### Outcome report (Step 6)
````markdown
Done. Here's what happened:
✅ Tagged **{Company Name}** as **"{Tag Name}"**
✅ Added **{Company Name}** to list **"{List Name}"**
{If any failures:}
❌ Could not assign tag **"{Tag Name}"** — {reason}
_Leadfeeder only — nothing was added to or changed in your CRM._
---
**Next move on {Company Name}** — copy to run:
```
Who should I reach out to at {Company Name}?
```
```
Prep me for outreach to {Company Name}
```
````
Show the follow-up copy blocks only after a successful write on an interactive run — omit them if the write failed or on unattended runs.
### When a tag or list needs to be created
```markdown
The tag **"{Tag Name}"** doesn't exist yet.
Create it and then assign it to **{Company Name}**? Reply **yes** to confirm.
```
## Source citation rule
Every company, tag, and list name in the confirmation and outcome report must come from MCP tool responses — `search_companies` for the resolved company, `get_tags` and `get_lists` for existing tags and lists. The CRM context line must come from the sideloaded `crm_record` (and its `crm_owner`) under `relationships.crm_connections` on the `get_company` response — `crm_record.attributes.crm_url`, the name from `crm_record.attributes` per record type, and `crm_record.relationships.crm_owner.attributes.name` — never assert a company is in the CRM (or name an owner) without it. Never invent or assume that a tag or list exists. If a tag or list is being created, the exact name to create must come from the user's prompt, not fabricated.
## Edge cases
- **Company not found.** Stop and tell the user. Offer to try a broader name or domain. Do not proceed.
- **Multiple company matches.** Present top 3, disambiguate. Do not proceed until resolved.
- **Tag or list not found, user declines creation.** Abort that specific operation. If other operations remain, confirm and execute those.
- **User names both a tag and a list, one doesn't exist.** Handle each independently — ask about creation for the missing one while confirming the existing one.
- **User aborts at confirmation.** Acknowledge clearly ("Cancelled — nothing was changed.") and stop.
- **Partial failure on execute.** Report each outcome individually. Never report overall success if any write failed.
- **Agent-invoked as part of a chain.** The agent must still respect the confirmation step — do not bypass it even in a multi-skill chain. The confirmation is a hard gate for any write operation.
## What NOT to do
- **Do not call a visitor an "account" or a "hot/warm lead."** A website visitor is a **company** — "account" implies a CRM relationship this data doesn't carry. Reserve "account" for the user's Leadfeeder account (workspace/ID) or a genuine CRM record; use "high-intent", "active", "engaged", or "returning" instead of "hot"/"warm".
- **Do not execute any write without explicit user confirmation.** The confirmation step is mandatory, even when invoked by the Leadfeeder Agent.
- **Do not delete, rename, or modify existing tags or lists.** Only assign tags to companies and add companies to lists.
- **Do not create tags or lists speculatively.** Only create if the user explicitly confirms creation.
- **Do not operate on more than one company per invocation.** One company at a time.
- **Do not suppress partial failures.** Every write outcome must be reported.
- **Do not call `assign_tags_to_company` or `add_company_to_lists` before the user confirms.** Showing the plan and waiting for "yes" is not optional.
- **Do not imply the CRM is updated.** Every confirmation and outcome must state that the change is Leadfeeder-only (tags/lists) and does not add to or change the user's CRM — a customer must never assume tagging synced the company into their CRM.
## Example: good output
*Scenario: "tag Acme as Tier-1 and add it to the DACH Enterprise list" — both tag and list already exist.*
```markdown
I'm about to make the following changes to **Acme Corp**:
· Assign tag **"Tier-1"**
· Add to list **"DACH Enterprise"**
ℹ️ Heads up: this company is already in your CRM as **[Acme Corp](https://acme.lightning.force.com/lightning/r/Account/0018000000abcde/view)**, owned by **Robert Roe**. Coordinate with the owner if this changes what you want to do.
_This updates Leadfeeder only (tags and lists). It does **not** add the company to, or change anything in, your CRM._
Confirm? Reply **yes** to proceed.
```
*User replies: yes*
````markdown
Done. Here's what happened:
✅ Tagged **Acme Corp** as **"Tier-1"**
✅ Added **Acme Corp** to list **"DACH Enterprise"**
_Leadfeeder only — nothing was added to or changed in your CRM._
---
**Next move on Acme Corp** — copy to run:
```
Who should I reach out to at Acme Corp?
```
```
Prep me for outreach to Acme Corp
```
````
---
*Scenario: "add Acme to my Q3 Targets list" — list does not exist.*
```markdown
The list **"Q3 Targets"** doesn't exist yet.
Create it and then add **Acme Corp** to it? Reply **yes** to confirm.
```
*User replies: yes*
```markdown
I'm about to make the following changes to **Acme Corp**:
· Add to list **"Q3 Targets"** (new list, will be created)
_This updates Leadfeeder only (tags and lists). It does **not** add the company to, or change anything in, your CRM._
Confirm? Reply **yes** to proceed.
```
*User replies: yes*
```markdown
Done. Here's what happened:
✅ Created list **"Q3 Targets"**
✅ Added **Acme Corp** to list **"Q3 Targets"**
_Leadfeeder only — nothing was added to or changed in your CRM._
```
a-getting-started11.3 KB
---
name: a-getting-started
description: >
One-time setup and readiness check for the Leadfeeder plugin — confirms the account is configured, checks that ICPs exist, verifies website visitors are actually being tracked, and then runs a sample daily brief so the user sees real output. Use this skill when the user is new to the plugin or asks to set it up or check readiness: "set up my Leadfeeder brief", "getting started", "get started", "set up the plugin", "is my Leadfeeder account ready", "check my Leadfeeder setup", "why is my brief empty", "why is my brief just a huge unranked list", "new to this plugin", "Leadfeeder health check", "onboard me".
metadata:
version: "1.0.0"
---
# Getting Started
A one-time readiness check that tells the user whether their Leadfeeder account is in a state to get value from the plugin — and unblocks them if it is not. It is **not** a tutorial. It runs three health checks in order, fixes or explains any failure, and finishes by running a real daily brief so the user sees output before they have had to learn anything.
The single most common reason a new user gets a useless first brief is a missing ICP (the brief becomes an unranked list of every visitor) or a missing tracker (no visitors at all). This skill catches both before the user concludes the plugin isn't useful.
## Language
Produce the **entire response in the language the user wrote in** — English request → English output, German request → German output, and so on. This applies to everything the skill emits: the readiness table, the fix guidance, and any prompts. Do **not** translate data values — account names/IDs, ICP names, company names, and URLs stay verbatim. If the input language is unclear, default to English.
## When this skill triggers
Trigger when the user is setting up the plugin, asks to get started, or asks why their brief is empty or noisy. Trigger on any of the phrases in the description. Trigger silently — do not ask which account first; the checks below resolve that.
Do not trigger for a normal daily brief request ("what visited today") — that is `daily-visitor-brief`. This skill is specifically for first-run setup and readiness.
## Workflow
Run the three checks **in order**. If a check hard-fails (account unresolved, or no ICP, or no visitors), report it, give the fix, and stop before the sample brief — a sample brief is pointless until the blocker is cleared. Report every check's status in the final readiness report regardless.
### Check 1 — Account resolved
Determine the `account_id`, in priority order:
1. **Configured Account ID.** The plugin's configured Leadfeeder Account ID is: `${user_config.account_id}`. If that resolves to a real, non-empty value (not blank, not the literal unsubstituted `${user_config.account_id}` token), the account is configured → **✅ pass**. Use it for the checks below.
2. **Not configured → resolve and guide.** Call `get_account_info` to list the available accounts. Then:
- If exactly one account exists, use it for the checks below, but still flag it as **⚠️ action recommended**: tell the user to paste that account ID into the plugin's **Leadfeeder Account ID** setting so no skill ever has to ask again (and so scheduled tasks can run unattended).
- If several accounts exist, show them (id + name) and mark **⚠️ action required**: tell the user to paste the correct account ID into the plugin's Leadfeeder Account ID setting. Ask which one to use for the rest of this check.
- Explain briefly *where* to set it: the plugin's configuration (the **Leadfeeder Account ID** field, set via the plugin settings / `/plugin` in an interactive client). Without it, every skill re-asks each session and scheduled tasks can't pick an account.
Do not attempt to write the setting yourself — the user sets it in the plugin configuration. This skill only tells them the value and where it goes.
### Check 2 — ICP configured
Call `get_icps` (free, no credits).
- **One or more ICPs configured → ✅ pass.** Note how many, and their names.
- **No ICPs configured → ❌ hard fail.** This is the most important check. Explain plainly why it matters: without an ICP, the daily brief cannot rank — it returns an undifferentiated list of *every* visitor (often hundreds or thousands), which reads as noise and is the top reason a first brief looks useless. Direct the user to configure at least one ICP (company segment) here: https://app.leadfeeder.com/l/settings/website/company-segments — give them that exact link. Stop here — do not run the sample brief until an ICP exists.
### Check 3 — Visitors are being tracked
Call `get_web_visits_companies` for the last 7 days (do **not** pass `include=company` — keep it lean).
- **Visitor companies returned, and at least some match a configured ICP → ✅ pass.** Note the rough count and that ICP matches exist.
- **Visitor companies returned, but none match any configured ICP → ⚠️ warn.** Traffic is flowing and the tracker works, but the ICP criteria may be too narrow for the actual audience. Tell the user their brief will be thin and suggest widening the ICP (or reviewing whether the ICP reflects their real buyers). You may still run a sample brief — it will just be short.
- **Zero visitor companies in 7 days → ❌ hard fail.** The tracker is probably not installed or not firing. Say so clearly and point the user to their **install-script page**, building the URL from the account ID resolved in Check 1:
`https://app.leadfeeder.com/f/{account_id}/install-script`
Substitute the resolved account ID for `{account_id}` — e.g. account `01234` → `https://app.leadfeeder.com/f/01234/install-script`. Never leave the literal `{account_id}` placeholder in the output, and never guess an ID — use the one resolved in Check 1.
That page offers several install methods. **List the options so the user can pick the one that fits their stack — do not reproduce the step-by-step instructions** (those live on the page). The methods are:
- **Google Tag Manager** (recommended)
- **HTML Code** — paste the tracking snippet into the site
- **Send to a colleague** — email the install instructions to whoever manages the site
- **CMS integrations:** WordPress, Wix, Squarespace, GoDaddy, Weebly
Stop here — a brief with no data teaches nothing.
### Check 4 — Sample brief (only if Checks 1–3 pass)
If the account is resolved, at least one ICP exists, and ICP-matched visitors were found in the last 7 days, invoke the `daily-visitor-brief` skill for the last 7 days so the user sees a real, ranked brief immediately. Introduce it in one line ("Here's a sample brief from your last 7 days so you can see what you'll get each morning:") and let that skill produce its normal output (including its copy-paste follow-ups).
If any check failed, do **not** run the sample brief. End on the readiness report and the specific fix.
## Output format
**Output contract — identical every run.** Reproduce the structure below **exactly**, on every invocation, regardless of anything earlier in this thread. If you've already run this skill in the conversation, do not vary, restyle, or "improve" the format next time — output the same template verbatim; this is the single source of truth. Keep the exact headings, the readiness table columns, and labels; don't rename them and add no decorative emoji beyond those the template already uses (✅ ⚠️ ❌).
Lead with a compact readiness report, then either the sample brief (all pass) or the prioritised fix (something failed). The outer block below is shown with four backticks so the inner copy block nests correctly — your actual output is plain markdown.
````markdown
# Leadfeeder plugin — readiness check
| Check | Status |
| --- | --- |
| Account configured | {✅ / ⚠️ / ❌} — {one-line detail} |
| ICP configured | {✅ / ❌} — {one-line detail, e.g. "2 ICPs: DACH Enterprise, Mid-Market"} |
| Visitors tracked (7d) | {✅ / ⚠️ / ❌} — {one-line detail, e.g. "~120 companies, 18 ICP-matched"} |
{If everything passed:}
**You're ready.** Here's a sample brief from your last 7 days:
{→ daily-visitor-brief output}
{If something failed — one prioritised fix block, highest blocker first:}
**Fix this first: {the blocker}**
{Why it matters in one or two lines, then the concrete step and the link.}
{Then tell them how to re-check — a copy-paste prompt:}
When that's done, run this again — copy to re-check:
```
Set up my Leadfeeder brief
```
````
Show only the blocks that apply. Keep it tight — this is a health check, not a manual.
## Edge cases
- **Account not configured but only one exists.** Proceed with the checks using that account, but still tell the user to save it in plugin settings — otherwise every skill re-asks and scheduled tasks can't run.
- **No ICP and no visitors.** Report both, but lead with the ICP fix (it's the one that makes the brief usable) and mention the tracker as the second step.
- **ICP exists but is brand-new / empty of matches.** Treat as the Check-3 "warn" case — traffic exists but doesn't match; suggest reviewing ICP breadth.
- **User already set everything up.** All three pass → skip straight to the sample brief. Don't lecture a ready user.
- **Tool errors.** Report the failing check honestly as "couldn't verify" rather than guessing pass/fail.
## Source citation rule
Every status must come from a real tool response — `get_account_info` for accounts, `get_icps` for ICPs, `get_web_visits_companies` for visitors. Never report a check as passed without the data. Never invent an account ID, an ICP name, or a visitor count. Only link to real Leadfeeder resources (the app and the help centre) — do not fabricate deep-link URLs; if the exact settings/guide URL isn't known, point to the Leadfeeder help centre and describe the in-app navigation.
## What NOT to do
- **Do not run the sample brief when a hard check failed.** A brief with no ICP or no data reinforces the "this plugin is useless" impression this skill exists to prevent.
- **Do not write the account ID into settings yourself.** Tell the user the value and where to paste it; the plugin configuration is theirs to set.
- **Do not trigger enrichment jobs** or any credit-consuming tool. This is a free, read-only readiness check.
- **Do not turn this into a tutorial.** Three checks, a fix or a sample brief, done.
- **Do not re-run automatically every session.** This is a one-time (or on-demand) setup check, not part of the daily flow.
## Example: good output (ICP missing — the common case)
````markdown
# Leadfeeder plugin — readiness check
| Check | Status |
| --- | --- |
| Account configured | ✅ Using account 01234 (Acme Ltd) |
| ICP configured | ❌ No ICPs configured |
| Visitors tracked (7d) | ✅ ~1,035 companies visiting — tracker is firing |
**Fix this first: configure an ICP.**
You have plenty of traffic (~1,035 companies in the last 7 days), but with no ICP the daily brief can't rank them — it returns all 1,035 as an undifferentiated list, which is why a first brief looks like noise. Define at least one Ideal Customer Profile (industry, size, region) in the Leadfeeder app so the brief can surface the ~dozen accounts worth your morning instead of the whole list.
Set it up here: https://app.leadfeeder.com/l/settings/website/company-segments
When that's done, run this again — copy to re-check:
```
Set up my Leadfeeder brief
```
````
daily-visitor-brief31.6 KB
---
name: daily-visitor-brief
description: >
Build a prioritised daily brief of website visitor companies from Leadfeeder, ranked by ICP fit and engagement signal. Use this skill when the user asks "what should I work on today", "show me today's visitors", "daily brief", "who visited my site", "show me hot leads", "morning prospecting", "any good leads today", "Leadfeeder brief", or any morning-prospecting question grounded in Leadfeeder visitor data.
metadata:
version: "1.0.0"
---
# Daily Visitor Brief
Produce a ranked, scannable brief of the most actionable website visitor companies for the user's outbound day. Anchor every recommendation in real visitor intent data from Leadfeeder. This is a read-only morning ritual: no writes, no enrichment jobs.
## Language
Produce the **entire response in the language the user wrote in** — English request → English output, German request → German output, and so on. This applies to everything the skill emits: the summary, the Trend line, all field labels, reasoning / why-now lines, edge-case messages, and the credit-consumption warning and confirmation. Do **not** translate data values — company names, contact names, URLs, page paths, email addresses, tag names, and ICP names stay verbatim as the tools return them. Copy-paste follow-up prompts should also be written in the user's language (they still trigger the right skill). If the input language is unclear, default to English.
## When this skill triggers
Trigger on any prompt that asks "who visited", "what should I work on", "daily brief", "morning prospecting", or "hot leads" in the context of Leadfeeder. Trigger silently — do not ask the user to confirm. Honor any explicit modifiers in the prompt (time window, ICP filter, result count).
## Inputs to determine before executing
Identify these from the user's prompt. Use defaults if unspecified — do not ask.
- **Time window** — default last 24 hours. Honor explicit overrides ("last 3 days", "this week", "since Monday", "yesterday").
- **Max results** — default top 10. Honor explicit overrides ("top 5", "top 20").
- **ICP / Persona filter** — default: use all configured ICPs and personas. Honor explicit overrides ("for my Enterprise ICP", "matching the SDR persona", "only Tech sector").
## Workflow
Execute these tool calls in order. Tools are exposed by the locally-connected Leadfeeder MCP server.
### Step 0 — Resolve the Leadfeeder account (before any other tool call)
Determine the `account_id` to use for every tool call in this skill, in priority order:
1. **Account named in the request (highest priority).** If the prompt or scheduled-task instruction explicitly names an account — an account ID (e.g. "account 01234") or an unambiguous account name — use that account for all tool calls. This overrides every source below and is the recommended way to make an unattended/scheduled task fully self-contained.
2. **Configured Account ID.** Otherwise, the plugin's configured Leadfeeder Account ID is: `${user_config.account_id}`. If that resolves to a real, non-empty account ID (not blank, and not the literal unsubstituted `${user_config.account_id}` token), use it for all tool calls and do **not** call `get_account_info` or ask the user.
3. **Already selected this session.** Otherwise, if an account was already chosen earlier in the session, reuse that.
4. **Fallback.** Otherwise call `get_account_info`. If exactly one account is returned, use it. If several are returned, ask the user to pick. When running unattended (e.g. a scheduled task) asking is impossible — if no account was named and none is configured, stop and report that a **Leadfeeder Account ID** must either be named in the task prompt or set in the plugin configuration for automated runs.
### Step 1 — Load ranking context (always re-fetch, never cache across runs)
Call `get_icps` to retrieve the user's configured ICPs. If the user is asking about a specific ICP, also call `get_icp` for that one to read its criteria. Call `get_buyer_personas` so persona context is available if relevant.
**Always re-fetch on every invocation of this skill.** Do NOT reuse ICPs or personas cached earlier in the session. Users update ICP and persona criteria in the Leadfeeder platform mid-session, and a stale cache produces incorrectly-filtered briefs (e.g. surfacing "no ICP matches" when the criteria have just been widened). The `get_icps` and `get_buyer_personas` calls do not consume credits, so re-fetching is free.
### Step 2 — Fetch visitor companies in the time window
Call `get_web_visits_companies` with the date range computed from the user's time window. Request roughly 2.5× the target result count (e.g. `page_size=25` when the user asked for top 10) so you have enough candidates to rank.
**Do NOT pass `include=company`.** It inflates the response 10x+ (one page can exceed 75 KB) and risks hitting context limits. Always fetch firmographics separately via `get_company` in Step 4. Keep `get_web_visits_companies` lean.
**Known limitation:** `get_web_visits_companies` does not return engagement metrics on the record itself (no session count, no page depth, no dwell time). To rank by engagement depth you must chain `search_web_visits` per company.
**Subscription context (important).** The companies returned by `get_web_visits_companies` are part of the user's active record subscription. **All read endpoints against them are subscription-covered** — `get_company`, `get_company_financials`, `search_web_visits`, and `search_companies_signals`. No incremental credit cost. You may fetch deep details across all visitors in the brief, not just the top 3. Credits only apply to enrichment **jobs** (`create_company_enrichment_job`, `create_find_contact_data_job`), which this skill does not invoke.
If the API supports filtering by ICP at the query level, apply the user's ICP filter here. Otherwise filter in Step 3.
### Step 3 — Rank candidates (intent × ICP matrix)
Order the brief by **intent-score tier crossed with ICP match**, in this exact priority — this is the top-down order the reader wants:
1. **🟢 High intent + ICP match**
2. **🟠 Medium intent + ICP match**
3. **🟢 High intent + no ICP match**
4. **🟠 Medium intent + no ICP match**
5. **⚪️ Low intent + ICP match**
6. **⚪️ Low intent + no ICP match**
Within each tier, break ties by: **re-engagement** (a re-engaged company first — see "Re-engagement detection"), then **engagement** (sessions / pages / time on page), then **recency**, then **persona alignment**.
**Data dependency (important).** This ordering needs each candidate's **intent tier** and **ICP verdict**, and both come from `get_company` (`attributes.intent.score_tier` + firmographics scored against the ICPs). So run `get_company` across the **candidate pool** (the ~2.5× set from Step 2), not just the final top N — it's subscription-covered (free) for visitor companies — apply the matrix, then take the top N. This means Step 4's enrichment effectively happens here for the pool; Steps 5–6 then add visit detail and signals for the top N only.
**Not-yet-scored companies:** slot by ICP match using engagement as the proxy — an ICP-matched unscored company sits around tier 2 (medium + ICP), a non-matched unscored one around tier 4 — and label it "not yet scored" on the card rather than inventing a tier.
Take the top N after ranking.
### Re-engagement detection
A **re-engaged** company is one that visited inside the current window *after a gap of silence*. Companies that visited 30–90 days ago and are back now are textbook high-intent, returning companies — a primary intent signal. Detect it from the wider visit history pulled in Step 5:
- Find the most recent visit **before** the current window (the prior session).
- Compute the gap between that prior session and the earliest visit inside the current window.
- **Gap ≥ 30 days → flag as re-engaged.** Capture the gap length in days for the badge (e.g. "last session was 45 days ago").
- Gap < 30 days, or continuous week-over-week activity → not re-engaged; no badge.
- No prior visit at all in the history window → this is a *new* company, not a re-engagement. Do not flag (a "first-time visitor" note in Why now is fine, but that is not the re-engagement badge).
Only surface the badge when the gap is real and computed from `search_web_visits` data. Never estimate or invent a gap.
### Step 4 — Enrich every visitor in the brief
**Credit-consuming-tool flag — use this reassuring framing, not the generic alarm.** `get_company` and `search_companies_signals` are marked credit-consuming by the MCP, so acknowledge them once, up front, and get a single go-ahead for the whole batch (not per company). But for *this* skill the companies come from the Web Visitors feed — they are already inside their 12-month access window — so records already unlocked in the last 12 months incur **no charge**. Flag it accurately and reassuringly, roughly:
> To build your brief, I need to pull firmographics and signals for the {N} companies using `get_company` and `search_companies_signals`. These are credit-consuming tools. Since these companies were identified through Web Visitors, their 12-month access window has already started — no credits are charged for records already unlocked within the last 12 months.
Do **not** use the generic "may consume 1 credit per company if not accessed in the last 12 months" wording here — for visitor companies that misrepresents the reality and needlessly alarms the user. After the go-ahead, report the actual `meta.credits.charged` (expected: 0 for already-unlocked records).
For every candidate company (the pool from Step 2 — needed for the intent × ICP ranking in Step 3), call `get_company` with `include=crm_connections.crm_record.crm_owner,tags` to retrieve firmographics, full CRM connection detail (if any), and assigned tags. These companies are subscription-covered — fetch freely across all candidates, not just the top 3.
**Capture from the response:**
- Firmographics: industry, employee count, location, B2B/B2C, founded year, revenue.
- **Intent score and tier** — read from the `get_company` (`CompanyV1`) response at **`attributes.intent.score`** (0–10 decimal, may be `null`) and **`attributes.intent.score_tier`** (`high` / `medium` / `low`). It is nested under `attributes` — not a top-level `intent.score` — so read the full path or you'll get nothing. This field exists **only** on `get_company`; the `get_web_visits_companies` list (Step 2) returns lightweight `company_location` records with no intent, so you must have called `get_company` (Step 4) to have it. If `attributes.intent.score` is `null`, the company isn't scored yet — show "Intent score: not yet scored" rather than dropping the line. These are first-class signals — surface them on every card. **Prefix the displayed score with a tier dot by `score_tier`: 🟢 high · 🟠 medium · ⚪️ low** (omit the dot when "not yet scored").
**You already have the payload — read it, don't default.** The Firmographics line comes from this same `get_company` response, so if you filled Firmographics in, the response is in hand and `attributes.intent.score` is right there. Extract it. Only write "not yet scored" when that exact field is genuinely `null`/absent in the payload — never as a fallback because the path wasn't checked. (Whole-account "not yet scored" across every card is a red flag that the path is being read wrong, not that no company is scored.)
- **CRM connections (sideloaded)**: use `include=crm_connections.crm_record.crm_owner` (plain `include=crm_connections` returns only a stub — `crm_record` as `{id, type}` with no name, URL, or owner). A populated `relationships.crm_connections` means the company is in the CRM. Each `crm_record` is JSON:API-shaped and **polymorphic** (`crm_record.type` = `crm_organization` | `crm_contact` | `crm_lead`): read **URL** from `crm_record.attributes.crm_url`; **name** by type (`crm_organization` → `attributes.name`; `crm_contact`/`crm_lead` → `attributes.first_name` + `attributes.last_name`); **owner** from `crm_record.relationships.crm_owner.attributes.name` (say "owner not set" if only a stub). **Render as a markdown link** — the record name is the clickable text and `crm_record.attributes.crm_url` is the target (e.g. `[Acme Corp](https://…crm_url…)`); never print the raw URL or leave the name unlinked. Surface this on the card.
- **ICP match — capture the exact ICP name(s).** When scoring a company against the configured ICPs (from Step 1), record *which* ICP(s) it matched by name (e.g. "DACH-Enterprise"), not just yes/no. Surface the exact name on the card — when several ICPs are configured, this tells the rep which motion the account belongs to and who should own it. If it matches more than one, list them; if none, it's "No ICP match".
- **Tags (sideloaded via `include=tags`)**: capture each assigned tag's `attributes.name` and `attributes.color`. Note: `color` is an integer palette **index** (0–N), not a hex code — the API doesn't expose the hex, and a plain markdown brief can't render background-filled chips anyway. So surface tags as plain-text chips (backtick `` `Hot Lead` `` style) using their **names**; do not invent a colour. Show tags because customers use them to prioritise accounts, so they inform the next best action.
**Ignore `web_engagement.last_visit_date` on the company profile.** This field lags the actual visits stream and was observed to be stale by days vs. real visits surfaced through `search_web_visits`. Treat the visits stream (Step 5) as the source of truth for visit timing. Use `get_company` only for firmographics, intent score, and CRM data — not for visit recency.
`get_company_financials` is subscription-covered for visitor companies — no incremental credit charge. Don't call it by default not because of cost, but because `get_company` already returns the headline revenue figure (sufficient for a morning brief). Call it on explicit request ("include financials", "balance sheet").
### Step 5 — Pull visit detail for every visitor in the brief
Call `search_web_visits` filtered by `company_id` for every company in the brief. Visitor companies are subscription-covered — `search_web_visits` is a free read. Use the response to populate the "Visit" line in each card with top landing pages, traffic sources, and timing.
**Plain-language metrics — no analytics jargon (this brief is read by sales reps too, not only marketers).** In the output, describe engagement in everyday words: say "**3 pages**" or "viewed 3 pages in one session" — never "page depth 3". Say "**14 seconds** on the page" or "spent 14s" — never "dwell". `page_depth` and `visit_length` are internal API fields: translate them into plain counts and seconds, never print the raw field name or the word "depth".
**Pull a wider lookback than the brief's time window** — request the last 90 days (not just the 24h window) so you can detect re-engagement (Step 3). The current-window visits populate the "Visit" line; the older visits are only used to find the prior session and compute the silence gap. If 90 days is expensive or the user set a longer window, pull at least `max(90 days, the brief window)`.
`search_web_visits` is the **source of truth for visit timing and engagement** in this skill. Do not cite `web_engagement.last_visit_date` from `get_company` — that field is stale. If `search_web_visits` returns no visit in the window for a company, say so transparently.
### Step 6 — Pull signals for every visitor in the brief
Call `search_companies_signals` for every visitor company in the brief. Signals are subscription-covered for visitor companies — fetch freely. Do not cap at top 3.
**Signal language — plain, and always explain relevance.** The category filtering below is an internal mechanic — **never surface it in the output.** Do not write "non-job-ad signals", category names, "excluding regulatory", or "90-day window" jargon. When nothing relevant is found, say exactly **"No notable signals in the last 90 days."** (or omit the line). When a signal *is* present, add a short plain-language reason it matters in this context — a raw event with no "so what" isn't useful. E.g. "Hiring 3 SDRs in EMEA — a growing outbound team, a fit for visitor intelligence"; "Raised Series B — fresh budget, likely expanding tooling".
**Default filters — always apply unless the user explicitly overrides:**
- **Recency window:** Pass `event_date.from` set to today minus 90 days. Older signals are usually stale and clutter the brief.
- **Exclude regulatory noise:** Pass an explicit `categories` array containing every category EXCEPT `regulatory_compliance_updates`. Regulatory filings (annual financial statements, register notices) dominate the raw signal feed but are rarely action-relevant for a morning sales brief.
The full default `categories` array to pass:
```
["business_expansion", "competitive_landscape", "event_participation", "industry_recognition", "leadership_changes", "corporate_challenges", "mergers_and_acquisitions", "customer_acquisition", "investment_activity", "product_and_service_development", "partnerships_and_collaborations", "financial_struggles", "job_ads", "register_updates"]
```
**User overrides:**
- "Include all signals" or "show regulatory signals" → drop the categories filter (pass none) so everything comes back.
- "Signals from this year" / "last 6 months" / specific date phrasing → use that window instead of the 90-day default.
- "Just hiring signals" / "only funding news" → narrow the categories array to just the matching ones (e.g. `["job_ads"]` or `["investment_activity"]`).
### Step 7 — Day-over-day trend (new vs. returning, vs. the prior window)
Give the reader a one-glance sense of momentum. This skill is stateless — there is no stored "yesterday's brief" — so derive the trend from the visit data, never from memory of a past run. Compute two things:
1. **New vs. returning (this window).** Classify each company in the brief using its 90-day visit history from Step 5:
- **New** — first-ever visit in the history window (no visit before the current window).
- **Returning** — visited before the current window. A subset are **re-engaged** (back after a ≥30-day gap, per the re-engagement detection).
2. **Vs. the prior window.** Call `get_web_visits_companies` once more for the immediately-preceding equal-length window (e.g. the previous 24 h) — subscription-covered, free — and compare the total company count: ↑ up, ↓ down, or → flat.
Render this as a single **Trend** line in the preamble (see Output format). Ground every number in the tool data. If the prior-window call fails or returns nothing, show the new/returning split and say "no prior-window data" — never invent a comparison.
## Output format
**Output contract — identical every run.** Reproduce the structure below **exactly**, on every invocation, no matter what appeared earlier in this thread. If you have already produced a brief in this conversation, do **not** vary, restyle, "improve", or summarise the format differently the second time — output the same template verbatim. This template is the single source of truth. Specifically, every run:
- Title exactly `# Daily Visitor Brief — {Date}, last {N} hours` — `{Date}` is a concrete date (e.g. `2026-07-16`), never "early morning", a weekday phrase, or an account ID.
- Cards numbered `## 1.`, `## 2.` — not `#1`, not `Tier 1 — …`. The Step 3 intent × ICP matrix controls the **order** of cards only; it does **not** become section headings in the output.
- Exact field labels in the exact order shown: **ICP**, **Intent score**, **Why now**, **Visit**, **Firmographics**, **Signals**, **Tags**, **CRM**, **Company website**, **Next**. Never rename them (it's **Next:**, not "Suggested action:"; **CRM:**, not "CRM status:").
- Use only the emoji in the template — ✅ ❌ ⚠️ ↩ and the 🟢 / 🟠 / ⚪️ intent dots. Do **not** add decorative emoji (📍 🎯 🗓️ 📅 or similar) to headings, labels, or lines.
Render as a ranked list of cards, top of brief = most actionable. Use ✅ / ⚠️ / ❌ status flags honestly — a card that doesn't fit shows "❌ No ICP match", and gaps (not in CRM, thin visit) read as plainly as wins. One flag per label. Use this exact structure:
````markdown
# Daily Visitor Brief — {Date}, last {N} hours
{One-sentence summary: e.g. "5 ICP-matched visits, 2 new companies, top signal: pricing-page deep dive at Acme Corp."}
**Trend:** {N} companies today ({↑ / ↓ / →} vs. {M} in the prior {window}) · {X} new · {Y} returning ({Z} re-engaged) {— or "no prior-window data" if the comparison couldn't be fetched}
---
## 1. {Company Name} · {✅ {exact ICP name}, or "❌ No ICP match"} {· ↩ Re-engaged — last session was {N} days ago ← include this segment ONLY when re-engagement is detected}
- **ICP:** {The exact configured ICP name(s) this company matches — e.g. "DACH-Enterprise". If it matches more than one, list each; if none, "No ICP match". When several ICPs are configured, naming the exact one tells the rep which motion the account belongs to and who should own it.}
- **Intent score:** {🟢 high / 🟠 medium / ⚪️ low} {score}/10 ({tier}) — {one-line reasoning grounded in visit/signal data, e.g. "driven by 5 sessions in 2 days, deep visit to /pricing, returning identified contact"}
- **Why now:** {Most action-relevant reason in one line — "deep visit to /pricing", "second session this week", "matches Enterprise ICP and just opened the API docs"}
- **Visit:** {N sessions · M pages · last seen {date} · top landing pages: /pricing, /integrations, /docs/api}
- **Firmographics:** {Industry · Employee size · Location}
- **Signals:** {If any — each with a short plain "why it matters", e.g. "Hiring 3 SDRs in EMEA — growing outbound team, a fit for visitor intelligence". If none: "No notable signals in the last 90 days." Never expose filter mechanics ("non-job-ad", category names, "regulatory excluded").}
- **Tags:** {Assigned tags as plain-text chips by name — e.g. `` `Hot Lead` ``, `` `DACH` ``. Omit the line if none. Tag colours are configured in Leadfeeder but can't render as background-filled chips in this markdown brief, so show names only.}
- **CRM:** {If connected: "[{record name}]({crm_record.attributes.crm_url}) · Owner: {owner name — or 'owner not set'}" (record name derived by type per the capture rules) — if more than one record is linked, list each on its own line. If NOT connected: "Not found in your CRM".}
- **Company website:** {company.url from get_company response}
- **Next:** {One suggested follow-on in plain language, outcome first — NOT "Run `skill`". Describe what the user gets: "Deep-dive this company", "See who to contact — names, titles, emails", "Get talking points to personalise outreach", or "Tag it and add it to a Leadfeeder list to track it". Pick the most relevant given the why-now reason.}
```
{ready-to-send prompt for the chosen next step, e.g. Give me a company brief for {Company Name}}
```
## 2. {Company Name} · {✅ {exact ICP name} or "❌ No ICP match"}
- **ICP:** ...
- **Intent score:** ...
- **Why now:** ...
- **Visit:** ...
- **Firmographics:** ...
- **Signals:** {Omit if none.}
- **Tags:** {Omit if none.}
- **CRM:** ...
- **Company website:** ...
- **Next:** {plain-language recommendation}
```
Who should I reach out to at {Company Name}?
```
## 3. {Next Company} ...
````
Sort by your ranking. The first card is the most actionable lead.
**Copy-block convention (one per card).** Every card ends with a single fenced code block containing the ready-to-send follow-on prompt — fenced blocks render a one-click **Copy** button in the client, so the user copies, pastes, and sends. Pick the one prompt that matches the card's `Next:` recommendation:
- Deep-dive the company → `Give me a company brief for {Company Name}`
- Find who to contact → `Who should I reach out to at {Company Name}?`
- Prep outreach → `Prep me for outreach to {Company Name}`
- Track it → `Add {Company Name} to a list in Leadfeeder`
One block per card, one prompt per block (the single most relevant action). This is markdown, not a widget — do not try to render buttons; the fenced block itself gives the copy affordance.
## Intent score reasoning
The Intent score line is one of the most important on the card. Construct the one-line reasoning from the data the skill already pulled:
- **Engagement signals** (plain language — no "page depth"/"dwell"): "deep visit to /pricing", "5 sessions in 2 days", "12 pages in one session", "71 seconds on /pricing"
- **Recency signals**: "returning today", "second session this week", "first visit in 3 weeks", "re-engaged after 45 days quiet"
- **Identification signals**: "identified contact returning", "single visitor across multiple sessions"
- **Source signals**: "branded paid-search visit", "direct traffic with bookmark pattern", "organic search on competitor terms"
- **Signal data**: "hiring spike correlates", "post-funding visit"
If the intent score is HIGH but the visit data is thin, say so plainly: "High intent score but limited visible activity in the window. Worth investigating in the Leadfeeder app."
If the intent score is LOW, do not invent justification. Just note it: "Low intent score (1.9). Single short visit."
If `attributes.intent.score` is null / not returned, do not force a number — show "Intent score: not yet scored" and rank the card on engagement depth, recency, and re-engagement instead.
## CRM connection treatment
Surface CRM info clearly because it changes the action:
- **Connected with an owner:** de-emphasise "outreach now" and instead suggest "loop in {owner name}" or "check {owner}'s pipeline before reaching out". Put this nuance in the Why now line.
- **Connected but no owner (orphaned):** flag this — it's a routing problem. Note in Why now: "In your CRM but no owner assigned. Worth routing."
- **Not in CRM:** flag this — it's a missed-routing gap. Note in Why now: "Not found in your CRM. Consider adding before outreach."
The **CRM** field itself is purely factual (platform, owner, link). The action interpretation belongs in Why now.
## Edge cases
- **No visitors in the time window.** Tell the user clearly. Offer to expand to 7 days. Do not pad the brief with stale data.
- **No ICPs configured.** Fall back to ranking by engagement depth alone. Mention in the preamble: "No ICPs configured — ranking by engagement only." Suggest the user configure ICPs in Leadfeeder for sharper recommendations.
- **More than 20 ICP matches in the window.** Show the top 10 by engagement. In the preamble, offer alternative cuts ("Want this re-ranked by industry vertical, or by deepest pages-per-session?").
- **Single strong match, nothing else.** Surface it normally, but flag it: "Only one ICP match today, but a strong one."
- **Tool errors or empty responses.** Report the error transparently. Do not invent data to fill the brief.
## Source citation rule
Every claim in the brief must be grounded in data returned by the Leadfeeder MCP tools. Use the company's website (`company.url` field from `get_company`) as the actionable link on each card — that field is reliably populated. Never invent a URL. Only use links returned by the MCP tools.
**Internal consistency (trust).** Within each card and the summary line, every restated fact — session/page counts, dates, page paths, the re-engagement gap, employee size, locations — must match across the fields. A stated count must equal the items you list, and a figure or location must read the same wherever it appears on the card. Two lines disagreeing about the same fact is a trust-breaker.
**Known gap:** The MCP `get_company` response does not currently include a Leadfeeder app deeplink (e.g. `app.leadfeeder.com/...`). Until the MCP adds this, the cards link out to the prospect's own website, not to the Leadfeeder app record. If a future MCP version returns an app URL field, update this skill to add an "Open in Leadfeeder" line below the "Company website" line.
## What NOT to do
- **Do not call a visitor an "account" or a "hot/warm lead."** A website visitor is a **company** — "account" implies a CRM relationship this data doesn't carry. Reserve "account" for the user's Leadfeeder account (workspace/ID) or a genuine CRM record; use "high-intent", "active", "engaged", or "returning" instead of "hot"/"warm". (User phrases like "hot leads" may still be honored as triggers — this governs *our* output wording.)
- **Do not trigger enrichment jobs** (`create_company_enrichment_job`, `create_find_contact_data_job`). Those cost credits per company and break the "morning ritual" cost model.
- **Do not call `get_company_financials` by default.** Not a cost reason (it's subscription-covered for visitor companies) but a brevity one — `get_company` already returns headline revenue. Only call it on explicit request ("include financials", "balance sheet").
- **Do not expose contact-level PII** (names, emails, phone numbers) in the brief. Keep it company-level. Contact details belong in the separate Find My Buyer skill.
- **Do not invent data.** ICP match status, signals, page views — only report what the tools return.
- **Do not chain to outreach drafting, CRM writes, or list management** on direct user invocation unless the user explicitly asks. Exception: if invoked by the Leadfeeder Agent as part of a declared multi-step chain, the agent may proceed with follow-on skills (`visitor-company-brief`, `find-my-buyer`, `outreach-companion`, `add-to-pipeline`) without waiting for user confirmation.
- **Do not ask the user to confirm before running.** If they triggered this skill, they want the brief now.
## Example: good output
````markdown
# Daily Visitor Brief — 2026-05-29, last 24 hours
7 visitor companies, 4 ICP-matched, 1 re-engaged after 45 days of silence. Strongest signal: Acme Corp is back for a 12-page session focused on pricing and integrations after going quiet in April.
**Trend:** 7 companies today (↑ vs. 5 yesterday) · 3 new · 4 returning (1 re-engaged)
---
## 1. Acme Corp · ✅ DACH-Enterprise · ↩ Re-engaged — last session was 45 days ago
- **ICP:** DACH-Enterprise
- **Intent score:** 🟢 10/10 (high) — driven by 12-page pricing+integrations deep dive, re-engaged after 45 days quiet, identified contact returning
- **Why now:** Back after 45 days of silence and went straight to /pricing and /integrations/salesforce — a warm re-engagement worth actioning today. Existing CRM owner should be looped in.
- **Visit:** 1 session · 12 pages · last seen 2026-05-29 · top landing pages: /pricing, /integrations/salesforce, /docs/api
- **Firmographics:** SaaS · 250–500 employees · Berlin, DE
- **Signals:** Hiring 3 SDRs in EMEA — a growing outbound team, a strong fit for visitor intelligence
- **Tags:** `Hot Lead` · `DACH`
- **CRM:** [Acme Corp](https://app.hubspot.com/contacts/12345/company/67890) · Owner: Jane Doe
- **Company website:** https://acme.com
- **Next:** Deep-dive this company for the full picture, then get talking points grounded in the pricing visit + hiring signal to personalise your opener.
```
Give me a company brief for Acme Corp
```
## 2. Acme GmbH · ✅ Mid-Market DACH
- **ICP:** Mid-Market DACH
- **Intent score:** 🟠 6.4/10 (medium) — first-time visitor, 5-page demo flow, paid LinkedIn campaign attribution
- **Why now:** First-time visitor matching the Mid-Market DACH ICP. Not in CRM yet — worth adding before outreach.
- **Visit:** 1 session · 5 pages · last seen 2026-05-29 · top landing pages: /features, /case-studies/saas, /demo
- **Firmographics:** Fintech · 50–250 employees · Munich, DE
- **Tags:** `Q3 Target`
- **CRM:** Not found in your CRM
- **Company website:** https://acme.de
- **Next:** See who to contact — a ranked shortlist with names, titles, and emails — before reaching out.
```
Who should I reach out to at Acme GmbH?
```
## 3. ...
````
Keep cards tight. Long brief = unread brief.
find-my-buyer24.9 KB
---
name: find-my-buyer
description: >
Identify the best 1–5 contacts to reach out to at a named company, ranked by Buyer Persona fit and visit identification, plus a buying-committee coverage read showing which departments are covered and which are gaps. Use this skill when the user asks "who should I reach out to at [company]?", "find my buyer at Acme", "who's the right contact at Acme", "find the decision-maker at [company]", "who do I call at [company]", "do I have the buying committee covered at [company]", or any prompt asking for a ranked contact shortlist at a specific company. Surfaces names, titles, and emails — PII exposure is intentional and consented by invocation.
metadata:
version: "1.0.0"
---
# Find My Buyer
Load the user's configured Buyer Personas, resolve the target company, search for matching contacts, cross-reference with visit identification data, and return a ranked shortlist of 1–5 contacts. This is the only skill in the suite that intentionally surfaces contact-level PII (name, title, email) — that is its purpose.
## Language
Produce the **entire response in the language the user wrote in** — English request → English output, German request → German output, and so on. This applies to everything the skill emits: the context line, all field labels, the buyer-persona coverage, edge-case messages, and any credit-consumption warning and confirmation. Do **not** translate data values — company names, contact names, titles, URLs, email addresses, tag names, and persona names stay verbatim as the tools return them. Copy-paste follow-up prompts should also be written in the user's language (they still trigger the right skill). If the input language is unclear, default to English.
## When this skill triggers
Trigger when the user names a specific company and asks who to contact, reach out to, or call. Trigger silently. Honor any explicit persona or seniority filter in the prompt ("the CFO", "someone in RevOps", "using the Enterprise Buyer persona").
Do not trigger for company-level research — that is `visitor-company-brief`. Do not trigger for outreach drafting — that is `outreach-companion`. This skill's job is contact identification only.
## Inputs to determine before executing
Identify these from the user's prompt. Use defaults if unspecified — do not ask.
- **Company** — name, domain, or Leadfeeder company ID. Required.
- **Persona filter** — default: all configured Buyer Personas. Honor explicit overrides ("using the SDR persona", "Enterprise Buyer only").
- **Seniority filter** — default: none (all seniorities). Honor explicit overrides ("VP or above", "C-level only", "manager level").
- **Visit window** — default last 90 days for visit cross-reference. Honor overrides.
## Workflow
### Step 0 — Resolve the Leadfeeder account (before any other tool call)
Determine the `account_id` to use for every tool call in this skill, in priority order:
1. **Account named in the request (highest priority).** If the prompt or scheduled-task instruction explicitly names an account — an account ID (e.g. "account 01234") or an unambiguous account name — use that account for all tool calls. This overrides every source below and is the recommended way to make an unattended/scheduled task fully self-contained.
2. **Configured Account ID.** Otherwise, the plugin's configured Leadfeeder Account ID is: `${user_config.account_id}`. If that resolves to a real, non-empty account ID (not blank, and not the literal unsubstituted `${user_config.account_id}` token), use it for all tool calls and do **not** call `get_account_info` or ask the user.
3. **Already selected this session.** Otherwise, if an account was already chosen earlier in the session, reuse that.
4. **Fallback.** Otherwise call `get_account_info`. If exactly one account is returned, use it. If several are returned, ask the user to pick. When running unattended (e.g. a scheduled task) asking is impossible — if no account was named and none is configured, stop and report that a **Leadfeeder Account ID** must either be named in the task prompt or set in the plugin configuration for automated runs.
### Step 1 — Resolve company and load personas (parallel)
Call in parallel:
- `search_companies` with the company name or domain to resolve `company_id`. If multiple plausible matches, present the top 3 and disambiguate before continuing.
- `get_buyer_personas` to load all configured personas. If the user specified a persona by name, also call `get_buyer_persona` for that one to read its full filter criteria.
### Step 2 — Search contacts and pull visit data (parallel, once company_id is known)
Call in parallel:
- `search_contacts` with `company_ids: [company_id]` (note: the parameter is an array, not a scalar — passing a bare `company_id` is silently ignored and the call returns an error). Fetch up to 50 candidates (`page_size: 50`) to ensure good persona coverage. **Optimization:** pass `buyer_persona_ids` containing all loaded persona IDs — the API will pre-filter to contacts matching at least one persona, reducing noise significantly. If no personas are configured, omit `buyer_persona_ids` and fetch all contacts. Also pass `filters: {has_email: true}` to skip contacts without an email address.
- `search_web_visits` with `filters: {company_id: company_id}`, using `start_date`/`end_date` at top level for the 90-day window. Extract the set of identified contact IDs that appear in the visit records — match strictly by a contact's own ID/email appearing on a visit record. A company or one of its office locations appearing in the visit stream does **not** mean any specific contact visited; only treat a person as "identified" when that person is the identified visitor.
- `get_company` with `include=crm_connections.crm_record.crm_owner` for the resolved `company_id` — to read the company's CRM status. Plain `include=crm_connections` returns only a stub (`crm_record` = `{id, type}`, no owner); the deeper path sideloads the record and owner. A populated `relationships.crm_connections` means the company is in the CRM. Each `crm_record` is polymorphic (`crm_record.type` = `crm_organization` | `crm_contact` | `crm_lead`): read **URL** from `crm_record.attributes.crm_url`, **name** by type (`crm_organization` → `attributes.name`; `crm_contact`/`crm_lead` → `attributes.first_name` + `attributes.last_name`), **owner** from `crm_record.relationships.crm_owner.attributes.name` (say "owner not set" if only a stub). **Render as a markdown link** — record name is the clickable text, `crm_record.attributes.crm_url` the target; never print the raw URL or leave the name unlinked. This complements the contact shortlist: if the company is already in the CRM, coordinate with the owner rather than cold-outreach the contacts you surface.
**Cost note (subscription coverage).** `search_contacts` and `search_web_visits` above are free, so the ranked shortlist costs nothing. The `get_company` CRM-status read charges **1 credit only if this company hasn't been accessed in the last 12 months**. Use the `search_web_visits` result as the free coverage signal:
- **Visits in the last 12 months → COVERED.** `get_company` is free — run it for CRM status as normal.
- **No visits in 12 months → UNCOVERED.** **Skip the `get_company` CRM read** — don't spend a credit just for CRM context. Still deliver the full contact shortlist (free), and set the CRM line to "Not checked — company is outside your free 12-month window (would cost 1 credit; ask if you want it)." Only run `get_company` for an uncovered company if the user explicitly asks. Report `meta.credits.charged` if you do.
Enrichment (`create_find_contact_data_job`) is separately credit-consuming and stays behind its own Cost Guard in Step 4, regardless of coverage.
### Step 3 — Rank contacts
Apply this rubric in order:
1. **Persona match.** Score each contact against all loaded Buyer Personas. Full match (all persona filters satisfied) ranks above partial match, which ranks above no match. If the user specified a persona, only full matches for that persona qualify for the shortlist.
2. **Site visit identification.** Contacts whose ID or email appears in the `search_web_visits` results (last 90 days) receive a boost — they are warm.
3. **Seniority.** Within the same persona-match tier, higher seniority ranks higher (C-level > VP/Director > Manager > IC). Apply the user's seniority filter if specified.
4. **Recency of visit identification.** Among visit-identified contacts, more recent identification wins.
Take the top 1–5 after ranking. Never pad to 5 if fewer strong matches exist — a shortlist of 2 good matches is better than 5 weak ones.
### Step 3.5 — Assess buying committee coverage
The shortlist answers "who do I call first". Enterprise deals also need "do I have the committee covered". Compute coverage across **all** contacts returned by `search_contacts` (not just the shortlist), so a strong contact you didn't shortlist still counts toward coverage.
**Check every configured Buyer Persona — return them all, drop none.** Load every persona via `get_buyer_personas` and evaluate each one against the contacts on file. The output must list **every** configured persona explicitly, each with its verdict and a one-line reason. Never omit, merge away, or silently drop a persona — including personas with zero matches, and including cases where only one persona is configured. Each persona gets one of:
- **✅ Covered** — ≥1 contact on file fully matches the persona; state the count (e.g. "3 contacts match").
- **⚠️ Thin** — only a partial match exists; say what's missing.
- **❌ Gap** — zero contacts on file match it; say "no contacts on file matching this persona".
Every verdict must be grounded in the `search_contacts` data — never invent a contact to fill a seat.
**If zero Buyer Personas are configured**, there are none to check: say "No Buyer Personas configured", suggest configuring them, and fall back to a department read of the contacts on file (which core functions are present vs. absent — only for functions plausibly relevant to the deal; don't assert a canonical committee the user never defined).
You may add, as a supplementary note *below* the persona list, a notable department gap relevant to the deal (e.g. "no IT/Security contact on file"). That is in addition to — never a replacement for — the full per-persona list. **The per-persona list is mandatory whenever ≥1 persona is configured; there is no skip condition for it.**
### Step 4 — Check for empty results → enrichment path
If Step 3 yields zero contacts (no contacts on file for this company at all, not just no persona matches):
1. Tell the user clearly: "No contacts on file for {Company}."
2. Offer to run a Find Contact Data enrichment job to discover contacts.
3. **Cost Guard — mandatory before any enrichment:**
a. Call `estimate_find_contact_data_job` for the company and surface the estimated credit cost to the user.
b. Wait for explicit "yes" confirmation before proceeding. Do not auto-trigger.
c. On confirmation: call `create_find_contact_data_job`.
d. Call `get_find_contact_data_job` to check status. If the job completes synchronously, re-run Step 2–3 with the new contacts. If it is async (still running), tell the user the job is in progress and they can re-run this skill once it completes.
If contacts exist but none match the specified persona or seniority filter, do not trigger enrichment — tell the user what is on file and suggest relaxing the filter.
## Output format
**Output contract — identical every run.** Reproduce the structure below **exactly**, on every invocation, regardless of anything earlier in this thread. If you've already run this skill in the conversation, do not vary, restyle, or "improve" the format next time — output the same template verbatim; this template is the single source of truth. Keep the exact headings, field labels, and order; don't invent new sections, don't renumber, don't rename fields, and add no decorative emoji beyond those the template already uses (✅ ⚠️ ❌).
Use this structure (the outer block is shown with four backticks so the copy blocks nest correctly — your actual output is plain markdown):
**Include a generation timestamp.** Put `*Generated {current date}*` directly under the title — use the current date (a point-in-time snapshot); it must always be present.
**Only show fields you actually have.** `search_contacts` returns a name, email, and a coarse seniority bucket — the **job title and department are often not returned**. If a contact's title or department isn't in the response, omit it from the header (`### {Full Name}`) entirely — never show a blank, "N/A", the raw seniority bucket as a title, or a guessed title/department. The real title, phone, and LinkedIn come from `get_contact` (credit-consuming) via the per-contact prompt below.
**Full-details prompt (per contact).** Give every listed contact its own copy block: `Get full contact details for {Full Name} at {Company Name}`. Running it calls the `get_contact` tool — a deep read that consumes **~1 credit** (unless that contact was accessed in the last 12 months); Claude will confirm the credit cost before fetching.
**Missing contact data is "not available", never a "gap" or a failing.** If contacts have no email (or no phone) on file, state it plainly and neutrally — e.g. "No email on file for these contacts (Leadfeeder has phone/social for some)." Do **not** call it a "data-quality gap", do **not** frame it as something Leadfeeder is failing to provide or is responsible for. If the data isn't there, it simply isn't available — report that fact without editorialising it as a shortfall. (Reserve "gap" for **persona/committee** coverage — a real absence of a matching contact — not for missing fields on the contacts you do have.)
**"Visited site" is person-level, not location-level.** This flag is strictly whether *this contact* was identified in the visit stream — i.e. their contact ID/email appears on a visit record. **Never** attach company-location or office activity to it as if the person visited: a "{Company} Switzerland office" showing up in the visit stream is not the same as identifying this contact, and phrasing like "Not identified in the last 90 days — but {Company} Switzerland office" is misleading. If location-level activity is genuinely worth noting, put it on its own line, phrased as explicitly unattributed: **"{Location} was active in the visit stream, but couldn't be attributed to this specific person."**
````markdown
## Find My Buyer — {Company Name}
*Generated {current date}*
{One-line context: ICP fit verdict + total contacts on file + how many matched.}
### Start here
{Lead with the answer so the reader never has to scroll for it. Two lines:}
{1. **The one contact to reach first** — name + one-line why (visit-identified, strongest persona fit, seniority).}
{2. **The headline read** — e.g. "3 of 8 configured personas covered." When nothing standard matches, say so plainly: "No standard sales/marketing/RevOps persona matches — this is an {type} firm; your strongest lead is behavioural: {Name}, a self-directed identified visitor."}
**CRM:** {If connected: "[{record name}]({crm_record.attributes.crm_url}) · Owner: {owner name — or 'owner not set'}" (record name derived by type per the capture rules) — coordinate with the owner before reaching out to the contacts below. If not connected: "Not found in your CRM — clean net-new company."}
---
### 1. {Full Name}{ · {Title} — only if returned}{ · {Department} — only if returned}
- **Seniority:** {C-level / VP / Director / Manager / IC}
- **Persona match:** {✅ Full match: "{Persona Name}" / ⚠️ Partial: "{Persona Name}" (missing: {criterion}) / ❌ No match}
- **Visited site:** {✅ Identified — this person appears in the visit stream, last seen {date} / ❌ Not identified in the last 90 days — this specific person hasn't been matched to a visit} {Optional, only if there was location-level activity: on the next line, "{Location} was active in the visit stream, but couldn't be attributed to this person."}
- **Email:** {email address}
- **Full details** (job title, phone, LinkedIn) — copy to fetch via `get_contact` (~1 credit):
```
Get full contact details for {Full Name} at {Company Name}
```
### 2. {Full Name}{ · {Title} if returned}{ · {Department} if returned}
- **Seniority:** ...
- **Persona match:** ...
- **Visited site:** ...
- **Email:** ...
- **Full details:**
```
Get full contact details for {Full Name} at {Company Name}
```
{Repeat for up to 5 contacts — each with its own "Get full contact details" copy block, and each omitting title/department if not returned. Stop earlier if quality drops sharply.}
---
**Buyer persona coverage** — every configured persona, checked against the contacts on file (list ALL of them, one line each; never drop a persona):
- ✅ "{Persona A}" — covered ({N} contacts match)
- ✅ "{Persona B}" — covered ({N} contacts match)
- ⚠️ "{Persona C}" — thin (1 partial match, no full match; missing {criterion})
- ❌ "{Persona D}" — gap (no contacts on file matching this persona)
{One line for EVERY configured persona — none omitted, even if all covered or all gaps. This is the detail section; the top-line read already appeared in "Start here".}
{Optional supplementary line: a notable **committee/persona** gap relevant to the deal, e.g. "No IT/Security contact on file." If contacts simply lack email/phone, state it neutrally ("no email on file for these contacts; Leadfeeder has phone/social for some") — never as a "data-quality gap" or a Leadfeeder failing.}
{Then a one-line so-what: e.g. "Strong on RevOps + Sales; the Marketing persona is thin and you have no IT/Security contact — likely a blocker for a security-sensitive deal."}
{If zero personas are configured, replace this block with "No Buyer Personas configured" + the department read.}
**Suggested next step:** phrase in plain language, outcome first (not "Run `skill`") — "Get visit-grounded talking points for {top contact name} to personalise your outreach" (`outreach-companion`), or "Tag {Company Name} and add it to a Leadfeeder list so it's tracked" (`add-to-pipeline` — writes tags/lists inside Leadfeeder; it does not create a CRM record or assign a CRM owner).{ If there is a committee gap, optionally add: " To fill the {gap} seat, re-run with a relaxed persona filter or run a Find Contact Data enrichment job."}
Copy to run the next step (fenced blocks give a one-click copy button):
```
Prep me for outreach to {top contact name} at {Company Name}
```
```
Add {Company Name} to a list in Leadfeeder
```
````
### When no contacts on file (enrichment path)
```markdown
## Find My Buyer — {Company Name}
No contacts on file for this company.
**Estimated enrichment cost:** {N} credits ({source from estimate_find_contact_data_job}).
Want me to run a Find Contact Data job to discover contacts? This will consume the credits above. Reply **yes** to confirm.
```
## Edge cases
- **Company not found.** Stop and tell the user. Offer to try a broader name or domain.
- **Multiple company matches.** Present top 3, disambiguate before continuing.
- **Contacts exist but none match persona/seniority filter.** Tell the user what is on file (total count, department/seniority breakdown). Suggest relaxing the filter or switching to a different persona. Do not trigger enrichment.
- **No Buyer Personas configured.** Tell the user and fall back to seniority-only ranking. Suggest they configure Buyer Personas in Leadfeeder for sharper results.
- **Enrichment job is async.** Tell the user the job is running and to re-run the skill once it completes. Do not poll indefinitely.
- **Enrichment job returns no new contacts.** Tell the user clearly. Do not retry automatically.
## Source citation rule
Every contact in the shortlist must come from `search_contacts` results. Visit identification flags must come from `search_web_visits` results. CRM record name, URL, and owner must come from the sideloaded `crm_record` (and its `crm_owner`) under `relationships.crm_connections` on the `get_company` response — `crm_record.attributes.crm_url`, the name from `crm_record.attributes` per record type, and `crm_record.relationships.crm_owner.attributes.name` — never assert the company is (or isn't) in the CRM without it. Buying committee coverage verdicts (covered / thin / gap) must be derived only from `search_contacts` results cross-referenced with the loaded Buyer Personas — never assert a covered seat without a matching contact on file, and never assert a gap for a persona or department the user has not configured. Do not infer or fabricate any of these.
**Internal consistency (trust).** Every restated fact — total counts, department/seniority breakdowns, the committee coverage summary vs. the per-seat lines, contact counts — must agree across the output. A stated count must equal the items you list (e.g. the "covered / thin / gap" summary line must match the per-seat verdicts below it). Re-check before finishing; two sections disagreeing about the same fact is a trust-breaker.
## What NOT to do
- **Do not call a visitor an "account" or a "hot/warm lead."** A website visitor is a **company** — "account" implies a CRM relationship this data doesn't carry. Reserve "account" for the user's Leadfeeder account (workspace/ID) or a genuine CRM record; use "high-intent", "active", "engaged", or "returning" instead of "hot"/"warm".
- **Do not trigger enrichment** (`create_find_contact_data_job`) without completing the Cost Guard flow — estimate first, explicit confirmation second.
- **Do not surface contact PII from other skills' context.** Only surface contacts resolved in this skill's own `search_contacts` call.
- **Do not rank by engagement alone** without persona scoring — persona fit is the primary signal.
- **Do not pad the shortlist** to reach 5 if fewer strong matches exist. Quality over quantity.
- **Do not handle bulk companies.** One company per invocation. For multiple companies, the user should invoke the skill once per company.
## Example: good output
````markdown
## Find My Buyer — Acme Corp
*Generated 2026-06-27*
✅ Full ICP match · 2,838 contacts on file · 3 personas matched.
### Start here
**Reach Jane Doe first** — Head of Revenue Operations, a full match for your "Revenue Operations Manager" persona, and the only contact identified in the visit stream (last seen 2026-06-20).
**Coverage:** 3 of 3 configured personas covered; strong commercial coverage, no technical (IT/Security) contact on file.
**CRM:** [Acme Corp](https://acme.lightning.force.com/lightning/r/Account/0018000000abcde/view) · Owner: Robert Roe — coordinate with Robert before reaching out to the contacts below.
---
### 1. Jane Doe · Head of Revenue Operations · Sales
- **Seniority:** Director
- **Persona match:** ✅ Full match: "Revenue Operations Manager"
- **Visited site:** ✅ Identified — this person appears in the visit stream, last seen 2026-06-20
- **Email:** jane.doe@acme.com
- **Full details** (job title, phone, LinkedIn) — copy to fetch via `get_contact` (~1 credit):
```
Get full contact details for Jane Doe at Acme Corp
```
### 2. John Sample · VP Sales EMEA · Sales
- **Seniority:** VP
- **Persona match:** ✅ Full match: "Sales Director"
- **Visited site:** ❌ Not identified in the last 90 days — this specific person hasn't been matched to a visit
The Acme Switzerland office was active in the visit stream, but couldn't be attributed to John.
- **Email:** j.sample@acme.com
- **Full details:**
```
Get full contact details for John Sample at Acme Corp
```
### 3. Mary Major
- **Seniority:** Director
- **Persona match:** ✅ Full match: "Head of Marketing"
- **Visited site:** ❌ Not identified in the last 90 days — this specific person hasn't been matched to a visit
- **Email:** m.major@acme.com
- **Full details:** *(title and department weren't returned by `search_contacts`, so they're omitted from the header above)*
```
Get full contact details for Mary Major at Acme Corp
```
---
**Buyer persona coverage** — every configured persona, checked against the contacts on file:
- ✅ "Head of Marketing" — covered (6 contacts match; Mary Major shortlisted)
- ✅ "Sales Director" — covered (11 contacts match; John Sample shortlisted)
- ✅ "Revenue Operations Manager" — covered (4 contacts match; Jane Doe shortlisted)
Supplementary: no IT/Security contact on file (department gap — not a configured persona).
All three configured personas are covered — you can open with Jane (RevOps, and she's visited). But there's no technical buyer on file, so if this deal touches data security or integration sign-off, that's a likely blocker.
**Suggested next step:** Get talking points for Jane Doe grounded in her recent visit to personalise your outreach, or tag Acme Corp and add it to a Leadfeeder list so it's tracked. To fill the IT / Security seat, re-run with a relaxed persona filter or run a Find Contact Data enrichment job.
Copy to run the next step:
```
Prep me for outreach to Jane Doe at Acme Corp
```
```
Add Acme Corp to a list in Leadfeeder
```
````
outreach-companion23.9 KB
---
name: outreach-companion
description: >
Pull visit context and recent signals for a named contact or company and return structured talking points for outreach. Use this skill when the user wants to prepare a personalised message — "draft outreach to Jane at Acme", "talking points for Acme", "prep me for outreach to Acme", "what do I say to [contact/company]", "help me reach out to [company]", "personalise my opener for [company]", "write an email to [contact]". Does NOT compose the email body — returns grounded talking points for Claude to write from.
metadata:
version: "1.0.0"
---
# Outreach Companion
Pull visit detail and recent signals for a named contact or company and return structured talking points. The talking points are the raw material — Claude composes the actual message from them. This keeps the output grounded in real data and the email voice in the user's control.
## Language
Produce the **entire response in the language the user wrote in** — English request → English output, German request → German output, and so on. This applies to everything the skill emits: the talking points, all field labels, edge-case messages, and any credit-consumption warning and confirmation. Note the talking points themselves should be drafted in the user's language so they're ready to use. Do **not** translate data values — company names, contact names, URLs, page paths, email addresses, and signal event names stay verbatim as the tools return them. Copy-paste prompts should also be written in the user's language. If the input language is unclear, default to English.
## When this skill triggers
Trigger when the user names a contact or company and asks for outreach help, talking points, or message prep. Also trigger when invoked by the Leadfeeder Agent as part of a chain (e.g. after `visitor-company-brief` surfaces a "Recommended next move" of running this skill). Trigger silently — do not ask the user to confirm.
Do not trigger on generic "write me a cold email" without a named target — that is not grounded in Leadfeeder data.
## Inputs to determine before executing
Identify these from the user's prompt. Use defaults if unspecified — do not ask.
- **Target** — a contact name + company, or a company alone. Required. Cannot proceed without at least a company.
- **Lookback window** — default last 90 days. Honor explicit overrides ("based on last week's visits", "last 30 days", "this quarter").
- **Format hint** — optional. If the user specifies a channel or length ("LinkedIn message", "cold email", "one-liner"), note it in the output so Claude can write appropriately. Do not alter the talking points structure — just surface the hint.
- **Draft language** — optional, and **independent of the chat language**. The user may work in English but want the message written in the prospect's language (e.g. German, Finnish). If they specify one ("draft in German", "in Finnish"), the drafted message must be in that language even though the rest of the response follows the chat language. If they don't specify, default the draft to the chat language and offer the option ("want this in the prospect's language instead?").
## Workflow
Once the target is resolved, `get_company`, `search_web_visits`, and `search_companies_signals` can be called in parallel. Keep total tool calls to four to stay within the 10-second budget.
### Step 0 — Resolve the Leadfeeder account (before any other tool call)
Determine the `account_id` to use for every tool call in this skill, in priority order:
1. **Account named in the request (highest priority).** If the prompt or scheduled-task instruction explicitly names an account — an account ID (e.g. "account 01234") or an unambiguous account name — use that account for all tool calls. This overrides every source below and is the recommended way to make an unattended/scheduled task fully self-contained.
2. **Configured Account ID.** Otherwise, the plugin's configured Leadfeeder Account ID is: `${user_config.account_id}`. If that resolves to a real, non-empty account ID (not blank, and not the literal unsubstituted `${user_config.account_id}` token), use it for all tool calls and do **not** call `get_account_info` or ask the user.
3. **Already selected this session.** Otherwise, if an account was already chosen earlier in the session, reuse that.
4. **Fallback.** Otherwise call `get_account_info`. If exactly one account is returned, use it. If several are returned, ask the user to pick. When running unattended (e.g. a scheduled task) asking is impossible — if no account was named and none is configured, stop and report that a **Leadfeeder Account ID** must either be named in the task prompt or set in the plugin configuration for automated runs.
### Step 1 — Resolve the target
**If a contact name is provided:**
- Always resolve the company first: call `search_companies` with the company name. Pick the highest-confidence match and capture the `company_id`. If the company is a group entity (role: "group"), note that contacts may be associated with subsidiary company IDs rather than the group parent — keep this in mind when cross-referencing.
- Then call `search_contacts` with the contact name as the only search term (do not pass `company_id` as a filter — the tool does not narrow results by company when used alongside name search terms). The call may return many same-name contacts across unrelated companies.
- Cross-reference manually: scan the returned contacts and match those whose `relationships.company.id` equals the resolved `company_id`. If the company is a group entity, also accept contacts whose company ID is listed as a known subsidiary if that information is available.
- If no match appears in the first page (default 25 results), paginate up to 2 additional pages (≤75 total). Stop after 3 pages regardless — do not paginate indefinitely for common names. If still no match, fall back to company-level data and inform the user: "Could not find [name] in the database for [company] — proceeding with company-level data only."
- If multiple plausible matches remain after cross-referencing, present the top 3 (name, title, company) and ask the user to disambiguate before continuing.
- **Once a contact is identified**, call `get_contact` with the contact's `id` to retrieve the full record including job title and email. Only the title and email from `get_contact` should appear in the Contact footer — `search_contacts` returns only a coarse `hierarchy_level` bucket, not the actual title.
- The `company_id` is already known from the `search_companies` call above — do not wait to extract it from the contact record.
**If company only (no contact named):**
- **If the input is already a numeric company ID**, use it directly as `company_id` and skip to Step 2.
- **If the input is a name or domain**, call `search_companies`. Pick the highest-confidence match.
- If multiple plausible matches, present the top 3 and disambiguate.
- If no match, tell the user clearly and stop.
### Step 1.5 — Subscription coverage & credit confirmation — before the deep reads in Step 2
`get_company` and `search_companies_signals` are credit-consuming — 1 credit per company **unless it was accessed within the last 12 months** (web-visits companies are inside that free window). First probe coverage for **free**: call `search_web_visits` for `company_id` over the **last 12 months** (this also supplies the visit data Step 2 needs — reuse it).
Then, before any credit-consuming call, flag it and get a single go-ahead. The target isn't guaranteed to be a visitor, so use this **conditional** wording — never the generic "1 credit per company" alarm:
> To prep your outreach I need **{Company}**'s visit detail, firmographics, and signals using `get_company` and `search_companies_signals` — credit-consuming tools. If {Company} is in your Web Visitors feed, its 12-month access window has already started, so no credits are charged for a record already unlocked within the last 12 months. If it hasn't visited in the last 12 months, this will consume about 2 credits (firmographics 1 + signals 1). Want me to proceed?
On a clear **yes** → run Step 2's deep calls and report the actual `meta.credits.charged` (0 for a covered/visitor record). On **no** → stop; if the company was uncovered, offer only the free basics (name, website) rather than the full prep.
### Step 2 — Fetch in parallel (once company_id is known)
Once Step 1.5 is **COVERED** or the user has consented to the credit cost, call all three in parallel (the `search_web_visits` call may already be done from the coverage probe — reuse it rather than repeat it):
- `get_company` with `include=crm_connections.crm_record.crm_owner` — firmographics: industry, employee count, location, intent score (read `attributes.intent.score` / `attributes.intent.score_tier` — nested under `attributes`, may be `null`), description. Also read CRM status: plain `include=crm_connections` returns only a stub (`crm_record` = `{id, type}`, no owner); the deeper path sideloads the record and owner. A populated `relationships.crm_connections` means the company is in the CRM. Each `crm_record` is polymorphic (`crm_record.type` = `crm_organization` | `crm_contact` | `crm_lead`): read **URL** from `crm_record.attributes.crm_url`, **name** by type (`crm_organization` → `attributes.name`; `crm_contact`/`crm_lead` → `attributes.first_name` + `attributes.last_name`), **owner** from `crm_record.relationships.crm_owner.attributes.name` (say "owner not set" if only a stub). **Render as a markdown link** — record name is the clickable text, `crm_record.attributes.crm_url` the target; never print the raw URL or leave the name unlinked. Do not call `get_company_financials`.
- `search_web_visits` — filtered by `company_id`, using the user's lookback window. Aggregate the same fields the Visit context section renders (keep terminology identical to `visitor-company-brief`): **total sessions**, **first seen and last seen**, **top landing pages** by frequency (use `landing_page_path` — the `engagements` array within each visit record is typically empty and cannot be used for per-page path analysis), **top sources** (`source` + `medium`), **deep visits** (`page_depth >= 3` or `visit_length >= 60s`), and the identified-vs-anonymous breakdown. **In the output, describe deep visits in plain language** (sales reps read this too): "viewed 4 pages", "spent 83 seconds" — never "page depth 4" or "dwell". `page_depth`/`visit_length` are internal fields; translate them into plain page counts and seconds.
- `search_companies_signals` — filtered by `company_id`. Apply default filters:
- **Recency:** `event_date.from` = today minus 90 days (or the user's lookback if longer).
- **Exclude regulatory noise:** Pass the full default `categories` array, omitting `regulatory_compliance_updates`:
```
["business_expansion", "competitive_landscape", "event_participation", "industry_recognition", "leadership_changes", "corporate_challenges", "mergers_and_acquisitions", "customer_acquisition", "investment_activity", "product_and_service_development", "partnerships_and_collaborations", "financial_struggles", "job_ads", "register_updates"]
```
### Step 3 — Synthesise talking points
Derive 2–4 talking points from the data. A talking point is not raw data — it is an interpreted angle the user can open with or reference in their message. Each point should say what to mention and why it resonates.
Prioritise talking points that connect visit behaviour to a business signal:
- A deep pricing visit + active hiring → buying evaluation + org growth
- A return visit to /integrations after a gap → re-engagement, specific use-case interest
- A leadership change + first visit → new decision-maker warming up
- A first visit from a LinkedIn ad → paid intent, already aware of the product
If visit data is thin or absent, fall back to signals-only talking points. If both are absent, tell the user plainly — do not pad with generic outreach advice.
**Factor in CRM status.** If the company is already owned in the CRM by someone else, the talking points still stand, but flag that outreach should be coordinated with the owner rather than sent cold. If it is not found in your CRM, that is a clean net-new outreach — worth tagging it and adding it to a Leadfeeder list to track it (`add-to-pipeline`; that writes tags/lists inside Leadfeeder, it does not create a CRM record).
**Keep talking points consistent with the data above.** A talking point restates facts from the Visit context / Recent signals sections — it must use the *exact same* values: the same set of countries/cities, the same counts, the same dates. If the Identified line says "5 contacts across DE, CH, AT, SE, FR", a talking point about geography must name that same set and count — do not re-derive it, mix cities with countries, or drop/add locations (e.g. don't turn "DE, CH, AT, SE, FR" into "Munich, Düsseldorf, Frankfurt, Sweden, Austria", which is 3 German cities + 2 countries and silently loses CH and FR). A count you assert ("five countries") must equal the items you list.
## Output format
**Output contract — identical every run.** Reproduce the structure below **exactly**, on every invocation, regardless of anything earlier in this thread. If you've already run this skill in the conversation, do not vary, restyle, or "improve" the format next time — output the same template verbatim; this template is the single source of truth. Keep the exact headings, field labels, and order; don't invent new sections, don't rename fields, and add no decorative emoji beyond those the template already uses (✅ ⚠️ ❌).
**Status flags — be honest about gaps.** Use ✅ (strong), ⚠️ (thin/caution), ❌ (missing) on the data-quality lines — flag "❌ no visits in the window" or "⚠️ all anonymous" as plainly as a strong signal, so the user knows how solid the grounding is. One flag per line.
The outer block below is shown with four backticks so the copy blocks nest correctly — your actual output is plain markdown.
````markdown
## Outreach Companion — {Contact Name at Company Name / Company Name}
**Why reach out now:** {One sentence — the single strongest reason to reach out today, grounded in data.}
---
### Visit context (last {N} days)
- **Total sessions:** {N}
- **First seen / Last seen:** {date} / {date}
- **Top landing pages:** {/path1, /path2, /path3}
- **Top sources:** {Direct, LinkedIn CPC, Google organic}
- **Deep visits:** {e.g. "1 deep visit — 4 pages, 83s, hit /pricing and /integrations/salesforce" or "All sessions single-page"}
- **Identified:** {e.g. "✅ 1 of 3 sessions identified" or "⚠️ All anonymous — no identified contact yet"}
{If no visit data in the window: "❌ No visits recorded in the last {N} days. Talking points are signals-only."}
### Recent signals
- {Signal 1 — date — plus a short plain line on why it's relevant to this outreach} *(most outreach-relevant first)*
- {Signal 2 — date — why it matters}
- {If none: omit this section, or if you mention the absence say plainly "No notable signals in the last 90 days." Never expose filter mechanics ("non-job-ad", category names, "regulatory excluded", "90-day window" jargon).}
### Talking points
1. **{Angle}** — {One sentence: what to say and why it lands. Ground it in the data above.}
2. **{Angle}** — ...
3. **{Angle}** — ...
{2–4 points max. Quality over quantity.}
---
{If contact was resolved} **Contact:** {Name} · {Title} · {Company}
{Email line: include ONLY if the user explicitly named the contact in the prompt} **Email:** {email}
{CRM line: if CRM-connected} **CRM:** [{record name}]({crm_record.attributes.crm_url}) · Owner: {owner name — or 'owner not set'} — coordinate with the owner before sending. {Record name derived by type per Step 2. If not connected, omit this line, or note "Not in CRM — clean net-new outreach."}
{If format hint provided} **Format hint:** {e.g. "LinkedIn message — keep it under 300 characters"}
---
**Draft it in one click** — copy the prompt for the channel you want (if a format hint was given, keep only the matching one; append a language if you want the message in the prospect's language, e.g. "… in German"). When the message is drafted, it must follow the **Drafting guardrails** below.
```
Write this as a cold email using the talking points above
```
```
Write this as a LinkedIn message using the talking points above
```
````
## Drafting guardrails (apply when the message is written from the talking points)
The talking points are internal prep. The **drafted message goes to the prospect**, so it must never read as surveillance ("spooky"). When you write the cold email / LinkedIn message from the points:
- **Never recite tracking specifics to the prospect.** No exact session durations ("you spent 23 minutes"), no page-by-page paths ("you looked at /pricing, /integrations, /demo"), no visit counts or cadence ("your 14th session", "a second deep visit last week"). Reciting exactly what someone did and for how long is what makes it creepy — it's the single thing to avoid.
- **Reference interest softly and at a high level** — "noticed your team's been exploring how we help with {topic}", "thought the timing might be right" — grounded in the signal without exposing the raw analytics behind it.
- **Lead with value and relevance to their world** (their hiring, expansion, use case, industry), not with what your tracker observed.
- The talking points may hold specifics for *your* understanding; the message must translate them into a natural, non-invasive opener. If a talking point can only be expressed by quoting tracking detail, drop it from the message rather than make it creepy.
- **Language:** write the message in the requested **Draft language** if one was given (it can differ from the chat language — e.g. English chat, German message); otherwise use the chat language.
## Edge cases
- **No visit data in the lookback window.** Say so clearly. Still produce signals-based talking points if signals exist. If neither exists, tell the user the company is cold and suggest running `visitor-company-brief` to see the broader engagement history.
- **Contact not found after 3 pages.** Common names (e.g. "Stefan Koch") return hundreds of results. Paginate up to 3 pages of `search_contacts`, cross-reference each page against the resolved `company_id`. If still no match, tell the user and proceed with company-level data only. Do not keep paginating indefinitely.
- **Contact not found — group company.** If the resolved company is a group entity, the contact may be stored under a subsidiary company ID rather than the group parent. Note this to the user if cross-referencing fails: "ATOSS is a group entity — the contact may be on a subsidiary record not captured here."
- **Multiple contact matches.** Present top 3 (name, title, company) and wait for disambiguation. Do not silently pick.
- **Company not found.** Stop and tell the user. Offer to try a broader name fragment or a domain.
- **No signals in the window.** Omit the signals section (or state it plainly as "No notable signals in the last 90 days"). Never expose the category filter — no "non-job-ad", category names, "regulatory excluded", or "90-day window" jargon. Do not fabricate events.
- **Agent-invoked with prior brief context.** If this skill is invoked by the Leadfeeder Agent after `visitor-company-brief`, the company_id is already known — skip Step 1 entirely and proceed to Step 2 directly.
## Source citation rule
Every talking point must be traceable to a specific tool response — a page URL, a signal event, a visit date. CRM record name, URL, and owner must come from the sideloaded `crm_record` (and its `crm_owner`) under `relationships.crm_connections` on the `get_company` response — `crm_record.attributes.crm_url`, the name from `crm_record.attributes` per record type, and `crm_record.relationships.crm_owner.attributes.name` — never assert a company is (or isn't) in the CRM without it. Do not invent angles. If the data is thin, fewer talking points is correct.
**Internal consistency (trust).** Derive each fact — counts, country/city lists, dates, page paths, numbers — once from the tool data and restate it identically everywhere in the output. A stated count must equal the items you actually list, and a location set must be the same set wherever it appears (cities and countries must resolve to the same places). Before finishing, re-read the talking points against the Visit context / signals sections and fix any mismatch — two sections disagreeing about the same fact is a trust-breaker.
## What NOT to do
- **Do not call a visitor an "account" or a "hot/warm lead."** A website visitor is a **company** — "account" implies a CRM relationship this data doesn't carry. Reserve "account" for the user's Leadfeeder account (workspace/ID) or a genuine CRM record; use "high-intent", "active", "engaged", or "returning" instead of "hot"/"warm".
- **Do not compose the email or message body.** Return talking points only. Claude writes the message from them.
- **Do not expose the contact's email** unless the user explicitly named the contact in their prompt (e.g. "draft outreach to jane.doe@acme.com" or "talking points for Jane Doe at Acme"). Company-level or agent-chained invocations do not qualify.
- **Do not trigger enrichment jobs** (`create_company_enrichment_job`, `create_find_contact_data_job`). This skill is free and read-only.
- **Do not call `get_company_financials`.** It charges per call and is not needed for outreach prep.
- **Do not fabricate visit pages, signal events, or contact details.** If data is absent, say so.
- **Do not pad with generic cold outreach advice** when Leadfeeder data is thin. Less is more.
- **Do not recite visitor-tracking specifics in a drafted message to the prospect** — exact session durations, page-by-page paths, or visit counts read as surveillance ("spooky"). Reference interest softly instead (see Drafting guardrails).
## Example: good output
````markdown
## Outreach Companion — Jane Doe at Acme Corp
**Why reach out now:** Acme returned for a third pricing-page session this week while actively hiring SDRs in EMEA — active evaluation with a growing sales team is a strong entry point.
---
### Visit context (last 90 days)
- **Total sessions:** 4
- **First seen / Last seen:** 2026-04-15 / 2026-06-23
- **Top landing pages:** /pricing, /integrations/salesforce, /docs/api, /case-studies/saas
- **Top sources:** LinkedIn CPC, Direct
- **Deep visits:** 2 deep visits — longest was 5 pages, 97s, concentrated on /pricing and /integrations/salesforce
- **Identified:** ✅ 1 of 4 sessions identified
### Recent signals
- Hiring: SDR EMEA (3 open roles) — 2026-06-10 — growing outbound team, a fit for visitor intelligence
- Hiring: Revenue Operations Manager — 2026-06-18 — building out RevOps, our core buyer
- Investment: Series B announced — 2026-05-14 — fresh budget, expansion mode
### Talking points
1. **Pricing evaluation in progress** — Three sessions on /pricing in the last week suggests active comparison or internal approval prep. Reference it without being creepy: "noticed companies like yours often come back to pricing when they're building the business case."
2. **SDR team expansion** — Three open SDR roles in EMEA means a growing outbound motion. Connect the product to the new headcount: "as you scale the team, visitor intelligence helps reps prioritise accounts before the first dial."
3. **Post-Series B timing** — Fresh capital and hiring momentum is a classic expansion window. Opening line: "congrats on the Series B — a lot of our best conversations happen right after a round, when pipeline pressure starts showing up."
---
**Contact:** Jane Doe · Head of Revenue Operations · Acme Corp
**Email:** jane.doe@acme.com
**CRM:** [Acme Corp](https://app.hubspot.com/contacts/12345/company/67890) · Owner: John Doe — coordinate with John before sending.
---
**Draft it in one click:**
```
Write this as a cold email using the talking points above
```
```
Write this as a LinkedIn message using the talking points above
```
````
visitor-company-brief29.3 KB
---
name: visitor-company-brief
description: >
Produce a deep, structured company brief on a single named company from Leadfeeder — firmographics, engagement history, contacts on file, recent intent signals, and a recommended next move. Use this skill when the user names a specific company and asks for research, such as "tell me about [company]", "brief me on [company]", "deep dive on [company]", "company brief for [company]", "account brief for [company]", "research [company]", "everything you have on [company]", "give me a profile of [company]", or asks for a full read on a single company they want to action.
metadata:
version: "1.0.0"
---
# Visitor Company Brief
Produce a structured deep-dive on a single named company. Anchor every section in real Leadfeeder data. This is the depth complement to the Daily Visitor Brief (which is breadth across many companies in a 24h window).
## Language
Produce the **entire response in the language the user wrote in** — English request → English output, German request → German output, and so on. This applies to everything the skill emits: the summary, all section headings and field labels, reasoning, edge-case messages, and the credit-consumption warning and confirmation. Do **not** translate data values — company names, contact names, URLs, page paths, email addresses, tag names, and ICP names stay verbatim as the tools return them. Copy-paste follow-up prompts should also be written in the user's language (they still trigger the right skill). If the input language is unclear, default to English.
## When this skill triggers
Trigger when the user names ONE specific company and asks for research, a profile, a brief, or a deep dive. Do not trigger if the user is asking about multiple companies or a ranked list across visitors — that is the Daily Visitor Brief skill. Trigger silently. Honor explicit modifiers in the prompt (engagement window, focus area, depth).
## Inputs to determine before executing
Identify these from the user's prompt. Use defaults if unspecified — do not ask.
- **Company identifier** — the name, domain, or Leadfeeder company ID provided in the prompt. Required. If multiple candidates match, see "Disambiguation" below.
- **Engagement window** — default last 90 days. Honor explicit overrides ("last 30 days", "this quarter", "since January").
- **Focus area** — default: full brief. If the user is narrow ("focus on hiring", "just engagement", "ICP fit only"), produce a focused version that drops the other sections.
## Workflow
Execute these tool calls. Always re-fetch ICPs/personas (do not cache across runs).
### Step 0 — Resolve the Leadfeeder account (before any other tool call)
Determine the `account_id` to use for every tool call in this skill, in priority order:
1. **Account named in the request (highest priority).** If the prompt or scheduled-task instruction explicitly names an account — an account ID (e.g. "account 01234") or an unambiguous account name — use that account for all tool calls. This overrides every source below and is the recommended way to make an unattended/scheduled task fully self-contained.
2. **Configured Account ID.** Otherwise, the plugin's configured Leadfeeder Account ID is: `${user_config.account_id}`. If that resolves to a real, non-empty account ID (not blank, and not the literal unsubstituted `${user_config.account_id}` token), use it for all tool calls and do **not** call `get_account_info` or ask the user.
3. **Already selected this session.** Otherwise, if an account was already chosen earlier in the session, reuse that.
4. **Fallback.** Otherwise call `get_account_info`. If exactly one account is returned, use it. If several are returned, ask the user to pick. When running unattended (e.g. a scheduled task) asking is impossible — if no account was named and none is configured, stop and report that a **Leadfeeder Account ID** must either be named in the task prompt or set in the plugin configuration for automated runs.
### Step 1 — Resolve the company
Determine the Leadfeeder `company_id` from the user's input.
- **If the input is already a numeric company ID** (e.g. `228035150`), skip to Step 2.
- **If the input is a company name or fragment** (e.g. "Acme Corp", "Acme"), call `search_companies` with the name as the query. Pick the highest-confidence match.
- **If the input is a domain** (e.g. "acme.com"), call `search_companies` with the domain.
- **If `search_companies` returns multiple plausible matches**, present the top 3 (name, country, employee count, industry) and ask the user to disambiguate before continuing. Never silently pick when there is real ambiguity (e.g. two German companies with similar names).
- **If `search_companies` returns no matches**, tell the user clearly. Offer to expand the search (e.g. with a broader name fragment) rather than fabricating a profile.
### Step 2 — Load ranking context
Call `get_icps` and `get_buyer_personas`. Always re-fetch on every invocation. The calls are free (no credits). Use the response to score the target company against ICP and persona criteria in Step 5.
### Step 2.5 — Subscription coverage & credit confirmation — before any credit-consuming call
`get_company`, `search_companies_signals`, and `get_company_financials` are credit-consuming — 1 credit per company **unless it was accessed within the last 12 months** (companies in your web-visits feed are inside that free window). First probe coverage for **free**: call `search_web_visits` filtered by `company_id` over the **last 12 months** (reuse it for Step 4 engagement).
Then, before any credit-consuming call, **do not call `get_company` or `search_companies_signals` yet** — flag it and get a single go-ahead. The target isn't guaranteed to be a visitor, so use this **conditional** wording, never the generic "1 credit per company" alarm:
> To build this brief I need **{Company}**'s firmographics and signals using `get_company` and `search_companies_signals` — credit-consuming tools. If {Company} is in your Web Visitors feed, its 12-month access window has already started, so no credits are charged for a record already unlocked within the last 12 months. If it hasn't visited in the last 12 months, this will consume about 2 credits (firmographics 1 + signals 1; +1 more only if you later ask for financials). Want me to proceed?
On a clear **yes** → proceed with Steps 3–6 and report the actual `meta.credits.charged` (0 for a covered/visitor record). On **no** → give only the free basics (name and website from `search_companies`, and "no visit activity in the last 12 months") and stop.
### Step 3 — Pull firmographics
Only proceed here once Step 2.5 is **COVERED** or the user has explicitly consented to the credit cost. Call `get_company` for the resolved `company_id` with `include=crm_connections.crm_record.crm_owner,tags`. Track `meta.credits.charged` (0 = it was in the free 12-month window; 1 = this call charged).
Capture: industry, employee_count and range, location (HQ city and country), B2B/B2C orientation, founded year, revenue/earnings/net_worth where present, **intent score and tier (from `attributes.intent.score` (0–10, may be `null`) and `attributes.intent.score_tier` — nested under `attributes`, not a top-level `intent.score`; if null, show "not yet scored"). Prefix the displayed score with a tier dot by `score_tier`: 🟢 high · 🟠 medium · ⚪️ low (omit the dot when "not yet scored")**, description, legal form, register status.
**Use one canonical figure per metric across the entire brief.** `get_company` returns both an exact `employee_count` (e.g. 382) and a coarser range/bucket. Treat the **exact `employee_count`** as the single source of truth and use that same number everywhere — the one-sentence intro, the Snapshot, and any reasoning. If you round it in prose, round the *same* number consistently (382 → "~380" or "380+" — never "230+" or a different bucket). Same rule for revenue and any other figure. Two different numbers for the same metric in one brief (e.g. intro says "230+ employees", Snapshot says "382") is a trust-breaker — the intro and Snapshot must never disagree.
**CRM connections (sideloaded).** Request `include=crm_connections.crm_record.crm_owner` (plain `include=crm_connections` returns only a stub — `crm_record` as `{id, type}` with no name, URL, or owner). A populated `relationships.crm_connections` means the company is in the CRM. Each `crm_record` is JSON:API-shaped and **polymorphic** (`crm_record.type` = `crm_organization` | `crm_contact` | `crm_lead`): read **URL** from `crm_record.attributes.crm_url`; **name** by type (`crm_organization` → `attributes.name`; `crm_contact`/`crm_lead` → `attributes.first_name` + `attributes.last_name`); **owner** from `crm_record.relationships.crm_owner.attributes.name` (say "owner not set" if only a stub). **Render as a markdown link** — the record name is the clickable text and `crm_record.attributes.crm_url` is the target; never print the raw URL or leave the name unlinked. This complements the firmographics with the company's status in the user's own CRM — surface it in the Snapshot and factor it into the recommendation.
**Tags (sideloaded via `include=tags`).** Capture each assigned tag's `attributes.name` and `attributes.color`. Note: `color` is an integer palette **index** (0–N), not a hex code — the API doesn't expose the hex, and a plain markdown brief can't render background-filled chips anyway. So surface tags as plain-text chips (backtick `` `Hot Lead` `` style) using their **names**; do not invent a colour. Show them in the Snapshot because customers use tags to prioritise accounts, so they inform the recommended next move.
**Ignore `web_engagement.last_visit_date`** — it lags the real visits stream. Use Step 4 as the source of truth for visit timing.
**Flag register status `out_of_business` or `non_company` immediately** and stop the brief. Tell the user the company is not actionable and skip the rest of the workflow.
Do not call `get_company_financials` by default. It charges per call. Only call it if the user explicitly asks for "financials", "company financials", "balance sheet", or similar.
### Step 4 — Pull engagement history
Call `search_web_visits` with `filters.company_id` set to the resolved company. Use the user's engagement window (default last 90 days).
Aggregate the response to produce:
- **Total sessions** in the window
- **First seen** and **last seen** timestamps in the window
- **Top landing pages** by visit count (deduplicate paths, show top 5)
- **Top traffic sources** (Direct, Google organic, Bing organic, Google CPC, LinkedIn, etc.)
- **Trend** — qualitative read: accelerating, stable, declining, dormant. Compare the most recent 30 days vs the 30 days before.
- **Identified visits** — count only. Do NOT expose contact names or emails. Note the number of distinct identified contacts.
- **Deep visits** — flag any session with `page_depth >= 3` or `visit_length >= 60s` as a "deep visit" worth calling out. **In the output use plain language** (this brief is read by sales reps too): "viewed 3 pages", "spent 71 seconds" — never "page depth 3" or "dwell". `page_depth`/`visit_length` are internal API fields; translate them into plain page counts and seconds, never print the field name or the word "depth".
### Step 5 — Pull contacts on file
Call `search_contacts` filtered by the company ID. Use the response to produce a **non-PII summary**:
- **Total contacts** on file (count)
- **Department breakdown** (e.g. 12 in Engineering, 8 in Sales, 5 in Marketing)
- **Seniority breakdown** (e.g. 2 C-level, 4 VPs, 10 managers, 15 ICs)
- **Buyer Persona matches** — for each configured persona, count how many contacts match; flag ✅ when ≥1 and ❌ when zero (an honest gap callout — surface the empty personas, don't hide them)
- **Decision-maker availability** — ✅ Yes / ⚠️ Limited / ❌ No: are there senior-enough contacts (top management, VP/Director level, etc.) on file to make outreach worth doing?
**Do NOT list individual contact names, emails, phone numbers, or LinkedIn URLs** in the brief output. Contact detail belongs in the separate Find My Buyer skill, which is invoked explicitly. This brief is a research overview, not a contact list.
### Step 6 — Pull recent signals
For a **COVERED** company (visited in the last 12 months) this is free — always run it; do not gate or skip it on cost grounds. For an **UNCOVERED** company it charges 1 credit (if the company has signals), so only run it under the consent obtained in Step 2.5. Call `search_companies_signals` for the single `company_id`. Apply the same default filters as the Daily Visitor Brief:
- **Recency window:** `event_date.from` = today minus 90 days (or override the user's engagement window if longer).
- **Exclude regulatory noise:** Pass an explicit `categories` array containing every category EXCEPT `regulatory_compliance_updates`. Full default categories:
```
["business_expansion", "competitive_landscape", "event_participation", "industry_recognition", "leadership_changes", "corporate_challenges", "mergers_and_acquisitions", "customer_acquisition", "investment_activity", "product_and_service_development", "partnerships_and_collaborations", "financial_struggles", "job_ads", "register_updates"]
```
Group the returned signals by category. Highlight the ones that move the recommended action (hiring spikes, funding events, leadership changes, M&A activity, financial stress).
**Signal language — plain, and always explain relevance.** The category filtering is an internal mechanic — **never surface it in the output** (no "non-job-ad", no category names, no "excluding regulatory", no "90-day window" jargon). When nothing relevant is found, say exactly **"No notable signals in the last 90 days."** When a signal *is* present, pair it with a short plain-language reason it matters in this context — a raw event with no "so what" isn't useful (e.g. "Hiring 3 SDRs in EMEA — a growing outbound team, a fit for visitor intelligence"; "Raised Series B — fresh budget, likely expanding tooling").
### Step 7 — Compute ICP match and synthesise recommendation
Score the company against each configured ICP from Step 2:
- **Full match** — all filters satisfied (location, employee count, industry, B2B/B2C orientation as applicable).
- **Partial match** — some filters satisfied. Call out which dimensions match and which don't (e.g. "DE + Pro Services match, employee count under threshold").
- **No match** — none of the configured ICPs apply.
Then synthesise a **Recommended next move** section. This is the one part of the brief where the skill takes a position. Base the recommendation on:
- ICP fit (sales-worthy or not)
- Engagement signal strength
- Recent signals that suggest a moment of opportunity (hiring HR roles, raised funding, lost a key exec, etc.)
- Decision-maker availability
- **CRM status** — this changes the action:
- *Connected with an owner* → do not recommend cold outreach; recommend coordinating with / looping in the owner and checking their pipeline first.
- *Connected but no owner (orphaned)* → flag it as a routing gap worth resolving.
- *Not in CRM* → flag it as a missed-routing gap; consider adding it (via `add-to-pipeline`) before outreach.
Surface 1–3 next-step actions. **Phrase each in plain language, outcome first — describe what it does for the user, then the copy-paste prompt. Never a bare "Run `skill`".** The follow-ons and their honest outcomes:
- **Find who to contact** — a ranked shortlist of the right people with names, titles, and emails. (`find-my-buyer`)
- **Prep outreach** — visit-grounded talking points for a personalised opener. (`outreach-companion`)
- **Add to your pipeline** — tags the company and adds it to a Leadfeeder list so it's tracked. (`add-to-pipeline`) Say exactly that — it writes tags and lists **inside Leadfeeder**; it does **not** create a CRM record or assign a CRM owner. (If the company should also live in the user's CRM, that's a separate manual step — don't imply `add-to-pipeline` does it.)
Pick the 1–3 most relevant given the ICP verdict, engagement signal, and available contacts. This is what makes the brief actionable.
## Output format
**Output contract — identical every run.** Reproduce the structure below **exactly**, on every invocation, regardless of anything earlier in this thread. If you've already run this skill in the conversation, do not vary, restyle, or "improve" the format next time — output the same template verbatim; this template is the single source of truth. Keep the exact headings, field labels, and order; don't invent new sections or tiers, don't renumber, don't rename fields, and add no decorative emoji beyond those the template already uses (✅ ❌ ⚠️ ↩ and the 🟢 / 🟠 / ⚪️ intent dots).
**Status flags — be honest about gaps.** On coverage/availability/status lines use ✅ (present/strong), ⚠️ (partial/thin), ❌ (gap/missing), and surface gaps as prominently as strengths — an honest ❌ ("no contacts on file for this persona") builds trust more than only showing wins. One flag per line; don't over-decorate.
**Stamp the brief with a generation timestamp.** The line directly under the title is `*Generated {current date} · engagement window: last {N} days*` — use the **current date** (the brief is a point-in-time snapshot). This is the *generation* timestamp ("as of" date); it is distinct from visit dates and must always be present, even when visit data is sparse. Do not substitute a visit date for it.
Render as a single structured brief. Use this exact structure (the outer block is shown with four backticks so the copy blocks nest correctly — your actual output is plain markdown):
````markdown
# Company Brief — {Company Name}
*Generated {current date} · engagement window: last {N} days*
{One-sentence positioning: what they do, key size signal, ICP verdict in one line. The size signal must use the SAME exact employee_count/revenue shown in the Snapshot below — never a different figure or bucket.}
## Snapshot
- **Industry:** {primary + secondary industries}
- **Size:** {employee count} · {revenue if present}
- **Location:** {city, country} (HQ)
- **Legal form / founded:** {legal form} · founded {year}
- **Intent score:** {🟢 high / 🟠 medium / ⚪️ low} {score} ({tier})
- **ICP match:** {✅ Full / ⚠️ Partial / ❌ No match} — {one-line reason}
- **Tags:** {Assigned tags as plain-text chips by name — e.g. `` `Hot Lead` ``, `` `DACH` ``. Omit the line if none. Tag colours are configured in Leadfeeder but can't render as background-filled chips in this markdown brief, so show names only.}
- **CRM:** {If connected: "[{record name}]({crm_record.attributes.crm_url}) · Owner: {owner name — or 'owner not set'}" (record name derived by type per the capture rules) — one line per record if more than one is linked. If not: "Not found in your CRM".}
## Engagement (last {N} days)
- **Total sessions:** {N}
- **First seen / Last seen:** {date} / {date}
- **Top landing pages:** {/path1, /path2, /path3}
- **Top sources:** {Direct, Google CPC, Bing organic}
- **Deep visits:** {count and short description of the deepest one, e.g. "1 deep visit yesterday — 3 pages, 71s, hit /pricing"}
- **Trend:** {accelerating / stable / declining / dormant} — {one-line interpretation}
- **Identified visits:** {N sessions from {M} distinct identified contacts} (names withheld; see Find My Buyer skill)
## People on file ({N} contacts)
- **Departments:** {top 3 departments with counts}
- **Seniority:** {C-level: X, VP/Director: Y, Manager: Z, IC: W}
- **Buyer Persona matches:** (flag each — ✅ if ≥1 match, ❌ if zero)
- ✅ "{Persona name}" → {N matching contacts}
- ❌ "{Persona name}" → 0 (gap: no contacts on file)
- **Decision-maker availability:** {✅ Yes / ⚠️ Limited / ❌ No} — {one-line on what's there}
## Recent signals (last 90 days)
{Group by category. Use a sub-heading per non-empty category. Omit empty categories entirely. Each grouping's cluster gets a one-line "why it matters"; never expose filter mechanics. If nothing relevant: replace this whole section with the single line "No notable signals in the last 90 days."}
### Hiring ({N} ads)
- {Role 1} — {date}
- {Role 2} — {date}
- ... (cap at 5, summarise the rest as "+N more")
*Why it matters: {one plain line, e.g. "hiring across HR + IT Security suggests an org rebuild — a fit for our RevOps narrative"}.*
### Leadership changes ({N})
- {Event} — {date}
*Why it matters: {e.g. "new decision-maker settling in — a natural moment to introduce a new approach"}.*
### Funding / Investment ({N})
- {Event} — {date}
*Why it matters: {e.g. "fresh capital — expansion mode, likely evaluating new tooling"}.*
(Other categories as relevant, each with its own "why it matters" line.)
## Recommended next move
{One short paragraph synthesising the read. Then 1–3 actions, formatted as a list.}
- **{Action 1}** — {short rationale, e.g. "Find who to contact — a ranked shortlist of the right people."}
- **{Action 2}** — {short rationale}
- **{Action 3}** — {short rationale}
Copy any of these to run the next step (each is a ready-to-send prompt with a one-click copy button):
```
Who should I reach out to at {Company Name}?
```
```
Prep me for outreach to {Company Name}
```
---
**Company website:** {company.url from get_company}
````
**Copy-block convention.** Emit one fenced code block per recommended action above — fenced blocks give the user a one-click **Copy** button. Map each action to its ready-to-send prompt: `find-my-buyer` → `Who should I reach out to at {Company Name}?`; `outreach-companion` → `Prep me for outreach to {Company Name}`; `add-to-pipeline` → `Add {Company Name} to a list in Leadfeeder`. Only include the blocks for the actions you actually recommended.
## Edge cases
- **Company not found by `search_companies`.** Tell the user clearly. Offer to expand search ("try a different name fragment or a domain"). Do not fabricate a profile.
- **Multiple plausible matches.** Present top 3 with disambiguation criteria (country, employee count, industry). Wait for user input before continuing.
- **Register status is `out_of_business` or `non_company`.** Stop the brief immediately. Tell the user and offer an alternative ("Did you mean a different entity?"). Do not waste tokens on a dead company.
- **No web visits in the engagement window.** Say so clearly. Optionally offer to expand the window. Do not invent visits.
- **No contacts on file.** Say so. Flag this as a gap — recommend running enrichment ("Want me to find contacts via the Find Contact Data job?") but do not auto-trigger it (cost).
- **No signals in the 90d window.** Say "No notable signals in the last 90 days." Move on. This is not unusual for smaller or quieter companies.
- **ICP mismatch.** Surface the verdict honestly. Do not pad with "but they could still be interesting" unless the engagement signal is unusually strong (e.g. deep pricing visits).
- **User asks for financials.** Then and only then call `get_company_financials`. Quote the credit cost in the response.
- **Tool errors.** Report transparently. Do not silently substitute stale data.
## Source citation rule
Every claim in the brief must be grounded in data returned by the Leadfeeder MCP tools. Use the company's website (`company.url` field from `get_company`) as the actionable link in the footer. CRM record name, URL, and owner must come from the sideloaded `crm_record` (and its `crm_owner`) under `relationships.crm_connections` — `crm_record.attributes.crm_url`, the name from `crm_record.attributes` per record type, and `crm_record.relationships.crm_owner.attributes.name` — never infer that a company is (or isn't) in the CRM without that data; if no connection is returned, treat it as "Not found in your CRM". Do not invent URLs.
**Internal consistency (trust).** Beyond the canonical-figure rule in Step 3, every fact restated across sections — counts, country/city lists, dates, page paths — must match exactly. A stated count must equal the items you list, and a location set must be the same set wherever it appears (cities and countries resolving to the same places). The intro, Snapshot, Engagement, People, and Recommended-next-move sections must never disagree about the same fact. Re-check before finishing.
**Known gap (same as Daily Visitor Brief):** The MCP `get_company` response does not currently include a Leadfeeder app deeplink. Until the MCP adds this, the brief footer links out to the prospect's own website only. If a future MCP version returns an app URL field, update this skill to add an "Open in Leadfeeder" line below the "Company website" line.
## What NOT to do
- **Do not call a visitor an "account" or a "hot/warm lead."** A website visitor is a **company** — "account" implies a CRM relationship this data doesn't carry. Reserve "account" for the user's Leadfeeder account (workspace/ID) or a genuine CRM record; use "high-intent", "active", "engaged", or "returning" instead of "hot"/"warm".
- **Do not expose individual contact PII** (names, emails, phone numbers, LinkedIn URLs). Counts, departments, seniority levels, and persona-match counts are fine. Identifying detail is not.
- **Do not trigger enrichment jobs** (`create_company_enrichment_job`, `create_find_contact_data_job`). This brief is research, not action. Offer to invoke them as a "Recommended next move" only.
- **Do not call `get_company_financials` by default.** It charges per call. Only on explicit request.
- **Do not invent data.** ICP match, signal counts, page views, contact counts — only report what the tools return.
- **Do not chain to outreach drafting, CRM writes, or list management** on direct user invocation without explicit opt-in. Recommend them as next moves using the skill names above. Exception: if invoked by the Leadfeeder Agent as part of a declared multi-step chain, the agent may proceed with `find-my-buyer`, `outreach-companion`, or `add-to-pipeline` without waiting for confirmation.
- **Do not produce a brief for an `out_of_business` company.** Stop and notify the user instead.
## Example: good output
````markdown
# Company Brief — Acme Corp
*Generated 2026-05-29 · engagement window: last 90 days*
European technical inspection, testing, certification, and training group. €1.58B revenue, 14,271 employees across 70+ countries. ✅ Full ICP match — DE · Pro Services · enterprise size · B2B.
## Snapshot
- **Industry:** Technical Testing & Analysis · Management Consultancy · Educational Support
- **Size:** 14,271 employees · €1.58B revenue (2023)
- **Location:** Hannover, DE (HQ) · 70+ country presence
- **Legal form / founded:** GmbH
- **Intent score:** 🟢 10 (HIGH)
- **ICP match:** ✅ Full — DE location, Pro Services industry, 10000+ employee bucket, B2B
- **Tags:** `Tier-1 Pro Services` · `DACH`
- **CRM:** Not found in your CRM
## Engagement (last 90 days)
- **Total sessions:** 4
- **First seen / Last seen:** 2026-03-12 / 2026-05-29
- **Top landing pages:** /
- **Top sources:** Bing organic, Direct
- **Deep visits:** None — all sessions were single-page
- **Trend:** Stable, single visits across the window. Today's visit is the first in 2 weeks.
- **Identified visits:** 1 session from 1 distinct identified contact (names withheld; see Find My Buyer skill)
## People on file (2,838 contacts)
- **Departments:** Engineering (612), IT (487), Operations (340), HR (210)
- **Seniority:** C-level: 14 · VP/Director: 87 · Manager: 412 · IC: 2,325
- **Buyer Persona matches:**
- ✅ "Head of Marketing" → 6 matching contacts
- ✅ "Sales Director" → 11 matching contacts
- ✅ "Revenue Operations Manager" → 4 matching contacts
- ❌ "IT Security Lead" → 0 (gap: no contacts on file)
- **Decision-maker availability:** ✅ Yes — strong senior coverage across HR, IT, Sales
## Recent signals (last 90 days)
### Hiring (13 ads)
- Strategic HR Business Partner — 2026-04-22
- IT Security Specialist (Cloud & Web) — 2026-04-08
- SAP Frontend Engineer — 2026-05-20
- HR Controlling / People Analytics Manager — 2026-05-06
- Working Student Finance — 2026-05-26
- +8 more across HR, Engineering, IT Security
*Why it matters: hiring is concentrated in strategic HR functions, IT Security, and SAP front-end — an org rebuild that aligns with our visitor-intent / RevOps narrative.*
## Recommended next move
Active expansion signal: 13 open roles in 90 days concentrated in HR strategic functions, IT Security, and SAP front-end. Combined with full ICP fit and 2,800+ contacts on file, this is a Tier-1 outbound target. The hiring pattern suggests org-design or function-rebuild work that aligns with our visitor-intent / RevOps narrative.
- **`find-my-buyer`** — surface the Strategic HR Business Partner and Head of Marketing contacts to lead with
- **`outreach-companion`** — draft a personalised opener grounded in the hiring spike + today's visit
- **Add to your pipeline** — not in your CRM yet (missed-routing gap): tag it "Tier-1 Pro Services" and add it to your DACH enterprise list in Leadfeeder so it's tracked. (`add-to-pipeline` — writes tags/lists in Leadfeeder; getting it into your CRM is a separate manual step.)
Copy any of these to run the next step:
```
Who should I reach out to at Acme Corp?
```
```
Prep me for outreach to Acme Corp
```
---
**Company website:** http://www.acme.com
````
Keep briefs tight and actionable. The reader is deciding "do I open this company today or not." The Recommended next move section is what converts the brief into work.
Package details
Publisher declarations from the archived package. These are separate from our research and the live service's terms.
- Package author
- Leadfeeder
Package observed Sep 30, 2026.
Technical details
- First seen
- Sep 30, 2026 · 22:02 UTC
- Last seen
- Oct 1, 2026 · 18:00 UTC
- Collection status
- Collected
plugin_asdk_app_6a3cee9650608191918a0a4773e892b1
Download plugin data (JSON)