Skip to content

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.

Raaj Raaj · · 23 min read
The Ultimate Workable to Greenhouse Migration Guide (2026)
TALK TO AN ENGINEER

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.

Warning

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, and job_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.

Warning

Do not map Workable's stage directly to a Greenhouse Candidate. Stages belong to the Application object in Greenhouse.

Info

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 requests
  • X-Rate-Limit-Remaining — remaining requests in current window
  • X-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 candidates
Tip

If 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:

  1. Export Candidate Details CSV from Workable, or request a full account export for resumes
  2. Create target jobs, stages, custom fields, sources, and rejection reasons in Greenhouse
  3. Reformat the CSV to match Greenhouse's bulk import template
  4. Upload via Greenhouse's Configure → Bulk Import tool
  5. 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:

  1. Extract all candidates, jobs, activities, and resume URLs from Workable API
  2. Download resume files from URLs
  3. Transform data: deduplicate candidates, map fields, split into Candidate + Application records
  4. Create jobs in Greenhouse (manually or via API)
  5. POST candidates to Greenhouse Harvest API
  6. POST applications under each candidate, linking to jobs
  7. 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-Limit header). 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-Of header — 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_id

Method 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.

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.

Warning

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:

  1. Create the dependency hierarchy in Greenhouse first
  2. Map Workable flat field values to the correct parent/child option pairs
  3. 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
Info

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

  1. List all jobs via GET /spi/v3/jobs (paginate with limit=100)
  2. For each job, fetch candidates via GET /spi/v3/jobs/{shortcode}/candidates
  3. For each candidate, fetch detail via GET /spi/v3/candidates/{id} — the collection endpoint omits verbose fields like cover_letter, answers, resume_url, tags, and social profiles (workable.readme.io)
  4. 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.
  5. Fetch custom attributes via GET /spi/v3/jobs/{shortcode}/custom_attributes
  6. Pull activities via /candidates/{id}/activities for 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

  1. Deduplicate candidates by email — group all Workable records for the same email into one Candidate record
  2. Map fields — rename and reformat per the mapping tables above
  3. Resolve source IDs — match Workable source strings to Greenhouse source IDs
  4. Resolve stage mappings — match Workable stages to Greenhouse interview plan stages (or default to initial stage). Use an explicit dictionary mapping, not label similarity.
  5. Base64-encode resume files for API upload
  6. Validate custom field values — ensure picklist values exist in Greenhouse, dates are ISO 8601, required fields are populated
  7. Handle missing required fields — Greenhouse will reject API calls if required fields like last_name are null. Inject placeholder data (e.g., "Unknown") during transformation.

Phase 3: Load

  1. Create jobs in Greenhouse (manually or via API) with correct departments, offices, and interview plans
  2. POST candidatesPOST /v1/candidates with applications array linking to Greenhouse job IDs
  3. Upload attachmentsPOST /v1/candidates/{id}/attachments with base64 content
  4. Add notesPOST /v1/candidates/{id}/activity_feed/notes for historical comments
  5. Set custom field valuesPATCH /v1/candidates/{id} with custom_fields payload

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
    )
Tip

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

  1. Record count comparison — total candidates created in Greenhouse vs. total extracted from Workable
  2. Field-level sampling — pick 50+ random candidates, compare every field value
  3. Resume spot check — open 20 candidate profiles in Greenhouse UI, verify resumes are downloadable
  4. Custom field audit — verify picklist values mapped correctly
  5. Relationship check — verify multi-application candidates show all applications under one profile
  6. 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.
Info

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

  1. Test migration with a sample batch — 50–100 candidates across 3–5 jobs
  2. Second test at scale — 1,000+ candidates across multiple jobs
  3. Verify in Greenhouse UI — check that profiles render correctly, resumes open, custom fields display
  4. Test auto-merge behavior — import a known duplicate and confirm expected handling
  5. Validate GDPR behavior — confirm consent emails are suppressed during import if intended
  6. 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 DELETE endpoints 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

  1. 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)
  2. Run at least two test migrations before the production run. First test: 100 candidates. Second test: 1,000+ candidates across multiple jobs.
  3. Use OAuth tokens for Workable extraction if available — 5× the rate limit of account tokens.
  4. Build for Greenhouse Harvest v3 now. v1/v2 are deprecated August 2026. Don't build on a dead API.
  5. Tag all migrated records with a consistent label for easy identification and potential rollback.
  6. Disable GDPR consent auto-emails during bulk import to avoid blasting thousands of candidates.
  7. Download resume files synchronously with extraction — don't defer. URLs expire.
  8. Validate in the Greenhouse UI, not just via API responses. What the API returns and what the recruiter sees can differ.
  9. Document every compromise. If a Workable field becomes a Greenhouse note or custom field, make that explicit before UAT.
  10. Preserve immutable source keys. Keep workable_candidate_id as 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))

More from our Blog

5
ATS

5 "Gotchas" in ATS Migration: Tackling Custom Fields, Integrations, and Compliance

Don't get derailed by hidden surprises. This guide uncovers the 5 critical "gotchas" that derail most projects, from mapping tricky custom fields and preventing broken integrations to navigating complex data compliance rules. Learn how to tackle these common challenges before they start and ensure your migration is a seamless success, not a costly failure.

Raaj Raaj · · 14 min read
Ensuring GDPR & CCPA Compliance When Migrating Candidate Data
ATS

Ensuring GDPR & CCPA Compliance When Migrating Candidate Data

This is your essential guide to ensuring full compliance with GDPR and CCPA. We provide a 7-step, compliance-first plan to manage your ATS data migration securely. Learn to handle lawful basis, data retention policies, DSARs, and secure transfers to avoid massive fines and protect sensitive candidate privacy.

Raaj Raaj · · 10 min read
In-House vs. Outsourced Data Migration: A Realistic Cost & Risk Analysis
General

In-House vs. Outsourced Data Migration: A Realistic Cost & Risk Analysis

Choosing between in-house and outsourced data migration? The sticker price is deceptive. An internal team might seem free, but hidden risks like data loss, project delays, and engineer burnout can create massive opportunity costs. This realistic analysis compares the true ROI, security implications, and hidden factors of both approaches, giving you a clear framework to make the right decision for your project.

Raaj Raaj · · 8 min read