← all tools
Recruiting CRM / ATS

Bullhorn

Write into Bullhorn, at the scale Bullhorn shops work at.

Cost per callfree
Your own keyrequired — contact us to enable
CategoryRecruiting CRM / ATS

Bullhorn is the enterprise end of recruitment CRM, and your agent talks to it properly: search candidates, contacts, companies and job orders, then create and update records. Writes are real and hard to undo, so it pilots one and shows you the result before continuing.

Bullhorn is the destination. Everything upstream converges here.

things you can ask for

  • Push these candidates in as new records
  • Search for anyone matching this brief url we already have, then source the gap with Apollo
  • What job orders are open? Build a longlist for the oldest one
  • Update the contact on this account with what ZoomInfo returned

works well with Bullhorn

  • Apollo

    source only the gap your database does not already cover

  • ZoomInfo

    enterprise-grade data for enterprise-sized accounts

  • SmartLead

    volume outreach off a Bullhorn list

  • Aircall

    calls logged onto the record automatically

Ask for the outcome and your agent composes the run itself. Or start from a skill, a whole pipeline it already knows end to end.

browse all skills →

What your agent reads before it touches Bullhorn

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 Bullhorn docs →
Show the raw playbookwritten for the agent

HOW in lib/bullhorn.py. Docs: bullhorn.github.io/rest-api-docs.

Auth — 3-leg flow (most complex adapter; handled in _login)

  1. GET auth.bullhornstaffing.com/oauth/authorize (client_id + username/password + action=Login) → 302 with ?code=.
  2. POST .../oauth/token (grant_type=authorization_code) → access_token.
  3. POST rest.bullhornstaffing.com/rest-services/login?version=*&access_token=…BhRestToken + restUrl. Then every call hits {restUrl}<path> with BhRestToken. The client re-logs in once on 401.
  • Env: BULLHORN_CLIENT_ID, BULLHORN_CLIENT_SECRET, BULLHORN_USERNAME, BULLHORN_PASSWORD, BULLHORN_REDIRECT_URI (must match the OAuth app). Never hardcode.
  • Datacenter caveat: auth/rest hosts can be cluster-specific — if your tenant differs, override AUTH_BASE/LOGIN_BASE. Verify the OAuth app + redirect_uri before first run. ping() = safe read-only pilot.

Operations (CONFIRMED from bullhorn.github.io/rest-api-docs, 2026-06-01)

search(entity, query, fields)GET /search/{Entity} Lucene (indexed: Candidate, JobOrder, ClientContact, ClientCorporation…). query(entity, where, fields)GET /query/{Entity} SQL-style (non-indexed). get_entity (GET /entity/{Entity}/{id}), create_entity (PUT /entity/{Entity}), update_entity (POST /entity/{Entity}/{id}). request() covers the rest. BhRestToken via query param is valid (also accepted as a header). All path patterns verified.

Guardrail

Production ATS writes — pilot one record, confirm, then bulk.

Handoff

ATS write target: enriched candidates → create_entity("Candidate", …).

Callable surface — lib/bullhorn.py

Import: from lib.bullhorn import Bullhorn → instantiate Bullhorn() (reads key from env). Generic passthrough: request(method, path, *, params, json).

  • create_entity(entity: str, payload: dict) -> Any — PUT /entity/{entity} — create a record.
  • get_entity(entity: str, entity_id: int, *, fields: str = '*') -> Any — GET /entity/{entity}/{id}?fields=...
  • ping() -> Any — GET ping — verify session (safe read-only pilot).
  • query(entity: str, where: str, *, fields: str = '*', **params) -> Any — GET /query/{entity}?where=...&fields=... — SQL-style where (non-indexed entities).
  • search(entity: str, query: str, *, fields: str = '*', **params) -> Any — GET /search/{entity}?query=...&fields=... — Lucene-style search (indexed entities).
  • update_entity(entity: str, entity_id: int, payload: dict) -> Any — POST /entity/{entity}/{id} — update a record.