Write it all back to your CRM, so nothing ends up in a spreadsheet.
Recruit CRM is where the work lands. Your agent pushes candidates, companies and contacts into it, reads your open roles, and moves people through hiring stages. Every write is real, so it does one record first and shows you the result before the rest.
Recruit CRM is the destination. Whatever the pipeline produced, this is where it gets filed.
A skill is a whole pipeline your agent already knows. These ones call Recruit CRM as part of the run.
Re-qualify cold Recruit CRM candidates against the current ICP. Warm leads back into active flow.
Every tool ships with a written playbook, and the agent loads it before the first call. Auth, rate limits, what each call costs, which actions need your approval, and the mistakes worth avoiding. It is the difference between an agent that knows the tool and one that guesses at it.
Read the Recruit CRM docs →Confidence: highest of the CRMs — every path is taken straight from Recruit CRM's own live docs (20 endpoint groups; cache:
reference/docs/recruitcrm/raw/). Basehttps://api.recruitcrm.io/v1, Bearer.
Routing layer for all Recruit CRM API work. This file tells you WHERE to go and the guardrails to respect; the HOW lives in the reference playbook, the recipes, and the Python client. Read the matching doc BEFORE making any call.
lib/recruit_crm.py (Python-only stack).reference/endpoints.md; task playbooks in recipes/*.md.https://api.recruitcrm.io/v1/Authorization: Bearer <API_TOKEN>RECRUITCRM_API_KEY. NEVER hardcode the key in any file, recipe, or commit. The Python
client raises if the env var is missing.STOP. Before writing or running a call:
| When the task involves... | Read this first |
|---|---|
| Any endpoint shape, params, pagination, field names | reference/endpoints.md |
| Pushing enriched leads (companies + contacts) from a CSV | recipes/push-enriched-leads.md |
| Calling the API from Python | lib/recruit_crm.py (docstrings) |
If the exact payload for an operation is marked TODO (paste schema) in
reference/endpoints.md, do NOT guess it — ask the user to paste that section of
https://docs.recruitcrm.io first. (The docs site is a JS-rendered SPA and cannot be scraped.)
Recruit CRM writes are real, irreversible changes to a production ATS. Before any create/update/delete or bulk push:
--rows 0:1 (one record) and show the user
the exact payload + response before continuing.reference/endpoints.md → "Account-specific IDs"). Confirm the mapping before writing.jobs/search, assign candidatesThe canonical flow: the sourcing + enrichment adapters produce an enriched CSV → this skill loads that CSV into Recruit CRM. Inspect the CSV (row count, columns, sample values) first before mapping columns to Recruit CRM fields. Never read large CSVs into context.
lib/recruit_crm.pyImport: from lib.recruit_crm import RecruitCRM → instantiate RecruitCRM() (reads key from env). Base: https://api.recruitcrm.io/v1. Generic passthrough: request(method, path, *, params, json).
apply_candidate_to_job(candidate_slug: str, payload: dict) -> Any — POST /v1/candidates/{candidate}/apply.assign_candidate_to_job(candidate_slug: str, payload: dict) -> Any — POST /v1/candidates/{candidate}/assign — assign candidate to a job (job slug in body).create_candidate(payload: dict) -> dict — POST /v1/candidates — create a candidate. Required: first_name.create_company(payload: dict) -> dict — POST /v1/companies — create a company. Required: company_name.create_contact(payload: dict) -> dict — POST /v1/contacts — create a contact. Required: first_name, last_name.create_job(payload: dict) -> dict — POST /v1/jobs — create a job. Required: name, number_of_openings, company_slug,delete_candidate(slug: str) -> Any — DELETE /v1/candidates/{slug}.delete_company(slug: str) -> Anydelete_contact(slug: str) -> Anydelete_job(slug: str) -> Anyget_candidate(slug: str) -> dict — GET /v1/candidates/{slug} — single candidate by slug.get_candidate_hiring_pipelines() -> Any — GET /v1/hiring-pipelines — pipelines + stage IDs (needed for hiring-stage moves).get_candidate_hiring_stages(slug: str) -> Any — GET /v1/candidates/{slug}/hiring-stages — all jobs+stages for the candidate.get_company(slug: str) -> dictget_contact(slug: str) -> dictget_job(slug: str) -> dictlist_candidates(sort_by: str = 'updatedon', sort_order: str = 'desc', limit: int = 100) -> Iterator[dict] — GET /v1/candidates — iterate all candidates. sort_by: createdon|updatedon.list_companies(sort_by: str = 'updatedon', sort_order: str = 'desc', limit: int = 100) -> Iterator[dict] — GET /v1/companies — iterate all companies.list_contacts(sort_by: str = 'updatedon', sort_order: str = 'desc', limit: int = 100) -> Iterator[dict] — GET /v1/contacts — iterate all contacts.list_custom_fields(entity: str = '') -> Any — GET /v1/custom-fields[/{entity}] — custom field IDs. entity e.g.list_jobs(sort_by: str = 'updatedon', sort_order: str = 'desc', limit: int = 100) -> Iterator[dict] — GET /v1/jobs — iterate all jobs.search_candidates(*, custom_fields: list[dict] | None = None, **filters) -> Iterator[dict] — GET /v1/candidates/search — query filters + optional custom_fields body.search_companies(*, custom_fields: list[dict] | None = None, **filters) -> Iterator[dict] — GET /v1/companies/search. Filters: company_name, company_slug, owner_email,search_contacts(*, custom_fields: list[dict] | None = None, **filters) -> Iterator[dict] — GET /v1/contacts/search — query filters + optional custom_fields body.search_jobs(*, custom_fields: list[dict] | None = None, **filters) -> Iterator[dict] — GET /v1/jobs/search — query filters + optional custom_fields body.update_candidate(slug: str, payload: dict) -> dict — POST /v1/candidates/{slug} — edit a candidate (POST, not PUT).update_candidate_hiring_stage(candidate_slug: str, job_slug: str, payload: dict) -> Any — POST /v1/candidates/{candidate}/hiring-stages/{job} — move candidate's stage in a job.update_company(slug: str, payload: dict) -> dict — POST /v1/companies/{slug} — edit (POST, not PUT). company_name required again.update_contact(slug: str, payload: dict) -> dict — POST /v1/contacts/{slug} — edit a contact (POST, not PUT).update_job(slug: str, payload: dict) -> dict — POST /v1/jobs/{slug} — edit a job (POST, not PUT).