The Ultimate Workable to Greenhouse Migration Guide (2026)
A technical guide to migrating data from Workable to Greenhouse — covering API rate limits, data model differences, resume extraction, and field mapping for CTOs and engineering teams.
Planning a migration?
Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.
Schedule a free call- 1,200+ migrations completed
- Zero downtime guaranteed
- Transparent, fixed pricing
- Project success responsibility
- Post-migration support included
Migrating from Workable to Greenhouse is a data-model translation problem. Workable treats candidates as flat records attached to jobs — one candidate profile per job pipeline, with stages, comments, and resume URLs bundled into a single entity. Greenhouse separates this into distinct Candidate (the person) and Application (the candidacy for a specific job) records, where a single Candidate can have multiple Applications across different Jobs.
A naive CSV export from Workable flattens this relationship, silently drops resumes and attachments, and leaves you rebuilding scorecards from scratch. (help.workable.com)
The real bottleneck isn't moving structured data — it's extracting resumes. Workable doesn't support bulk resume downloads natively, and its Candidate Details CSV report excludes attachment files entirely. You need API scripts or a full account data export (which requires archiving all active jobs) to get the files out. On the Greenhouse side, you're writing into an API that enforces strict rate limits and requires base64-encoded file content for each attachment upload.
This guide covers the object-mapping decisions, API constraints on both sides, every viable migration method with trade-offs, and the edge cases that break most DIY attempts.
For related ATS migration topics, see our coverage of common ATS migration gotchas, GDPR/CCPA compliance during candidate data transfers, and the Lever to Greenhouse migration guide for a deeper look at Greenhouse's target data model.
Greenhouse Harvest API v1 and v2 will be deprecated and unavailable after August 31, 2026. Any new migration integration should target Harvest v3, which uses OAuth 2.0 instead of Basic Auth. If you're planning a migration in Q3 2026 or later, build directly against v3.
Why Companies Migrate from Workable to Greenhouse
The drivers fall into three categories:
- Structured hiring enforcement. Greenhouse's scorecard system, interview kits, and multi-stage approval workflows give talent operations teams consistent, auditable processes across departments and geographies. Greenhouse forces hiring managers to define scorecards and interview kits before a job goes live. Workable supports structured interviews but doesn't enforce scorecard completion or approval chains with the same rigor.
- Enterprise reporting and BI. Greenhouse's Business Intelligence Connector delivers a nightly ETL of your recruiting data keyed on
candidate_id,application_id, andjob_id— a normalized schema purpose-built for warehouse integration into Snowflake or Redshift. Workable's reporting has improved but remains more limited for custom analytics. - Integration ecosystem depth. Greenhouse offers 500+ pre-built integrations and five distinct APIs (Harvest, Job Board, Ingestion, Assessment, Onboarding). For companies that need deep connections to HRIS, background check, and assessment platforms, Greenhouse's ecosystem is broader.
Workable has its own strengths — particularly AI-powered sourcing, transparent pricing, and faster time-to-first-hire for smaller teams. The migration typically happens when a company outgrows Workable's workflow rigidity and needs the process-enforcement model that Greenhouse provides.
The Data Model Shift: Candidates vs. Applications
This is the single most important concept to understand before writing a line of migration code.
In Workable: A candidate is associated with a specific job. If the same person applied to three jobs, they exist as three separate candidate records (though Workable can detect duplicates via email). Each candidate record carries its own stage, comments, tags, and resume.
In Greenhouse: A Candidate is the person. An Application is the candidacy for a specific job. One Candidate ID can have multiple Application IDs, each linked to a different Job ID. Resumes, scorecards, and interview feedback attach at the Application level. (support.greenhouse.io)
| Concept | Workable | Greenhouse |
|---|---|---|
| The person | Candidate (per-job) | Candidate (global) |
| Their job candidacy | Candidate record itself | Application (links Candidate → Job) |
| Resume / attachments | On candidate profile | On Application (also aggregated at Candidate level) |
| Pipeline stage | Candidate's stage in a job | Application's current_stage within the job's interview plan |
| Scorecard | Evaluation tied to candidate | Scorecard tied to Application + Interview |
The transformation rule: deduplicate Workable candidates by email, create one Greenhouse Candidate per unique person, then create one Application per Workable job-candidate pair under that Candidate.
Do not map Workable's stage directly to a Greenhouse Candidate. Stages belong to the Application object in Greenhouse.
Greenhouse's bulk import has an auto-merge feature. If enabled, candidates added through bulk import are evaluated against existing profiles using configured match criteria (e.g., email address). Note that referral or specific agency sources are excluded from auto-merge even when they would otherwise match. Keep this in mind when planning import batches to avoid unintended merges or missed duplicates. (support.greenhouse.io)
Exporting from Workable: The Resume Bottleneck
There are three ways to get data out of Workable. Each has significant trade-offs.
1. Candidate Details Report (CSV)
Workable's built-in Candidate Details report can be exported as a CSV. It includes structured fields — name, email, phone, source, stage, tags, and custom field answers.
What it does NOT include: resumes, cover letters, attachments, or evaluations. This is explicitly confirmed in Workable's documentation. (help.workable.com)
When to use it: Quick audit of candidate counts and field coverage. Not usable as the sole data source for a full migration.
2. Full Account Data Export
Workable can provide a complete data export when you're leaving the platform. This includes per-area CSVs for candidates, comments, events, ratings, messages, and jobs, plus resume files organized into job folders with subfolders per candidate. Signed offers can be included as well. (help.workable.com)
The catch: The best time to request this export is when all your jobs are archived, so there are no new activities. This export cannot be customized for specific date ranges — it's all-or-nothing. For an active enterprise recruiting team, freezing the ATS is rarely an option. Contact Workable support to initiate it.
You'll also need to write a script to match resume files back to candidate records by folder path.
3. API-Based Extraction
The Workable API (/spi/v3/candidates/:id) returns full candidate profiles including resume download URLs. This is the only method that gives you programmatic, filterable access to all data.
Rate limit: Account tokens are limited to 10 requests per 10 seconds. OAuth 2.0 and Partner tokens get 50 requests per 10 seconds. Exceeding these limits returns HTTP 429. (help.workable.com)
Workable's API returns rate-limit headers for throttle management:
X-Rate-Limit-Limit— maximum allowed requestsX-Rate-Limit-Remaining— remaining requests in current windowX-Rate-Limit-Reset— timestamp of next interval
At 10 requests per 10 seconds with an account token, extracting 10,000 candidates with their detail records takes approximately 2.8 hours of continuous API calls — assuming one call per candidate and zero errors.
# Workable candidate extraction with rate limiting
import requests
import time
WORKABLE_TOKEN = "your_api_token"
SUBDOMAIN = "your-company"
BASE_URL = f"https://{SUBDOMAIN}.workable.com/spi/v3"
HEADERS = {"Authorization": f"Bearer {WORKABLE_TOKEN}"}
def fetch_candidates(job_shortcode):
candidates = []
url = f"{BASE_URL}/jobs/{job_shortcode}/candidates?limit=100"
while url:
resp = requests.get(url, headers=HEADERS)
if resp.status_code == 429:
reset_at = int(resp.headers.get("X-Rate-Limit-Reset", time.time() + 10))
sleep_for = max(reset_at - time.time(), 1)
time.sleep(sleep_for)
continue
resp.raise_for_status()
data = resp.json()
candidates.extend(data.get("candidates", []))
url = data.get("paging", {}).get("next")
# Respect rate limits proactively
remaining = int(resp.headers.get("X-Rate-Limit-Remaining", 1))
if remaining < 2:
time.sleep(10)
return candidatesIf you need resumes, you must hit the individual candidate endpoint (/candidates/:id) for each record to get the resume URL, then download the file separately. That's two requests per candidate minimum — list + detail — which doubles your extraction time. Use OAuth tokens if available — they provide 5× the throughput.
Migration Approaches: CSV vs. API vs. Managed Service
There are four primary methods. Here's what each actually involves.
Method 1: CSV Export → Greenhouse Bulk Import
How it works:
- Export Candidate Details CSV from Workable, or request a full account export for resumes
- Create target jobs, stages, custom fields, sources, and rejection reasons in Greenhouse
- Reformat the CSV to match Greenhouse's bulk import template
- Upload via Greenhouse's Configure → Bulk Import tool
- Optionally attach a .zip of resumes (max 5 GB)
Complexity: Low
When to use: Small accounts (<500 candidates), you're willing to lose scorecards and detailed activity history, and you don't have engineering resources.
Limitations:
- Greenhouse recommends importing no more than 8,000 candidates per batch (support.greenhouse.io)
- The bulk import tool is only available on Plus and Pro subscription tiers
- Bulk import supports only short textbox, long textbox, number, single select, URL, and Yes/No custom fields — anything more complex needs the API (support.greenhouse.io)
- You lose interview feedback, scorecards, and activity timelines
- Historical candidates can't be placed into accurate pipeline stages without workarounds (Greenhouse recommends a "container job" approach for historical imports)
- Interviews cannot be backdated, though you can backdate scorecards (support.greenhouse.io)
- Resume attachment during bulk import depends on Greenhouse parsing an email from the resume and matching it to the imported candidate row — if parsing fails, the resume silently drops (support.greenhouse.io)
Method 2: API-to-API Migration (DIY ETL)
How it works:
- Extract all candidates, jobs, activities, and resume URLs from Workable API
- Download resume files from URLs
- Transform data: deduplicate candidates, map fields, split into Candidate + Application records
- Create jobs in Greenhouse (manually or via API)
- POST candidates to Greenhouse Harvest API
- POST applications under each candidate, linking to jobs
- Upload attachments as base64-encoded content
Complexity: High
When to use: Engineering team available, need full-fidelity migration with resumes and activity history, >500 candidates.
Key constraints on the Greenhouse side:
- Harvest API rate limits: Typically 50 requests per 10-second window for approved integrations (returned via
X-RateLimit-Limitheader). Harvest v3 uses a 30-second fixed window. (developers.greenhouse.io) - Authentication: Harvest v1/v2 uses Basic Auth. Harvest v3 requires OAuth 2.0. Since v1/v2 are deprecated August 31, 2026, build for v3.
- Write operations require the
On-Behalf-Ofheader — every POST, PATCH, DELETE must include a valid Greenhouse user ID for the audit trail. - Attachments must be base64-encoded or provided as a direct download URL. Shareable links from Google Drive or similar services will result in corrupted files. (developers.greenhouse.io)
- Pagination: v1/v2 uses RFC-5988 Link headers (page-based). v3 uses cursor-based pagination — never construct cursor URLs manually.
- Greenhouse can return asynchronous or truncated create responses for some POSTs, so you need follow-up GETs before attaching downstream records.
# Greenhouse candidate creation with attachment upload
import requests
import base64
GH_API_KEY = "your_greenhouse_api_key"
GH_USER_ID = "12345" # On-Behalf-Of user
GH_BASE = "https://harvest.greenhouse.io/v1"
def create_candidate_with_resume(first_name, last_name, email, resume_bytes, job_id, stage_id=None):
# Step 1: Create candidate with application
candidate_payload = {
"first_name": first_name,
"last_name": last_name,
"email_addresses": [{"value": email, "type": "personal"}],
"applications": [{
"job_id": job_id,
**( {"initial_stage_id": stage_id} if stage_id else {})
}]
}
resp = requests.post(
f"{GH_BASE}/candidates",
json=candidate_payload,
auth=(GH_API_KEY, ""),
headers={"On-Behalf-Of": GH_USER_ID}
)
resp.raise_for_status()
candidate_id = resp.json()["id"]
# Step 2: Attach resume
attachment_payload = {
"filename": f"{first_name}_{last_name}_resume.pdf",
"type": "resume",
"content": base64.b64encode(resume_bytes).decode("utf-8"),
"content_type": "application/pdf"
}
resp = requests.post(
f"{GH_BASE}/candidates/{candidate_id}/attachments",
json=attachment_payload,
auth=(GH_API_KEY, ""),
headers={"On-Behalf-Of": GH_USER_ID}
)
resp.raise_for_status()
return candidate_idMethod 3: iPaaS / Middleware (Zapier, Make)
How it works: Use Zapier or Make to connect Workable triggers to Greenhouse actions. Zapier exposes triggers such as New Candidate and Updated Candidate Stage, and Greenhouse actions such as Create Candidate and Create Prospect. (zapier.com)
Complexity: Medium
When to use: Ongoing sync of new candidates only. Not historical migration.
Limitations:
- Zapier's Greenhouse integration works for up to 100 jobs — beyond that, performance degrades
- These tools are trigger-based (new candidate created, candidate moved). They can't backfill historical data efficiently.
- No built-in resume download/upload pipeline
- Rate limits apply to both APIs, and these platforms don't expose fine-grained throttle controls
- These platforms are best treated as sync glue after go-live, not as archival ETL (zapier.com)
Method 4: Managed Migration Service
How it works: A migration service extracts data via APIs, transforms it to the target schema, runs dry runs, loads production data, and validates results.
Complexity: Low (for your team)
When to use: >500 candidates with resumes, need activity history, limited engineering bandwidth, or strict timeline requirements.
Comparison Table
| Approach | Resume Migration | Scorecards / History | Engineering Effort | Scalability | Main Failure Mode |
|---|---|---|---|---|---|
| CSV Export → Bulk Import | Partial (manual .zip) | ❌ Lost | Low | <8K candidates/batch | Flattened history, manual cleanup |
| API-to-API (DIY) | ✅ Full | ✅ Possible | High (3–6 weeks) | Enterprise-scale | 429s, dedupe, async writes |
| iPaaS (Zapier/Make) | ❌ | ❌ | Medium | New records only | Weak backfill, no relationships |
| Managed Service | ✅ Full | ✅ Full | Minimal | Enterprise-scale | Vendor quality |
Recommendations by Scenario
- Small team (<500 candidates), no engineering bandwidth: CSV export with manual resume .zip. Accept the loss of historical scorecards.
- Mid-market (500–10,000 candidates), need resumes: Managed migration service or a dedicated engineer for 3–6 weeks.
- Enterprise (>10,000 candidates), strict timeline: Managed migration service. The rate-limit math alone makes DIY risky on a deadline.
- Ongoing sync (new hires to HRIS): Zapier/Make for forward-looking triggers after migration is complete.
Navigating API Rate Limits
Rate limits are the mechanical bottleneck in any API-based migration. Here's the math.
Workable Extraction Throughput
| Token Type | Limit | Interval | Effective Rate |
|---|---|---|---|
| Account token | 10 requests | 10 seconds | 60/min |
| OAuth 2.0 token | 50 requests | 10 seconds | 300/min |
| Partner token | 50 requests | 10 seconds | 300/min |
With an account token, extracting 5,000 candidate detail records (one API call each) takes ~83 minutes. Adding resume downloads doubles this. Always monitor X-Rate-Limit-Remaining and pause before hitting zero. Workable returns 429 with no Retry-After header — calculate the wait from X-Rate-Limit-Reset. (help.workable.com)
Greenhouse Ingestion Throughput
| Version | Limit | Window | Notes |
|---|---|---|---|
| Harvest v1/v2 | ~50 requests | 10-second rolling window | Deprecated Aug 31, 2026 |
| Harvest v3 | Varies (check header) | 30-second fixed window | Uses OAuth 2.0 |
Greenhouse's Harvest API responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset on every call. On 429, the Retry-After header tells you exactly how long to wait. (developers.greenhouse.io)
For a migration creating candidates + applications + attachments, budget 3 API calls per candidate minimum (create candidate, create application, upload resume). At 50 requests per 10 seconds, that's ~16 candidates per 10-second window, or ~96 per minute. 10,000 candidates ≈ 1.7 hours of API time, assuming zero retries.
Unlisted vendors on Greenhouse may be subject to additional rate limits beyond the standard 50/10s. If you're building a custom integration, register it in Greenhouse's Dev Center first to ensure you get the standard allocation.
Data Mapping & Custom Field Dependencies
Standard Object Mapping
| Workable Object | Greenhouse Equivalent | Notes |
|---|---|---|
| Job | Job | Map Workable shortcode to Greenhouse job_id |
| Candidate (per job) | Candidate + Application | Deduplicate by email → one Candidate, one Application per job |
| Pipeline Stage | Interview Plan Stage | Map stage names; Greenhouse uses milestones (Application, Assessment, Face to Face, Offer) |
| Tags | Candidate Tags | Direct 1:1 mapping |
| Source | Source | Map Workable source names to Greenhouse source IDs |
| Comments / Notes | Notes (on Application) | Preserve author and timestamp in note body |
| Resume | Attachment (type: resume) | Download from Workable, base64-encode, POST to Greenhouse |
| Cover Letter | Attachment (type: cover_letter) | Same flow as resume |
| Evaluations / Ratings | Notes or Scorecards | Greenhouse scorecards are tied to specific interview stages — Workable evaluations usually become notes |
| Custom Fields | Custom Candidate/Application Fields | Type-aware mapping required; see below |
| Hiring Team | Job Hiring Team | Map recruiter/coordinator to Greenhouse user IDs |
| Offer | Offer object | Custom offer fields may have dependencies |
For a broader look at common mapping failures, see our guide on ATS migration gotchas.
Custom Field Type Mapping
Greenhouse supports the following custom field types: short_text, long_text, yes_no, single_select, multi_select, currency, currency_range, number, number_range, date, url, and user.
Workable's custom attributes are simpler — typically free text, single-select, or multi-select. During mapping, validate that:
- Single-select field options match exactly (case-sensitive) in Greenhouse
- Multi-select values use the correct option IDs, not display names
- Date formats align (Greenhouse expects ISO 8601)
Greenhouse distinguishes candidate fields from application fields. Workable answers and job custom attributes may need to land in either Greenhouse candidate fields, application fields, or notes depending on whether the answer varies per job. (help.workable.com)
Greenhouse's 3-Level Dependent Custom Fields
Greenhouse supports nested dependent custom fields up to three levels deep (grandparent → parent → child). This is available on the Expert subscription tier for offers, jobs, and openings. (support.greenhouse.io)
For example, selecting "Engineering" (grandparent) reveals "Backend" (parent), which reveals "Python" (child).
Workable does not support this hierarchical structure natively — custom fields are flat. When migrating:
- Create the dependency hierarchy in Greenhouse first
- Map Workable flat field values to the correct parent/child option pairs
- Set values in the correct order during API import — parent before child
If you push a child value without the corresponding parent value, the Greenhouse API will reject the payload. Only Yes/No and Single-select field types can be parent fields. Single-select parents cannot have more than 25 options.
Detailed Field Mapping Reference
| Workable Field | Greenhouse Field | Target Object | Notes |
|---|---|---|---|
name |
first_name + last_name |
Candidate | Split on first space |
email |
email_addresses [0].value |
Candidate | Primary dedup key |
phone |
phone_numbers [0].value |
Candidate | |
headline |
title |
Candidate | |
address |
addresses [0].value |
Candidate | |
summary |
Note body | Application Note | No direct Candidate field |
education_entries |
educations |
Candidate | Map school, degree, field |
experience_entries |
N/A (notes or custom) | Application Note | No structured work history field in Greenhouse |
tags |
tags |
Candidate | |
source |
source.id |
Application | Map to Greenhouse source IDs |
stage |
current_stage.id |
Application | Map to interview plan stage — do not map by label similarity alone |
disqualified |
rejected_at + rejection_reason |
Application | |
resume_url |
Attachment (type: resume) | Candidate/Application | Download → base64 → POST |
cover_letter_url |
Attachment (type: cover_letter) | Application | Same flow |
answers (custom questions) |
Custom application fields | Application | Type-aware mapping |
custom_attributes |
Custom candidate fields | Candidate | Validate picklist values |
If your internal worksheet uses CRM language, translate it before mapping: contact → candidate, lead → prospect or early-stage application, opportunity → application, activity → note, email, scorecard, or event. Neither Workable nor Greenhouse is a general-purpose CRM.
Pre-Migration Planning
Data Audit Checklist
Before extracting anything, inventory what's in Workable:
- Total candidate count — by job, by stage, by date range
- Active vs. archived jobs — decide which jobs' candidates need migration
- Resume coverage — what percentage of candidates have resumes attached?
- Custom fields in use — list all custom attributes, their types, and whether they have data
- Evaluation/scorecard data — is this worth migrating, or will you start fresh in Greenhouse?
- Source tracking — list all source values in Workable; map to Greenhouse source list
- GDPR/CCPA obligations — candidate consent status, data retention periods, right-to-be-forgotten requests
- Duplicate candidates — how many candidates exist across multiple jobs?
- Events, interviews, and offer data — determine what needs operational preservation
- Owners, recruiters, coordinators — map to Greenhouse user IDs and permission model
If candidate PII crosses regions or retention regimes, validate transfer and retention rules before exporting files. Greenhouse's historical candidate imports can trigger consent emails unless consent extension rules are disabled. See our candidate-data compliance guide for the governance side. (support.greenhouse.io)
Define Migration Scope
Not everything should move. Common exclusions:
- Candidates older than 2–3 years (GDPR data minimization)
- Candidates in "Rejected" stage with no evaluations
- Draft jobs with zero candidates
- Test/demo candidate records
Defining a clear cutoff date reduces API payload size and keeps Greenhouse clean from day one.
Choose a Cutover Strategy
| Strategy | Best For | Risk |
|---|---|---|
| Big bang | Small accounts, tight timeline | All-or-nothing; if it fails, you restart |
| Phased by job | Mid-market; migrate active jobs first, then historical | Lower risk; allows validation per batch |
| Incremental | Enterprise with ongoing hiring | Requires delta-tracking logic (Workable's updated_after param helps); most complex |
For most Workable-to-Greenhouse migrations, phased by job works best: migrate active/open jobs first, validate thoroughly, then backfill historical data. Greenhouse's "container job" pattern works well for historical candidate imports. (help.workable.com)
Big-bang cutovers in ATS environments risk split-brain scenarios where recruiters check two systems to verify a candidate's history. If you go big-bang, execute over a weekend.
Step-by-Step Migration Process
Phase 1: Extract
- List all jobs via
GET /spi/v3/jobs(paginate withlimit=100) - For each job, fetch candidates via
GET /spi/v3/jobs/{shortcode}/candidates - For each candidate, fetch detail via
GET /spi/v3/candidates/{id}— the collection endpoint omits verbose fields likecover_letter,answers,resume_url, tags, and social profiles (workable.readme.io) - Download resume files from the URLs in the response — do this immediately. Workable's resume download URLs are time-limited and will expire if you defer.
- Fetch custom attributes via
GET /spi/v3/jobs/{shortcode}/custom_attributes - Pull activities via
/candidates/{id}/activitiesfor comments, ratings, and events
Store everything in a staging database or structured JSON files. Keep the Workable candidate ID as the immutable source key.
Phase 2: Transform
- Deduplicate candidates by email — group all Workable records for the same email into one Candidate record
- Map fields — rename and reformat per the mapping tables above
- Resolve source IDs — match Workable source strings to Greenhouse source IDs
- Resolve stage mappings — match Workable stages to Greenhouse interview plan stages (or default to initial stage). Use an explicit dictionary mapping, not label similarity.
- Base64-encode resume files for API upload
- Validate custom field values — ensure picklist values exist in Greenhouse, dates are ISO 8601, required fields are populated
- Handle missing required fields — Greenhouse will reject API calls if required fields like
last_nameare null. Inject placeholder data (e.g., "Unknown") during transformation.
Phase 3: Load
- Create jobs in Greenhouse (manually or via API) with correct departments, offices, and interview plans
- POST candidates —
POST /v1/candidateswithapplicationsarray linking to Greenhouse job IDs - Upload attachments —
POST /v1/candidates/{id}/attachmentswith base64 content - Add notes —
POST /v1/candidates/{id}/activity_feed/notesfor historical comments - Set custom field values —
PATCH /v1/candidates/{id}withcustom_fieldspayload
Include the On-Behalf-Of header on all write operations. For deleted Workable users whose historical notes need attribution, map them to a generic "System Admin" account in Greenhouse to preserve the audit trail.
def migrate_workable_candidate(workable_id):
src = workable.get_candidate(workable_id)
activities = workable.get_candidate_activities(workable_id)
person_key = normalize_email(src.get('email')) or f'wk-{workable_id}'
gh_candidate_id = greenhouse.upsert_candidate(
person_key, map_candidate_fields(src)
)
gh_application_id = greenhouse.create_or_update_application(
gh_candidate_id,
job_id=job_map[src['job_shortcode']],
payload=map_application_fields(src)
)
greenhouse.add_notes(gh_candidate_id, map_notes(activities))
greenhouse.add_attachments(
gh_candidate_id,
gh_application_id,
download_and_prepare_files(src)
)
reconcile.record(
workable_candidate_id=workable_id,
greenhouse_candidate_id=gh_candidate_id,
greenhouse_application_id=gh_application_id
)Keep three mapping tables: person_key → greenhouse_candidate_id, workable_candidate_id → greenhouse_application_id, and source_attachment_checksum → greenhouse_attachment_ref. That is what makes retries safe and idempotent.
Phase 4: Validate
- Record count comparison — total candidates created in Greenhouse vs. total extracted from Workable
- Field-level sampling — pick 50+ random candidates, compare every field value
- Resume spot check — open 20 candidate profiles in Greenhouse UI, verify resumes are downloadable
- Custom field audit — verify picklist values mapped correctly
- Relationship check — verify multi-application candidates show all applications under one profile
- Test auto-merge behavior — import a known duplicate and confirm Greenhouse handles it per your configuration
Validate in the Greenhouse UI, not just via API responses. What the API returns and what the recruiter sees can differ for custom fields and attachments.
Error Logging
Every API call in your migration script should log failures with enough context for debugging:
error_log = {
"timestamp": "2026-04-21T14:30:00Z",
"workable_candidate_id": "abc123",
"greenhouse_endpoint": "POST /v1/candidates",
"status_code": 422,
"response_body": {"errors": [{"message": "Email is already in use"}]},
"action_taken": "skipped - duplicate"
}Build a retry queue for 429s and 5xx errors. For 422 (validation) errors, log and skip — these need manual review.
If you cannot answer which Workable record created which Greenhouse record, you do not have a supportable migration.
Edge Cases That Break DIY Migrations
- Duplicate records across jobs. The same person applied to 5 jobs in Workable → 5 separate records. If you don't deduplicate before import, Greenhouse's auto-merge may or may not catch them depending on your configuration. If auto-merge is disabled, you'll have 5 separate Candidate profiles.
- Candidates without email addresses. Greenhouse uses email as the primary match key for auto-merge. Sourced candidates in Workable sometimes lack emails. These will create orphan records that can't be merged later.
- Resume URLs that expire. Workable's API returns resume download URLs that are time-limited. If you extract candidate data one day and download resumes a week later, the URLs may have expired. Download files immediately after extraction.
- Custom field data type mismatches. A "number" field in Workable containing the string "$75,000" will fail Greenhouse's number validation. Clean these during the transform phase.
- Scorecard data. Workable evaluations don't map 1:1 to Greenhouse scorecards, which are tied to specific interview stages in a job's interview plan. You'll likely need to import these as notes rather than structured scorecards.
- GDPR consent auto-emails. When importing candidates into Greenhouse jobs configured for GDPR compliance, Greenhouse automatically emails consent requests to imported candidates. Disable this during bulk imports or use the container job method. (support.greenhouse.io)
- Large attachment payloads. Base64-encoding a 10 MB resume inflates it to ~13.3 MB. The Greenhouse API may reject very large payloads. Check content size before upload and consider URL-based attachment upload for large files.
- Orphaned activities. If a user account was deleted in Workable, their historical notes and scorecards might fail to attach in Greenhouse. Map deleted users to a generic "System Admin" account to preserve the audit trail.
- Resume matching failures in bulk import. Greenhouse can fail to attach resumes during bulk import if it cannot parse the file, cannot find an email, or finds an email that doesn't match the imported candidate row. (support.greenhouse.io)
- Private field access. Greenhouse bulk import visibility for private custom fields depends on the importing user's permissions on all jobs in scope. (support.greenhouse.io)
Limitations & What You'll Lose
What Greenhouse Can't Do
- No custom objects. Greenhouse has a fixed schema: Candidates, Applications, Jobs, Offers, Users, Departments, Offices. You can add custom fields to these objects, but you cannot create new object types.
- Scorecards are interview-plan-specific. You can't import a free-form evaluation from Workable as a structured scorecard. It becomes a note.
- Bulk import caps at 8,000 rows per batch and the resume .zip at 5 GB.
- Interview history can't be backdated. You can backdate scorecards, but not the interview event itself. (support.greenhouse.io)
What You'll Probably Lose
- Activity timeline fidelity. Workable's activity feed (emails sent, stage moves, comments) can be partially captured as Greenhouse notes, but the timestamps won't create a native Greenhouse activity timeline.
- Evaluation scores. Unless you rebuild scorecards from scratch in Greenhouse's structure, numerical ratings from Workable have no target field.
- Email thread history. Candidate email conversations in Workable don't have a direct import path into Greenhouse.
- Experience entries. Greenhouse has no structured work history field — these become notes or custom fields.
Greenhouse support docs can conflict on some candidate upload limits and on which subscription tiers expose bulk import features. Test in a sandbox and get written confirmation from Greenhouse before you promise exact boundaries.
Validation, Testing & Rollback
Pre-Go-Live Testing
- Test migration with a sample batch — 50–100 candidates across 3–5 jobs
- Second test at scale — 1,000+ candidates across multiple jobs
- Verify in Greenhouse UI — check that profiles render correctly, resumes open, custom fields display
- Test auto-merge behavior — import a known duplicate and confirm expected handling
- Validate GDPR behavior — confirm consent emails are suppressed during import if intended
- Load test your script — run against the full dataset in a Greenhouse sandbox before production
A practical sampling strategy:
- 10 active candidates in open jobs
- 10 rejected historical candidates
- 10 hired candidates
- 5 records with attachments
- 5 records with custom answers
- Every known duplicate edge case
Rollback Planning
Greenhouse doesn't have a "rollback migration" button. Your rollback plan:
- Before import: Take a full export of your Greenhouse instance (if you have existing data)
- During import: Track every created record ID. If the migration fails, use
DELETEendpoints to remove imported records (requires appropriate API permissions) - Tag everything: Mark all migrated records with a date-stamped tag (e.g.,
workable-migration-2026-04-21). This makes cleanup possible if needed.
Keep Workable accessible until sign-off is complete. Workable notes that closing the account permanently removes access, and recovery may not be possible. (help.workable.com)
Post-Migration Tasks
Once data is loaded into Greenhouse, the technical migration is done — but the operational transition begins.
- Rebuild interview plans and scorecards — these don't migrate. Budget 1–2 weeks for configuration.
- Recreate email templates — Greenhouse email templates are separate from Workable's.
- Configure job board integrations — Greenhouse connects to job boards differently than Workable.
- Train recruiters and hiring managers — users coming from Workable need orientation on scorecards, approvals, and the Candidate vs. Application distinction.
- Update integrations — any tools connected to your Workable API (HRIS sync, background checks) need to be reconnected to Greenhouse.
- Monitor for data inconsistencies — run weekly spot checks for the first month. Look for missing resumes, orphan applications, and custom field gaps.
- Decommission Workable — revoke API keys and keep Workable in read-only state for 30 days before full deletion. Keep the reconciliation ledger and final source export somewhere durable.
When to Use a Managed Migration Service
Build in-house when you have a dedicated engineer for 4–6 weeks, your candidate count is manageable (<5,000), and you're comfortable with the API constraints on both sides.
Don't build in-house when:
- You have >5,000 candidates with resumes — the rate-limit math and error-handling complexity compound fast
- Your timeline is under 4 weeks — there isn't enough time to build, test, fix edge cases, and validate
- You need scorecard or activity history migration — this requires custom transformation logic that's hard to get right on the first pass
- Your engineering team doesn't have ATS API experience — the Workable and Greenhouse APIs have undocumented behaviors and quirks that cost time
Hidden costs of DIY:
- Engineer time at fully-loaded cost ($150–250/hr × 160–240 hours)
- Opportunity cost — your engineer isn't shipping product features
- Risk of silent data loss discovered weeks after go-live
- Second and third migration attempts when edge cases surface
If you're evaluating build vs. buy for this migration, see our broader analysis in In-House vs. Outsourced Data Migration: A Realistic Cost & Risk Analysis.
Best Practices
- Back up everything before migration. Export Workable's full data set. Export your existing Greenhouse data if you have any. Store both in a secure, versioned location. (help.workable.com)
- Run at least two test migrations before the production run. First test: 100 candidates. Second test: 1,000+ candidates across multiple jobs.
- Use OAuth tokens for Workable extraction if available — 5× the rate limit of account tokens.
- Build for Greenhouse Harvest v3 now. v1/v2 are deprecated August 2026. Don't build on a dead API.
- Tag all migrated records with a consistent label for easy identification and potential rollback.
- Disable GDPR consent auto-emails during bulk import to avoid blasting thousands of candidates.
- Download resume files synchronously with extraction — don't defer. URLs expire.
- Validate in the Greenhouse UI, not just via API responses. What the API returns and what the recruiter sees can differ.
- Document every compromise. If a Workable field becomes a Greenhouse note or custom field, make that explicit before UAT.
- Preserve immutable source keys. Keep
workable_candidate_idas a custom field in Greenhouse so you can trace records back to the source system.
What to Do Next
If you're planning a Workable-to-Greenhouse migration, start with the data audit. Run GET /spi/v3/jobs to get your full job list, then sample 5–10 jobs with GET /spi/v3/jobs/{shortcode}/candidates to understand your data shape. Count total candidates, check resume coverage, and inventory custom fields.
That 30-minute exercise will tell you whether this is a weekend CSV project or a multi-week engineering effort — and whether it makes sense to bring in help.
ClonePartner has handled hundreds of ATS and CRM migrations, including complex candidate data transfers between systems with mismatched data models. Our approach to Workable-to-Greenhouse specifically:
- Built-in rate-limit management with backoff and retry logic for both Workable's 10/10s limit and Greenhouse's Harvest API constraints
- Resume and attachment extraction via API, bypassing the need for full account data exports or manual .zip assembly
- Candidate deduplication and relational mapping — transforming Workable's flat per-job records into Greenhouse's Candidate/Application structure automatically
- Custom field mapping with type validation, including support for Greenhouse's 3-level dependent field hierarchies
- Complete validation and error reporting before go-live, including field-level sampling and record count reconciliation
Frequently Asked Questions
- Can I export resumes from Workable in bulk?
- No. Workable's Candidate Details CSV excludes resumes and files. You can either request a full account data export from Workable support (which requires archiving all jobs and is all-or-nothing) or use the Workable API to download resumes individually from URLs in candidate detail responses. ([help.workable.com](https://help.workable.com/hc/en-us/articles/115014887828-How-do-I-export-candidate-data))
- What are the Workable API rate limits for data extraction?
- Workable limits account tokens to 10 requests per 10 seconds and OAuth/Partner tokens to 50 requests per 10 seconds. Exceeding these returns HTTP 429. Monitor the X-Rate-Limit-Remaining header and pause before hitting zero. ([help.workable.com](https://help.workable.com/hc/en-us/articles/4903195036183-Troubleshooting-API-issues))
- How does Greenhouse's data model differ from Workable's?
- Workable associates candidates directly with jobs as flat records. Greenhouse separates Candidates (the person) from Applications (the candidacy for a specific job). One Candidate can have multiple Applications. You must deduplicate Workable records by email and create one Candidate per person with separate Applications per job. ([support.greenhouse.io](https://support.greenhouse.io/hc/en-us/articles/360001905032-Business-Intelligence-Connector-data-model))
- Is Greenhouse Harvest API v1 being deprecated?
- Yes. Harvest API v1 and v2 will be deprecated and unavailable after August 31, 2026. All new integrations should use Harvest v3, which requires OAuth 2.0 authentication instead of Basic Auth and uses a 30-second fixed-window rate limit.
- How many candidates can I bulk import into Greenhouse at once?
- Greenhouse recommends a maximum of 8,000 candidates per bulk import batch. The resume .zip file has a 5 GB size limit. The bulk import tool is only available on Plus and Pro subscription tiers. For larger datasets, split into multiple imports or use the Harvest API. ([support.greenhouse.io](https://support.greenhouse.io/hc/en-us/articles/38329467264027-Adding-historical-candidates-to-Greenhouse-using-bulk-import))