@anysiteio/agent-skills 2.0.0 → 2.1.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/README.md +1 -0
- package/bin/install.js +41 -31
- package/bundles.json +1 -0
- package/package.json +1 -1
- package/skills/anysite-competitor-analyzer/SKILL.md +1 -1
- package/skills/anysite-competitor-intelligence/SKILL.md +1 -1
- package/skills/anysite-crm-account-brief/SKILL.md +18 -9
- package/skills/anysite-crm-audit/SKILL.md +9 -0
- package/skills/anysite-crm-champions/SKILL.md +7 -2
- package/skills/anysite-crm-competitor-intel/SKILL.md +8 -0
- package/skills/anysite-crm-enrich/SKILL.md +45 -7
- package/skills/anysite-crm-inbound/SKILL.md +68 -0
- package/skills/anysite-crm-lookalikes/SKILL.md +17 -2
- package/skills/anysite-crm-prospect/SKILL.md +6 -3
- package/skills/anysite-crm-score/SKILL.md +27 -6
- package/skills/anysite-crm-setup/SKILL.md +4 -0
- package/skills/anysite-crm-signals/SKILL.md +48 -15
- package/skills/anysite-mcp/SKILL.md +103 -19
package/README.md
CHANGED
|
@@ -41,6 +41,7 @@ Get unlimited requests through Remote MCP Server with no request limits. Ideal f
|
|
|
41
41
|
| `anysite-crm-score` | Score CRM companies against your ICP with an explicit rubric and write the score into the mapped field. | [SKILL.md](skills/anysite-crm-score/SKILL.md) |
|
|
42
42
|
| `anysite-crm-competitor-intel` | Displacement hunting: who uses a competitor (technographics), what their users complain about (review mining), tagged into the CRM. | [SKILL.md](skills/anysite-crm-competitor-intel/SKILL.md) |
|
|
43
43
|
| `anysite-crm-account-brief` | Pre-meeting one-pager for a CRM account: CRM context + funding, exec changes, news, key people's recent activity. | [SKILL.md](skills/anysite-crm-account-brief/SKILL.md) |
|
|
44
|
+
| `anysite-crm-inbound` | Instant read-only verdict on one inbound lead - identity, company reality check, ICP fit, route and talking points in 2-5 calls. | [SKILL.md](skills/anysite-crm-inbound/SKILL.md) |
|
|
44
45
|
| `anysite-crm-lookalikes` | Derive your real ICP from closed-won customers and find lookalike companies across 70M+ company records. | [SKILL.md](skills/anysite-crm-lookalikes/SKILL.md) |
|
|
45
46
|
<!-- END_SKILLS_TABLE -->
|
|
46
47
|
|
package/bin/install.js
CHANGED
|
@@ -30,10 +30,11 @@ const PKG_VERSION = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf
|
|
|
30
30
|
|
|
31
31
|
const HOME = process.env.HOME || homedir();
|
|
32
32
|
const CLAUDE_DIR = join(HOME, ".claude");
|
|
33
|
-
const CLAUDE_SKILLS_DIR = join(CLAUDE_DIR, "skills");
|
|
34
33
|
const CODEX_DIR = join(HOME, ".codex");
|
|
35
34
|
const CODEX_CONFIG = join(CODEX_DIR, "config.toml");
|
|
36
|
-
|
|
35
|
+
// Both agents use the same skills convention: <dir>/skills/<name>/SKILL.md
|
|
36
|
+
const SKILLS_DST = { claude: join(CLAUDE_DIR, "skills"), codex: join(CODEX_DIR, "skills") };
|
|
37
|
+
const manifestPath = (dstRoot) => join(dstRoot, ".anysite-skills.json");
|
|
37
38
|
|
|
38
39
|
const MCP_NAME = "anysite";
|
|
39
40
|
const MCP_URL = "https://mcp.anysite.io/mcp";
|
|
@@ -73,8 +74,8 @@ function skillDescription(name) {
|
|
|
73
74
|
} catch { return ""; }
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
function readManifest() {
|
|
77
|
-
try { return JSON.parse(readFileSync(
|
|
77
|
+
function readManifest(dstRoot) {
|
|
78
|
+
try { return JSON.parse(readFileSync(manifestPath(dstRoot), "utf8")); } catch { return null; }
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
function detectTargets() {
|
|
@@ -91,21 +92,21 @@ function hasClaudeCli() {
|
|
|
91
92
|
|
|
92
93
|
// ── skills install / uninstall ──────────────────────────────────────────────
|
|
93
94
|
|
|
94
|
-
function installSkills(names) {
|
|
95
|
-
mkdirSync(
|
|
95
|
+
function installSkills(names, dstRoot) {
|
|
96
|
+
mkdirSync(dstRoot, { recursive: true });
|
|
96
97
|
const installed = [];
|
|
97
98
|
const missing = [];
|
|
98
99
|
for (const name of names) {
|
|
99
100
|
const src = join(SKILLS_SRC, name);
|
|
100
101
|
if (!existsSync(join(src, "SKILL.md"))) { missing.push(name); continue; }
|
|
101
|
-
const dst = join(
|
|
102
|
+
const dst = join(dstRoot, name);
|
|
102
103
|
rmSync(dst, { recursive: true, force: true });
|
|
103
104
|
cpSync(src, dst, { recursive: true });
|
|
104
105
|
installed.push(name);
|
|
105
106
|
}
|
|
106
|
-
const prev = readManifest();
|
|
107
|
+
const prev = readManifest(dstRoot);
|
|
107
108
|
const all = [...new Set([...(prev?.skills ?? []), ...installed])].sort();
|
|
108
|
-
writeFileSync(
|
|
109
|
+
writeFileSync(manifestPath(dstRoot), JSON.stringify({
|
|
109
110
|
version: PKG_VERSION,
|
|
110
111
|
bundle: flags.bundle ?? prev?.bundle ?? null,
|
|
111
112
|
installedAt: new Date().toISOString(),
|
|
@@ -114,21 +115,21 @@ function installSkills(names) {
|
|
|
114
115
|
return { installed, missing };
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
function uninstallSkills() {
|
|
118
|
-
const manifest = readManifest();
|
|
118
|
+
function uninstallSkills(dstRoot) {
|
|
119
|
+
const manifest = readManifest(dstRoot);
|
|
119
120
|
const tracked = new Set(manifest?.skills ?? []);
|
|
120
121
|
const removed = [];
|
|
121
|
-
if (existsSync(
|
|
122
|
-
for (const d of readdirSync(
|
|
122
|
+
if (existsSync(dstRoot)) {
|
|
123
|
+
for (const d of readdirSync(dstRoot, { withFileTypes: true })) {
|
|
123
124
|
if (!d.isDirectory()) continue;
|
|
124
125
|
// remove tracked skills plus legacy anysite-* installs; never touch anything else
|
|
125
126
|
if (tracked.has(d.name) || d.name.startsWith("anysite-")) {
|
|
126
|
-
rmSync(join(
|
|
127
|
+
rmSync(join(dstRoot, d.name), { recursive: true, force: true });
|
|
127
128
|
removed.push(d.name);
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
|
-
rmSync(
|
|
132
|
+
rmSync(manifestPath(dstRoot), { force: true });
|
|
132
133
|
return removed;
|
|
133
134
|
}
|
|
134
135
|
|
|
@@ -170,8 +171,7 @@ if (flags.help) {
|
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
if (flags.list) {
|
|
173
|
-
const
|
|
174
|
-
const installed = new Set(manifest?.skills ?? []);
|
|
174
|
+
const installed = new Set(detectTargets().flatMap((t) => readManifest(SKILLS_DST[t])?.skills ?? []));
|
|
175
175
|
console.log("\nBundles:");
|
|
176
176
|
for (const [name, b] of Object.entries(BUNDLES)) {
|
|
177
177
|
console.log(` ${name} (${b.skills.length} skills) — ${b.description}`);
|
|
@@ -186,21 +186,27 @@ if (flags.list) {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
if (flags.status) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
189
|
+
let any = false;
|
|
190
|
+
for (const t of detectTargets()) {
|
|
191
|
+
const manifest = readManifest(SKILLS_DST[t]);
|
|
192
|
+
if (!manifest) { console.log(`\n[${t}] no anysite skills installed.`); continue; }
|
|
193
|
+
any = true;
|
|
194
|
+
console.log(`\n[${t}] ${manifest.skills.length} skills (package v${manifest.version}, ${manifest.installedAt})${manifest.bundle ? `, bundle: ${manifest.bundle}` : ""}`);
|
|
195
|
+
for (const s of manifest.skills) console.log(` ✓ ${s}`);
|
|
196
|
+
if (any) console.log(`\nUpdate: npx @anysiteio/agent-skills@latest ${manifest.bundle ?? ""}`);
|
|
197
|
+
}
|
|
198
|
+
console.log("");
|
|
195
199
|
process.exit(0);
|
|
196
200
|
}
|
|
197
201
|
|
|
198
202
|
if (flags.uninstall) {
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
203
|
+
for (const t of detectTargets()) {
|
|
204
|
+
const removed = uninstallSkills(SKILLS_DST[t]);
|
|
205
|
+
console.log(removed.length
|
|
206
|
+
? `\n[${t}] removed ${removed.length} skills from ${SKILLS_DST[t]}`
|
|
207
|
+
: `\n[${t}] nothing to remove.`);
|
|
208
|
+
}
|
|
209
|
+
console.log(`\nThe MCP entries are kept. To remove: claude mcp remove ${MCP_NAME} / edit ~/.codex/config.toml\n`);
|
|
204
210
|
process.exit(0);
|
|
205
211
|
}
|
|
206
212
|
|
|
@@ -218,9 +224,13 @@ if (!targets.length) {
|
|
|
218
224
|
process.exit(1);
|
|
219
225
|
}
|
|
220
226
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
227
|
+
let installedTotal = 0;
|
|
228
|
+
for (const t of targets) {
|
|
229
|
+
const { installed, missing } = installSkills(names, SKILLS_DST[t]);
|
|
230
|
+
installedTotal += installed.length;
|
|
231
|
+
console.log(`\nSkills [${t}] → ${SKILLS_DST[t]}: ${installed.length} installed${missing.length ? `, unknown: ${missing.join(", ")}` : ""}`);
|
|
232
|
+
}
|
|
233
|
+
if (!installedTotal) {
|
|
224
234
|
console.error("Nothing was installed — check skill names with --list.");
|
|
225
235
|
process.exit(1);
|
|
226
236
|
}
|
package/bundles.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anysiteio/agent-skills",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Official anysite agent skills + one-line setup: installs skills into Claude Code / Codex and registers the anysite remote MCP server. Includes the GTM bundle (CRM enrichment, signals, prospecting).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: anysite-competitor-analyzer
|
|
3
|
-
description: Deep competitive intelligence combining web scraping, LinkedIn data, social media monitoring, leadership analysis, GitHub activity, Glassdoor sentiment, and community insights. Analyzes founders/C-level profiles, tracks real-time signals vs quarterly reports, and creates comprehensive competitor profiles. Use
|
|
3
|
+
description: Deep competitive intelligence combining web scraping, LinkedIn data, social media monitoring, leadership analysis, GitHub activity, Glassdoor sentiment, and community insights. Analyzes founders/C-level profiles, tracks real-time signals vs quarterly reports, and creates comprehensive competitor profiles. Use for a DEEP DOSSIER ON ONE named competitor - leadership/founder profiling, product/pricing teardown, strategic threat assessment of a single company. For tracking a landscape of several competitors over time use anysite-competitor-intelligence; for CRM-tied displacement lists use anysite-crm-competitor-intel.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Competitor Analyzer
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: anysite-competitor-intelligence
|
|
3
|
-
description: Competitive intelligence gathering using anysite MCP server across LinkedIn, social media, Y Combinator, and the web. Track competitor activities, analyze hiring patterns, monitor content strategies, benchmark market positioning, research startup competitors, and gather strategic intelligence. Supports LinkedIn (companies, employees, posts), Instagram, Twitter/X, Reddit, YouTube, Y Combinator, and web scraping. Use
|
|
3
|
+
description: Competitive intelligence gathering using anysite MCP server across LinkedIn, social media, Y Combinator, and the web. Track competitor activities, analyze hiring patterns, monitor content strategies, benchmark market positioning, research startup competitors, and gather strategic intelligence. Supports LinkedIn (companies, employees, posts), Instagram, Twitter/X, Reddit, YouTube, Y Combinator, and web scraping. Use for tracking a LANDSCAPE of several competitors over time - competitive movements, hiring velocity, content strategies, market positioning across a set of companies. For a deep dossier on one named competitor use anysite-competitor-analyzer; for CRM-tied displacement lists use anysite-crm-competitor-intel.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# anysite Competitor Intelligence
|
|
@@ -21,16 +21,22 @@ an update candidate for `anysite-crm-enrich`/`anysite-crm-champions`.
|
|
|
21
21
|
|
|
22
22
|
### 2. Company snapshot
|
|
23
23
|
|
|
24
|
-
- `execute crunchbase/search {keywords: name}` → alias
|
|
25
|
-
funding history, `leadership_hires[]
|
|
26
|
-
|
|
24
|
+
- `execute crunchbase/search {keywords: name}` → alias (verify name+domain, first hit may
|
|
25
|
+
be a namesake) → `crunchbase/company` → funding history, `leadership_hires[]` (often empty
|
|
26
|
+
for smaller companies — not a negative), `news[]`, `layoffs[]`, employee range, investors.
|
|
27
|
+
Same response, free extras for the brief: `related.competitors[]` (their competitive set),
|
|
28
|
+
`bombora_surges[]` (what their team is researching — mention only if relevant to the
|
|
29
|
+
meeting), `predictions.funding_score` (likelihood of a next round).
|
|
30
|
+
- `search_sql_companies {website: "<domain>", count: 10}` + exact `website` match check
|
|
31
|
+
(substring search returns look-alike domains — verify before trusting) → description,
|
|
27
32
|
specialities, locations, employee_count.
|
|
28
33
|
|
|
29
34
|
### 3. What's happening now
|
|
30
35
|
|
|
31
|
-
- Hiring: `
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
- Hiring: `search_companies {keywords: "<name>", count: 5}` → pick the right company by
|
|
37
|
+
name/industry/alias (first hit is often a namesake), its `urn` comes back already as the
|
|
38
|
+
`{type, value}` object → `search_jobs {company: [<urn object>], sort: "recent", count: 20}`
|
|
39
|
+
— what functions they're growing (that's their current priorities, use in talking points).
|
|
34
40
|
- News: crunchbase `news[]` first (already fetched); add
|
|
35
41
|
`techmeme/stories/stories_search {keyword: "<name>", count: 5}` for tech companies.
|
|
36
42
|
- Employer sentiment (optional, for bigger companies): resolve the employer id first via
|
|
@@ -48,9 +54,12 @@ execute linkedin/user/user_posts {urn, count: 10,
|
|
|
48
54
|
```
|
|
49
55
|
Caveat: `user` called with a URL may omit the `urn` in its response, and `user_posts`
|
|
50
56
|
accepts ONLY a URN. If the urn is missing, recover it via
|
|
51
|
-
`search_users {first_name, last_name,
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
`search_users {first_name, last_name, current_company: [<company urn object>], count: 3}`
|
|
58
|
+
— the company-filtered search returns the URN directly (same cascade crm-enrich uses for
|
|
59
|
+
email-only contacts). Posts are personalization gold: real interests, stated problems,
|
|
60
|
+
conference activity. Quiet posters: `user_comments` and `user_reactions` (posts they
|
|
61
|
+
engaged with) reveal what a lurker actually reads — often better meeting fuel than their
|
|
62
|
+
own posts.
|
|
54
63
|
No posts ≠ no signal — check `user_comments` for lurker activity if it matters.
|
|
55
64
|
|
|
56
65
|
### 5. The brief
|
|
@@ -35,6 +35,15 @@ Audit scope: whole portal if small; else the working list + a stated sample. Say
|
|
|
35
35
|
"neither" group is unenrichable and un-dedupable — flag it).
|
|
36
36
|
- **Duplicate candidates:** same normalized email; same domain with several company records;
|
|
37
37
|
same person name + company. Candidates only — never auto-merge.
|
|
38
|
+
- **Email ↔ employer mismatch:** contact's email domain vs their linked company's domain.
|
|
39
|
+
A mismatch has THREE causes needing different handling (all seen live): the person changed
|
|
40
|
+
jobs; the COMPANY rebranded/changed domain (old domain usually still receives mail — the
|
|
41
|
+
contact is fine); or the email is genuinely stale. Free discriminator already in the data:
|
|
42
|
+
if crunchbase under the domain-derived alias returns a different company NAME than the
|
|
43
|
+
LinkedIn page of that same domain — that's a rebrand, not a departure. FLAG ONLY, never
|
|
44
|
+
auto-fix in either direction — in two causes out of three there is nothing to "fix", and
|
|
45
|
+
"correcting" the company from the email domain overwrites good data with stale. Job-change
|
|
46
|
+
suspects go to anysite-crm-champions.
|
|
38
47
|
- **Staleness:** if `anysite_last_enriched_at` (or similar) is mapped — age distribution;
|
|
39
48
|
contacts with no activity fields; companies with dead domains (spot-check a few via
|
|
40
49
|
`webparser/parse` only if the user asks).
|
|
@@ -35,8 +35,13 @@ Cap a run at ~100 contacts (one profile call each); more → propose batching by
|
|
|
35
35
|
|
|
36
36
|
Per contact:
|
|
37
37
|
- `linkedin_url` → `execute linkedin/user/user {user: <url>}` → current experience.
|
|
38
|
-
- email only → `execute linkedin/email/email_sql_user {email}` →
|
|
39
|
-
|
|
38
|
+
- email only → try `execute linkedin/email/email_sql_user {email}` (→ live `email_user`),
|
|
39
|
+
but expect misses; the reliable path uses what the CRM already knows: email domain →
|
|
40
|
+
resolve company → `organizational_urn` → `search_users {first_name, last_name,
|
|
41
|
+
current_company: [{"type": "company", "value": "<id>"}]}`. NOTE: for champion tracking
|
|
42
|
+
search by the CRM company tells you where they WERE — a zero-result search there is
|
|
43
|
+
itself a move signal; re-search without the company filter and disambiguate by
|
|
44
|
+
headline/history before concluding.
|
|
40
45
|
|
|
41
46
|
Compare the profile's **current company** against the CRM company. Normalize before
|
|
42
47
|
comparing (legal suffixes, casing, known rebrands); when unsure, treat as "same" — false
|
|
@@ -47,6 +47,14 @@ only). Filter low-rating reviews with `query_cache` (free), then extract with th
|
|
|
47
47
|
recurring pains, switching triggers, praised alternatives, verbatim quotes worth reusing.
|
|
48
48
|
Keep 3–7 pains with quote + source URL each — this is the personalization ammunition.
|
|
49
49
|
|
|
50
|
+
Second ammunition source — the competitor's own ads (`ad-transparency` sources, incl.
|
|
51
|
+
LinkedIn Ad Library): their current claims and positioning in their own words. Scope it to
|
|
52
|
+
the 1–3 competitors under analysis (NOT per-account sweeps — per-ad detail is a separate
|
|
53
|
+
call each); use server filters (`impressions_min`, `countries`, `date_option`) and open
|
|
54
|
+
only the cards that matter. ~10–15cr for a whole competitor analysis. Their engagement
|
|
55
|
+
graph (`post_comments`/`post_reactions` on the competitor's page — a seed with a real
|
|
56
|
+
audience) adds who's actively following them.
|
|
57
|
+
|
|
50
58
|
### 3. Cross-reference with the CRM
|
|
51
59
|
|
|
52
60
|
```
|
|
@@ -35,9 +35,15 @@ Page through everything in scope. Locally split records into:
|
|
|
35
35
|
### 2. Resolve identities (contacts)
|
|
36
36
|
|
|
37
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,
|
|
39
|
-
remainder via `email_user` (live). Both take ONE email per call — loop, don't
|
|
40
|
-
|
|
38
|
+
- Only email → try reverse lookup first: `execute linkedin/email/email_sql_user` (cached,
|
|
39
|
+
cheap) → remainder via `email_user` (live). Both take ONE email per call — loop, don't
|
|
40
|
+
batch, and expect misses (verified to return empty even for people who are on LinkedIn).
|
|
41
|
+
Then the cascade that actually works, because the CRM knows the name: email domain →
|
|
42
|
+
resolve company (verified, per anysite-mcp recipe) → `organizational_urn` →
|
|
43
|
+
`search_users {first_name, last_name, current_company: [{"type": "company",
|
|
44
|
+
"value": "<id>"}]}` → usually exactly one match, WITH the profile URN as a bonus.
|
|
45
|
+
Company filter mandatory — bare names return namesakes. Still nothing → leave record,
|
|
46
|
+
report.
|
|
41
47
|
- Needs email → `user_email` (batch ≤10 profiles, low yield — set expectations honestly).
|
|
42
48
|
A higher-yield `find_email_by_url` exists in the API but may be disabled in MCP — trust
|
|
43
49
|
`discover("linkedin", "user")`; if absent, stop at `user_email` and report coverage as-is.
|
|
@@ -46,10 +52,42 @@ Re-use cache (`query_cache`) instead of re-fetching anything twice.
|
|
|
46
52
|
|
|
47
53
|
### 3. Resolve companies
|
|
48
54
|
|
|
49
|
-
- By domain
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
|
|
55
|
+
- By domain — with MANDATORY exact verification on every resolve (the `website` search is
|
|
56
|
+
substring match: stripe.com → Soundstripe; stlabs.com → five other *labs.com companies).
|
|
57
|
+
Default one domain per call; OR-DSL batching is an optimization with a tax — any domain
|
|
58
|
+
that didn't come back exact-matched gets re-queried individually:
|
|
59
|
+
```
|
|
60
|
+
execute linkedin/search/search_sql_companies {website: "acme.com", count: 5}
|
|
61
|
+
# batched variant: {website: "acme.com|globex.io", count: 10× domains}, then per domain:
|
|
62
|
+
query_cache {conditions: [{"field": "website", "op": "=", "value": "acme.com"}],
|
|
63
|
+
limit: <fetched count>}
|
|
64
|
+
# query_cache filters over the WHOLE cached set; `limit` (default 10) caps only how many
|
|
65
|
+
# rows come BACK. One domain → the default is fine; a multi-domain batch → pass a limit.
|
|
66
|
+
```
|
|
67
|
+
Only an exact-website match (normalized: lowercase, no protocol/www/path) counts as
|
|
68
|
+
resolved. Gives industry, employee_count, description, locations.
|
|
69
|
+
|
|
70
|
+
**No exact match? Do NOT stop there** — a whole class of domains never appears in its own
|
|
71
|
+
substring results (verified: `{website: "stlabs.com", count: 5}` returns five other
|
|
72
|
+
*labs.com companies and never STLabs, a live company with a LinkedIn page). Standard second
|
|
73
|
+
step, ~1cr:
|
|
74
|
+
```
|
|
75
|
+
execute webparser/parse {url: "https://acme.com", extract_minimal: true}
|
|
76
|
+
→ top-level `title` = who they say they are
|
|
77
|
+
→ links[] usually carries their own linkedin.com/company/... URL
|
|
78
|
+
execute linkedin/company {company: "<that URL>"} # exact, no fuzzy matching
|
|
79
|
+
```
|
|
80
|
+
Only after that fails is the domain genuinely unresolved — report it, never write.
|
|
81
|
+
- Deeper firmographics (funding, size range): take the alias from `crunchbase_link`, which
|
|
82
|
+
the domain-resolve above ALREADY returned — free. Only when that field is empty and the
|
|
83
|
+
company is plausibly venture-backed, fall back to the live `crunchbase/search` by name
|
|
84
|
+
(20cr, fuzzy — verify name+domain before trusting it):
|
|
85
|
+
```
|
|
86
|
+
# crunchbase_link: "https://www.crunchbase.com/organization/acme" → alias "acme"
|
|
87
|
+
execute crunchbase/company {company: "acme"}
|
|
88
|
+
```
|
|
89
|
+
Only when the profile maps such fields. Normalize `contacts.email` before use — trailing
|
|
90
|
+
dots observed ("founders@reducto.ai."), and a match key with a trailing dot matches nothing.
|
|
53
91
|
|
|
54
92
|
### 4. Write back
|
|
55
93
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: anysite-crm-inbound
|
|
3
|
+
description: Instant read-only verdict on ONE inbound lead - who they really are, whether the company is real and ICP-fit, route suggestion and talking points - in 2-5 anysite calls. Takes an email, a name+company, or a linkedin_url; checks the CRM for prior history but writes nothing. Use when a single new lead just arrived (demo request, reply, form fill, DM) and the user wants a fast qualify/route decision. For batch prospecting use anysite-crm-prospect; for accounts already worked in the CRM use anysite-crm-account-brief.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CRM Inbound — one lead, one minute
|
|
7
|
+
|
|
8
|
+
Every skill in this pack is batch-shaped except the most frequent moment in GTM life:
|
|
9
|
+
a single lead just landed and someone asks "is this real / are they our ICP / who takes
|
|
10
|
+
the call?". This skill answers that in 2 calls (verdict) to 4–5 calls (full mini-brief).
|
|
11
|
+
Strictly read-only.
|
|
12
|
+
|
|
13
|
+
## Input
|
|
14
|
+
|
|
15
|
+
Whatever the user has: an email, a name + company, a linkedin_url, or a forwarded
|
|
16
|
+
demo-request text. Extract identifiers yourself; don't interrogate the user.
|
|
17
|
+
|
|
18
|
+
## Flow (cheap-first, stop as soon as the verdict is clear)
|
|
19
|
+
|
|
20
|
+
### 1. Identify the person and company
|
|
21
|
+
|
|
22
|
+
- **linkedin_url** → `execute linkedin/user/user {user: <url>}` → done.
|
|
23
|
+
- **email with a work domain** → resolve the company by domain (anysite-mcp recipe: exact
|
|
24
|
+
verify; `webparser/parse` fallback for common-token domains) → `organizational_urn` →
|
|
25
|
+
`search_users {first_name, last_name, current_company: [{"type": "company",
|
|
26
|
+
"value": "<id>"}]}`. Note: `email_sql_user` reverse lookup is a cheap first try but
|
|
27
|
+
verified to miss often — don't stop on its empty result.
|
|
28
|
+
- **email with a personal domain** (gmail etc.) → reverse lookup try, else name+company if
|
|
29
|
+
the form/message carries them. A lead reachable ONLY via personal email = flag it.
|
|
30
|
+
- **name + company** → resolve company → `search_users` with the company filter (bare
|
|
31
|
+
names return namesakes).
|
|
32
|
+
|
|
33
|
+
### 2. Company reality check (1 call, often already done in step 1)
|
|
34
|
+
|
|
35
|
+
The verified `search_sql_companies` row gives industry, employee_count, locations,
|
|
36
|
+
description, `crunchbase_link`, `organizational_urn`. Funding stage matters →
|
|
37
|
+
`crunchbase/company` via the free alias from `crunchbase_link` (skip for obviously
|
|
38
|
+
non-venture companies).
|
|
39
|
+
|
|
40
|
+
### 3. CRM history (free, crm_* reads)
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
crm_query_records(object_type="contacts", emails=[...]) # known already?
|
|
44
|
+
crm_query_records(object_type="companies", search="<domain>") # account history?
|
|
45
|
+
```
|
|
46
|
+
Existing record with an owner → this is a routing question, not a research question; say
|
|
47
|
+
who owns it. Closed-lost history → the verdict must mention it.
|
|
48
|
+
|
|
49
|
+
### 4. Verdict
|
|
50
|
+
|
|
51
|
+
One compact block, in this order:
|
|
52
|
+
1. **Real?** — person verified (profile ↔ claimed company match), company verified.
|
|
53
|
+
2. **ICP fit** — against the profile's known criteria (or the user's stated ICP); one line
|
|
54
|
+
of evidence per criterion, "unknown" where no data.
|
|
55
|
+
3. **Route** — new/known, suggested owner if CRM history names one, urgency (fresh funding
|
|
56
|
+
or hiring in the buyer function raises it).
|
|
57
|
+
4. **3 talking points** with dates and links (recent posts, funding, launches, hiring).
|
|
58
|
+
5. **Red flags** — personal-email-only, unresolvable company, competitor employee,
|
|
59
|
+
student/job-seeker pattern.
|
|
60
|
+
|
|
61
|
+
## Rules
|
|
62
|
+
|
|
63
|
+
- **Read-only.** No upserts, no dry-runs. If the user wants the lead in the CRM afterwards,
|
|
64
|
+
hand off to `anysite-crm-prospect` (its dedup and create rules apply).
|
|
65
|
+
- Never claim "verified" on an unverified identity — the namesake trap applies to inbound
|
|
66
|
+
more than anywhere (people misspell their own company in forms).
|
|
67
|
+
- Cost: ~2 credits for a verdict, 4–5 calls for the full brief. Cheap enough to run on
|
|
68
|
+
every inbound; say so if the user hesitates.
|
|
@@ -21,10 +21,20 @@ explicit pick of 10–30 names. Fewer than ~8 seeds → warn that the pattern wi
|
|
|
21
21
|
|
|
22
22
|
### 2. Profile the seeds
|
|
23
23
|
|
|
24
|
-
Resolve each seed to structured firmographics
|
|
24
|
+
Resolve each seed to structured firmographics — exact verification is mandatory on every
|
|
25
|
+
resolve (the `website` search is substring match and can return only look-alike domains;
|
|
26
|
+
a wrong seed poisons the whole ICP pattern downstream):
|
|
25
27
|
```
|
|
26
|
-
execute linkedin/search/search_sql_companies {website: "
|
|
28
|
+
execute linkedin/search/search_sql_companies {website: "seed1.com", count: 5} # per seed
|
|
29
|
+
# batched variant allowed, but: any seed without an exact match must be re-queried
|
|
30
|
+
# individually. query_cache filters the WHOLE cached set; `limit` (default 10) caps only
|
|
31
|
+
# how many rows come back — pass one when a batch should return more than 10 matches.
|
|
32
|
+
query_cache {conditions: [{"field": "website", "op": "=", "value": "seed1.com"}], limit: 50}
|
|
27
33
|
```
|
|
34
|
+
A seed with no exact website match is NOT dropped yet — resolve it via the site itself
|
|
35
|
+
(`webparser/parse {url, extract_minimal: true}` → top-level `title` + own linkedin.com/company
|
|
36
|
+
URL in `links[]` → `linkedin/company`), or via crunchbase → `contacts.linkedin_url`. Only a
|
|
37
|
+
seed that survives neither is excluded from profiling, and say which ones.
|
|
28
38
|
Plus `crunchbase/company` for stage/funding on a subset (venture-relevant seeds only).
|
|
29
39
|
Derive the pattern in-session and SHOW it:
|
|
30
40
|
|
|
@@ -33,6 +43,11 @@ Industries: X (60%), Y (25%) · Size: 11-200 dominant · Geo: US+UK 80%
|
|
|
33
43
|
Stage: seed-B · Common traits: has API docs page, hiring in data roles, ...
|
|
34
44
|
```
|
|
35
45
|
|
|
46
|
+
Build the size band from `employee_count`, never from `employee_count_range` — the two can
|
|
47
|
+
contradict each other in one record (verified: `employee_count: 1465` with
|
|
48
|
+
`employee_count_range: "201-500"`), and a wrong band here propagates into every search
|
|
49
|
+
below. Bucket the exact counts yourself.
|
|
50
|
+
|
|
36
51
|
The user confirms/edits the pattern — it's their ICP, the data only proposes it.
|
|
37
52
|
|
|
38
53
|
### 3. Search for lookalikes
|
|
@@ -65,9 +65,12 @@ crm_upsert_contacts(records=[{email | linkedin_url,
|
|
|
65
65
|
associate_company_domain: <domain>}],
|
|
66
66
|
allow_create=true, dry_run=true) → confirm → write
|
|
67
67
|
```
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
Before pushing, split found emails by domain: personal addresses (gmail/yahoo/outlook and
|
|
69
|
+
similar — `user_email` returns mostly these) are NOT work emails — never feed them into a
|
|
70
|
+
work-email sequence; keep those leads in a "personal email only" bucket alongside
|
|
71
|
+
"pending email", and say so in the report. Server requires email to create a contact;
|
|
72
|
+
contacts without email that don't match an existing record will be skipped with a
|
|
73
|
+
warning — report them as "found, pending email", don't retry blindly. When associating to companies created in the same run, prefer
|
|
71
74
|
`associate_company_id` from the company upsert result. Save `run_id`s.
|
|
72
75
|
|
|
73
76
|
Note: crm_* tools cannot add records to CRM lists (list_id is read-only in queries). If
|
|
@@ -36,12 +36,33 @@ explainable and reproducible.
|
|
|
36
36
|
```
|
|
37
37
|
crm_query_records(object_type="companies", ...) → record_id, name, domain, existing fields
|
|
38
38
|
```
|
|
39
|
-
- Base firmographics: `
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
`
|
|
39
|
+
- Base firmographics: `search_sql_companies` by `website` — default one domain per call;
|
|
40
|
+
OR-DSL batching (`{website: "a.com|b.com|...", count: 10× domains}`) is an optimization
|
|
41
|
+
with a verification tax (see the anysite-mcp resolve recipe). Never `count: 1` — the search
|
|
42
|
+
is substring match, and a common-token domain comes back with only look-alikes even in a
|
|
43
|
+
single-domain call. Verify the exact `website` match per domain via `query_cache` with an
|
|
44
|
+
explicit `limit` (default is 10 — a 20-domain batch needs more). Unverified match = no
|
|
45
|
+
evidence, score that criterion "unknown"; a domain that never comes back exact-matched is
|
|
46
|
+
resolved via `webparser/parse` on the site itself, per the same recipe.
|
|
47
|
+
- Stage/funding (only if the rubric needs it): take the alias from `crunchbase_link`, which
|
|
48
|
+
the domain-resolve above ALREADY returned — free, no lookup. Only when it is empty and the
|
|
49
|
+
company is plausibly venture-backed, fall back to the live `crunchbase/search` (20cr, fuzzy
|
|
50
|
+
— verify name+domain) → `crunchbase/company`. Skip entirely for obviously non-venture
|
|
51
|
+
companies. Note `leadership_hires[]` is unusable as an ICP criterion for SMB/startup targets
|
|
52
|
+
— measured empty on 6 of 6 live accounts, including a 281-person one.
|
|
53
|
+
- Hiring probe (only if in rubric): prefer the numeric id from `organizational_urn` of the
|
|
54
|
+
domain-resolve you already did → `search_jobs {company: [{"type": "company", "value":
|
|
55
|
+
"<id>"}], count: 20}`. No resolve → `search_companies {keywords: name, count: 5}` +
|
|
56
|
+
verify by name/industry (its `urn` is already the `{type, value}` object).
|
|
57
|
+
- Team-shape evidence (great for "engineering-led vs sales-led" criteria):
|
|
58
|
+
`linkedin/company/company_employee_stats` (1cr, needs company URN) — absolute headcounts
|
|
59
|
+
by function (verified: Engineering 26 / Sales 14 on a 79-person company). Don't sum its
|
|
60
|
+
`locations` array (nested buckets: US ⊃ state ⊃ metro); cross-check totals against
|
|
61
|
+
`employee_count`.
|
|
62
|
+
|
|
63
|
+
Company size in the rubric: use `employee_count`, never `employee_count_range` — the two
|
|
64
|
+
can contradict each other in one record (verified: 1465 vs "201-500"), and the range would
|
|
65
|
+
misfile the size band silently. Range only as fallback when the count is empty, noted.
|
|
45
66
|
|
|
46
67
|
Skip any evidence source whose rubric weight is zero. State per-company data gaps —
|
|
47
68
|
a company with missing data gets a confidence note, not a silently low score.
|
|
@@ -61,6 +61,8 @@ Ask ONLY about ambiguous or high-stakes decisions, one compact block, not an int
|
|
|
61
61
|
- Overwrite policy for volatile fields: "Job titles: overwrite on change or fill blanks only?"
|
|
62
62
|
- Working list: "Which list do you enrich most — 'Inbound' or 'Outbound Q3'?" (only if lists exist)
|
|
63
63
|
- Creation policy: "May agents create new contacts, or update existing only?"
|
|
64
|
+
- Plan: "Are you on MCP Unlimited or a credit-based plan?" — future sessions use this to
|
|
65
|
+
decide between credit estimates (credit plans) and time estimates (Unlimited) before bulk runs
|
|
64
66
|
|
|
65
67
|
Confirm the full mapping as ONE list for approval, then save.
|
|
66
68
|
|
|
@@ -97,6 +99,8 @@ Generated <date> by anysite-crm-setup from live schema. Do not edit manually.
|
|
|
97
99
|
- allow_create: <true/false, as agreed>
|
|
98
100
|
- Never write (user protected, on top of server-enforced): <list or "-">
|
|
99
101
|
- Enum properties and their valid values: <property>: <values>
|
|
102
|
+
- MCP plan: <unlimited | credits>; bulk-run policy: <credit estimates | time estimates>
|
|
103
|
+
- crunchbase aliases cache: <name → alias, append as resolved — saves ~20cr/account/sweep>
|
|
100
104
|
```
|
|
101
105
|
|
|
102
106
|
Only include rows that were actually agreed or auto-mapped. Never invent mappings for data types
|
|
@@ -32,26 +32,44 @@ propose splitting or narrowing.
|
|
|
32
32
|
|
|
33
33
|
Run the cheap universal chain for every account; add optional probes when relevant.
|
|
34
34
|
|
|
35
|
-
**Funding / exec hires / news / layoffs (one lookup covers
|
|
35
|
+
**Funding / exec hires / news / layoffs / intent (one lookup covers five signals):**
|
|
36
36
|
```
|
|
37
|
-
|
|
37
|
+
# The domain-resolve you already did (search_sql_companies) returns `crunchbase_link` —
|
|
38
|
+
# extract the alias from it for FREE. Only when crunchbase_link is empty AND the company
|
|
39
|
+
# is plausibly venture-backed, fall back to the expensive live search:
|
|
40
|
+
execute crunchbase/search {keywords: "<company name>", count: 3} # 20cr/50 — last resort
|
|
38
41
|
execute crunchbase/company {company: "<alias>"}
|
|
39
42
|
→ funding_rounds[] (date, type, amount, lead investors)
|
|
40
|
-
→ leadership_hires[] (date, role, description)
|
|
43
|
+
→ leadership_hires[] (date, role, description) — measured live: EMPTY on 6 of 6
|
|
44
|
+
SMB/startup accounts (incl. a 281-person, 10-year-old company; sample: US tech/AI).
|
|
45
|
+
Never build the Act-now tier on its presence for that ICP; empty ≠ "no hires"
|
|
41
46
|
→ news[] (title, date, publisher)
|
|
42
47
|
→ layoffs[]
|
|
48
|
+
→ bombora_surges[] (free intent bonus: topics the account's staff is researching.
|
|
49
|
+
Count it ONLY if a topic matches the user's product category — it reflects what
|
|
50
|
+
they buy, not that they need you; weak-moderate on its own, good as a stack booster)
|
|
43
51
|
```
|
|
44
|
-
Alias
|
|
45
|
-
|
|
52
|
+
Alias hygiene: aliases are case-sensitive (412 on miss → re-resolve once, likely rebrand).
|
|
53
|
+
Keep a name → alias table in the local profile file so future sweeps skip resolution
|
|
54
|
+
entirely — the profile is a local file, it works even when CRM custom fields can't be
|
|
55
|
+
created. Coverage honesty: `crunchbase_link` is filled mostly for venture-backed companies
|
|
56
|
+
(≈3/10 in a live batch); bootstrapped/service companies often have NO Crunchbase record —
|
|
57
|
+
for them skip the crunchbase probe entirely instead of fuzzy-searching a wrong match.
|
|
46
58
|
|
|
47
59
|
**Hiring (what they're building):**
|
|
48
60
|
```
|
|
49
|
-
|
|
50
|
-
|
|
61
|
+
# Preferred: the domain-resolve response already carries `organizational_urn`
|
|
62
|
+
# ("company:1441") — take the numeric id from it, no extra search needed:
|
|
51
63
|
execute linkedin/search/search_jobs {company: [{"type": "company", "value": "1441"}],
|
|
52
64
|
count: 20, sort: "recent"}
|
|
65
|
+
# Only for accounts that were never domain-resolved:
|
|
66
|
+
execute linkedin/search/search_companies {keywords: "<name>", count: 5}
|
|
67
|
+
→ pick the RIGHT company by name + industry + alias (first hit is often a namesake:
|
|
68
|
+
"Notion" returns a Media Production company first, notionhq second)
|
|
69
|
+
→ its urn is already {"type": "company", "value": "<id>"} — pass through as-is
|
|
53
70
|
```
|
|
54
|
-
|
|
71
|
+
A wrong-company URN turns someone else's vacancies into a fake hiring signal — worse than
|
|
72
|
+
no signal. Unsure which company is right → skip the hiring probe for that account, say so.
|
|
55
73
|
Look for roles in the buyer function (e.g. RevOps/Growth/Data roles for a data product).
|
|
56
74
|
|
|
57
75
|
**Mentions / social activity (optional):**
|
|
@@ -61,13 +79,28 @@ execute linkedin/search/search_posts {keywords: "\"<company name>\"",
|
|
|
61
79
|
execute techmeme/stories/stories_search {keyword: "<company name>", count: 5}
|
|
62
80
|
```
|
|
63
81
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
82
|
+
the author/context before counting a mention as a signal. For SMALL accounts, where keyword
|
|
83
|
+
search finds nothing, the better probe is the company's own feed:
|
|
84
|
+
`linkedin/company/company_posts` (~1cr/10) — hiring announcements there name new people in
|
|
85
|
+
`mentioned[]` with vanity aliases (new-hire signal + a warm contact in one call).
|
|
86
|
+
|
|
87
|
+
### 3. Score and stack — and filter out what was already reported
|
|
88
|
+
|
|
89
|
+
**Novelty check first:** if the profile maps signal fields, you pulled `last_signal_date` /
|
|
90
|
+
`last_signal_type` in step 1 — a "signal" older than or equal to what the CRM already
|
|
91
|
+
records is NOT news. Without it, a funding round from three months ago gets re-announced as
|
|
92
|
+
fresh on every sweep and the user stops trusting the report. Previously-known signals go
|
|
93
|
+
into a collapsed "already reported" section, never into Act now.
|
|
94
|
+
|
|
95
|
+
Then, per account, count NEW signals in the last 30/90 days, weighted by conversion value —
|
|
96
|
+
and the weights are ICP-dependent, because signal AVAILABILITY is:
|
|
97
|
+
- **SMB/startup ICP:** lead with funding rounds and job postings in the buyer function —
|
|
98
|
+
both filled on every account measured; treat `leadership_hires[]` as a bonus when present
|
|
99
|
+
(it was empty on the whole live sample), catching exec changes via job postings and
|
|
100
|
+
company_posts instead.
|
|
101
|
+
- **Enterprise/press-covered ICP:** exec hire in buyer function > funding round > hiring
|
|
102
|
+
surge in relevant roles > news > mentions (appointments there do reach the press feeds
|
|
103
|
+
that fill leadership_hires). Layoffs = negative budget signal for expansion, positive for cost-saving pitches —
|
|
71
104
|
interpret against the user's product.
|
|
72
105
|
|
|
73
106
|
Output tiers: **Act now** (2+ fresh signals), **Watch** (1 signal), **Quiet**.
|
|
@@ -30,8 +30,13 @@ is the map: how to call them, which sources cover which GTM need, and how to not
|
|
|
30
30
|
and paging of that result is free. Never re-run `execute` to look at the same data twice.
|
|
31
31
|
4. **Cheap-first cascade.** When several endpoints can answer, call the cached/DB one first
|
|
32
32
|
(`*/db/*`, `*sql*` endpoints, ~1 credit) and the live one only for the remainder.
|
|
33
|
-
5. **Estimate volume before bulk runs
|
|
34
|
-
|
|
33
|
+
5. **Estimate volume before bulk runs — plan-aware.** First know the user's plan (the CRM
|
|
34
|
+
profile stores it after setup; if unknown, ask once: MCP Unlimited or credit-based?).
|
|
35
|
+
- **Credit-based plan:** before anything above ~100 calls, state the estimate
|
|
36
|
+
(`N targets × credits-per-call`) and get a nod. Prefer cheap DB endpoints, batch hard.
|
|
37
|
+
- **MCP Unlimited:** credit warnings off, but keep batch sizes sane anyway — the real
|
|
38
|
+
limits are latency and upstream rate limits, so cap sweeps the same way and say
|
|
39
|
+
"this will take ~N minutes" instead of a price.
|
|
35
40
|
6. **Avoid `gdelt`** — it has repeatedly timed out in practice. Use techmeme or google news
|
|
36
41
|
instead.
|
|
37
42
|
|
|
@@ -40,8 +45,42 @@ is the map: how to call them, which sources cover which GTM need, and how to not
|
|
|
40
45
|
**Company discovery (bulk):**
|
|
41
46
|
- `linkedin/search/search_sql_companies` — the workhorse. Up to 1000 companies per call with
|
|
42
47
|
DSL filters (keywords, industry_name, employee_count_min/max, country_hq, founded_on_min/max,
|
|
43
|
-
has_website). Also does batch lookup by `urn` list and search by `website
|
|
44
|
-
|
|
48
|
+
has_website). Also does batch lookup by `urn` list and search by `website`.
|
|
49
|
+
⚠️ **`website` search is SUBSTRING match, ordered by last_modified. Verification is
|
|
50
|
+
MANDATORY on every resolve — position in the results means nothing.** Verified live:
|
|
51
|
+
`{website: "stripe.com", count: 1}` → Soundstripe; `{website: "stlabs.com", count: 5}` →
|
|
52
|
+
five *labs.com companies, none of them stlabs.com (common tokens flood the result even in
|
|
53
|
+
a single-domain call). The domain-resolve rules:
|
|
54
|
+
1) **Verify exact `website` match** (normalize both sides: lowercase, strip
|
|
55
|
+
protocol/`www.`/path) on EVERY resolve, single or batched. No exact match =
|
|
56
|
+
**unresolved** — never write anything to the CRM for it; wrong-company data lands in
|
|
57
|
+
blank fields where nobody will catch it.
|
|
58
|
+
2) Default: one domain per call, small count. OR-DSL batching
|
|
59
|
+
(`{website: "a.com|b.io", count: 10× domains}`) is an optimization with a verification
|
|
60
|
+
tax: a domain with a common token can be flooded out of the batch entirely — every
|
|
61
|
+
domain that didn't come back exact-matched must be re-queried individually.
|
|
62
|
+
3) `query_cache` filters over the WHOLE cached set (verified) but returns at most `limit`
|
|
63
|
+
rows (default 10) — pass an explicit `limit` when you expect more matches back. Sanity
|
|
64
|
+
rule: `aggregate {op: "count"}` should equal the `total` from execute; if not, page
|
|
65
|
+
with `get_page` before concluding anything.
|
|
66
|
+
4) A whole class of domains never appears in its own substring results (common-token
|
|
67
|
+
domains like stlabs.com) — so the website's own page is the STANDARD second step, not
|
|
68
|
+
an emergency: `webparser/parse {url: "https://<domain>", extract_minimal: true}` →
|
|
69
|
+
top-level `title` says who they are, `links[]` usually carries their own
|
|
70
|
+
linkedin.com/company/... URL → `linkedin/company` for the exact URN (verified, ~1cr).
|
|
71
|
+
Live shape on stlabs.com: `title: "STLabs — Intelligent Service Management"` at the TOP
|
|
72
|
+
level, while `metadata` came back `{}` and `cleaned_html` empty — read `title`, and treat
|
|
73
|
+
`metadata` as a fallback only, not the primary location.
|
|
74
|
+
Secondary fallback: `crunchbase/search` by name → `contacts.linkedin_url`. Name search
|
|
75
|
+
alone is never a source of truth.
|
|
76
|
+
Bonus from a successful resolve: the `search_sql_companies` row already carries
|
|
77
|
+
`crunchbase_link` (free crunchbase alias — skip the live 20cr search) and
|
|
78
|
+
`organizational_urn` (`company:<id>` — the numeric id goes straight into `search_jobs`).
|
|
79
|
+
⚠️ For company SIZE use `employee_count`, never `employee_count_range` — the two fields
|
|
80
|
+
can contradict each other in the same record (verified: Clay returns `employee_count:
|
|
81
|
+
1465` alongside `employee_count_range: "201-500"`). The range field looks like the natural
|
|
82
|
+
key for size segmentation and would misfile that company by ~3x, silently. Fall back to
|
|
83
|
+
the range only when the exact count is empty, and say that you did.
|
|
45
84
|
- `crunchbase/db/db_search` — filters by funding stage, last funding date, investors,
|
|
46
85
|
employee range; count ≤100, dates as Unix timestamps. 1 credit/result. Response includes
|
|
47
86
|
`funding_rounds[]`, `leadership_hires[]`, `layoffs[]`, `news[]`, `technologies[]`,
|
|
@@ -53,8 +92,37 @@ is the map: how to call them, which sources cover which GTM need, and how to not
|
|
|
53
92
|
early-stage supplements (check discover for exact endpoint names before calling).
|
|
54
93
|
|
|
55
94
|
**Company detail:** `crunchbase/company` (by alias — resolve via `crunchbase/search` first),
|
|
56
|
-
`linkedin/company`.
|
|
57
|
-
|
|
95
|
+
`linkedin/company`. One `crunchbase/company` call also carries free extras worth reading:
|
|
96
|
+
`bombora_surges[]` (B2B intent topics — but they show what THAT company's staff researches,
|
|
97
|
+
i.e. what they BUY; treat as a signal only when a topic matches what the user sells),
|
|
98
|
+
`related.competitors[]`, `predictions.funding_score`, `awards[]`. Coverage caveat:
|
|
99
|
+
`leadership_hires[]` is often EMPTY for smaller companies — absence of the field is not
|
|
100
|
+
absence of hires. Normalize `contacts.email` (trailing dots observed: "x@y.ai.").
|
|
101
|
+
A third resolve path when crunchbase is already fetched: `contacts.linkedin_url` →
|
|
102
|
+
`linkedin/company` → exact URN (verified; bypasses both fuzzy searches).
|
|
103
|
+
Note: `owler` endpoints need an owler alias and its search has no name/keyword parameter —
|
|
104
|
+
not usable for looking up a named account.
|
|
105
|
+
|
|
106
|
+
**Engagement graph (who interacted with content):** `linkedin/post/post_comments`,
|
|
107
|
+
`post_reactions`, `post_reposts` and `linkedin/company/company_posts` (~1cr/10) — answers
|
|
108
|
+
"who paid attention to this content", incl. people outside your title filters. Identifiers:
|
|
109
|
+
comments/reposts carry a vanity alias; reactions give only an obfuscated `/in/ACoAA...` URL
|
|
110
|
+
plus `internal_id` — `user_email` accepts the `internal_id`, never the obfuscated URL.
|
|
111
|
+
Honest scaling: volume follows the SEED's audience, not the target's importance (large brand
|
|
112
|
+
post → dozens of engagers; 80-person company → 0–2 per post), and on a small account those
|
|
113
|
+
few are mostly the company's OWN staff plus engagement farmers (verified: 3 of 4 commenters
|
|
114
|
+
were employees) — filter by the author's company first and expect nothing left. Use this on
|
|
115
|
+
seeds with a real audience (a competitor's page), not on SMB target lists. For small accounts
|
|
116
|
+
the reliable nugget is `company_posts` → `mentioned[]`: hiring announcements name new people
|
|
117
|
+
with their vanity aliases. `linkedin/company/company_employee_stats` (1cr) gives
|
|
118
|
+
function/skill/location breakdown — cross-check totals against `employee_count` from
|
|
119
|
+
`linkedin/company` before trusting absolutes, and never sum the `locations` array: its
|
|
120
|
+
buckets are nested (US ⊃ California ⊃ SF Bay Area), so summing double-counts badly. Its
|
|
121
|
+
`llm_hint` promises seniority and growth trends that the response does not contain.
|
|
122
|
+
|
|
123
|
+
**Ads as a budget signal:** the `ad-transparency` sources (incl. LinkedIn Ad Library) are
|
|
124
|
+
untapped — a company running B2B ads is telling you it has budget and who its ICP is.
|
|
125
|
+
Listing is cheap; per-ad detail is a separate call each (N+1) — budget accordingly.
|
|
58
126
|
|
|
59
127
|
**People:** `linkedin/search/search_users` (use `job_title` + `current_company` or
|
|
60
128
|
`company_keywords`; never bare `keywords` alone — returns empty), `linkedin/user` (full
|
|
@@ -62,20 +130,34 @@ profile, needs alias/URL/URN — never guess the alias), `linkedin/user/user_pos
|
|
|
62
130
|
`user_experience`, `user_comments`.
|
|
63
131
|
|
|
64
132
|
**Email finding (cascade, cheap → expensive):**
|
|
65
|
-
1. `linkedin/user/user_email` — batch up to 10 profiles, cheap, low yield.
|
|
133
|
+
1. `linkedin/user/user_email` — batch up to 10 profiles, cheap, low yield. Truths from live
|
|
134
|
+
testing: it returns mostly PERSONAL addresses (gmail/yahoo), one row per EMAIL — not per
|
|
135
|
+
profile (group by `alias`/`internal_id` or you duplicate contacts), and its `found` field
|
|
136
|
+
is always true (useless as a check). Personal addresses are not work emails — never
|
|
137
|
+
present them as outreach-ready.
|
|
66
138
|
2. `linkedin/user/find_email_by_url` — by vanity URL, high yield but expensive (50cr).
|
|
67
139
|
**May be disabled on the server** — trust `discover("linkedin", "user")`: if it is not
|
|
68
140
|
listed there, it does not exist; stop at step 1 and say so honestly.
|
|
69
|
-
3. No email found → keep the lead anyway; CRM contact upserts match by `linkedin_url`
|
|
70
|
-
(but note: creating a NEW contact requires an email — no email means update-only).
|
|
71
|
-
|
|
72
|
-
**Reverse lookup (email → person)
|
|
73
|
-
`linkedin/email/email_user` (live)
|
|
141
|
+
3. No work email found → keep the lead anyway; CRM contact upserts match by `linkedin_url`
|
|
142
|
+
too (but note: creating a NEW contact requires an email — no email means update-only).
|
|
143
|
+
|
|
144
|
+
**Reverse lookup (email → person), reliability order:**
|
|
145
|
+
1. `linkedin/email/email_sql_user` (cached DB) → `email_user` (live) — cheap, but verified
|
|
146
|
+
to return empty even for people who are definitely on LinkedIn. Try, don't rely.
|
|
147
|
+
2. The cascade that works when you know the name (a CRM does): email domain → resolve the
|
|
148
|
+
company (verified, see above) → `organizational_urn` → `search_users {first_name,
|
|
149
|
+
last_name, current_company: [{"type": "company", "value": "<id>"}]}` → usually exactly
|
|
150
|
+
one match, delivered WITH the `fsd_profile` URN needed for `user_posts`. The company
|
|
151
|
+
filter is mandatory — a bare name returns namesakes.
|
|
74
152
|
|
|
75
153
|
**Hiring signals:**
|
|
76
154
|
- `linkedin/search/search_jobs` — by company; works for any company. The `company` param
|
|
77
|
-
takes `[{"type": "company", "value": "<numeric id>"}]`
|
|
78
|
-
`
|
|
155
|
+
takes `[{"type": "company", "value": "<numeric id>"}]`. `linkedin/search/search_companies`
|
|
156
|
+
returns `urn` ALREADY in that object form — pass it through as-is. Only `search_sql_companies`
|
|
157
|
+
returns string URNs (`fsd_company:<id>`) — there, extract the numeric id yourself. And
|
|
158
|
+
verify the company before using its URN: the first search hit is often a namesake
|
|
159
|
+
(verified: "Notion" → NOTION Media Production first, the real notionhq second) — check
|
|
160
|
+
name + industry + alias.
|
|
79
161
|
- `greenhouse/jobs/jobs_search {board_token, count}` — full descriptions via `content=true`;
|
|
80
162
|
`ashby/jobs/jobs_search {board_name, count}` — descriptions always included. Both need the
|
|
81
163
|
company slug; 412 = wrong token, fall back to linkedin jobs.
|
|
@@ -111,12 +193,14 @@ Covers any URL when no named source fits. Web search: `duckduckgo/search`, `brav
|
|
|
111
193
|
The standard pattern for account signals (used by the crm-signals skill):
|
|
112
194
|
|
|
113
195
|
```
|
|
114
|
-
company
|
|
115
|
-
→
|
|
116
|
-
|
|
117
|
-
→
|
|
118
|
-
→
|
|
196
|
+
company domain
|
|
197
|
+
→ search_sql_companies {website} + exact verify (firmographics
|
|
198
|
+
↳ crunchbase_link → alias FREE ↳ organizational_urn)
|
|
199
|
+
→ crunchbase/company {alias} → funding_rounds, leadership_hires, news, layoffs, bombora
|
|
200
|
+
→ search_jobs {company: [{type, value from organizational_urn}]} → what they hire for
|
|
201
|
+
→ search_posts (company name, past-month) → mentions
|
|
119
202
|
```
|
|
203
|
+
Three paid calls per account instead of four — the live crunchbase/search drops out.
|
|
120
204
|
|
|
121
205
|
Stack signals: one signal is a guess, 2–3 signals within ~30 days is a pattern worth acting on.
|
|
122
206
|
|