@anysiteio/agent-skills 1.4.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +49 -1
- package/.claude-plugin/plugin.json +5 -1
- package/README.md +27 -33
- package/bin/install.js +186 -135
- package/bundles.json +19 -0
- package/package.json +3 -2
- package/skills/anysite-audience-analysis/SKILL.md +110 -38
- package/skills/anysite-audience-analysis/references/PLATFORM_COVERAGE.md +38 -19
- package/skills/anysite-audience-analysis/references/TOOL_MAPPING.md +16 -11
- package/skills/anysite-brand-reputation/SKILL.md +105 -43
- package/skills/anysite-competitor-analyzer/SKILL.md +170 -111
- package/skills/anysite-competitor-intelligence/SKILL.md +201 -116
- package/skills/anysite-competitor-intelligence/references/ANALYSIS_PATTERNS.md +17 -13
- package/skills/anysite-content-analytics/SKILL.md +101 -33
- package/skills/anysite-crm-account-brief/SKILL.md +71 -0
- package/skills/anysite-crm-audit/SKILL.md +59 -0
- package/skills/anysite-crm-champions/SKILL.md +76 -0
- package/skills/anysite-crm-competitor-intel/SKILL.md +74 -0
- package/skills/anysite-crm-enrich/SKILL.md +84 -0
- package/skills/anysite-crm-lookalikes/SKILL.md +64 -0
- package/skills/anysite-crm-prospect/SKILL.md +90 -0
- package/skills/anysite-crm-score/SKILL.md +72 -0
- package/skills/anysite-crm-setup/SKILL.md +147 -0
- package/skills/anysite-crm-signals/SKILL.md +96 -0
- package/skills/anysite-influencer-discovery/SKILL.md +123 -71
- package/skills/anysite-lead-generation/SKILL.md +288 -256
- package/skills/anysite-lead-generation/references/LINKEDIN_STRATEGIES.md +79 -71
- package/skills/anysite-lead-generation/references/WEB_SCRAPING.md +121 -87
- package/skills/anysite-market-research/SKILL.md +117 -68
- package/skills/anysite-mcp/SKILL.md +128 -0
- package/skills/anysite-mcp-migration/SKILL.md +19 -8
- package/skills/anysite-person-analyzer/SKILL.md +86 -53
- package/skills/anysite-trend-analysis/SKILL.md +119 -55
- package/skills/anysite-vc-analyst/SKILL.md +12 -2
- package/skills/competitor-discovery/SKILL.md +350 -0
- package/skills/customer-pain-mining/SKILL.md +318 -0
- package/skills/positioning-map/SKILL.md +314 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: anysite-crm-enrich
|
|
3
|
+
description: Enrich existing CRM records (HubSpot contacts and companies) with fresh data from anysite - job titles, LinkedIn profiles, firmographics, emails. Reads records from the connected CRM, finds gaps in mapped fields, fills them from LinkedIn/Crunchbase/web sources, and writes back safely (fill-blank, dry-run, undo). Use when the user asks to enrich CRM records, fill missing fields, update contact or company data, or refresh a CRM list. Requires an active CRM connection and the anysite-crm-profile mapping.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CRM Enrich
|
|
7
|
+
|
|
8
|
+
Fill gaps in existing CRM records with anysite data. The most common flow: pull a list from
|
|
9
|
+
the CRM, enrich only what is missing, write back only to mapped fields.
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
1. `crm_list_connections` → an `active` connection. Missing → send the user to
|
|
14
|
+
`/anysite-crm-setup` (or Profile → CRM Integration in the dashboard).
|
|
15
|
+
2. The `anysite-crm-profile` skill exists → its mapping is law. Missing → minimal safe mode:
|
|
16
|
+
standard properties only, recommend running setup.
|
|
17
|
+
3. Read the Writing rules in `anysite-crm-setup` — they apply to every write below.
|
|
18
|
+
|
|
19
|
+
## Flow
|
|
20
|
+
|
|
21
|
+
### 1. Scope — what to enrich
|
|
22
|
+
|
|
23
|
+
Ask (or infer from the request): which records and which fields. Pull them:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
crm_query_records(object_type="contacts", list_id=<working list> | search=... | emails=[...],
|
|
27
|
+
properties=[record_id + match keys + mapped target fields])
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Page through everything in scope. Locally split records into:
|
|
31
|
+
- **complete** — all mapped target fields filled → skip (report count),
|
|
32
|
+
- **enrichable** — has a match key (email / linkedin_url / domain) and gaps,
|
|
33
|
+
- **unmatchable** — no key at all → report, do not guess identities.
|
|
34
|
+
|
|
35
|
+
### 2. Resolve identities (contacts)
|
|
36
|
+
|
|
37
|
+
- Has `linkedin_url` → `execute linkedin/user/user` (full profile: title, company, location).
|
|
38
|
+
- Only email → reverse lookup: `execute linkedin/email/email_sql_user` (cached, cheap) →
|
|
39
|
+
remainder via `email_user` (live). Both take ONE email per call — loop, don't batch.
|
|
40
|
+
No profile found → leave record, report.
|
|
41
|
+
- Needs email → `user_email` (batch ≤10 profiles, low yield — set expectations honestly).
|
|
42
|
+
A higher-yield `find_email_by_url` exists in the API but may be disabled in MCP — trust
|
|
43
|
+
`discover("linkedin", "user")`; if absent, stop at `user_email` and report coverage as-is.
|
|
44
|
+
|
|
45
|
+
Re-use cache (`query_cache`) instead of re-fetching anything twice.
|
|
46
|
+
|
|
47
|
+
### 3. Resolve companies
|
|
48
|
+
|
|
49
|
+
- By domain: `execute linkedin/search/search_sql_companies {website: "<domain>", count: 1}` —
|
|
50
|
+
batch by looping domains; gives industry, employee_count, description, locations.
|
|
51
|
+
- Deeper firmographics (funding, size range): `crunchbase/search` by name → alias →
|
|
52
|
+
`crunchbase/company` — only when profile maps such fields (cost-aware: 20cr each).
|
|
53
|
+
|
|
54
|
+
### 4. Write back
|
|
55
|
+
|
|
56
|
+
Build upsert records keyed by the CRM `record_id` you pulled in step 1 — never re-search
|
|
57
|
+
the CRM for a record you already hold, and never create from an enrich flow:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
crm_upsert_contacts(records=[{record_id, properties:{<mapped fields only>}}],
|
|
61
|
+
allow_create=false,
|
|
62
|
+
overwrite_properties=[<only fields marked overwrite in profile>],
|
|
63
|
+
dry_run=true)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Show the diff (old → new, counts of fill/skip), get confirmation, re-run with
|
|
67
|
+
`dry_run=false`. Companies go through `crm_upsert_companies`, which matches ONLY by
|
|
68
|
+
`domain` — always include `domain` in the properties you pull in step 1; a company record
|
|
69
|
+
without a domain is unwritable (report it, don't improvise a match). Keep request batches
|
|
70
|
+
reasonable (≤100 records per call). Save the returned `run_id`.
|
|
71
|
+
|
|
72
|
+
### 5. Report
|
|
73
|
+
|
|
74
|
+
Written / filled-blank-skipped (`fill_blank_skip` = policy working, not an error) /
|
|
75
|
+
enum warnings / unmatchable. Mention `crm_undo(run_id)` availability. If the profile maps
|
|
76
|
+
`anysite_last_enriched_at`, it was stamped by the mapping — say so.
|
|
77
|
+
|
|
78
|
+
## Rules specific to enrichment
|
|
79
|
+
|
|
80
|
+
- Never write a value you did not get from a source this session. No invented data.
|
|
81
|
+
- A profile field with no fresh source value → leave it out of `properties` entirely.
|
|
82
|
+
- Enum targets (e.g. industry): pick from the CRM schema `options` list, translating the
|
|
83
|
+
source value; no match → skip with a note, don't force.
|
|
84
|
+
- >10 records or any overwrite → dry-run first, always.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: anysite-crm-lookalikes
|
|
3
|
+
description: Derive the actual ICP from the CRM's closed-won/best customers and find lookalike companies with anysite bulk search (LinkedIn company DB, Crunchbase filters), scored and deduplicated against the CRM. Use when the user asks to find companies like their best customers, expand the target list, derive their real ICP from data, or seed a prospecting campaign. Requires an active CRM connection with some won/customer records.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CRM Lookalikes
|
|
7
|
+
|
|
8
|
+
Your real ICP is written in your closed-won list, not in your pitch deck. Extract the
|
|
9
|
+
pattern, then search 70M+ companies for more of it.
|
|
10
|
+
|
|
11
|
+
## Flow
|
|
12
|
+
|
|
13
|
+
### 1. Collect the seed set
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
crm_query_records(object_type="companies", list_id=<customers list> | search=...,
|
|
17
|
+
properties=[record_id, name, domain, industry, <size/stage if mapped>])
|
|
18
|
+
```
|
|
19
|
+
Need the user's help to identify "best": a customers list, a lifecycle/status field, or an
|
|
20
|
+
explicit pick of 10–30 names. Fewer than ~8 seeds → warn that the pattern will be weak.
|
|
21
|
+
|
|
22
|
+
### 2. Profile the seeds
|
|
23
|
+
|
|
24
|
+
Resolve each seed to structured firmographics:
|
|
25
|
+
```
|
|
26
|
+
execute linkedin/search/search_sql_companies {website: "<domain>", count: 1} # per seed
|
|
27
|
+
```
|
|
28
|
+
Plus `crunchbase/company` for stage/funding on a subset (venture-relevant seeds only).
|
|
29
|
+
Derive the pattern in-session and SHOW it:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
Industries: X (60%), Y (25%) · Size: 11-200 dominant · Geo: US+UK 80%
|
|
33
|
+
Stage: seed-B · Common traits: has API docs page, hiring in data roles, ...
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The user confirms/edits the pattern — it's their ICP, the data only proposes it.
|
|
37
|
+
|
|
38
|
+
### 3. Search for lookalikes
|
|
39
|
+
|
|
40
|
+
- `execute linkedin/search/search_sql_companies` — industry_name/keywords DSL from the
|
|
41
|
+
pattern, employee_count band, country filter, count up to 1000.
|
|
42
|
+
- `execute crunchbase/db/db_search` — when stage matters (`last_funding_type`,
|
|
43
|
+
`last_funding_date_after`); `crunchbase/search` live for `hiring: true` or
|
|
44
|
+
`shares_investors_with: [<seed investors>]` (a strong hidden-similarity filter).
|
|
45
|
+
- Niche supplements per pattern: `yc/search/search_companies` (early-stage), `builtin`
|
|
46
|
+
(US tech hubs), `producthunt` (product-led).
|
|
47
|
+
|
|
48
|
+
Search wide, profile narrow: the searches themselves are cheap even at count 1000, but do
|
|
49
|
+
NOT enrich every candidate — score on the fields the search already returned, and fetch
|
|
50
|
+
extra evidence (crunchbase lookups etc.) only for the top ~50. State the credit estimate
|
|
51
|
+
before any per-candidate enrichment.
|
|
52
|
+
|
|
53
|
+
### 4. Score and dedup
|
|
54
|
+
|
|
55
|
+
Score candidates against the confirmed pattern (same rubric discipline as
|
|
56
|
+
`anysite-crm-score` — weighted criteria, evidence per company, no guessed values).
|
|
57
|
+
Dedup against the CRM by domain (`crm_query_records`) — existing accounts drop out or get
|
|
58
|
+
flagged "already in CRM, unworked".
|
|
59
|
+
|
|
60
|
+
### 5. Hand off
|
|
61
|
+
|
|
62
|
+
Output: top-N table (name, domain, why-it-matches, score) + the confirmed ICP pattern for
|
|
63
|
+
reuse. Pushing to CRM → `anysite-crm-prospect` (its dedup/create/working-list rules apply);
|
|
64
|
+
finding people at these companies → same skill. This skill itself writes nothing.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: anysite-crm-prospect
|
|
3
|
+
description: Find net-new leads with anysite (LinkedIn and Crunchbase search, email finding) and push them into the CRM deduplicated - companies first, then contacts with associations. Creating records is gated by the profile's allow_create. Use when the user asks to find new leads/prospects/accounts AND add them to the CRM, build a list in HubSpot, or import prospects. For research without CRM push, prefer anysite-lead-generation. Requires an active CRM connection and profile.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CRM Prospect
|
|
7
|
+
|
|
8
|
+
Search → resolve → dedup → create. Order matters: companies before contacts, dedup before
|
|
9
|
+
create, dry-run before both.
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
Active CRM connection + profile (`allow_create: true` agreed in profile — if not, stop and
|
|
14
|
+
ask). Read Writing rules in `anysite-crm-setup`.
|
|
15
|
+
|
|
16
|
+
## Flow
|
|
17
|
+
|
|
18
|
+
### 1. Define the search
|
|
19
|
+
|
|
20
|
+
Get concrete criteria from the user (persona titles, industry, geography, size, stage).
|
|
21
|
+
Estimate volume and confirm before running anything large.
|
|
22
|
+
|
|
23
|
+
**Companies:**
|
|
24
|
+
- `execute linkedin/search/search_sql_companies` — main path: `keywords`/`industry_name`
|
|
25
|
+
DSL, `employee_count_min/max`, `country_hq`, up to 1000/call, 1cr-class.
|
|
26
|
+
- `execute crunchbase/db/db_search` — when stage/funding filters matter
|
|
27
|
+
(`last_funding_type`, `last_funding_date_after`, `investors`).
|
|
28
|
+
- `execute crunchbase/search` — live, adds `hiring: true`, `it_spend_*`, `valuation_*`
|
|
29
|
+
filters (20cr/50 — use for precision, not volume).
|
|
30
|
+
|
|
31
|
+
**People at those companies:**
|
|
32
|
+
- `execute linkedin/search/search_users {job_title, current_company: [urn] |
|
|
33
|
+
company_keywords, location, count}` — never bare `keywords` alone (empty results).
|
|
34
|
+
Company URNs come from the company search results.
|
|
35
|
+
|
|
36
|
+
### 2. Emails (cheap-first cascade)
|
|
37
|
+
|
|
38
|
+
1. `execute linkedin/user/user_email` — batches of ≤10 profiles. Warn the user upfront:
|
|
39
|
+
yield is low, a large share of leads will come back email-less.
|
|
40
|
+
2. A higher-yield `find_email_by_url` exists in the API but may be disabled in MCP — check
|
|
41
|
+
`discover("linkedin", "user")`; if absent, don't promise it. If present, estimate cost
|
|
42
|
+
(50cr × remainder) before running.
|
|
43
|
+
3. Still nothing → **keep the lead in the report**, but know the server requires an email
|
|
44
|
+
to CREATE a contact — email-less leads can only update existing records (matched by
|
|
45
|
+
`linkedin_url`). Report them as "found, pending email"; never silently drop them.
|
|
46
|
+
|
|
47
|
+
### 3. Dedup against the CRM (before any create)
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
crm_query_records(object_type="companies", search=<domain>) # or batch by domains
|
|
51
|
+
crm_query_records(object_type="contacts", emails=[...])
|
|
52
|
+
```
|
|
53
|
+
Dedup is reliable by email and domain. By `linkedin_url` it is best-effort only (free-text
|
|
54
|
+
`search`) — for a lead with no email whose search comes up empty, do NOT create; put it in
|
|
55
|
+
a manual-review bucket and say why. Existing company → reuse its record; existing contact →
|
|
56
|
+
update, not create. Report how many were already known — it calibrates the user's trust.
|
|
57
|
+
|
|
58
|
+
### 4. Push — companies first, then contacts
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
crm_upsert_companies(records=[{domain, properties per profile}],
|
|
62
|
+
allow_create=true, dry_run=true) → confirm → write
|
|
63
|
+
crm_upsert_contacts(records=[{email | linkedin_url,
|
|
64
|
+
properties per profile,
|
|
65
|
+
associate_company_domain: <domain>}],
|
|
66
|
+
allow_create=true, dry_run=true) → confirm → write
|
|
67
|
+
```
|
|
68
|
+
Server requires email to create a contact; contacts without email that don't match an
|
|
69
|
+
existing record will be skipped with a warning — report them as "found, pending email",
|
|
70
|
+
don't retry blindly. When associating to companies created in the same run, prefer
|
|
71
|
+
`associate_company_id` from the company upsert result. Save `run_id`s.
|
|
72
|
+
|
|
73
|
+
Note: crm_* tools cannot add records to CRM lists (list_id is read-only in queries). If
|
|
74
|
+
the user wants the new leads in a HubSpot list, suggest an active-list filter on a mapped
|
|
75
|
+
property (e.g. `lead_source = "anysite"`) — set that property during the upsert instead.
|
|
76
|
+
|
|
77
|
+
### 5. Report
|
|
78
|
+
|
|
79
|
+
Created / updated / already-known / pending-email / manual-review. Never call data
|
|
80
|
+
"verified" unless a verification step actually ran.
|
|
81
|
+
|
|
82
|
+
## Boundaries
|
|
83
|
+
|
|
84
|
+
- Creating records (`allow_create=true`) is permitted here and in `anysite-crm-champions`,
|
|
85
|
+
in both cases only when the profile's `allow_create` agrees. If the user wants research
|
|
86
|
+
without CRM push, hand off to `anysite-lead-generation`.
|
|
87
|
+
- Don't set owner, lifecycle stage, or any protected field — routing belongs to the CRM's
|
|
88
|
+
own automation.
|
|
89
|
+
- ICP scoring of the found leads → `anysite-crm-score`; lookalike seeding →
|
|
90
|
+
`anysite-crm-lookalikes`.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: anysite-crm-score
|
|
3
|
+
description: Score CRM companies or contacts against the user's ICP using anysite data (firmographics, funding stage, hiring, tech signals) and write the score into the single mapped score field. Use when the user asks to score leads, rank accounts, prioritize the pipeline, or apply ICP criteria to CRM records. Requires an active CRM connection and a profile with a score field marked overwrite.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CRM Score
|
|
7
|
+
|
|
8
|
+
Deterministic-ish prioritization: explicit rubric, evidence per company, score written to
|
|
9
|
+
exactly one mapped field.
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
|
|
13
|
+
Active CRM connection. Profile must map a score target field with `mode: overwrite`
|
|
14
|
+
(scores are re-computed by design). Not mapped → offer to store nothing and just report,
|
|
15
|
+
or send the user to re-run `/anysite-crm-setup`. The Writing rules in `anysite-crm-setup`
|
|
16
|
+
apply to every write. Cap a scoring run at ~50 companies and state the credit estimate
|
|
17
|
+
(evidence calls × price) before fetching; more → propose tiers or a narrower list.
|
|
18
|
+
|
|
19
|
+
## Flow
|
|
20
|
+
|
|
21
|
+
### 1. Fix the rubric BEFORE fetching data
|
|
22
|
+
|
|
23
|
+
Get ICP criteria from the user, or derive them with `anysite-crm-lookalikes` logic from
|
|
24
|
+
closed-won records. Turn them into a written rubric with weights, e.g.:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
industry match (0-3), size band (0-2), geo (0-1), funding stage (0-2),
|
|
28
|
+
hiring in buyer function (0-1), tech/context signal (0-1) → 0-10
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Show the rubric, get a nod. The rubric goes into the report verbatim — scores must be
|
|
32
|
+
explainable and reproducible.
|
|
33
|
+
|
|
34
|
+
### 2. Fetch evidence (cheap-first)
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
crm_query_records(object_type="companies", ...) → record_id, name, domain, existing fields
|
|
38
|
+
```
|
|
39
|
+
- Base firmographics: `execute linkedin/search/search_sql_companies {website: <domain>}` —
|
|
40
|
+
batch resolve; industry, employee_count, locations, description.
|
|
41
|
+
- Stage/funding (only if the rubric needs it): `crunchbase/search` → alias →
|
|
42
|
+
`crunchbase/company` (cache aliases; skip for obviously non-venture companies).
|
|
43
|
+
- Hiring probe (only if in rubric): `linkedin/search/search_jobs` with
|
|
44
|
+
`company: [{"type": "company", "value": "<numeric id from fsd_company urn>"}]`.
|
|
45
|
+
|
|
46
|
+
Skip any evidence source whose rubric weight is zero. State per-company data gaps —
|
|
47
|
+
a company with missing data gets a confidence note, not a silently low score.
|
|
48
|
+
|
|
49
|
+
### 3. Score
|
|
50
|
+
|
|
51
|
+
Apply the rubric in-session. For every company keep one line of evidence per criterion.
|
|
52
|
+
No evidence → that criterion scores 0 with an "unknown" marker, never a guessed value.
|
|
53
|
+
|
|
54
|
+
### 4. Write and report
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
crm_upsert_companies(records=[{domain: "<domain>", properties:{<score field>: <value>}}],
|
|
58
|
+
allow_create=false, overwrite_properties=[<score field>],
|
|
59
|
+
dry_run=true) → confirm → write → run_id
|
|
60
|
+
```
|
|
61
|
+
Company upserts match ONLY by domain — pull `domain` when querying records; companies
|
|
62
|
+
without one get a score in the report but no write. Write ONLY the score field (plus
|
|
63
|
+
`scored_at` if mapped). Report: top-N with evidence lines,
|
|
64
|
+
distribution summary, gaps. Contacts scoring (persona fit) works the same way against
|
|
65
|
+
contact records with `linkedin/user` evidence — same rubric-first discipline.
|
|
66
|
+
|
|
67
|
+
## Boundaries
|
|
68
|
+
|
|
69
|
+
- Score ≠ routing: never touch owner/stage/status based on a score.
|
|
70
|
+
- Re-scoring overwrites by design — that's why the profile must explicitly mark the field.
|
|
71
|
+
- Intent-level signals (fresh funding, exec hires) belong to `anysite-crm-signals`; this
|
|
72
|
+
skill measures fit. The two compose: fit × recency of signals = priority.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: anysite-crm-setup
|
|
3
|
+
description: Connect and configure the user's CRM (HubSpot) for safe AI-driven enrichment through anysite MCP crm_* tools. Runs a short discovery - reads the CRM schema including custom properties, agrees a deterministic field mapping with the user in 3-5 questions, and saves a local CRM profile that all future enrichment sessions follow. Use when the user wants to connect their CRM, set up CRM enrichment, configure field mapping, re-run CRM discovery after schema changes, or asks why CRM writes are skipped. Also defines the mandatory rules every agent must follow when writing to CRM with crm_upsert_contacts / crm_upsert_companies.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Anysite CRM Setup
|
|
7
|
+
|
|
8
|
+
Connect the user's CRM and produce a **local CRM profile** — a saved field mapping that makes every
|
|
9
|
+
future enrichment write deterministic: the same data always lands in the same CRM properties.
|
|
10
|
+
|
|
11
|
+
## When to use
|
|
12
|
+
|
|
13
|
+
- First-time setup: "connect my HubSpot", "set up CRM enrichment"
|
|
14
|
+
- Re-discovery: CRM schema changed, mapping feels stale, or writes are being skipped unexpectedly
|
|
15
|
+
- Reference: the **Writing rules** section below applies to EVERY session that calls
|
|
16
|
+
`crm_upsert_contacts` / `crm_upsert_companies`, even outside setup
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
The `crm_*` tools are visible only when the user has enabled CRM integration in their Anysite
|
|
21
|
+
dashboard (Profile → CRM Integration). If the tools are missing, ask the user to enable it there.
|
|
22
|
+
After they enable it, the tools do NOT appear instantly: the server flag takes up to a minute to
|
|
23
|
+
propagate, and the agent's tool list is cached per session — ask the user to reconnect the MCP
|
|
24
|
+
server (or start a new session), then retry. This is expected behavior, not an error.
|
|
25
|
+
|
|
26
|
+
## Setup flow
|
|
27
|
+
|
|
28
|
+
### 1. Ensure a connection
|
|
29
|
+
|
|
30
|
+
Call `crm_list_connections`.
|
|
31
|
+
|
|
32
|
+
- If there is an active connection — proceed to step 2.
|
|
33
|
+
- If not, call `crm_connect(provider="hubspot")`, give the user the `connect_url` to open in a
|
|
34
|
+
browser, then poll `crm_connect_status(pending_id)` every few seconds until `active`.
|
|
35
|
+
|
|
36
|
+
### 2. Read the schema
|
|
37
|
+
|
|
38
|
+
Call `crm_get_schema()`. You get:
|
|
39
|
+
|
|
40
|
+
- `properties.contacts` / `properties.companies` — every property with `label`, `type`,
|
|
41
|
+
`options` (valid enum values), `read_only`, `custom`
|
|
42
|
+
- `lists` — the user's CRM lists (id, name, processing type)
|
|
43
|
+
- `protected_properties` — fields the server will never let you write
|
|
44
|
+
|
|
45
|
+
### 3. Classify and auto-map
|
|
46
|
+
|
|
47
|
+
Silently classify properties:
|
|
48
|
+
|
|
49
|
+
- **Standard equivalents** — map automatically, do not ask: `email→email`, `jobtitle`,
|
|
50
|
+
`firstname`/`lastname`, `phone`, `city`, `country`, `company`, `website`, `domain`, `industry`,
|
|
51
|
+
`numberofemployees`, `linkedin_url→hs_linkedin_url`.
|
|
52
|
+
- **Custom enrichment targets** — custom properties whose name/label suggests enrichment
|
|
53
|
+
(score, tier, segment, tech stack, intent, source): candidates for user questions.
|
|
54
|
+
- **Ignore** — `read_only: true`, calculated, `hs_*` system internals, protected fields.
|
|
55
|
+
|
|
56
|
+
### 4. Ask 3-5 questions maximum
|
|
57
|
+
|
|
58
|
+
Ask ONLY about ambiguous or high-stakes decisions, one compact block, not an interview:
|
|
59
|
+
|
|
60
|
+
- Ambiguous targets: "Intent score → `Lead Score` or `ICP Fit Score`?"
|
|
61
|
+
- Overwrite policy for volatile fields: "Job titles: overwrite on change or fill blanks only?"
|
|
62
|
+
- Working list: "Which list do you enrich most — 'Inbound' or 'Outbound Q3'?" (only if lists exist)
|
|
63
|
+
- Creation policy: "May agents create new contacts, or update existing only?"
|
|
64
|
+
|
|
65
|
+
Confirm the full mapping as ONE list for approval, then save.
|
|
66
|
+
|
|
67
|
+
### 5. Save the local profile
|
|
68
|
+
|
|
69
|
+
Write the agreed profile to `~/.claude/skills/anysite-crm-profile/SKILL.md` (create the directory).
|
|
70
|
+
Use exactly this structure:
|
|
71
|
+
|
|
72
|
+
```markdown
|
|
73
|
+
---
|
|
74
|
+
name: anysite-crm-profile
|
|
75
|
+
description: The user's saved CRM field mapping and write rules for HubSpot enrichment via anysite MCP. ALWAYS consult this before calling crm_upsert_contacts or crm_upsert_companies - it defines which CRM properties to write, which may be overwritten, and which list is the working list. Generated by /anysite-crm-setup - do not edit manually, re-run setup to change.
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
# CRM Profile (HubSpot portal <portal_id>, connection <connection_id>)
|
|
79
|
+
|
|
80
|
+
Generated <date> by anysite-crm-setup from live schema. Do not edit manually.
|
|
81
|
+
|
|
82
|
+
## Field mapping — contacts
|
|
83
|
+
| Anysite data | CRM property | mode |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| job title | jobtitle | fill_blank |
|
|
86
|
+
| linkedin url | hs_linkedin_url | fill_blank |
|
|
87
|
+
| intent score | icp_fit_score | overwrite |
|
|
88
|
+
|
|
89
|
+
## Field mapping — companies
|
|
90
|
+
| Anysite data | CRM property | mode |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| industry | industry | fill_blank |
|
|
93
|
+
| employee count | numberofemployees | overwrite |
|
|
94
|
+
|
|
95
|
+
## Rules
|
|
96
|
+
- Working list: "<name>" (id <id>)
|
|
97
|
+
- allow_create: <true/false, as agreed>
|
|
98
|
+
- Never write (user protected, on top of server-enforced): <list or "-">
|
|
99
|
+
- Enum properties and their valid values: <property>: <values>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Only include rows that were actually agreed or auto-mapped. Never invent mappings for data types
|
|
103
|
+
that were not discussed.
|
|
104
|
+
|
|
105
|
+
## Writing rules (every session, not only setup)
|
|
106
|
+
|
|
107
|
+
1. **Consult the profile first.** If `~/.claude/skills/anysite-crm-profile/SKILL.md` exists, its
|
|
108
|
+
mapping is law: write ONLY to properties listed there, in the listed mode. If a new data type
|
|
109
|
+
has no mapping, ask the user once and suggest re-running setup — do not guess.
|
|
110
|
+
2. **Creating a new contact requires email.** record_id and linkedin_url only MATCH existing
|
|
111
|
+
contacts; a record without email is skipped with `create_requires_email` when no match exists.
|
|
112
|
+
Never invent or guess emails — if no verified work email is available, report the person as
|
|
113
|
+
not imported and say why.
|
|
114
|
+
3. **No profile? Minimal safety.** Map only obvious standard properties; ask before writing
|
|
115
|
+
anything to a custom property; recommend running `/anysite-crm-setup`.
|
|
116
|
+
4. **Dry-run before bulk.** For more than ~10 records, or any `overwrite_properties`, first call
|
|
117
|
+
the upsert with `dry_run=true`, show the user the old → new diff, and write only after
|
|
118
|
+
confirmation.
|
|
119
|
+
5. **Overwrite only per profile.** Pass a property in `overwrite_properties` only if the profile
|
|
120
|
+
marks it `overwrite` (or the user explicitly asked in this session).
|
|
121
|
+
6. **Enum values must come from the schema.** Check `options` before writing enum properties;
|
|
122
|
+
the server skips invalid values with an `enum_option_missing` warning — do not retry blindly.
|
|
123
|
+
When sources disagree on an enum like industry (e.g. YC vertical vs LinkedIn category), pick the
|
|
124
|
+
schema option closest to how the company describes itself and mention the choice to the user.
|
|
125
|
+
Use `crm_get_schema(object_type=..., properties=[...])` to fetch just the enum you need — the
|
|
126
|
+
unfiltered schema is very large.
|
|
127
|
+
7. **Report warnings honestly.** `fill_blank_skip` means the field already had a value — that is
|
|
128
|
+
the policy working, not an error. Summarize results/skipped/warnings for the user after writes.
|
|
129
|
+
8. **Undo exists.** Every real write returns `run_id`. If the user is unhappy with a write,
|
|
130
|
+
`crm_undo(run_id)` restores previous values (fields changed since are reported as conflicts).
|
|
131
|
+
|
|
132
|
+
## Tools reference
|
|
133
|
+
|
|
134
|
+
| Tool | Purpose |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `crm_list_connections` | Connections and their status |
|
|
137
|
+
| `crm_connect` / `crm_connect_status` | OAuth connect flow |
|
|
138
|
+
| `crm_get_schema` | Properties (with enum options), lists, protected fields |
|
|
139
|
+
| `crm_query_records` | Read CRM: list members, by ids/emails, free-text search, paging |
|
|
140
|
+
| `crm_upsert_contacts` | Write contacts (match by email / record_id / linkedin_url) |
|
|
141
|
+
| `crm_upsert_companies` | Write companies (match by domain) |
|
|
142
|
+
| `crm_undo` | Revert a write run by run_id |
|
|
143
|
+
|
|
144
|
+
Property values are strings; numbers and booleans are accepted and coerced server-side.
|
|
145
|
+
|
|
146
|
+
Server-enforced regardless of anything in this skill: fill-blank by default, protected fields,
|
|
147
|
+
no empty writes, create only with `allow_create`, full write log.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: anysite-crm-signals
|
|
3
|
+
description: Sweep CRM target accounts for buying signals - funding rounds, executive hires, hiring surges, layoffs, news, brand mentions - using Crunchbase, LinkedIn jobs/posts and news sources, then prioritize accounts and optionally stamp signal fields back into the CRM. Use when the user asks "what's new with my accounts", wants signal-based prioritization, account monitoring, or a "who should I reach out to today" answer. Pairs with a cron/loop for always-on monitoring. Requires an active CRM connection.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CRM Signals
|
|
7
|
+
|
|
8
|
+
Turn a static account list into a prioritized "act now" list. One signal is a guess; 2+
|
|
9
|
+
signals within ~30 days is a pattern. Signal-triggered outreach converts several times
|
|
10
|
+
better than cold cadence (vendor-reported benchmarks: exec hires and job changes lead,
|
|
11
|
+
then funding) — treat the ordering as solid, the exact percentages as marketing.
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
Active CRM connection (`crm_list_connections`). Profile optional for read-only sweeps;
|
|
16
|
+
required if the user wants signal fields written back — and then the Writing rules in
|
|
17
|
+
`anysite-crm-setup` apply.
|
|
18
|
+
|
|
19
|
+
## Flow
|
|
20
|
+
|
|
21
|
+
### 1. Pick the account set
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
crm_query_records(object_type="companies", list_id=<target list> | search=...,
|
|
25
|
+
properties=[record_id, name, domain, <mapped signal fields if any>])
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Cap a single sweep at ~50 accounts (each account costs several source calls). More →
|
|
29
|
+
propose splitting or narrowing.
|
|
30
|
+
|
|
31
|
+
### 2. Per-account signal collection
|
|
32
|
+
|
|
33
|
+
Run the cheap universal chain for every account; add optional probes when relevant.
|
|
34
|
+
|
|
35
|
+
**Funding / exec hires / news / layoffs (one lookup covers four signals):**
|
|
36
|
+
```
|
|
37
|
+
execute crunchbase/search {keywords: "<company name>", count: 3} # resolve alias, once
|
|
38
|
+
execute crunchbase/company {company: "<alias>"}
|
|
39
|
+
→ funding_rounds[] (date, type, amount, lead investors)
|
|
40
|
+
→ leadership_hires[] (date, role, description)
|
|
41
|
+
→ news[] (title, date, publisher)
|
|
42
|
+
→ layoffs[]
|
|
43
|
+
```
|
|
44
|
+
Alias resolution is case-sensitive and costs credits — if the profile maps a
|
|
45
|
+
`crunchbase_alias` field, read/write it so each account is resolved once, ever.
|
|
46
|
+
|
|
47
|
+
**Hiring (what they're building):**
|
|
48
|
+
```
|
|
49
|
+
execute linkedin/search/search_companies {keywords: "<name>", count: 1}
|
|
50
|
+
→ urn like "fsd_company:1441" → numeric id "1441"
|
|
51
|
+
execute linkedin/search/search_jobs {company: [{"type": "company", "value": "1441"}],
|
|
52
|
+
count: 20, sort: "recent"}
|
|
53
|
+
```
|
|
54
|
+
The `company` filter takes typed objects with the numeric id, not raw URN strings.
|
|
55
|
+
Look for roles in the buyer function (e.g. RevOps/Growth/Data roles for a data product).
|
|
56
|
+
|
|
57
|
+
**Mentions / social activity (optional):**
|
|
58
|
+
```
|
|
59
|
+
execute linkedin/search/search_posts {keywords: "\"<company name>\"",
|
|
60
|
+
date_posted: "past-month", count: 20}
|
|
61
|
+
execute techmeme/stories/stories_search {keyword: "<company name>", count: 5}
|
|
62
|
+
```
|
|
63
|
+
Do NOT use gdelt (times out). Filter false positives for generic company names by checking
|
|
64
|
+
the author/context before counting a mention as a signal.
|
|
65
|
+
|
|
66
|
+
### 3. Score and stack
|
|
67
|
+
|
|
68
|
+
Per account, count signals in the last 30/90 days, weighted by conversion value:
|
|
69
|
+
exec hire in buyer function > funding round > hiring surge in relevant roles > news >
|
|
70
|
+
mentions. Layoffs = negative budget signal for expansion, positive for cost-saving pitches —
|
|
71
|
+
interpret against the user's product.
|
|
72
|
+
|
|
73
|
+
Output tiers: **Act now** (2+ fresh signals), **Watch** (1 signal), **Quiet**.
|
|
74
|
+
|
|
75
|
+
### 4. Report (and optionally write back)
|
|
76
|
+
|
|
77
|
+
Always produce the human report first: account → signals → suggested angle ("congratulate
|
|
78
|
+
on Series B, reference the new VP Sales hire").
|
|
79
|
+
|
|
80
|
+
If the profile maps signal fields (e.g. `last_signal_type`, `last_signal_date`,
|
|
81
|
+
`signal_summary`) and the user wants them stored:
|
|
82
|
+
```
|
|
83
|
+
crm_upsert_companies(records=[{domain: "<domain>", properties:{...}}], allow_create=false,
|
|
84
|
+
overwrite_properties=[<signal fields — they are volatile by nature,
|
|
85
|
+
profile must mark them overwrite>], dry_run=true)
|
|
86
|
+
```
|
|
87
|
+
Company upserts match ONLY by domain — pull `domain` in step 1; accounts without one are
|
|
88
|
+
report-only. → confirm → write → report `run_id`.
|
|
89
|
+
|
|
90
|
+
## Recurrence
|
|
91
|
+
|
|
92
|
+
This skill is a one-shot sweep. For always-on monitoring suggest scheduling: a Claude Code
|
|
93
|
+
cron / `/loop`, or an operator habit ("run signals every Monday"). Note what was swept and
|
|
94
|
+
when in your report so the next run compares against it. Post search granularity is coarse
|
|
95
|
+
(`date_posted`: past-24h / past-week / past-month only) — a weekly cadence fits it best;
|
|
96
|
+
funding/news items carry their own dates, filter those by date in-session.
|