# Anysite Crm Signals

> 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.

- Skill: `anysiteio/anysite-crm-signals` (Agent Skill)
- Install (CLI): `npx skillmds@latest add anysiteio/anysite-crm-signals`
- Raw SKILL.md: https://api.skillmd.com/api/skills/anysiteio/anysite-crm-signals/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: anysiteio (https://skillmd.com/u/anysiteio)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/anysiteio/anysite-crm-signals

---


# CRM Signals

Turn a static account list into a prioritized "act now" list. One signal is a guess; 2+
signals within ~30 days is a pattern. Signal-triggered outreach converts several times
better than cold cadence (vendor-reported benchmarks: exec hires and job changes lead,
then funding) — treat the ordering as solid, the exact percentages as marketing.

## Prerequisites

Active CRM connection (`crm_list_connections`). Profile optional for read-only sweeps;
required if the user wants signal fields written back — and then the Writing rules in
`anysite-crm-setup` apply.

## Flow

### 1. Pick the account set

```
crm_query_records(object_type="companies", list_id=<target list> | search=...,
                  properties=[record_id, name, domain, <mapped signal fields if any>])
```

Cap a single sweep at ~50 accounts (each account costs several source calls). More →
propose splitting or narrowing.

### 2. Per-account signal collection

Run the cheap universal chain for every account; add optional probes when relevant.

**Funding / exec hires / news / layoffs / intent (one lookup covers five signals):**
```
# The domain-resolve you already did (search_sql_companies) returns `crunchbase_link` —
# extract the alias from it for FREE. Only when crunchbase_link is empty AND the company
# is plausibly venture-backed, fall back to the expensive live search:
execute crunchbase/search {keywords: "<company name>", count: 3}   # 20cr/50 — last resort
execute crunchbase/company {company: "<alias>"}
  → funding_rounds[] (date, type, amount, lead investors)
  → leadership_hires[] (date, role, description) — measured live: EMPTY on 6 of 6
    SMB/startup accounts (incl. a 281-person, 10-year-old company; sample: US tech/AI).
    Never build the Act-now tier on its presence for that ICP; empty ≠ "no hires"
  → news[] (title, date, publisher)
  → layoffs[]
  → bombora_surges[] (free intent bonus: topics the account's staff is researching.
    Count it ONLY if a topic matches the user's product category — it reflects what
    they buy, not that they need you; weak-moderate on its own, good as a stack booster)
```
Alias hygiene: aliases are case-sensitive (412 on miss → re-resolve once, likely rebrand).
Keep a name → alias table in the local profile file so future sweeps skip resolution
entirely — the profile is a local file, it works even when CRM custom fields can't be
created. Coverage honesty: `crunchbase_link` is filled mostly for venture-backed companies
(≈3/10 in a live batch); bootstrapped/service companies often have NO Crunchbase record —
for them skip the crunchbase probe entirely instead of fuzzy-searching a wrong match.

**Hiring (what they're building):**
```
# Preferred: the domain-resolve response already carries `organizational_urn`
# ("company:1441") — take the numeric id from it, no extra search needed:
execute linkedin/search/search_jobs {company: [{"type": "company", "value": "1441"}],
                                     count: 20, sort: "recent"}
# Only for accounts that were never domain-resolved:
execute linkedin/search/search_companies {keywords: "<name>", count: 5}
  → pick the RIGHT company by name + industry + alias (first hit is often a namesake:
    "Notion" returns a Media Production company first, notionhq second)
  → its urn is already {"type": "company", "value": "<id>"} — pass through as-is
```
A wrong-company URN turns someone else's vacancies into a fake hiring signal — worse than
no signal. Unsure which company is right → skip the hiring probe for that account, say so.
Look for roles in the buyer function (e.g. RevOps/Growth/Data roles for a data product).

**Mentions / social activity (optional):**
```
execute linkedin/search/search_posts {keywords: "\"<company name>\"",
                                      date_posted: "past-month", count: 20}
execute techmeme/stories/stories_search {keyword: "<company name>", count: 5}
```
Do NOT use gdelt in sweep loops — not broken, but 10–50s per call by design, which
multiplied by N accounts wrecks the sweep; techmeme/google news answer in seconds. Filter
false positives for generic company names by checking the author/context before counting
a mention as a signal. For SMALL accounts, where keyword
search finds nothing, the better probe is the company's own feed:
`linkedin/company/company_posts` (~1cr/10) — hiring announcements there name new people in
`mentioned[]` with vanity aliases (new-hire signal + a warm contact in one call).

### 3. Score and stack — and filter out what was already reported

**Novelty check first:** if the profile maps signal fields, you pulled `last_signal_date` /
`last_signal_type` in step 1 — a "signal" older than or equal to what the CRM already
records is NOT news. Without it, a funding round from three months ago gets re-announced as
fresh on every sweep and the user stops trusting the report. Previously-known signals go
into a collapsed "already reported" section, never into Act now.

Then, per account, count NEW signals in the last 30/90 days, weighted by conversion value —
and the weights are ICP-dependent, because signal AVAILABILITY is:
- **SMB/startup ICP:** lead with funding rounds and job postings in the buyer function —
  both filled on every account measured; treat `leadership_hires[]` as a bonus when present
  (it was empty on the whole live sample), catching exec changes via job postings and
  company_posts instead.
- **Enterprise/press-covered ICP:** exec hire in buyer function > funding round > hiring
  surge in relevant roles > news > mentions (appointments there do reach the press feeds
  that fill leadership_hires). Layoffs = negative budget signal for expansion, positive for cost-saving pitches —
interpret against the user's product.

Output tiers: **Act now** (2+ fresh signals), **Watch** (1 signal), **Quiet**.

### 4. Report (and optionally write back)

Always produce the human report first: account → signals → suggested angle ("congratulate
on Series B, reference the new VP Sales hire"). Hand a dated signal + the angle to
`anysite-outreach` to draft the first touch.

If the profile maps signal fields (e.g. `last_signal_type`, `last_signal_date`,
`signal_summary`) and the user wants them stored:
```
crm_upsert_companies(records=[{domain: "<domain>", properties:{...}}], allow_create=false,
                     overwrite_properties=[<signal fields — they are volatile by nature,
                     profile must mark them overwrite>], dry_run=true)
```
Company upserts match ONLY by domain — pull `domain` in step 1; accounts without one are
report-only. → confirm → write → report `run_id`.

## Recurrence

This skill is a one-shot sweep. For always-on monitoring suggest scheduling: a Claude Code
cron / `/loop`, or an operator habit ("run signals every Monday"). Note what was swept and
when in your report so the next run compares against it. Post search granularity is coarse
(`date_posted`: past-24h / past-week / past-month only) — a weekly cadence fits it best;
funding/news items carry their own dates, filter those by date in-session.

