How to build a AI-powered Candidate Sourcing Application with Unified.to
May 26, 2025
Sourcing candidates from multiple job boards and using multiple ATS platforms is no easy feat. So let's build a candidate sourcing application that can automate all that and support dozens of ATS integrations with one simple API, using Unified.to.
Watch the video on our Youtube channel.
With this application we will be:
- Fetching jobs from an ATS (we'll use Greenhouse as our example)
- Filter for Active jobs
- Match relevant candidates from the database to those jobs using Unified's GenAI integration.
Prerequisites for the application
- Node JS (v 19 or higher)
- Unified.to account with a Greenhouse connection and the Unified.to API token for your workspace
- A database of candidates (with attributes and resumes)
Step 1: Setting up your project
Let's start the project by setting things up and installing your dependencies.
mkdir candidate-sourcing
cd candidate-sourcing
npm init -y
npm install express dotenv @unified-api/typescript-sdk
Next, add the following credentials to your .env
file.
UNIFIED_API_KEY=your_unified_api_key
CONNECTION_GREENHOUSE=your_greenhouse_connection_id
CONNECTION_GENAI=your_genai_connection_id
PORT=3000
Initialize your SDK and express.
import 'dotenv/config';
import express from 'express';
import { UnifiedTo } from '@unified-api/typescript-sdk';
const app = express();
app.use(express.json());
const { UNIFIED_API_KEY, CONNECTION_GREENHOUSE, CONNECTION_GENAI, PORT } = process.env;
const sdk = new UnifiedTo({
security: { jwt: UNIFIED_API_KEY! },
});
Step 2: Fetching and filtering active jobs from the ATS
To match candidates to jobs, you first need to know which jobs are open, we will now fetch all the jobs on the ATS and filter them by the active ones.
async function getActiveJobs(connectionId: string) {
const jobsResponse = await sdk.ats.listAtsJobs({ connectionId });
// Filter for jobs that are open/active
return (jobsResponse.jobs || []).filter(
job => job.status === 'OPEN'
);
}
Step 3: Loading Candidates from your Database
Now it's time to load all the candidates you have for the jobs. For the example, each candidate in your DB has to have a name, email and a link to a resume.
// Example candidate models
type Candidate = {
id: string;
name: string;
email: string;
resume: string; // Plain text URL to PDF would work!
attributes: Record<string, any>;
};
// This is an example, you should replace it with the query to load all candidates in your DB.
async function getAllCandidates(): Promise<Candidate[]> {
// This too should be replaced with your actual DB logic.
return [
{
id: '1',
name: 'Jane Doe',
email: 'jane@example.com',
resume: 'Jane has 5 years of experience in backend development...',
attributes: { skills: ['Node.js', 'TypeScript', 'AWS'] }
},
// ...more candidates
];
}
Step 4: Matching the Candidates to the Jobs using GenAI
Now that we've used the application to fetch both the candidates and the jobs, we will be using Unified's GenAI integrations to score and rank candidates for the jobs.
We will be using the AI integration in this step using a very simple prompt. For the example, the prompt we are using is: ```
Job Description:
${job.description}
Candidate Resume:
${candidate.resume}
Based on the job description and candidate resume, provide a match score (0-100) and a brief explanation.
Format:
Score: <number>
Explanation: <one sentence>
Function to do the grading:
async function scoreCandidateForJob(candidate: Candidate, job: any, genaiConnectionId: string) {
const prompt = `
Job Description:
${job.description} Candidate Resume:
${candidate.resume} Based on the job description and candidate resume, provide a match score (0-100) and a brief explanation.
Format:
Score: <number>
Explanation: <one sentence>
`;
const result = await sdk.genai.createGenaiPrompt({
connectionId: genaiConnectionId,
prompt: {
messages: [{ role: "USER", content: prompt }],
maxTokens: 100,
temperature: 0.2
}
});
const content = result.choices?.[0]?.message?.content || '';
const scoreMatch = content.match(/Score:\s*(\d+)/i);
const explanationMatch = content.match(/Explanation:\s*(.+)/i);
return {
score: scoreMatch ? parseInt(scoreMatch[1], 10) : null,
explanation: explanationMatch ? explanationMatch[1].trim() : "No explanation provided."
};
}
Step 5: Filtering out the top candidates for each Job
Now let's identify the candidates chosen in the previous step.
async function matchCandidatesToJobs() {
const jobs = await getActiveJobs(CONNECTION_GREENHOUSE!);
const candidates = await getAllCandidates();
for (const job of jobs) {
const scoredCandidates = await Promise.all(
candidates.map(async candidate => {
const aiResult = await scoreCandidateForJob(candidate, job, CONNECTION_GENAI!);
return { ...candidate, score: aiResult.score, explanation: aiResult.explanation };
})
);
// Filter and sort candidates by score
const topCandidates = scoredCandidates
.filter(c => c.score !== null && c.score >= 75)
.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
console.log(`Top candidates for job "${job.name}":`);
for (const c of topCandidates) {
console.log(`- ${c.name} (${c.email}): ${c.score} - ${c.explanation}`);
// 1. Create candidate in ATS
const candidateId = await createCandidate(CONNECTION_GREENHOUSE!, c);
console.log(` Candidate created in ATS with ID: ${candidateId}`);
// 2. Create application in ATS
const appId = await createApplication(CONNECTION_GREENHOUSE!, candidateId, job.id);
console.log(` Application created in ATS with ID: ${appId}`);
}
}
}
Step 6: Creating Applications and Candidates in the ATS
Now that we have identified the top candidates for each job, we will move to create them as candidates in the ATS and then create applications for them.
// Create a candidate in the ATS
import { AtsCandidate } from '@unified-api/typescript-sdk/dist/sdk/models/shared';
async function createCandidate(connectionId: string, candidate: Candidate) {
const atsCandidate: AtsCandidate = {
name: candidate.name,
emails: [{ value: candidate.email }],
// Add other fields as needed, e.g., phones, location, etc.
};
const result = await sdk.ats.createAtsCandidate({
atsCandidate,
connectionId,
});
return result.atsCandidate?.id;
}
// Create an application in the ATS
async function createApplication(connectionId: string, candidateId: string, jobId: string) {
const result = await sdk.ats.createAtsApplication({
atsApplication: {
candidateId,
jobId,
},
connectionId,
});
return result.atsApplication?.id;
}
And just like that, you have used Unified.to to create a powerful candidate sourcing application, which leverages the power of AI 🎉