⚠️ Prerequisites — load these first
- connect-ai-base skill
Before proceeding, locate and read the connect-ai-base skill. If it is not available in the current environment (not loaded, not enabled, or not installed), stop immediately. Do not proceed with the task. Tell the user that the connect-ai-base skill is required and ask them to install and enable it before continuing.
This skill provides Bullhorn CRM-specific guidance for querying Bullhorn data through CData Connect AI. It composes on top of the connect-ai-base skill, which handles the discovery workflow, error recovery, SQL dialect, and query naming convention.
Precedence
This skill replaces getInstructions for the BullhornCRM driver. Do not call getInstructions for BullhornCRM — the guidance it provides is already incorporated here. Proceed directly to schema discovery (getTables / getColumns) after identifying the Bullhorn connection via getCatalogs.
Schema
BullhornCRM is a single-schema driver. The schema name is BullhornCRM.
SELECT [ID], [FirstName], [LastName], [Status]
FROM [YourBullhornConnection].[BullhornCRM].[Candidate]
LIMIT 100
Query Process
Bullhorn is a recruitment-centric relational model built around three core entities: Candidates, JobOrders, and Placements. Most analyses start with one of these and join outward.
- Identify the connection — if unknown, call
getCatalogsand filter forDRIVER = 'BullhornCRM'. - Pick the core entity — Candidate for talent/pipeline questions, JobOrder for open-role/client-demand questions, Placement for hires/revenue questions.
- Inspect columns — call
getColumnson the chosen entity before writing queries. Bullhorn instances are heavily customized; do not assume the schema matches Bullhorn UI labels. - Sample first — run a
SELECT * … LIMIT 5to validate structure and value formats (status fields are string picklists; valid values vary by tenant). - Filter on date columns —
DateAddedandDateLastModifiedexist on most major entities; always include a date predicate when result sets could be large. - Join outward incrementally — JobSubmission links Candidate ↔ JobOrder; Placement represents successful hires. Validate each join with a small LIMIT before adding the next.
Data Model
Key Tables
Bullhorn's data model centers on the recruiting lifecycle:
- Candidate — Primary recruiting entity (people in the talent pool).
- ClientCorporation — Companies / clients. Supports parent-child hierarchies via
ParentCompanyid. - ClientContact — People who work at client companies; linked to
ClientCorporationviaCompanyid. - JobOrder — Open job requisitions (may also be referred to as job listings).
- JobSubmission — Many-to-many link between Candidate and JobOrder; tracks who was submitted for which role and the submission status.
- Placement — A successful hire; one Placement represents a candidate placed into a JobOrder. Carries direct FKs to both the JobOrder (
Jobid) and the client company (Companyid).
Activity Tables
- Task, Appointment, Sendout, Tearsheet — Activity tracking.
- Note — Email content is not exposed via the Bullhorn API by default. If the customer routes emails through Bullhorn Automation, those emails populate the
Notetable and can be queried there.
Payroll & Financial Tables
- PayCheck, EmployeePay, Deduction — Payroll records.
- PlacementCommission — Commission attributable to a Placement.
- JobOrderRateCard, PlacementRateCard — Rate cards (billing/pay rates per job or placement).
History (Edit) Tables
For auditing field-level changes. Table names vary by tenant and driver version — confirm with getTables before querying:
- CandidateHistory (may also appear as CandidateEditHistory)
- JobOrderEditHistory
- PlacementEditHistory
- ClientCorporationEditHistory
These tables can be very large. Always include filters (entity ID + date range) before querying.
Key Relationships
| Child table | Join column | Parent table |
|---|---|---|
| ClientContact | Companyid |
ClientCorporation |
| ClientCorporation | ParentCompanyid |
ClientCorporation (self — company hierarchy) |
| JobOrder | ClientCompanyid |
ClientCorporation |
| JobOrder | Contactid |
ClientContact |
| JobSubmission | Candidateid |
Candidate |
| JobSubmission | Jobid |
JobOrder |
| Placement | Candidateid |
Candidate |
| Placement | Jobid |
JobOrder |
| Placement | Companyid |
ClientCorporation (direct, not only via JobOrder) |
Important Columns
Candidate
ID— Primary key (numeric)FirstName,LastName,Name—Nameis a derived/display field; writeFirstName/LastNameseparatelyStatus— String picklist (e.g., Active, Inactive, Placed)DateAdded,DateLastModified— Timestamps for incremental queriesEmail1,Email2,Email3,MobilePhone,WorkPhone— Contact fieldsCurrentCompany— Most recent employer (text)YearsExperience,DesiredSalary,DesiredPayRateOwnerCorporationid— FK to ClientCorporation (the owning agency)BranchID— FK to Branch (note: uppercaseID, not the typical lowercaseid)IsDeleted— Soft-delete flag; filter tofalsefor active recordsIsAnonymized— GDPR/privacy flag
ClientCorporation
ID— Primary keyCompanyNameStatus,TypeDateAdded,DateLastModifiedMainPhone,BillingPhone,FaxParentCompanyid— Self-referential FK for company hierarchyBranchID— UppercaseID(FK casing exception, see Conventions)AnnualRevenue(Millions),Sector,BusinessSectors,NetTerms- No
IsDeletedcolumn — ClientCorporation does not expose soft-delete
ClientContact
IDFirstName,LastName,NameCompanyid— FK toClientCorporation.IDEmail1,Email2,Email3,DirectPhone,MobilePhoneTitle,Department,OfficeOwnerid— FK to the user who owns the contactReportstoid— Self-referential FK (reporting hierarchy)StatusIsDeleted— Soft-delete flagIsDefaultContact— Whether this contact is the default for the companyDateAdded,DateLastModified
JobOrder
IDJobTitleStatus— String picklist (e.g., Open, Closed, Accepting Candidates) — distinct fromOpen/ClosedbelowOpen/Closed— BOOLEAN column (literal name, slash included) — separate fromStatus. JobOrder has both, and they don't necessarily move together. Always quote with[]to escape the slash:[Open/Closed]ClientCompanyid— FK toClientCorporation.IDContactid— FK toClientContact.ID(primary contact for the requisition)DateAdded,DateClosed,DateLastPublished,LastUpdatedOwnerid— User who owns the requisitionBranch,BranchID(uppercaseID)EmploymentType,StartDate,EstimatedEndDate,ScheduledEndLocationid— FK to Location#ofOpenings— Literal column name (use[#ofOpenings])SalaryLow,SalaryHigh,PayRate,ClientBillRate,Mark-up%JobDescription,JobPostingURLIsDeleted
JobSubmission
ID— Primary keyCandidateid— FK toCandidate.IDJobid— FK toJobOrder.IDStatus— Submission stage (e.g., Submitted, Client Interview, Rejected)DateAdded,DateLastModified,DateWebResponseStartDate,EndDateBillRate,PayRate,SalaryAddedByid— FK to user who created the submissionSource,CommentsIsDeleted,IsHidden- Screening columns:
ScreeningStatus,ScreeningScore,ScreeningFeedbackRating,ScreeningCompletedDate
Placement
ID— Primary keyCandidateid— FK toCandidate.IDJobid— FK toJobOrder.IDCompanyid— FK toClientCorporation.ID(direct, not only via JobOrder)Contactid,BillingContactid,ApprovingClientContactid— Various contact referencesStartDate,ScheduledEnd,EstimatedEndDate,EmploymentStartDate,EffectiveDate,DocumentDeadlineStatusSalary,BillRate,PayRate,OvertimeBillRate,OvertimePayRate,DoubletimeBillRate,DoubletimePayRatePlacementFee(%),PlacementFee(Flat),Mark-up%,OTMark-up%EmploymentType,EmployeeType,TerminationReasonDateAdded,DateLastModified
Common Query Patterns
Recently added candidates
SELECT [ID], [FirstName], [LastName], [Status], [DateAdded]
FROM [YourConnection].[BullhornCRM].[Candidate]
WHERE [IsDeleted] = false
AND [DateAdded] >= DATEADD(day, -30, GETDATE())
ORDER BY [DateAdded] DESC
LIMIT 50
Candidates submitted to a specific job
SELECT c.[FirstName], c.[LastName], js.[Status], js.[DateAdded]
FROM [YourConnection].[BullhornCRM].[JobSubmission] js
JOIN [YourConnection].[BullhornCRM].[Candidate] c
ON js.[Candidateid] = c.[ID]
WHERE js.[Jobid] = 12345
ORDER BY js.[DateAdded] DESC
Active placements with candidate names
SELECT p.[ID], c.[FirstName], c.[LastName], p.[StartDate], p.[Status], p.[BillRate]
FROM [YourConnection].[BullhornCRM].[Placement] p
JOIN [YourConnection].[BullhornCRM].[Candidate] c
ON p.[Candidateid] = c.[ID]
WHERE p.[Status] = 'Active'
ORDER BY p.[StartDate] DESC
Open job orders (using either Status or Open/Closed)
-- By Status picklist
SELECT [ID], [JobTitle], [Status], [DateAdded]
FROM [YourConnection].[BullhornCRM].[JobOrder]
WHERE [Status] = 'Open'
AND [IsDeleted] = false
ORDER BY [DateAdded] DESC
-- Or by the boolean Open/Closed column (slash in the name requires brackets)
SELECT [ID], [JobTitle], [Status], [Open/Closed]
FROM [YourConnection].[BullhornCRM].[JobOrder]
WHERE [Open/Closed] = true
AND [IsDeleted] = false
Recruiting funnel: JobOrder → JobSubmission → Candidate → Placement
SELECT jo.[JobTitle], jo.[Status] AS JobStatus,
js.[Status] AS SubmissionStatus,
c.[FirstName], c.[LastName],
p.[ID] AS PlacementID, p.[StartDate]
FROM [YourConnection].[BullhornCRM].[JobOrder] jo
LEFT JOIN [YourConnection].[BullhornCRM].[JobSubmission] js ON js.[Jobid] = jo.[ID]
LEFT JOIN [YourConnection].[BullhornCRM].[Candidate] c ON js.[Candidateid] = c.[ID]
LEFT JOIN [YourConnection].[BullhornCRM].[Placement] p
ON p.[Candidateid] = c.[ID] AND p.[Jobid] = jo.[ID]
WHERE jo.[DateAdded] >= '2025-01-01'
ORDER BY jo.[DateAdded] DESC
LIMIT 100
Client revenue rollup (Placement × ClientCorporation)
SELECT cc.[CompanyName],
COUNT(p.[ID]) AS Placements,
SUM(p.[BillRate]) AS TotalBillRate
FROM [YourConnection].[BullhornCRM].[Placement] p
JOIN [YourConnection].[BullhornCRM].[ClientCorporation] cc ON p.[Companyid] = cc.[ID]
WHERE p.[StartDate] >= '2025-01-01'
GROUP BY cc.[CompanyName]
-- ORDER BY on aggregate aliases (TotalBillRate) is not supported by the driver; sort client-side
Candidate pipeline by submission status
SELECT js.[Status], COUNT(*) AS CandidateCount
FROM [YourConnection].[BullhornCRM].[JobSubmission] js
WHERE js.[DateAdded] >= '2025-01-01'
GROUP BY js.[Status]
-- ORDER BY on COUNT(*) / aggregate aliases is not supported by the driver; sort client-side
Stored Procedures
The BullhornCRM driver exposes file-attachment procedures: DownloadFile and UploadFile. Both involve disk-based or stream-based parameters and have specific cloud-compatibility caveats.
DownloadFile
Downloads a file attached to a specified Bullhorn entity.
| Parameter | Required | Notes |
|---|---|---|
EntityType |
Yes | The Bullhorn entity type (e.g., Candidate, JobOrder) |
EntityId |
Yes | The ID of the entity that owns the file |
FileId |
Yes | The ID of the specific file to download |
OutputFolder |
No | Disk path to write the file. Omit in cloud environments — disk write fails. |
FileStream |
No | Stream-based delivery. Omit in cloud environments — InputStream/OutputStream parameters cannot be passed through MCP. |
Cloud-compatible call shape — omit both OutputFolder and FileStream; the response contains file content as base64. This matches the established CData download pattern across drivers.
{
"catalogName": "YourBullhornConnection",
"schemaName": "BullhornCRM",
"procedureName": "DownloadFile",
"parameters": {
"EntityType": "Candidate",
"EntityId": "12345",
"FileId": "67890"
}
}
UploadFile — Not currently supported in cloud
UploadFile accepts either FileLocation (server-side disk path, unavailable in cloud) or Content (Java InputStream, unable to pass through the MCP interface). There is no base64 string alternative for the upload path on the BullhornCRM driver. Treat file uploads as a current limitation — do not invent parameter names the driver does not accept. Support for file uploads via stored procedures is planned — check for updates if this capability is needed.
Write Operations
The BullhornCRM driver supports a limited set of writes — the most common are INSERT INTO Candidate and UPDATE JobOrder. Other tables may or may not be writable; check the column-level Readonly flag via getColumns (Readonly=false means writable) and test in a sandbox before relying on it.
INSERT example
INSERT INTO [YourConnection].[BullhornCRM].[Candidate]
([FirstName], [LastName], [Email1], [Status])
VALUES
('Jane', 'Smith', 'jane@example.com', 'Active')
UPDATE example
UPDATE [YourConnection].[BullhornCRM].[JobOrder]
SET [Status] = 'Closed', [Open/Closed] = false
WHERE [ID] = 12345
Write access is controlled by two layers: the Connect AI connection's readonly setting (CData side) and the authenticated Bullhorn API user's permissions (Bullhorn side). If a write fails, check both.
Bullhorn-Specific Conventions
- Prefer literal
true/falseon boolean columns. Some boolean columns reject numeric1/0(a few such asIsDeleteddo accept them, but not all) — e.g.WHERE [Open/Closed] = 1fails with"Cannot decipher where clause as Boolean."UseWHERE [IsDeleted] = false,WHERE [Open/Closed] = true. This applies to both SELECT filters and UPDATE SET clauses. ORDER BYcannot reference aggregate functions or aggregate aliases. The CData BullhornCRM driver rejectsORDER BY COUNT(*) DESC,ORDER BY <aggregate_alias> DESC(e.g.,ORDER BY TotalBillRate DESCwhereTotalBillRateis aSUM), and ordinal references likeORDER BY 2 DESC— all surface as"Invalid column name 'COUNT'/'SUM'/etc. for table '<TableName>'".GROUP BYwith aggregate expressions in theSELECTclause works correctly; only theORDER BYclause is affected. Workaround: drop theORDER BYon aggregates and sort client-side, orORDER BYa non-aggregate column already in the result set.- FK column casing is mixed, not uniform. Most FK columns use a lowercase
idsuffix (Candidateid,Jobid,Companyid,ClientCompanyid,Ownerid,Contactid,Reportstoid,AddedByid,Categoryid,ParentCompanyid), but several use uppercaseID:BranchID,MasterUserID,ClientContactID.ExternalIDis an external/legacy reference, not a true FK. Always confirm casing withgetColumnsbefore joining — Bullhorn does not have a single consistent rule. - Primary keys are always uppercase
ID. Only FK columns vary; the table's own primary key is consistentlyID. - Column names commonly contain special characters. Slashes (
Open/Closed,Culture/Perks), hashes (#ofOpenings), percent signs (Mark-up%,Tax%,PlacementFee(%)), parentheses (Reportingto(contact)id,WillRelocate?), question marks.[]quoting is required, not stylistic. Plain identifiers will fail to parse on many columns. JobOrder.StatusandJobOrder.[Open/Closed]are distinct fields.Statusis aVARCHARpicklist (Open / Closed / Accepting Candidates / etc.);[Open/Closed]is a separateBOOLEAN. They don't necessarily move together in legacy data — check both for "is this job open?" questions, and confirm which one the user means.- Status fields are string picklists, not numeric codes. Filter with literal strings (
'Active','Open','Submitted'). Picklist values differ by Bullhorn configuration; confirm withSELECT DISTINCT [Status] FROM <table>. IsDeletedis present on most transactional tables but NOT on ClientCorporation. Candidate, ClientContact, JobOrder, JobSubmission, and Placement all exposeIsDeletedand should be filtered tofalsefor active queries. ClientCorporation does not have this column.- History tables are append-only and very large.
CandidateHistory(orCandidateEditHistory— name varies by tenant; confirm viagetTables),JobOrderEditHistory,PlacementEditHistory,ClientCorporationEditHistorytrack every field change. Always filter on entity ID and a date range. - Custom fields are pervasive and use mixed casing. Bullhorn tenants are heavily customized — every entity has many
customText,customInt,customFloat,customDate,customTextBlockslots. Casing of the first letter varies within the same table (e.g.,customDate1vsCustomDate10); always confirm exact names viagetColumns. Encrypted custom fields (CustomEncryptedText1throughCustomEncryptedText10) exist on Candidate and Placement for PII data. Notetable is sparse without Bullhorn Automation. Email content is not inNoteby default — only present when the tenant uses Bullhorn Automation to ingest emails.- Permission errors return HTTP 403 with "No read rights". If a query against an entire entity returns 403 / "No read rights", the API user lacks read scope for that object; route the user to their Bullhorn administrator. If only specific fields return errors (e.g., "No read rights. Shift" on JobOrder), drop those fields from the SELECT and re-run.
- Use
DateAddedfor incremental loads. It's monotonically increasing and exists on every transactional entity.DateLastModifiedis also available where mutations are common. - Join on numeric
ID/idcolumns, not names.CompanyName,JobTitle, candidate names are not guaranteed unique. DownloadFileworks in cloud;UploadFiledoes not. Omit bothOutputFolderandFileStreamonDownloadFilefor a base64 response.UploadFilehas no base64 alternative for the upload path — support is planned but not currently available.