> Source: https://builder.ema.ai/v2/recruiter/candidate-search
> Title: Candidate Search

# Candidate Search

Candidate search is how a Recruiter search finds people. You describe the role in plain language or upload a job description; Recruiter uses an LLM to turn that into structured filters, then searches Ema's large external index of professional profiles and pulls matching candidates into the search. This page explains both the discovery flow and the structured filters that drive it.

> [INFO]
> **Beta.** Recruiter is in beta. Search behavior, filter dimensions, and the underlying index may change.

## Concepts

Two ideas underpin search:

-   **The index.** Recruiter searches a large external index of professional profiles (backed by People Data Labs) — these are the **public profiles**. When an ATS is connected, the search can also include **internal talent** from your pipeline.
-   **Filters.** Every search runs against a structured filter object. Recruiter generates it for you from your query or JD via LLM extraction, and you can edit it before searching. Filters are organized by dimension (job title, location, company, skills, education, and more), and each value carries one of three intents: **must**, **preferred**, or **exclude**.

A search starts from a natural-language query or a JD — you can't skip describing the role.

## Describing who you're looking for

You give Recruiter the role in one of two ways:

-   **Natural language.** Type a description of the ideal candidate — for example, "Software engineers in the United States with React experience." Recruiter sends it to the LLM, which extracts structured filters.
-   **Job description.** Upload or paste a JD. Recruiter extracts text from the file (PDF and DOCX are parsed in-process; plain text is read directly) and uses it to derive filters and to seed the [scorecard](/builder/v2/recruiter/scoring-criteria). Very long JDs are truncated before being sent to the LLM (the scorecard generator uses roughly the first 4,000 characters of JD text).

> [TIP]
> If a JD upload produces filters that look off, paste the JD as text into the natural-language box instead. Scanned PDFs with no extractable text layer and heavily formatted files produce noisier parses. An image-only PDF yields no text at all — Recruiter treats it as if no JD text was provided.

### How extraction works

When you provide a query (or a JD) without explicit filters, Recruiter runs LLM-based filter extraction. The extraction fans out into several parallel LLM calls and assembles the structured filter object from the results. If the primary model fails, Recruiter retries with a fallback model; if extraction still fails, it falls back to a plain keyword match so a search always returns something rather than erroring. The extracted filters are shown to you so you can review and edit them before the search runs, and they're saved to the search so they survive a page refresh.

## Filters

Filters constrain the candidate pool. Each filter value carries one of three intents:

Intent

Effect

**Must**

Hard filter — every returned candidate matches this. Stacking many musts narrows results sharply.

**Exclude**

Hard filter — every returned candidate does _not_ match this.

**Preferred**

Soft signal — does not filter the pool, but boosts a matching candidate. Use it for nice-to-haves that shouldn't disqualify anyone.

Under the hood, must / preferred / exclude map to the search engine's must / should / must-not clauses. Numeric dimensions (years of experience, graduation year, revenue, funding) use a min/max range instead of must/preferred/exclude.

> [WARNING]
> **Over-restrictive musts are the most common cause of a tiny result set.** Every must narrows the pool, and combining several (a specific city _and_ a specific company _and_ a narrow seniority band) can shrink it to almost nothing. Move non-essential constraints to **preferred** — they still influence ranking without filtering candidates out.

### Filter dimensions

Recruiter supports a broad set of filter dimensions, grouped by what they constrain:

Group

Dimensions

What it constrains

Role

Job titles, job functions, seniority levels, skills

The candidate's current role and capabilities.

Experience

Total years of experience, years in current company

How long they've worked, in total and in their current company.

Location

Countries, states/regions, cities, continents

Where the candidate is based.

Company

Companies, current companies, past companies, company types, employee counts, target industries, company locations

The companies the candidate has worked at.

Company financials

Revenue range, funds raised, company ranking, last funding round type

The size and stage of those companies.

Industry

Target industries, industry keywords

The industry of the candidate's employers.

Education

Schools, degrees, field of study, graduation year range, school locations

The candidate's educational background.

Languages

Languages

Languages the candidate speaks.

Edit any dimension before searching — add or remove values and change their intent. The exact set of dimensions exposed in the UI may evolve as Recruiter matures.

## Running a search and fetching candidates

When you launch a search, Recruiter fetches candidates from the index into the search in the background. Fetching is paginated and runs as a background job — you don't wait on the page for it to finish. The list fills in as candidates arrive, and [scoring](/builder/v2/recruiter/scoring-criteria) runs against them as they come in.

A few limits shape how much gets pulled:

Limit

Default

What it controls

Fetch target

500

Candidates fetched per search run before the job pauses.

Max candidates

5,000

Hard ceiling on total candidates a single search can hold.

These are per-tenant defaults and can be tuned. If you want more candidates after the initial fetch, you can ask the search to **evaluate more** — it resumes from where it left off and appends the next batch, up to the max-candidates ceiling. Re-running a search after editing the query or filters cancels any in-flight fetch, clears non-shortlisted candidates, and starts fresh; shortlisted candidates are always preserved across re-runs.

## Public profiles and internal talent

A launched search lists candidates under two sources:

-   **Public profiles** — candidates from Ema's external index. This is the default source for every search.
-   **Internal talent** — candidates pulled in from a connected ATS. They're scored against the same scorecard as public profiles and deduplicated against the external index, so the same person doesn't appear twice.

> [INFO]
> **Beta.** ATS connections are in beta and currently support reading candidates _into_ Recruiter only — pushing candidates back to the ATS is not yet supported, and the set of supported ATS providers is shown during connection. Internal talent appears only when an ATS is connected from the Configuration tab.

## Search API (full access)

Beyond the in-app flow, full-access tenants can initiate a candidate search programmatically. The service exposes a small set of endpoints (mounted under the recruiter API):

Method & path

Purpose

`POST /v1/candidate-search`

Start a search from a job description. Returns a search ID and URLs for status and results.

`GET /v1/candidate-search/{id}/status`

Poll search status and stage.

`GET /v1/candidate-search/{id}/results`

Fetch scored results once the search completes.

The initiate call takes a `job_description` (between 50 and 20,000 characters) and a `persona_id` (the Recruiter AI Employee to attach the search to), and responds with `202 Accepted` plus a payload containing `search_id`, `project_id`, a `ui_url` to open the search in the app, and `status_url` / `results_url` for polling. The API is rate-limited per tenant; the defaults allow up to **5** concurrent searches and **20** searches per rolling hour, and a `429` response carries a `Retry-After` header.

```http
POST /v1/candidate-search
Content-Type: application/json

{
  "job_description": "Senior backend engineer with deep experience operating distributed systems in production...",
  "persona_id": "00000000-0000-0000-0000-000000000000"
}
```

## What's next

-   [Scoring Criteria](/builder/v2/recruiter/scoring-criteria) — how the scorecard is generated and how fetched candidates are scored.
-   [Outreach](/builder/v2/recruiter/outreach) — reach out to the candidates you shortlist.
