Use whenever the user wants to find, shortlist, vet, or enrich US recruiting and staffing firms — executive search/retained search, RPO, tech/sales/healthcar...
---
name: find-recruiting-firm
description: Use whenever the user wants to find, shortlist, vet, or enrich US recruiting and staffing firms — executive search/retained search, RPO, tech/sales/healthcare recruiting, contingent/contract staffing, and temp staffing. Triggers on "find me an executive search firm for a CFO search", "shortlist three retained-search boutiques in NY focused on tech", "we need RPO support for a 50-engineer hiring push", or "pull contact info for these 8 staffing firm domains", even when described indirectly (need help hiring at scale, executive recruiter for senior roles). Drives the ServiceGraph API (api.servicegraph.co) — a 100k+ US firm catalog filterable by industry, services, location, size, ratings. Skip when the user wants to hire someone as their own employee (job-board questions, in-house recruiter hires, "where should I post the role"), individual job-seekers looking for recruiters to represent them, candidate-side career coaching, non-US firms, individual freelance recruiters.
version: "0.2.0"
metadata:
api_base: https://api.servicegraph.co
dataset_id: pro_services
industry: hr_recruiting_staffing
---
# find-recruiting-firm
Drive the **ServiceGraph API** (`https://api.servicegraph.co`) to find,
shortlist, and enrich US recruiting and staffing firms via the
`pro_services` dataset.
**This skill is for procuring an external recruiting/staffing firm**
to do hiring on the user's behalf. It is NOT for:
- recruiting an in-house employee (the user wants to hire someone for their own team — that's job-posting, not procurement),
- candidate-side asks (an individual job-seeker looking for someone to represent them).
Both share keyword overlap with the positive case ("recruiter", "hire"), so the boundary matters.
**Always pin `industry:hr_recruiting_staffing`.** Sub-types
(executive search, RPO, contingent staffing, temp, vertical
specializations) are NOT separate tags — sub-type specialization is
a keyword substring search on firm text.
Any HTTP client works (curl, fetch, requests). Examples below use curl.
## When NOT to use this skill
- "I want to hire a [role] for my team — where should I post the job?" → recruiting-an-employee.
- "Find me a recruiter to represent me in my job search" → candidate side.
- "Hire an in-house recruiter / Head of Talent" → recruiting an employee.
- "Help me write a job description" → DIY/do-the-work.
- ATS or HR-software comparisons (Greenhouse vs Lever, Workday).
- Career coaching for individual job-seekers.
- Non-US firms / individual freelance recruiters.
## MCP server (preferred for authed calls)
If your harness has the ServiceGraph MCP server loaded (tools
containing `servicegraph`), prefer those — OAuth 2.1 + PKCE keeps the
token in the harness sandbox. Otherwise use the REST flow below.
## API surface (dataset id: `pro_services`)
Every endpoint requires the bearer (`Authorization: Bearer vk_…`).
No anonymous tier.
| Endpoint | Cost | Use it for |
|---|---|---|
| `GET /v1/datasets/pro_services/fields[?include_values=1]` | free | Confirm `hr_recruiting_staffing` is in the `industry` value list. |
| `GET /v1/datasets/pro_services/check?filter=…` | free | Validate filter. |
| `POST /v1/datasets/pro_services/translate-intent` | free | `{intent}` → DSL filter + sanity count. |
| `GET /v1/datasets/pro_services/search?filter=…&limit=` | free | Brief firm cards + per-row unlock hint + total. |
| `GET /v1/datasets/pro_services/:apex` | free | One row brief; detail only if unlocked. |
| `POST /v1/datasets/pro_services/unlocks` | **10 credits / firm** | `{apexes:[...]}` ≤100; atomic; 30-day TTL on detail. |
| `GET /v1/me/credits` | free | Balance. |
**Cost model.** Discovery / validation / search / brief reads are
free. Detail (url, phone, email, social, address, full `platforms`
map) costs **10 credits per firm** and lasts **30 days**.
## Auth
`vk_*` API keys minted in the dashboard. **Keep the token out of the
LLM context** — never read `.env*` into your context; dispatch via
shell.
1. **Try the call first** through a shell wrapper that sources `.env.local`:
```bash
( set -a; [ -f .env.local ] && . ./.env.local; set +a;
curl -sS -H "Authorization: Bearer $SERVICEGRAPH_API_KEY" \
'https://api.servicegraph.co/v1/datasets/pro_services/fields' )
```
2. **On `401`** prompt the user:
> "Open **https://servicegraph.co/profile/api-keys**, create a
> key, and add `SERVICEGRAPH_API_KEY=vk_…` to `.env.local` here
> (or export it). Tell me when done. Please don't paste the key
> into chat."
3. **Retry** after the user signals ready.
## Filter DSL
GitHub-search-style.
```
filter := orExpr
orExpr := andExpr ("OR" andExpr)*
andExpr := notExpr (("AND")? notExpr)* # whitespace = implicit AND
notExpr := ("NOT" | "-") notExpr | atom
atom := "(" filter ")" | predicate
predicate:= IDENT op valueOrList | bareword
op := ":" | "=" | ">=" | "<=" | ">" | "<"
valueOrList := value ("," value)*
value := IDENT | NUMBER | tagAtEvidence
tagAtEvidence := IDENT "@" ("low"|"medium"|"high")
bareword := IDENT | NUMBER # → keyword:<bareword>
```
**Four rules that bite:** AND binds tighter than OR (use parens);
comma list = OR within one predicate; negation is `-x` or `NOT x`;
bareword = keyword search (quote multi-word phrases).
**Recruiting-flavored examples** (validate yours with `/check`):
```
industry:hr_recruiting_staffing "executive search"
industry:hr_recruiting_staffing "retained search" state:NY tech
industry:hr_recruiting_staffing rpo state:CA
industry:hr_recruiting_staffing contingent staffing
industry:hr_recruiting_staffing healthcare state:TX,FL
industry:hr_recruiting_staffing sales saas
industry:hr_recruiting_staffing rating>=4 has:clutch
```
**Sub-type → keyword mapping**:
| User asks for | Add as keyword(s) |
|---|---|
| Executive search / retained search | `executive`, `retained` |
| RPO (recruitment process outsourcing) | `rpo`, `"recruitment process outsourcing"` |
| Contingent / contract staffing | `contingent`, `contract` |
| Temp / temporary staffing | `temp`, `temporary` |
| Tech recruiting | `tech`, `technical`, `engineering` |
| Sales recruiting | `sales` |
| Healthcare recruiting | `healthcare`, `clinical`, `nursing` |
| Finance / accounting recruiting | `finance`, `accounting` |
| Legal recruiting | `legal`, `attorney` |
## Identifying firms — `apex`
Firms are identified by their **apex domain** (`korn-ferry.com`, not
`www.korn-ferry.com/about`).
## Recipes
### A. Executive search for a CFO
```
GET /v1/datasets/pro_services/search?filter=industry:hr_recruiting_staffing+executive+search&limit=10
# Present, get pick of 3. "Unlocking 3 = 30 credits, 30-day TTL."
POST /v1/datasets/pro_services/unlocks
{ "apexes": ["firm-a.com", "firm-b.com", "firm-c.com"] }
```
### B. Retained boutique in a state, vertical
```
GET /v1/datasets/pro_services/search?filter=industry:hr_recruiting_staffing+retained+state:NY+tech+-company_size_signal:large_50plus&limit=10
```
### C. RPO for a hiring surge
```
GET /v1/datasets/pro_services/search?filter=industry:hr_recruiting_staffing+rpo+(tech OR engineering)&limit=10
```
### D. Indirect intent — "scaling fast, need help hiring"
```
GET /v1/datasets/pro_services/search?filter=industry:hr_recruiting_staffing+(rpo OR contingent)&limit=10
```
Or use the translator:
```
POST /v1/datasets/pro_services/translate-intent
{ "intent": "RPO or volume recruiting partner for a 50-engineer hiring push" }
```
### E. Vertical: healthcare staffing
```
GET /v1/datasets/pro_services/search?filter=industry:hr_recruiting_staffing+healthcare+state:OH,IL,MI,IN,WI,MN&limit=10
```
### F. Quality threshold + tech-sector
```
GET /v1/datasets/pro_services/search?filter=industry:hr_recruiting_staffing+executive+search+tech+rating>=4&limit=10
```
### G. BYO apex list — enrich domains
User pastes 8–20 staffing/recruiting firm domains:
1. `GET /v1/datasets/pro_services/:apex` per domain — free brief
(404 = not in catalog, no charge).
2. User picks N to fully enrich. `POST /unlocks` = **10×N credits**,
atomic, detail returned.
3. Re-runs within 30-day TTL are free.
## Gotchas
- **Always pin `industry:hr_recruiting_staffing`.** Without it, "recruiter" / "executive search" keywords leak into other industries.
- **Distinguish "find me a recruiting firm" (procurement, fires) from "find me a recruiter / hire a recruiter for our team" (recruiting-an-employee, refuses).** When ambiguous, lean on context: explicit firm/agency/RPO language or volume framing → procurement; "for our team" / "to post the job" / "I want to hire" → in-house hire.
- **Candidate-side asks** ("represent me as a candidate", "find me a job") are out of scope.
- **Career coaching for individuals** is a different need (and shares the `executive-coaching` keyword with management consulting). Refuse — this skill is firm-procurement.
- **Sub-types are keyword-only.** Multi-word sub-types split into ANDed barewords unless quoted (`"executive search"` → one phrase).
- **Briefs DO include `apex`, `name`, location, ratings.** They DON'T include `url`, `phone_primary`, `email_primary`, `legal_name`, `address_full`, full `platforms` — those require an unlock.
- **`not_found` / `not_in_dataset` 404 = not in `pro_services`.** Skip; not charged.
- **Unlock is atomic.** N apexes either all charge (up to 10×N credits) or none on 402.
- **Within-TTL re-views are free** (`was_cached:true`).
## Errors
JSON envelope: `{"error": {"code": "...", "message": "..."}}`.
| Status | Code | What to do |
|---|---|---|
| 400 | `filter_parse_error` | `position` included; fix and re-validate with `/check`. |
| 400 | `kind_in_filter` | Strip any `kind:` from filter. |
| 400 | `field_not_in_dataset` | Drop the disallowed field. |
| 400 | `invalid_apex` | Re-normalize. |
| 401 | `unauthorized` / `invalid_audience` | Re-prompt for fresh `vk_…`. |
| 402 | `insufficient_credits` | `needed` and `balance`; nothing charged. |
| 404 | `not_found` / `not_in_dataset` | Skip; not charged. |
| 429 | `rate_limited` | Honor `Retry-After`. |
## End-to-end example
User: *"Three retained executive search firms in NY focused on tech
CFOs, ideally with 4-star ratings and a Clutch profile."*
```
GET /v1/datasets/pro_services/fields?include_values=1
GET /v1/datasets/pro_services/check?filter=industry:hr_recruiting_staffing+retained+executive+state:NY+tech+rating>=4+has:clutch
GET /v1/datasets/pro_services/search?filter=...&limit=10
# Present briefs. "Unlocking 3 = 30 credits, 30-day TTL."
POST /v1/datasets/pro_services/unlocks
{ "apexes": ["firm-a.com", "firm-b.com", "firm-c.com"] }
GET /v1/me/credits
```
don't have the plugin yet? install it then click "run inline in claude" again.
added explicit decision points for auth failure, intent ambiguity, zero-result fallbacks, rate limits, and cached re-access; clarified edge cases (404 not-in-catalog, atomic unlock failure, TTL re-reads); separated inputs into api/auth, mcp server, user context, and scope-validation clues; reformatted procedure as 9 discrete steps with explicit inputs/outputs; preserved all original filter dsL, sub-type mapping, recipes, and gotchas.
---
name: find-recruiting-firm
slug: find-recruiting-firm
description: find, shortlist, vet, or enrich US recruiting and staffing firms , executive search, RPO, tech/sales/healthcare recruiting, contingent/contract staffing, temp staffing.
version: "0.2.0"
original_author: nostrband
source: clawhub
metadata:
api_base: https://api.servicegraph.co
dataset_id: pro_services
industry: hr_recruiting_staffing
---
# find-recruiting-firm
## intent
use this skill when a user wants to find, shortlist, vet, or enrich an external recruiting or staffing firm to hire talent on their behalf. applies to executive search, retained search, RPO (recruitment process outsourcing), contingent/contract staffing, temp staffing, and vertical specializations (tech, sales, healthcare, finance, legal recruiting). do not use for in-house recruiting (posting jobs, hiring a recruiter for the user's own team), candidate-side job search help, ATS software comparisons, career coaching, or non-US firms.
## inputs
**ServiceGraph API access**
- API base: `https://api.servicegraph.co`
- Dataset: `pro_services` (100k+ US recruiting/staffing firms)
- Auth: bearer token `vk_*` (no anonymous tier)
- Setup: generate key at `https://servicegraph.co/profile/api-keys`, store as `SERVICEGRAPH_API_KEY` in `.env.local` (never paste into chat or log)
- cost model: search/validation/brief reads are free; detail unlock (url, phone, email, address, social, full platforms) costs 10 credits per firm, cached for 30 days
**MCP server (preferred)**
- if your harness has servicegraph tools loaded, use those instead of REST calls (OAuth 2.1 + PKCE keeps token in sandbox)
**User intent inputs**
- firm type/service (executive search, RPO, contingent, temp, vertical specialty)
- location (state, multi-state, national)
- industry verticals (tech, sales, healthcare, finance, legal, etc.)
- optional: firm size, rating threshold, specific domains to enrich
- optional: number of firms to shortlist (default 10 in search)
**Context clues to distinguish "firm procurement" from out-of-scope**
- "we need RPO support", "hire an external recruiting firm", "staffing agency" → in scope
- "find me a recruiter to represent me", "post a job", "hire a head of talent for our team" → out of scope
## procedure
**step 1: validate auth and scope**
- input: user intent query
- action: try shell call to `/v1/datasets/pro_services/fields` with `SERVICEGRAPH_API_KEY` from `.env.local`
- output: either 200 (auth valid, dataset reachable) or 401
- if 401: prompt user to create key at dashboard, add to `.env.local`, signal when ready; retry
**step 2: confirm firm procurement (not in-house hire or candidate-side)**
- input: user query
- decision: is user asking to procure external firm OR asking to post a job / hire someone for their team / get candidate representation?
- if ambiguous (e.g., "we're hiring fast"): ask clarifying follow-up before proceeding
- output: proceed flag
**step 3: translate intent to filter DSL**
- input: user intent (free-form or structured)
- action: either construct filter manually from sub-type/location/vertical keywords, or POST `/v1/datasets/pro_services/translate-intent` with user intent string
- output: filter string (GitHub-search-style DSL, always pinned with `industry:hr_recruiting_staffing`)
- note: sub-type mapping (executive → `executive`, retained → `retained`, RPO → `rpo`, contingent → `contingent`, temp → `temp`, etc.); vertical mapping (tech → `tech` or `engineering`, healthcare → `healthcare` or `clinical`, sales → `sales`, finance → `finance`, legal → `legal`, etc.)
**step 4: validate filter**
- input: filter string from step 3
- action: GET `/v1/datasets/pro_services/check?filter=<filter>`
- output: either valid (200, includes count hint) or error (400 with position/field guidance)
- if error: fix and re-validate
**step 5: search for firms**
- input: validated filter, limit (typically 10-20)
- action: GET `/v1/datasets/pro_services/search?filter=<filter>&limit=<N>`
- output: brief firm cards (apex domain, name, location, rating, key tags, per-row unlock hint, total match count)
- note: briefs do NOT include url, phone, email, address, full platforms (those require unlock)
**step 6: present and pick**
- input: search results
- action: display briefs to user in ranked order (rating, match quality), let user pick which firms to detail-unlock
- output: selected apex domains (typically 3-5 firms)
- cost signal: "unlocking N firms = 10N credits, 30-day TTL. proceed?"
**step 7: unlock detail (atomic)**
- input: list of apex domains ≤100
- action: POST `/v1/datasets/pro_services/unlocks` with `{"apexes": [...]}`
- output: full detail (url, phone, email, address, social, full platforms map) + `was_cached` flag (true = re-access within TTL, no charge)
- if 402 (insufficient credits): report needed vs balance; no charge yet
- if success: detail TTL is 30 days; re-reads are free
**step 8: enrich by domain (BYO apex list)**
- input: user-provided list of 8-20 recruiting firm domains/apexes
- action: loop GET `/v1/datasets/pro_services/:apex` per domain (free brief, 404 = not in catalog, no charge)
- output: brief data per firm; user picks which to unlock
- then: POST `/v1/datasets/pro_services/unlocks` with selected apexes (10 credits per firm, atomic)
**step 9: check credit balance**
- input: (optional, after unlock)
- action: GET `/v1/me/credits`
- output: current balance (informational, helps user plan future queries)
## decision points
**if auth fails (401)**
- do: prompt user to create `vk_*` key at `https://servicegraph.co/profile/api-keys`, add to `.env.local`, signal when done; then retry step 1
**if intent is ambiguous (could be procurement or in-house hire)**
- example: "we're hiring fast and need recruiting help"
- do: ask "are you looking to hire an external recruiting firm or staffing agency to do the hiring, or are you looking to hire a recruiter as an employee on your own team?"
- if external firm: proceed to step 3
- if in-house hire: route to recruiting-an-employee skill; decline this skill
**if user asks for candidate-side help ("find me a recruiter to represent me", "I'm a job-seeker looking for an agency")**
- do: decline; this skill is firm procurement only, not candidate career services
**if filter validation fails (400 parse error)**
- do: read error position, fix (e.g., strip `kind:` if disallowed, drop bad field), re-validate with `/check`
**if search returns zero results**
- do: offer to relax filter (remove location, lower rating threshold, broaden vertical keyword, etc.) and re-search
- or: offer to use translate-intent to generate alternative filter
**if user hits rate limit (429)**
- do: honor `Retry-After` header, wait, retry
**if insufficient credits (402)**
- do: report `needed` vs `balance`, ask user to top up or reduce unlock scope, retry (no charge incurred yet)
**if apex not found in dataset (404)**
- do: skip that firm, no charge; report "not in catalog"
**if unlock is atomic and one apex fails**
- do: entire batch fails (none charged); report which apex is problematic, ask user to remove and retry
**if user re-accesses detail within 30-day TTL**
- do: return cached detail (free, `was_cached:true` in response), no charge
## output contract
**search results** (step 5, free)
- json array of firm briefs: `[{apex: "firm-a.com", name: "Firm A", location: "NY", state: "NY", rating: 4.2, tags: ["executive search", "tech"], total_count: 47}]`
- includes per-row unlock cost hint ("10 credits to unlock full detail")
**detail unlock** (step 7, 10 credits per firm)
- json object per firm: `{apex: "firm-a.com", name: "Firm A", url: "https://firm-a.com", phone_primary: "212-555-1234", email_primary: "bd@firm-a.com", address_full: "...", social: [{platform: "linkedin", url: "..."}], platforms: {...}, was_cached: false}`
- detail TTL: 30 days from unlock
**credit balance** (step 9, free)
- json: `{balance: 450, last_updated: "2025-01-15T10:30:00Z"}`
**error responses**
- json: `{"error": {"code": "...", "message": "..."}}`
- codes: `filter_parse_error`, `unauthorized`, `insufficient_credits`, `not_found`, `rate_limited`, etc.
## outcome signal
- user receives shortlist of 3-10 recruiting/staffing firm briefs ranked by relevance (rating, match to intent)
- user sees contact details (url, phone, email, address, social profiles) after confirming unlock cost
- if re-accessing within 30 days, user sees `was_cached:true` (zero charge)
- user can paste their own apex list and enrich selectively
- user is informed of credit spend before any charge (atomic unlock = all-or-nothing)
- if search returns no results, user is offered filter relaxation alternatives
- if user hits rate limit or auth failure, they are re-prompted with actionable next steps (wait + retry, create key, top up credits)
---
**credits:** original skill authored by nostrband (clawhub). enriched for implexa quality standards.