--- name: abm-lookalike-targeting description: Build a tiered ABM target list by taking the top closed-won accounts from whichever CRM is connected (HubSpot, Salesforce, Pipedrive, MS Dynamics 365 Sales), finding lookalike companies in Firmable, scoring them for fit, and excluding existing customers. CRM-agnostic — detects the connected CRM rather than assuming HubSpot. Use this skill whenever the user asks to build an ABM list, find similar companies to our best customers, expand pipeline using lookalikes, build a Q3 (or any quarter) target list, or anything along the lines of "find companies like our top accounts" or "who should we be targeting next". Trigger on phrases like "ABM target list", "lookalike accounts", "find companies similar to our customers", "net-new target list", or "tiered target list". --- # ABM lookalike targeting Take the top closed-won accounts from whichever CRM is connected, expand each into lookalike companies using Firmable's `find_similar_companies`, score the net-new candidates for fit, and produce a tiered target list saved to Firmable lists with a summary table in chat. This skill is CRM-agnostic. The Firmable side (steps 2, 3, 5, 6) is identical regardless of CRM — Firmable's own `push_to_crm` already auto-detects HubSpot, Salesforce, Pipedrive, MS Dynamics 365 Sales, or JobAdder. The CRM-specific part is **reading** deal/company data in steps 1 and 4 — see step 0. ## Defaults (confirmed with Ash, apply unless told otherwise) | Decision | Default | |---|---| | "Top N closed-won accounts" | Ranked by **total closed-won deal value, all-time**, aggregated per company (not per-deal) | | "Net-new" | Excludes companies that have at least one **won/customer** deal in the CRM. Companies with only open or lost deals are still eligible targets | | Output | A set of Firmable lists (one per tier) + a summary table in chat | | Seed count | 25 unless the user specifies a different number | If the user asks for a different seed count, ranking window (e.g. "last 12 months"), or net-new definition, just swap the filter/query accordingly and say what changed. ## Workflow ### 0. Detect the connected CRM Check which CRM's tools are already loaded (e.g. `HubSpot:*` tools present means HubSpot). If nothing's loaded yet, `tool_search` for "CRM" or the name of a specific CRM to see what's connected. Don't assume HubSpot — the read/query steps below differ by CRM: - **HubSpot connected** → follow `references/hubspot.md` for the exact stage-resolution and query syntax in steps 1 and 4. - **Any other CRM (Salesforce, Pipedrive, MS Dynamics 365 Sales, JobAdder, or something else)** → follow `references/other-crms.md`, which gives the generic requirements and per-CRM notes on what "closed-won" maps to, since tool names and query syntax aren't baked in for those. Note that step 6 (pushing Tier 1 to CRM) doesn't need this detection at all — `push_to_crm` handles it automatically. ### 1. Pull the top N closed-won accounts from the CRM Closed-won accounts must be ranked by **aggregate** deal value per company, not by individual deal size — a company with three $20k wins should outrank a company with one $50k win only if you're explicitly asked to rank by single deal. Default is aggregate. Use the reference file identified in step 0 for the concrete query. Whatever the CRM, you need: company name, a domain (or other identifier that lets you resolve the company in Firmable), and total closed-won value, for the top 25 companies. Some companies won't have a domain populated. Flag these separately; they'll need a name-based Firmable lookup instead of a domain-based one in step 2. ### 2. Resolve each seed account to a Firmable company id This step costs Firmable credits (`bulk_get_companies` is credit-chargeable, roughly 1 credit per company). Tell the user the expected cost before running it — for 25 seeds, that's up to 25 credits. ``` bulk_get_companies({ items: [{ domain: "acme.com" }, { domain: "beta.io" }, ...] }) ``` For seeds with no domain, fall back to a free `filter_search` name match instead of spending a credit: ``` filter_search({ category: "company", filters: { company_id: { "contains:acme": "acme" } } }) ``` Confirm the match against the CRM record before treating it as resolved — company names collide across regions more often than domains do. Keep a mapping of `crm_company_id -> firmable_company_id -> name` for the rest of the workflow. Drop any seed that can't be resolved and tell the user which ones (small companies or very new ones sometimes aren't in Firmable's index yet). ### 3. Expand each seed into lookalikes `find_similar_companies` is free — no credit cost. Call it once per resolved seed: ``` find_similar_companies({ id: "", country: "AU", size: "50" }) ``` Use `size: "50"` by default (25 seeds × 50 = up to 1,250 raw candidates before dedup, which is plenty). Set `country` to match where the seed account is based; if the user's ICP spans AU, NZ, and SEA, run the call once per relevant country per seed only if lookalikes are meaningfully different by region — otherwise just use the seed's own country. As you collect results, track two things per candidate company id: - **`overlap_count`**: the number of distinct seed accounts it showed up as a lookalike for - **`best_rank`**: the best (lowest) position it held in any single seed's lookalike list — Firmable returns lookalikes already ordered by its own similarity ranking, so position within a list is meaningful ### 4. Remove seeds and existing customers Two exclusion passes: 1. **Remove the 25 seeds themselves** if they show up in each other's lookalike lists (common — your best customers tend to look like each other). 2. **Remove existing customers.** Per the confirmed net-new definition, this means any candidate with a **won/customer** deal in the CRM — companies with only open or lost deals stay in play. Check candidates against the CRM in batches, using the same reference file from step 0 (`references/hubspot.md` or `references/other-crms.md`) for the concrete query against whichever CRM is connected. Anything that comes back as an existing customer gets dropped from the candidate pool, regardless of `overlap_count`. ### 5. Score and tier the survivors Rank purely on the two signals from step 3, in this order: | Tier | Rule | |---|---| | Tier 1 | `overlap_count >= 3` | | Tier 2 | `overlap_count == 2` | | Tier 3 | `overlap_count == 1`, sorted by `best_rank` ascending, capped so the **combined tiered list stays around 150 accounts** | `overlap_count` is the primary signal because a company resembling several different closed-won customers is a stronger, more validated fit signal than resembling just one. `best_rank` only breaks ties within Tier 3, where overlap can't discriminate. If Tier 1 + Tier 2 already exceed ~150 accounts, don't force a cap on them — that's a real signal the account base has strong network effects worth acting on. Only trim Tier 3 to keep the whole list executable. ### 6. Save to Firmable lists Create three lists (or reuse if they already exist — check `list_lists` first): ``` create_list({ name: "Q3 2026 ABM Target List - Tier 1", isShared: true }) create_list({ name: "Q3 2026 ABM Target List - Tier 2", isShared: true }) create_list({ name: "Q3 2026 ABM Target List - Tier 3", isShared: true }) ``` Then `add_profiles_to_list` (free, no credit cost) with the resolved list id and the candidate company ids for each tier, in batches of up to 500. Name the lists with the actual quarter/year the user asked for, not a hardcoded label — ask if it's ambiguous from context. ### 7. Present the summary table in chat ``` Q3 2026 ABM target list: 25 seed accounts -> 1,180 raw lookalikes -> 142 net-new after dedup and exclusions. **Tier 1 (14 accounts, overlap with 3+ closed-won accounts)** | Company | Overlap | Matched seeds | Industry | Size | | Acme Robotics | 4 | Beta Corp, Gamma Inc, Delta Ltd, Epsilon Co | Industrial automation | 200-500 | ... **Tier 2 (31 accounts, overlap with 2 closed-won accounts)** ... **Tier 3 (97 accounts, single closed-won match, ranked by similarity)** ... Saved to Firmable lists: [Tier 1], [Tier 2], [Tier 3]. ``` Keep each tier's table to the top 10-15 rows in chat with a note on the full count; the Firmable list has everything. Offer to push Tier 1 to the CRM via `push_to_crm` (it auto-detects whichever CRM is connected) since that's the tier with the strongest signal, but don't do it without asking — it costs crm_push credits and creates real CRM records. ## Edge cases worth knowing - **A seed account resolves to a company with 0 lookalikes returned.** Rare, but happens for very niche or very large companies. Note it and move on; don't retry with a different country unless the user asks. - **A candidate is a subsidiary or brand of an existing customer** (e.g. already-won "Acme AU" and candidate "Acme NZ"). Firmable IDs are usually distinct per legal entity, so this slips past the domain-based exclusion check. If a candidate's name closely matches an existing customer's name, flag it rather than silently including it. - **The CRM has multiple pipelines or deal stages that all count as "won."** Always resolve the current stage/status values fresh rather than reusing ones from a previous session — pipelines and stages get added or renamed. See the relevant reference file in `references/` for CRM-specific detail. - **Currency mixing.** If the CRM tracks multiple currencies (common for ANZ + US deals), always aggregate on a home-currency or converted field, never raw amounts across currencies. - **Credit cost surprises.** `bulk_get_companies` in step 2 is the only credit-charged call in the whole workflow. Everything else (`filter_search`, `find_similar_companies`, all list operations) is free. Quote the expected cost once, upfront, before running step 2. - **CRM connector changes mid-workflow.** If the connected CRM changes between sessions (e.g. a migration from HubSpot to Salesforce), re-run step 0 rather than assuming the previous session's CRM is still current. ## Why this skill exists Firmable's own product is built for exactly this kind of lookalike expansion, but doing it by hand for 25 seed accounts means 25+ manual lookups, no consistent way to prioritise the results, and no systematic check against who's already a customer. Overlap across multiple closed-won accounts is a much stronger fit signal than any single similarity match, so scoring on that (rather than treating the first lookalike list as the target list) is what turns this into something Sales can actually work off a tiered list, not a raw export. --- # Reference: HubSpot # HubSpot: closed-won aggregation and exclusion queries Use this reference when HubSpot is the connected CRM (i.e. `HubSpot:*` tools are loaded). ## Resolve the closed-won stage id(s) Don't assume a label or id — pipelines get added and stage ids differ per pipeline. ``` get_properties({ objectType: "deals", propertyNames: ["dealstage"] }) ``` Look for the stage(s) whose label is "Closed Won". There may be more than one if there are multiple pipelines (e.g. New Business vs Renewals). ## Step 1: top N closed-won accounts by aggregate value ```sql SELECT COMPANY.name, COMPANY.domain, SUM(amount_in_home_currency) FROM DEAL WHERE dealstage IN ('', '') GROUP BY COMPANY.name, COMPANY.domain ORDER BY SUM(amount_in_home_currency) DESC LIMIT 25 ``` Use `query_crm_data`. Always aggregate on `amount_in_home_currency`, never raw `amount` — deals span AUD and USD and raw amounts can't be summed across currencies. Some companies won't have `domain` populated. Flag these; they need a name-based Firmable lookup in step 2 of the main workflow instead of a domain-based one. ## Step 4: exclude existing customers ```sql SELECT COMPANY.domain FROM DEAL WHERE dealstage IN ('', '') AND COMPANY.domain IN ('', '', ...) ``` Anything returned here gets dropped from the candidate pool, regardless of `overlap_count`. Batch this in groups of ~50-100 domains per call to stay within HubSpot's query size limits. ## Notes - `search_crm_objects` with `filterGroups` also works for both steps if you prefer filter-based lookups over SQL, but `query_crm_data` is more direct for the aggregation in step 1. - If the workspace has multiple currencies, `amount_in_home_currency` is already converted — state the total is in the portal's home currency when presenting numbers. --- # Reference: Other CRMs # Other CRMs: Salesforce, Pipedrive, MS Dynamics 365 Sales, JobAdder Firmable's `push_to_crm` and `bulk_push_list_to_crm` already auto-detect whichever CRM is connected — nothing to change there regardless of which CRM the workspace uses. The part that varies by CRM is **reading** deal and company data for steps 1 and 4 of the main workflow (finding the top closed-won accounts, and checking whether a lookalike candidate is already a customer). HubSpot has a dedicated reference (`hubspot.md`) because its tools are documented and tested here. For any other CRM, no MCP tool schema is baked into this skill — work it out live: 1. **Find the CRM's tools.** Run `tool_search` with the CRM's name (e.g. "Salesforce opportunities", "Pipedrive deals") to load whatever query/read tools are connected for it. Tool names and schemas differ per CRM MCP; don't assume HubSpot's tool names or SQL dialect carry over. 2. **Find the equivalent of "closed-won."** Each CRM models this differently: - **Salesforce**: `Opportunity` records where `IsWon = true` (or `StageName` matching the org's closed-won stage label — orgs customise this). - **Pipedrive**: `Deal` records with `status = 'won'`. - **MS Dynamics 365 Sales**: `Opportunity` records with `statecode = Won` (statuscode varies by org). - **JobAdder**: this is a recruitment ATS, not a deal-stage CRM. "Closed-won" doesn't map cleanly to placements vs jobs vs companies. If JobAdder is what's connected, check with the user before assuming what "closed-won account" should mean here. 3. **Aggregate by company, not by individual deal/opportunity.** The ranking is total won value per company, so whatever query tool is available needs to group by the associated account/organisation field, not list raw deal rows. 4. **Currency.** If the CRM tracks multi-currency deals, look for a home-currency or converted-amount field (as HubSpot's `amount_in_home_currency`) rather than summing raw amounts across currencies. 5. **The exclusion check in step 4** needs the same "closed-won, grouped by company" query, just filtered down to the specific candidate companies rather than ranked top 25. If a CRM tool for this doesn't seem to exist in the connected set, say so plainly rather than guessing at query syntax, and ask the user whether they want to point you at a specific tool or export.