@anysiteio/agent-skills 1.4.0 → 2.0.1

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.
Files changed (37) hide show
  1. package/.claude-plugin/marketplace.json +49 -1
  2. package/.claude-plugin/plugin.json +5 -1
  3. package/README.md +27 -33
  4. package/bin/install.js +196 -135
  5. package/bundles.json +19 -0
  6. package/package.json +3 -2
  7. package/skills/anysite-audience-analysis/SKILL.md +110 -38
  8. package/skills/anysite-audience-analysis/references/PLATFORM_COVERAGE.md +38 -19
  9. package/skills/anysite-audience-analysis/references/TOOL_MAPPING.md +16 -11
  10. package/skills/anysite-brand-reputation/SKILL.md +105 -43
  11. package/skills/anysite-competitor-analyzer/SKILL.md +170 -111
  12. package/skills/anysite-competitor-intelligence/SKILL.md +201 -116
  13. package/skills/anysite-competitor-intelligence/references/ANALYSIS_PATTERNS.md +17 -13
  14. package/skills/anysite-content-analytics/SKILL.md +101 -33
  15. package/skills/anysite-crm-account-brief/SKILL.md +71 -0
  16. package/skills/anysite-crm-audit/SKILL.md +59 -0
  17. package/skills/anysite-crm-champions/SKILL.md +76 -0
  18. package/skills/anysite-crm-competitor-intel/SKILL.md +74 -0
  19. package/skills/anysite-crm-enrich/SKILL.md +84 -0
  20. package/skills/anysite-crm-lookalikes/SKILL.md +64 -0
  21. package/skills/anysite-crm-prospect/SKILL.md +90 -0
  22. package/skills/anysite-crm-score/SKILL.md +72 -0
  23. package/skills/anysite-crm-setup/SKILL.md +147 -0
  24. package/skills/anysite-crm-signals/SKILL.md +96 -0
  25. package/skills/anysite-influencer-discovery/SKILL.md +123 -71
  26. package/skills/anysite-lead-generation/SKILL.md +288 -256
  27. package/skills/anysite-lead-generation/references/LINKEDIN_STRATEGIES.md +79 -71
  28. package/skills/anysite-lead-generation/references/WEB_SCRAPING.md +121 -87
  29. package/skills/anysite-market-research/SKILL.md +117 -68
  30. package/skills/anysite-mcp/SKILL.md +128 -0
  31. package/skills/anysite-mcp-migration/SKILL.md +19 -8
  32. package/skills/anysite-person-analyzer/SKILL.md +86 -53
  33. package/skills/anysite-trend-analysis/SKILL.md +119 -55
  34. package/skills/anysite-vc-analyst/SKILL.md +12 -2
  35. package/skills/competitor-discovery/SKILL.md +350 -0
  36. package/skills/customer-pain-mining/SKILL.md +318 -0
  37. 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.