--- name: account-expansion-signals description: Find expansion signals on managed CRM accounts this quarter — headcount growth, new office openings, or funding raised — using Firmable signal data. Use this whenever the user asks which accounts are growing, expanding, showing buying signals, hiring, opening offices, or raising funding, or asks for expansion/upsell candidates among existing customers. Trigger on phrases like "expansion signals", "which accounts are growing", "who's hiring/expanding/raising funding", "upsell candidates", "seat growth signals", or "account growth this quarter". --- # Account expansion signals For a set of managed CRM accounts (all of the user's, one account, or a segment — always ask), check Firmable for company-growth signals this quarter: headcount growth, new office openings, and funding raised. These are the most direct proxy for where the next seat/plan upgrade is likely to come from, since most expansion is seat growth or plan upgrades tracking company growth. Output is a summary table in chat, grouped by signal type, with the CRM account owner included so it's directly actionable. ## Workflow ### 0. Detect the connected CRM Check which CRM's tools are already loaded, or `tool_search` for "CRM" if nothing's loaded yet. This determines which reference file to use for step 2: - **HubSpot** → `references/hubspot.md` - **Any other CRM (Salesforce, Pipedrive, MS Dynamics 365 Sales, etc.)** → `references/other-crms.md` ### 1. Ask which accounts to check Always ask — don't assume. Use a single-select question: - **All my managed accounts** — accounts owned by the requesting user in the CRM - **One specific account** — ask for the name - **A segment** — ask what defines it (a tier, region, plan, renewal-date window, or other CRM filter/list) If they pick "all," default to accounts owned by the current CRM user (resolve "me" via the CRM's own current-user tool, e.g. `get_user_details` for HubSpot). If they want the whole book across the team instead of just their own, that's a segment, not "all." ### 2. Pull the matching accounts from the CRM Using the reference file from step 0, get name + domain for every account in scope. A domain is required to resolve the company in Firmable in step 3 — flag any account missing one. Also pull the CRM owner for each account while you're at it (needed for the final table) — don't make a separate round trip for this later. ### 3. Resolve each account to a Firmable company This is credit-chargeable. `bulk_get_companies` costs roughly 1 credit per company — tell the user the expected cost (number of accounts in scope) before running it. ``` bulk_get_companies({ items: [{ domain: "acme.com" }, { domain: "beta.io" }, ...] }) ``` For accounts 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. Drop and note any account that can't be resolved. ### 4. Find or set up signal coverage Signal agents are how Firmable timestamps company events (headcount growth, new office, funding raised) — this is the right mechanism for "this quarter," since it's event-based rather than a single snapshot. ``` list_signal_agents() ``` For each agent, `get_signal_agent({ agentId })` and check: - Does it monitor **company** signals, and do the configured `companySignals` include anything matching headcount/employee growth, new office/location, or funding/investment? Match by keyword since the exact signal type names vary — don't assume a fixed enum. - Does it monitor a list that contains (or can be made to contain) the accounts resolved in step 3? **If a matching agent exists:** call `agent_signal_hits({ agentId, category: "company" })`, paginating with `from`/`size` as needed. Hits arrive newest-first with no native date filter, so page through and stop once hits fall outside the current quarter. Filter the results down to just the company ids resolved in step 3. **If no matching agent exists (common on first run):** this skill can't create a signal agent — that's app-side configuration, not exposed over MCP. Instead: 1. Build or update a Firmable list with the step 3 company ids: `create_list` (check `list_lists` first to avoid duplicating one) then `add_profiles_to_list` (free, no credit cost). 2. Tell the user plainly: no signal agent currently monitors this list for headcount growth, new office, or funding signals, and this is a one-time setup they (or their Firmable admin) need to do in the Firmable app, pointing the agent at the list just created and enabling those three company signal types. Once that's done, re-running this skill will pull real hits. 3. As an immediate, lower-confidence stand-in for that first run, offer to check current snapshot data instead: pull each company's current headcount band, office/location count, and last-known funding round via the same `bulk_get_companies`/`get_company` data already fetched in step 3, and flag anything that looks notably large or recent. Be explicit in the output that this is a **snapshot, not a quarter-over-quarter change** — it can't confirm something happened *this quarter* the way a signal hit can, only that it's true as of today. ### 5. Present the summary table Group by signal type, and always include the CRM account owner so the output is directly actionable: ``` Checked 42 of your managed accounts against Firmable signal agent "Customer Growth Watch" — 6 hits in Q3 2026 (Jul-Sep). **Headcount growth** | Account | Owner | Detail | |---|---|---| | Acme Pty Ltd | Sam Chen | Grew from ~120 to ~180 employees (signal dated 3 weeks ago) | **New office opened** | Account | Owner | Detail | |---|---|---| | Beta Corp | Priya Nair | Opened a Singapore office (signal dated 5 weeks ago) | **Funding raised** | Account | Owner | Detail | |---|---|---| | Gamma Inc | Sam Chen | Raised Series B (signal dated 6 weeks ago) | 3 accounts had no domain on file and couldn't be checked: [names]. 33 accounts showed no signal hits this quarter. ``` If step 4 fell back to a snapshot check instead of real signal hits, label the whole table clearly as "current snapshot, not confirmed this-quarter activity" so nobody mistakes it for timestamped signal data. ## Edge cases worth knowing - **"This quarter" is calendar quarter by default** (e.g. Jul-Sep for Q3). If the user's fiscal year differs, ask once and use that instead. - **An account has multiple signal hits of the same type in the quarter.** Report the account once with the most recent/most significant hit, not once per hit — a table row per event would bury the signal that actually matters (e.g. don't list three incremental headcount ticks separately). - **A resolved company has zero employees data or no funding history at all** (private, bootstrapped, or just sparse in Firmable's index). That's a "no signal" result, not an error — don't flag it as suspicious or worth following up on. - **The user's "all my managed accounts" includes accounts that aren't really expansion candidates** (e.g. accounts in an active support escalation, or already at the top plan tier). This skill doesn't know account health — it only reports growth signals. Don't editorialize about whether an account is a good expansion target; that's the account owner's call to make with the context this table gives them. - **Credit cost surprises.** `bulk_get_companies` in step 3 is the only credit-charged call in the whole workflow. Everything else (`filter_search`, `list_signal_agents`, `get_signal_agent`, `agent_signal_hits`, list operations) is free. Quote the expected cost once, upfront, before running step 3. ## Why this skill exists Most expansion in a seat-based or usage-based product tracks company growth directly — a customer that just grew headcount, opened a new office, or raised a funding round is far more likely to need more seats or a higher plan than one that hasn't changed. Firmable already tracks these events as timestamped signals; the gap is connecting them back to the specific accounts a rep or CS owner is responsible for in the CRM, and surfacing only the quarter's activity rather than a wall of historical data. --- # Reference: HubSpot # HubSpot: managed accounts and owners Use this reference when HubSpot is the connected CRM (i.e. `HubSpot:*` tools are loaded). ## Resolving "me" for the "all my managed accounts" scope ``` get_user_details() ``` Use the returned `ownerId` to filter companies by owner below. ## Pulling accounts in scope **All my managed accounts:** ``` search_crm_objects({ objectType: "companies", filterGroups: [{ filters: [{ propertyName: "hubspot_owner_id", operator: "EQ", value: "" }] }], properties: ["name", "domain", "hubspot_owner_id"] }) ``` **One specific account:** search by name first to resolve the company id, then pull its `domain` and `hubspot_owner_id`. **A segment:** translate whatever the user described (tier, region, plan, renewal window, a specific list/view) into `filterGroups`. If it's a property-based segment (e.g. `plan_tier = Enterprise`), a straightforward filter works. If it's more like "accounts renewing in the next 90 days," you may need an associated deal/renewal-date property rather than a company property — check `get_properties({ objectType: "companies" })` and `get_properties({ objectType: "deals" })` if it's not obvious which object holds the relevant field. ## Owner name lookup `hubspot_owner_id` is a numeric id, not a name. Use `search_owners` to resolve it to a display name for the final table, rather than showing raw ids. ## Notes - Always pull `domain` — it's the primary key used to resolve the account in Firmable in step 3 of the main workflow. Flag any company missing it. - If the user's segment spans a large number of accounts, batch the `search_crm_objects` calls (up to 200 results per page) rather than trying to pull everything in one call. --- # Reference: Other CRMs # Other CRMs: Salesforce, Pipedrive, MS Dynamics 365 Sales No MCP tool schema is baked into this skill for these — work it out live: 1. **Find the CRM's tools.** `tool_search` for the CRM's name (e.g. "Salesforce accounts", "Pipedrive organizations") to load whatever query/read tools are connected. Tool names and schemas differ per CRM MCP. 2. **Resolve "me" for the owner-based scope.** Every CRM MCP should expose some form of current-user lookup (Salesforce: `Whoami`-style call or the current user's Id; Pipedrive: current user id from the API context; MS Dynamics: the current `systemuser`). Use whatever the connected tools expose. 3. **Pull accounts by owner, name, or segment.** - **Salesforce**: `Account` records filtered by `OwnerId`, or by whatever fields express the requested segment (industry, `AnnualRevenue`, a custom tier field, etc.). Renewal-based segments likely need a join against `Opportunity` or a contract object. - **Pipedrive**: `Organization` records filtered by owner, or by custom fields for the segment. - **MS Dynamics 365 Sales**: `Account` entity filtered by `ownerid`, or by whatever attributes express the segment. 4. **Always pull a domain/website field per account** — it's the key used to resolve the account in Firmable. Flag any account missing one. 5. **Resolve owner names**, not just ids, for the final table — most CRMs need a follow-up lookup against a users/owners object to turn an id into a display name. If a query capability genuinely doesn't seem to exist in the connected tool set, say so plainly rather than guessing at syntax, and ask the user whether they want to point you at a specific tool, view, or export instead.