---
title: "Bullhorn to Crelate Migration: The CTO's Technical Guide"
slug: bullhorn-to-crelate-migration-the-ctos-technical-guide
date: 2026-04-23
author: Raaj
categories: [Migration Guide, Crelate, Bullhorn]
excerpt: "Technical guide to migrating from Bullhorn to Crelate: API rate limits, data model mapping, custom field constraints, and step-by-step migration process for CTOs."
tldr: "Bullhorn-to-Crelate migrations are bottlenecked by Crelate's 120 req/min API limit and 20 custom fields per entity cap. Plan for data model compression, not just field mapping."
canonical: https://clonepartner.com/blog/bullhorn-to-crelate-migration-the-ctos-technical-guide/
---

# Bullhorn to Crelate Migration: The CTO's Technical Guide


Migrating from Bullhorn to Crelate is a data-model compression problem disguised as a vendor switch. Bullhorn is built around a deeply relational staffing data model — `Candidate`, `ClientContact`, `ClientCorporation`, `JobOrder`, `JobSubmission`, `Placement` — with up to 35 custom object instances per entity and virtually unlimited custom field mappings. Crelate uses a streamlined ATS+CRM schema where `Contacts`, `Companies`, `Jobs`, and `Opportunities` share a unified data model, but custom fields are capped at **20 per entity**. ([bullhorn.github.io](https://bullhorn.github.io/rest-api-docs/entityref.html))

The core engineering challenge: Bullhorn enforces the full staffing lifecycle — Candidates are submitted to JobOrders via JobSubmissions, which become Placements when filled. Crelate collapses this into a pipeline-stage model where Contacts flow through workflow stages on Jobs. Moving data between these two systems means compressing Bullhorn's deeply nested, multi-table relational structure into Crelate's flatter schema — while preserving placement history, candidate-to-job relationships, and activity timelines that your recruiters depend on daily.

The rate-limit mismatch makes this harder. Bullhorn allows extraction at 1,500 requests per minute, but Crelate's API v3 throttles ingestion at **120 requests per minute per IP**. Without careful queue management, your migration script will spend more time waiting than working. ([kb.bullhorn.com](https://kb.bullhorn.com/ats/Content/BHATS/Topics/understandingBHAPIUsageLimitsVersioningBackwardCompatibility.htm))

This guide covers the exact API constraints on both sides, concrete object-mapping decisions, every realistic migration approach, and the edge cases that cause silent data loss.

For broader ATS migration pitfalls, see [5 "Gotchas" in ATS Migration](https://clonepartner.com/blog/blog/ats-migration-gotchas/). For patterns on when CSVs work versus when they break, read [Using CSVs for SaaS Data Migrations](https://clonepartner.com/blog/blog/csv-saas-data-migration/).

> [!WARNING]
> Crelate deprecated API v1 and v2 endpoints as of November 1, 2025, throttling them to 30 requests per minute. All new migration scripts **must** target API v3. If you're reusing old integration code, rewrite it before starting. ([postman.com](https://www.postman.com/crelate/crelate-api-v3/collection/18339486-bdd67bad-67f9-4480-8e64-f358b5a3c29b))

## Why Companies Migrate from Bullhorn to Crelate

The migration drivers are predictable:

- **Cost:** Crelate starts at $99/user/month. Bullhorn's pricing scales significantly higher, especially with add-ons for automation, analytics, and back-office integrations. For a 15-person agency, the annual savings can reach $30K+.
- **UI and recruiter adoption:** Crelate delivers a modern, intuitive interface compared to Bullhorn's steeper learning curve. A common framing from agency operators: "Crelate is Bullhorn for people who hate Bullhorn's interface."
- **Implementation speed:** Crelate advertises a 14-day implementation timeline. Bullhorn implementations, especially for multi-division agencies, frequently take 6–12 weeks.
- **Agency fit:** Crelate's integrated ATS+CRM works well for small-to-midsize staffing firms (3–50 recruiters) that don't need Bullhorn's VMS integrations, back-office suite, or enterprise compliance features.

> [!NOTE]
> Bullhorn is purpose-built for high-volume, transactional staffing with deep back-office integrations (payroll, VMS, credentialing). If your agency depends on Fieldglass/Beeline VMS integrations or Bullhorn Back Office, Crelate may not be a like-for-like replacement. Audit your integration dependencies before committing.

## Data Model Differences: Bullhorn vs Crelate

This is where most migration plans break. The two platforms don't just use different field names — they use fundamentally different data architectures.

### Bullhorn's Entity Model

Bullhorn organizes staffing data across deeply interconnected entities:

- **Candidate** — the person seeking work
- **ClientContact** — the hiring manager or client-side contact
- **ClientCorporation** — the client company
- **JobOrder** — the open requisition
- **JobSubmission** — a candidate formally submitted to a JobOrder
- **Placement** — a filled position (one Candidate, one JobOrder)
- **Opportunity** / **Lead** — pre-job business development records
- **Note** / **Appointment** / **Task** — activity records linked to any entity
- **Custom Objects** — up to 35 instances per Person entity and 10 per JobOrder, Placement, or Opportunity entity

Bullhorn supports extensive custom field mappings per entity (`customText1` through `customTextN`, `customDate` fields, etc.), plus full custom objects that function as child tables with their own fields. ([bullhorn.github.io](https://bullhorn.github.io/rest-api-docs/entityref.html))

### Crelate's Entity Model

Crelate uses a flatter, more unified structure:

- **Contacts** — all people (candidates, hiring managers, references)
- **Companies** — client organizations
- **Jobs** — open requisitions with pipeline workflow stages
- **Opportunities** — sales/business development pipeline items
- **Activities** — calls, emails, meetings, tasks
- **Notes** — text records attached to any entity
- **Placements** — filled positions

**The critical constraint:** Crelate limits custom fields to **20 per entity** (Jobs, Contacts, Companies). If your Bullhorn instance uses 40+ custom fields on Candidates — common in healthcare, government, or specialized staffing — you need to consolidate, archive, or serialize overflow data into note fields. This is the single most impactful architectural limitation for Bullhorn-to-Crelate migrations. ([postman.com](https://www.postman.com/crelate/crelate-api-v3/collection/18339486-bdd67bad-67f9-4480-8e64-f358b5a3c29b))

When mapping Bullhorn's custom fields to Crelate, you have three options for handling overflow:

1. **Map the top 20** to Crelate's native custom fields, prioritizing by usage and business value.
2. **Serialize the remainder** into a structured JSON string or formatted text block appended as a pinned Note on the Crelate Contact record.
3. **Convert boolean and picklist fields** into Crelate Tags, which do not count against the 20-field limit.

For more strategies on handling custom field limitations during a platform switch, see [5 "Gotchas" in ATS Migration](https://clonepartner.com/blog/blog/ats-migration-gotchas/).

## Object Mapping Reference

Mapping Bullhorn to Crelate requires translating distinct entities into Crelate's unified structure.

| Bullhorn Entity | Crelate Equivalent | Mapping Notes |
|---|---|---|
| Candidate | Contact (with candidate tag/status) | Crelate uses a unified Contact record; candidates are differentiated by workflow status and tags |
| ClientContact | Contact (with client tag) | Same entity type — use tags or custom fields to distinguish from candidates |
| ClientCorporation | Company | Direct mapping; parent/child company hierarchies require manual reconstruction |
| JobOrder | Job | Stages map to Crelate workflow stages; Crelate supports up to 30 stages (Enterprise) |
| JobSubmission | Pipeline stage on a Job | No direct equivalent — submissions become Contacts placed at a stage on a Job's pipeline |
| Placement | Placement | Crelate has native Placement records with bill/pay rate support |
| Opportunity | Opportunity (Sales Pipeline) | Choose the correct Opportunity Type at creation — Crelate does not allow changing it after creation ([help.crelate.com](https://help.crelate.com/en/articles/6229333-can-i-change-the-opportunity-type-for-an-existing-opportunity)) |
| Lead | Opportunity or Contact | Depends on pipeline stage; early-stage leads → BD Pipeline, qualified leads → Sales Pipeline |
| Note | Note | Direct mapping; attach to appropriate parent entity by ID |
| Appointment | Activity | Map to Crelate's activity types (Meeting, Call, etc.) |
| Task | Task | Direct mapping; Crelate tasks attach to Contacts, Jobs, or Companies |
| Custom Objects | No direct equivalent | Must be flattened into custom fields (max 20), tags, or serialized into notes |
| FileAttachment | Document / Artifact | Migrate in a separate binary pass via API |

Two mapping decisions matter more than the rest:

**JobSubmission is the hidden join record.** It explains why a candidate was attached to a job, what stage they reached, and how the submittal history unfolded. In Crelate, this becomes a second-pass load after Contact and Job already exist. Every CSV migration plan loses this data. ([bullhorn.github.io](https://bullhorn.github.io/rest-api-docs/entityref.html))

**Opportunity Type is schema, not data.** Crelate does not let you change the Opportunity Type on an existing record. A wrong type decision during initial load forces record recreation or manual cleanup. Decide this before your first test run, not after. ([help.crelate.com](https://help.crelate.com/en/articles/6229333-can-i-change-the-opportunity-type-for-an-existing-opportunity))

## API Rate Limits: The 1,500/min → 120/min Bottleneck

This rate-limit mismatch is the single biggest engineering constraint in a Bullhorn-to-Crelate migration.

### Bullhorn API Limits

Bullhorn's official rate limits apply to all ATS editions **except** ATS Growth (formerly Team Edition), which does not include API access: ([kb.bullhorn.com](https://kb.bullhorn.com/ats/Content/BHATS/Topics/understandingBHAPIUsageLimitsVersioningBackwardCompatibility.htm))

- **1,500 requests per minute**
- **100,000 total API calls per month** (unless negotiated)
- **50 concurrent active sessions**
- **50 active API subscriptions**

API usage from validated Bullhorn Marketplace partners does not count against your limits. Custom extraction pipelines **do** count. Bullhorn returns `429 Too Many Requests` when you exceed per-minute limits; rate-limited calls don't count against your monthly total.

> [!WARNING]
> Bullhorn API access is not included in the ATS Growth (formerly Team Edition) tier. If you are on this plan, you must rely on manual CSV exports or upgrade to Corporate/Enterprise editions to use the REST API for migration. ([kb.bullhorn.com](https://kb.bullhorn.com/ats/Content/BHATS/Topics/understandingBHAPIUsageLimitsVersioningBackwardCompatibility.htm))

### Crelate API v3 Limits

Crelate's constraints are tighter: ([postman.com](https://www.postman.com/crelate/crelate-api-v3/collection/18339486-bdd67bad-67f9-4480-8e64-f358b5a3c29b))

- **120 requests per minute per IP address**
- Aggressive throttling applied earlier for resource-heavy operations or repeated error codes (400, 401, 403, 500)
- **Pagination:** max 100 records per page (offset-based)
- **Authentication:** API key passed as query parameter or header
- **Date format:** All dates must be ISO 8601 UTC (e.g., `2026-01-20T15:00:00Z`) — non-UTC dates may fail silently
- **Creating new custom picklist fields is not supported** by the published API — configure these in Crelate's UI before migration

### What This Means for Your Migration

At 120 requests/min on the Crelate side, importing 50,000 candidate records — each requiring a POST to create the contact, a second call to attach tags, and potentially a third to upload a resume — means approximately **150,000 API calls** on the import side alone. At 120/min, that's ~20 hours of continuous, error-free execution.

Your extraction from Bullhorn can run 12.5× faster than your load into Crelate. Build accordingly:

1. **Extract first, load second.** Pull all Bullhorn data into a local staging database before touching Crelate's API.
2. **Implement exponential backoff with jitter** on 429 responses from Crelate — not just a flat retry.
3. **Monitor your Bullhorn monthly budget.** At 100,000 calls/month, a single full extraction of a large Bullhorn instance can consume your entire monthly allowance. Plan extraction during a billing cycle where you can absorb the usage.

```python
import time
import random
import requests

def crelate_post_with_backoff(url, headers, payload, max_retries=5):
    for attempt in range(max_retries):
        response = requests.post(url, headers=headers, json=payload)
        if response.status_code in (200, 201):
            return response.json()
        if response.status_code == 429:
            wait = (2 ** attempt) + random.uniform(0, 1)
            print(f"Rate limited. Retrying in {wait:.1f}s...")
            time.sleep(wait)
            continue
        response.raise_for_status()
    raise Exception("Max retries exceeded on Crelate API")
```

> [!CAUTION]
> Crelate's documentation explicitly warns that repeated error status codes (400, 401, 403, 500) will aggressively trigger rate limiting. If your script attempts to link a Placement to a Job UUID that failed to create in a previous step, Crelate returns a 400. If this happens in a loop, your IP will be throttled entirely, halting the migration.

## Migration Approaches Compared

There are five realistic paths. Each has sharp trade-offs.

### 1. Native CSV Export → Crelate Import

**How it works:** Export list views from Bullhorn as CSV files. Clean and reformat in Excel. Import into Crelate using its spreadsheet import tool (supports Contacts and Companies).

**When to use:** Small datasets (<5,000 records), flat data with no complex relationships, or when Bullhorn API access isn't available (ATS Growth edition). ([kb.bullhorn.com](https://kb.bullhorn.com/ats/Content/BHATS/Topics/exportingDataFromBullhorn.htm?Highlight=export+csv))

**Limitations:**
- Bullhorn's CSV export only includes fields visible in your list view columns
- Browser may time out on large exports; Bullhorn recommends breaking into multiple files
- Bullhorn Automation caps CSV exports at **50,000 records**
- Crelate's importer handles Contacts and Companies — no direct CSV import for Jobs, Placements, or Activities
- Relationships (Candidate → JobSubmission → Placement) are **completely lost** in CSV
- File attachments and custom object data cannot be exported via CSV
- Crelate can undo a record-creation import if the run was wrong

For deeper CSV trade-offs, see [Using CSVs for SaaS Data Migrations](https://clonepartner.com/blog/blog/csv-saas-data-migration/).

### 2. API-Based Custom ETL Pipeline

**How it works:** Write custom scripts to extract data from Bullhorn's REST API, transform it in a staging layer, and load it into Crelate's API v3.

**When to use:** Mid-to-large migrations (10K–500K records), when preserving relationships and placement history is required, when you have dedicated engineering capacity.

**Key steps:**
1. Authenticate to Bullhorn via OAuth 2.0 → obtain `BhRestToken` and `restUrl`
2. Extract entities in dependency order using `search` (Lucene-based) and `query` (JPQL-based) endpoints
3. Store extracted JSON in a staging database with Bullhorn IDs
4. Transform: consolidate custom fields into Crelate's 20-field limit, convert epoch timestamps to ISO 8601 UTC, normalize picklist values
5. Load into Crelate API v3 in dependency order, collecting Crelate UUIDs
6. Build a Bullhorn ID → Crelate ID mapping table to reconstruct relationships

**Risks:**
- Bullhorn's 100,000 monthly API call limit can be exhausted in a single extraction run
- Crelate's 120 req/min throttle makes large loads slow
- Custom object data requires querying nested `customObject1s` through `customObject35s` endpoints
- File attachments require separate API calls per file per record

### 3. Third-Party Migration Service

**How it works:** A specialized migration team handles extraction, transformation, loading, relationship reconstruction, and validation using API-led infrastructure with rehearsals, crosswalks, and post-load validation.

**When to use:** When engineering bandwidth is limited, when data integrity is non-negotiable, or when the migration needs to complete in days rather than weeks.

**Risks:** Quality varies across providers. Some flatten too much or run CSV under the hood. Ask how relationships are rebuilt and how validation is performed.

For a deeper analysis of build-vs-buy tradeoffs, see [Why Data Migration Isn't Implementation](https://clonepartner.com/blog/blog/data-migration-vs-implementation-guide/).

### 4. Custom ETL Platform

**How it works:** Your team builds dedicated extract workers, a raw landing store, a mapper, a rate-limited load queue, a dead-letter queue, and a QA suite.

**When to use:** You have a platform team, repeat migrations ahead, or strict compliance requirements.

**Risks:** Highest engineering cost and longest lead time. For one-time migrations, this is almost always more expensive than teams expect. The pipeline becomes another system to own after the move. For a broader warning, see [Why DIY AI Scripts Fail](https://clonepartner.com/blog/blog/why-ai-migration-scripts-fail/).

### 5. Middleware/iPaaS (Zapier, Make)

**How it works:** Trigger-based flows connect Bullhorn and Crelate via API connectors. Crelate has a native Zapier integration.

**When to use:** Low-volume delta sync after cutover — not historical migration. ([postman.com](https://www.postman.com/crelate/crelate-api-v3/collection/18339486-bdd67bad-67f9-4480-8e64-f358b5a3c29b))

**Limitations:**
- Designed for event-triggered workflows, not bulk historical data transfer
- Crelate's Zapier integration covers limited entity types
- No practical way to migrate 100K+ historical records through trigger-based flows
- Every record becomes several actions, easily overrunning Crelate's 120 req/min limit
- Multi-step relationship rebuilding is extremely difficult to orchestrate in iPaaS workflows

### Comparison Table

| Approach | Complexity | Preserves Relationships | Handles Attachments | Scalability | Timeline |
|---|---|---|---|---|---|
| CSV Export/Import | Low | ❌ No | ❌ No | Small only (<5K) | 1–3 days |
| Custom ETL Pipeline | High | ✅ Yes | ✅ Yes (with effort) | Large (with throttle mgmt) | 2–6 weeks |
| Managed Migration Service | Low (for you) | ✅ Yes | ✅ Yes | Enterprise-scale | Days |
| Custom ETL Platform | High | ✅ Yes | ✅ Yes | Enterprise | 4–12 weeks |
| iPaaS (Zapier/Make) | Medium | ⚠️ Partial | ❌ No | Small only | Ongoing |

### Recommendations by Scenario

- **Small agency (<5K records), flat data, no placements to preserve:** CSV export is sufficient.
- **Mid-size agency (5K–100K records), active placements, custom fields:** API-based ETL or managed service.
- **Enterprise agency (100K+ records), complex relationships, compliance requirements:** Managed service. The engineering cost of building and debugging a custom pipeline against both APIs typically exceeds the cost of hiring specialists.
- **Ongoing sync after historical migration:** iPaaS for new records only.
- **Hybrid approach:** Use CSV for reference data (Companies, static Contacts) and API for relationships, activities, placements, and files.

## Pre-Migration Planning

Skipping this step is the #1 cause of migration failures.

### Data Audit Checklist

Inventory every Bullhorn entity type and record count:

- [ ] **Candidates** — total active + archived
- [ ] **ClientContacts** — total, including inactive
- [ ] **ClientCorporations** — total, including parent/child relationships
- [ ] **JobOrders** — open + closed + archived
- [ ] **JobSubmissions** — total across all jobs
- [ ] **Placements** — active + historical
- [ ] **Leads / Opportunities** — total pipeline records
- [ ] **Notes** — total across all entity types
- [ ] **Appointments / Tasks** — total activity records
- [ ] **Custom Objects** — list all active custom object types and instance counts
- [ ] **File Attachments** — total count and estimated storage size
- [ ] **Custom Fields** — list all custom fields per entity with data types

Mark each dataset as **move**, **transform**, **archive**, or **drop**. Use Bullhorn's `/meta/{Entity}` endpoint to inspect schema programmatically. ([bullhorn.github.io](https://bullhorn.github.io/rest-api-docs/index.html))

### Identify What NOT to Migrate

Most Bullhorn instances accumulate significant data debt. Migrating everything wastes time and budget:

- Candidates with no activity in 3+ years
- Duplicate records (run Bullhorn's duplicate check before export)
- Test/sandbox data
- Closed jobs older than your reporting window
- Unused custom object instances
- Draft or unsent email templates

### Cutover Strategy

| Strategy | Best For | Risk Level |
|---|---|---|
| **Big Bang** | Small datasets, clear cutover date | Higher — no fallback if issues found post-migration |
| **Phased** | Large datasets, multiple offices/divisions | Lower — migrate by entity type or division, validate between phases |
| **Incremental** | Ongoing operations, can't afford downtime | Lowest — migrate historical data first, then sync delta changes |
| **Hybrid** | CSV works for reference data, API required for relationships | Medium — fastest for mixed complexity |

For most Bullhorn-to-Crelate migrations, **incremental** is safest because Crelate's write throughput is the bottleneck. Migrate historical data first, run validation, then sync deltas before cutover.

### Risk Mitigation

- Take a **full Bullhorn data backup** before starting (request via Bullhorn Support)
- Run a **test migration** with a subset (e.g., one division or 1,000 records) before full execution
- Maintain the Bullhorn instance in **read-only mode** during final cutover to prevent data drift
- Document a **rollback plan** — if Crelate data is corrupt, delete migrated records via Crelate API and re-run from staging

> [!TIP]
> **Build your crosswalk table before the first test run.** A crosswalk is a persistent map from Bullhorn integer IDs to Crelate GUIDs. Without it, retries create duplicates and validation becomes guesswork.

## Step-by-Step API Migration Process

### Step 1: Authenticate to Bullhorn

Bullhorn uses OAuth 2.0 to obtain an access token, then a separate login call to get a REST session:

```bash
# 1. Get authorization code
GET https://auth.bullhornstaffing.com/oauth/authorize?
  client_id={client_id}&response_type=code&redirect_uri={redirect_uri}

# 2. Exchange for access token
POST https://auth.bullhornstaffing.com/oauth/token?
  grant_type=authorization_code&code={auth_code}&
  client_id={client_id}&client_secret={client_secret}

# 3. Get REST session token
GET https://rest.bullhornstaffing.com/rest-services/login?
  version=*&access_token={access_token}
# Response: { "BhRestToken": "xxx", "restUrl": "https://rest99.bullhornstaffing.com/rest-services/e999/" }
```

Use `loginInfo` to resolve the correct data center URL before the login call. ([bullhorn.github.io](https://bullhorn.github.io/rest-api-docs/index.html))

### Step 2: Extract Entities from Bullhorn

Extract in dependency order. Use the `search` endpoint for indexed entities (Lucene-based) and `query` for smaller reference entities (JPQL-based). Paginate in batches of 500. Always use the `fields` parameter to request only the data you need:

```python
def extract_bullhorn_entity(rest_url, token, entity, fields, batch_size=500):
    results = []
    start = 0
    while True:
        resp = requests.get(
            f"{rest_url}search/{entity}",
            params={
                "query": "id:>0",
                "fields": fields,
                "count": batch_size,
                "start": start,
                "BhRestToken": token
            }
        )
        data = resp.json()
        results.extend(data.get("data", []))
        if len(data.get("data", [])) < batch_size:
            break
        start += batch_size
    return results
```

Store raw JSON payloads in your staging database. You will need them when mapping rules change mid-project.

### Step 3: Transform Data in Staging

Key transformations:

- **Consolidate custom fields:** Map Bullhorn's `customText1`–`customTextN` into Crelate's 20 custom field slots. Serialize overflow fields into a structured JSON note.
- **Normalize picklists:** Bullhorn picklist values must match Crelate's configured picklist options, or be pre-created in Crelate Settings before migration. Creating new custom picklist fields is not supported by the Crelate API — configure them in the UI. ([postman.com](https://www.postman.com/crelate/crelate-api-v3/collection/18339486-bdd67bad-67f9-4480-8e64-f358b5a3c29b))
- **Convert dates:** Bullhorn returns timestamps as epoch milliseconds. Crelate requires ISO 8601 UTC strings.
- **Merge Candidates and ClientContacts:** Both become Crelate Contacts. Use tags (e.g., `{"Default": ["Candidate"]}` vs `{"Default": ["Client Contact"]}`) to differentiate.
- **Detect duplicates:** The same person can exist as both a Candidate and a ClientContact in Bullhorn. Merge by email or name+phone, preserving the most complete data from each.
- **Build ID mapping tables:** Create a `bullhorn_id → crelate_id` crosswalk for every entity to reconstruct relationships after loading.

### Step 4: Load into Crelate API v3

Load in dependency order: Companies → Contacts → Jobs → Pipeline placements → Activities → Notes → Attachments.

Use Crelate's `/info` metadata endpoint to inspect logical names and allowed values before building payloads. ([postman.com](https://www.postman.com/crelate/crelate-api-v3/collection/18339486-bdd67bad-67f9-4480-8e64-f358b5a3c29b))

```python
# Create a company in Crelate
response = crelate_post_with_backoff(
    url="https://app.crelate.com/api3/companies",
    headers={"Content-Type": "application/json"},
    payload={
        "Name": company_data["name"],
        "Phone": company_data.get("phone"),
        "Website": company_data.get("website"),
        "CustomFields": {
            "bullhorn_id": str(company_data["id"])
        },
        "Tags": {"Default": ["Migrated"]}
    }
)
crelate_company_id = response["Id"]
id_map["companies"][bullhorn_id] = crelate_company_id
```

### Step 5: Rebuild Relationships

This is where most DIY scripts fail. After creating all records:

1. **Link Contacts to Companies** using the appropriate lookup fields
2. **Place Contacts on Job pipelines** at the correct workflow stage (map Bullhorn submission statuses to Crelate stage names)
3. **Create Placement records** referencing the correct Crelate Contact ID and Job ID
4. **Attach Notes and Activities** to parent records using the ID mapping table

Load and validate **one level at a time.** Don't batch-insert Placements before confirming every referenced Contact and Job exists in Crelate.

### Step 6: Validate

Run automated checks after every load batch:

- **Record count comparison:** Bullhorn entity count vs. Crelate entity count per type
- **Field-level sampling:** Pull 50 random records from each entity type, compare field values against Bullhorn source
- **Relationship integrity:** Verify that every Placement links to a valid Contact and Job
- **Attachment verification:** Spot-check that resumes/files are accessible on the correct Contact records

## Common Failure Modes and Edge Cases

### Custom Fields Overflow

Bullhorn's custom field capacity dwarfs Crelate's 20-per-entity limit. If your Bullhorn Candidate entity uses `customText1` through `customText40`, your options are:

1. **Prioritize the 20 most-used fields** for Crelate's custom field slots. Archive the rest into a structured note or external system.
2. **Concatenate related fields** (e.g., merge "Clearance Level" and "Clearance Expiry" into a single Crelate custom field).
3. **Convert categorical fields to Tags**, which don't count against the 20-field limit.

There is no workaround — Crelate does not support custom objects or unlimited custom fields.

### Custom Objects Have No Equivalent

Bullhorn supports up to 35 `PersonCustomObjectInstance` records and 10 `JobOrderCustomObjectInstance` records per entity. These function as child tables — for example, tracking multiple certifications per candidate or multiple interview rounds per submission.

Crelate has **no custom object support**. Every custom object must be flattened into custom fields (within the 20-field limit), tags (for categorical data), notes (for structured text), or activities (for event-type data). ([bullhorn.github.io](https://bullhorn.github.io/Custom-Objects/))

### Duplicate Records

Bullhorn maintains separate `Candidate` and `ClientContact` entities. The same person can exist as both. In Crelate, they both become `Contacts`. Your transformation layer must:

1. Detect duplicates by email or name+phone
2. Merge records, preserving the most complete data from each
3. Carry forward both sets of activity history

### The Attachment Trap

Bullhorn stores file attachments accessible via the `/entityFiles/{entityType}/{entityId}` and `/file/{entityType}/{entityId}/{fileId}` endpoints. Each file requires a separate API call to download. For an agency with 100,000 candidates averaging 2 files each, that's 200,000+ extraction calls — which can consume your monthly Bullhorn API budget twice over.

Uploading is equally slow. At Crelate's 120 requests per minute, uploading 100,000 resumes takes a minimum of **14 hours** of continuous, uninterrupted API traffic. Any network timeout or 500 error breaks the sequence if your script lacks robust state management.

**Solutions:**
- Request a **full data backup from Bullhorn** (includes files) instead of extracting via API
- Negotiate a temporary API limit increase with Bullhorn support for the migration period

### Opportunity Type Lock-In

Crelate does not allow changing the Opportunity Type on an existing record. If your initial load assigns the wrong type, you must delete and recreate the record. Decide Opportunity Type mappings during pre-migration planning, not after load. ([help.crelate.com](https://help.crelate.com/en/articles/6229333-can-i-change-the-opportunity-type-for-an-existing-opportunity))

### Tag Overwrites

Crelate's tag updates are **full replacement**, not additive. If you PATCH a record's tags and omit existing tags from the payload, those tags are deleted. Always read the current tag set before updating. ([postman.com](https://www.postman.com/crelate/crelate-api-v3/collection/18339486-bdd67bad-67f9-4480-8e64-f358b5a3c29b))

> [!CAUTION]
> Do not PATCH Crelate tags as if they were additive. Crelate documents tag updates as full replacement: any tags omitted from the update payload are lost.

### External Primary Key Trap

Crelate's External Primary Key field is only assigned to records created via spreadsheet import. If you're doing an API-led migration, don't assume that field exists on your records. Maintain your own crosswalk store or store the Bullhorn ID in a custom field. ([help.crelate.com](https://help.crelate.com/en/articles/7020758-external-primary-key/))

### Multi-Level Relationship Chains

Bullhorn's `Company → Contact → JobOrder → JobSubmission → Placement` chain is five levels deep. Each level depends on IDs from the previous level. If any link in the chain fails to import, downstream records become orphaned. Load and validate one level at a time.

For a deep dive into how scripts break under load, read [Why DIY AI Scripts Fail](https://clonepartner.com/blog/blog/why-ai-migration-scripts-fail/).

## Crelate Constraints That Limit Migration Fidelity

Be explicit with stakeholders about what Crelate cannot replicate from Bullhorn:

| Bullhorn Capability | Crelate Constraint |
|---|---|
| Unlimited custom fields per entity | Max 20 custom fields per entity |
| 10–35 custom objects per entity | No custom objects |
| Separate Candidate and Contact entities | Single unified Contact entity |
| JobSubmission as a discrete entity | No direct equivalent — modeled as pipeline stage |
| Effective-dated entity versions | No effective dating |
| VMS/Back Office integrations | No native VMS integration |
| Workflow stages (unlimited) | 10 (Pro), 20 (Business), 30 (Enterprise) |
| Custom picklist field creation via API | Not supported — UI configuration only |
| Opportunity Type changes | Cannot change after record creation |

## Validation and Testing Protocol

### Pre-Go-Live Validation

1. **Record count reconciliation:** Total records per entity type in Bullhorn vs. Crelate. Tolerance: 0% variance.
2. **Field-level validation:** Sample 5% of records per entity type. Compare every mapped field value.
3. **Relationship integrity audit:** For every Placement, verify the linked Contact and Job exist and are correct.
4. **Attachment spot check:** Download 20 random files from Crelate and compare to Bullhorn originals.
5. **Picklist validation:** Ensure all migrated picklist values match Crelate's configured options.

### UAT Process

- Have 2–3 recruiters use Crelate with migrated data for 2–3 days before full cutover
- Provide a checklist: find a specific candidate, verify their submission history, check a placement's bill rate, confirm an attached resume
- Document every discrepancy and trace it back to the transformation layer

### Rollback Plan

- Maintain Bullhorn access for at least 30 days post-migration
- If Crelate data quality is unacceptable, delete migrated records via Crelate API and re-run from staging
- Keep the staging database intact as your single source of truth during the transition

Crelate exposes backup endpoints in API v3 and admins can request full exports — use these for checkpoints and rollback insurance. ([help.crelate.com](https://help.crelate.com/en/articles/8922247-march-2024-what-s-new-24-3-release))

## Post-Migration Tasks

### Rebuild in Crelate

- **Workflow stages:** Recreate your Bullhorn submission statuses as Crelate workflow stages (Settings → Workflows)
- **Email templates:** Bullhorn templates won't transfer — rebuild in Crelate's template editor
- **Automations:** Bullhorn Automation rules have no equivalent export format — document and manually recreate in Crelate's automation engine
- **Integrations:** Reconnect job boards, email sync, calendar integrations, and any third-party tools

### User Training

- Crelate's unified Contact model (candidates + client contacts in one entity) requires workflow adjustment for recruiters accustomed to Bullhorn's separated entities
- Train on tag-based filtering to replace Bullhorn's entity-type navigation
- Demonstrate pipeline views, which replace Bullhorn's submission-based workflows

### Monitoring

For the first 30 days:
- Run weekly record count comparisons
- Monitor Crelate API error logs for failed relationship links
- Track recruiter-reported data quality issues and batch-fix from the staging layer

## Sample Field-Level Mapping Reference

| Bullhorn Field | Type | Crelate Field | Type | Notes |
|---|---|---|---|---|
| `firstName` | String(50) | `FirstName` | String | Direct map |
| `lastName` | String(50) | `LastName` | String | Direct map |
| `email` | String(100) | `Email` | String | Direct map |
| `phone` | String(20) | `Phone` | String | Direct map |
| `status` | String(30) | `EntityStatus` | Picklist | Map values to Crelate statuses |
| `source` | String(100) | `Source` | String | Direct map |
| `owner.id` | Integer | `PrimaryOwner_Id` | GUID | Map via user ID table |
| `dateAdded` | Timestamp (ms) | `CreatedOn` | ISO 8601 UTC | Convert epoch → ISO |
| `customText1`–`customText20` | String | `CustomFields.{logical_name}` | Varies | Map first 20; serialize remainder into notes |
| `skills.name` | Association | `Tags.Skills` | Tag array | Convert skill associations to tag array |
| `address.city` | String | `City` | String | Direct map |
| `salary` | BigDecimal | Custom field | Decimal | Map to a custom field slot |
| `clientCorporation.id` | To-one assoc | `Company_Id` | GUID | Map via company crosswalk table |
| `Candidate.id` | Integer | Custom field or crosswalk | String | Store Bullhorn ID for replay and dedupe |
| `JobOrder.numOpenings` | Integer | Opportunity openings | Integer | Default null to 1 only if business agrees |

## Best Practices

- **Back up everything** before starting — request a full Bullhorn data backup from support
- **Run a test migration** with 500–1,000 records from each entity type before committing to full load
- **Validate incrementally** — don't wait until the end to check data quality
- **Map custom fields early** — the 20-field limit shapes your entire transformation logic
- **Monitor API budgets** — track Bullhorn monthly call usage and Crelate per-minute throughput
- **Preserve Bullhorn access** for 30+ days post-migration as a safety net
- **Automate validation** — write scripts to compare record counts, field values, and relationship integrity between source and target
- **Document every transformation decision** — when a recruiter asks "where did my custom field go?" six months later, you'll need the answer
- **Store Bullhorn source IDs** somewhere durable; don't rely on Crelate External Primary Key alone for API-loaded records ([help.crelate.com](https://help.crelate.com/en/articles/7020758-external-primary-key/))
- **Keep raw extraction payloads** — you'll need them when mapping rules change mid-project
- **Version your field maps and lookup translations** — treat them as code, not one-time config

## When to Use a Managed Migration Service

**Build in-house when:**
- Your dataset is small (<10K records) and flat
- You have a dedicated engineer with API migration experience who can commit 3–4 weeks
- You don't need to preserve placement history or complex relationships

**Hire a specialist when:**
- Your Bullhorn instance has 50K+ records with active placements
- You have significant custom object usage that needs creative mapping
- Your Bullhorn API call budget is limited (100K/month won't cover extraction + testing + re-runs)
- Your recruiters can't tolerate a multi-week migration window
- You've already attempted a DIY migration and hit rate-limit walls or data integrity issues

The hidden cost of DIY migrations isn't the initial build — it's the debugging. Orphaned records, silently dropped custom fields, and broken placement chains often surface weeks after go-live when a recruiter can't find a candidate's submission history. By then, tracing the issue back to a transformation bug requires re-extracting and re-comparing against the source.

The cleanest Bullhorn-to-Crelate projects start with one uncomfortable step: deciding what the Crelate system of record should look like after Bullhorn's extra entities, custom objects, and field banks are reduced into a smaller schema. Once that target model is explicit, the rest is engineering. If you skip that design step, you don't avoid the work — you just move it into recruiter cleanup after go-live.

## How ClonePartner Handles Bullhorn-to-Crelate Migrations

ClonePartner has completed 1,200+ data migrations across CRM, ATS, and helpdesk platforms. For Bullhorn-to-Crelate, the approach addresses the three hardest problems:

1. **Rate-limit bridging:** Managed queue infrastructure absorbs Bullhorn's high-speed extraction (1,500 req/min) and meters it into Crelate's tighter import window (120 req/min) with automatic 429 retry handling and IP rotation — so the migration runs continuously without manual intervention.
2. **Custom field compression:** Our engineers audit your Bullhorn custom fields and objects, build a prioritized mapping to Crelate's 20-field limit, and serialize overflow data into structured, searchable formats — so nothing is silently dropped.
3. **Relationship reconstruction:** Every Candidate → JobSubmission → Placement chain is rebuilt in Crelate with verified ID mappings. Post-load validation confirms zero orphaned records.

Most Bullhorn-to-Crelate migrations complete in **3–5 business days** with zero downtime. Your team keeps working in Bullhorn until cutover. We run a final delta sync over the weekend — recruiters log out of Bullhorn on Friday and log into a fully populated Crelate instance on Monday.

> Migrating from Bullhorn to Crelate? ClonePartner's engineering team handles the rate-limit mismatch, custom field mapping, and relationship reconstruction — completed in days with zero downtime. Book a 30-minute technical scoping call.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30&utm_source=blog&utm_medium=button&utm_campaign=demo_bookings&utm_content=cta_click&utm_term=demo_button_click)

## Frequently asked questions

### How long does a Bullhorn to Crelate migration take?

A CSV-only migration of small, flat data can complete in 1–3 days. API-based migrations with relationship preservation typically take 2–6 weeks for DIY builds or 3–5 business days with a managed migration service, depending on data volume and complexity.

### What are the Bullhorn and Crelate API rate limits?

Bullhorn allows up to 1,500 requests per minute and 100,000 API calls per month (unless negotiated). Crelate API v3 enforces 120 requests per minute per IP address. Deprecated v1/v2 endpoints are throttled to 30 req/min. Repeated error codes on Crelate trigger even more aggressive throttling.

### Can I migrate Bullhorn custom objects to Crelate?

Not directly. Crelate does not support custom objects. Bullhorn custom objects (up to 35 per Person entity) must be flattened into Crelate's custom fields (max 20 per entity), tags, notes, or activity records during transformation.

### What Bullhorn data is lost in a CSV export?

CSV exports from Bullhorn flatten relational data, losing all Candidate-to-JobSubmission-to-Placement relationships, file attachments, custom object data, and activity history. Only fields visible in your current list view columns are included.

### Does Bullhorn's ATS Growth plan support API access for migration?

No. Bullhorn's ATS Growth edition (formerly Team Edition) does not include API access. Users on this plan must rely on CSV exports or upgrade to Corporate/Enterprise editions to use the REST API.
