- Taking each Job Match record (a custom object that stores a numeric
score
for a Contact–Job pairing), - Ranking all Job-Match records belonging to the same Job ID, and
- Flagging the top 20 % of matches (at least two) with
is_top_applicant = true
.
While the example focuses on recruiting, this ranking technique applies anywhere you need to score and surface the best records—think partner programs, grant applications, lead prioritization, or warranty claim triage.
When to Use This Pattern
Scenario | Why It Helps |
---|---|
Recruiting platforms | Auto-highlight the strongest applicants for each open job so recruiters spend time on the best fits first. |
Channel / partner programs | Score partner applications and flag the top tier for expedited onboarding. |
Grants & scholarships | Rank submissions and mark those above the funding threshold. |
Lead scoring & routing | Identify the top-X % leads for each campaign and trigger fast-track workflows. |
Step-by-Step Implementation
1. Set Up the Custom Objects & Properties
Object | Key Properties | Purpose |
---|---|---|
Job (custom object) | job_id , employer_id , job details |
The open role the employer wants to fill. |
Job Match (custom object) | job_id (lookup to Job)contact_id (lookup to Contact)score (number)is_top_applicant (boolean) |
Holds the match score for a Contact ↔ Job pair and the top applicant flag. |
2. Generate a Private App Token
- Settings → Integrations → Private Apps → Create app
- Name it “Job-Match Ranking”.
- Enable scopes:
•crm.objects.custom.read
&crm.objects.custom.write
- Copy the token and paste it into the custom code action’s Secrets as
JOB_MATCH_RANKER
.
3. Drop the Custom Code Action Into Your Workflow
Create a Job-Match–based workflow triggered whenever a new Job Match is created or its score
changes. Add a Custom Code action and paste the code below. Replace the hard-coded object type ID and token with your own.
/**
* Tag the top-20 % (min-2) Job-Match records for a Job.
* Runtime: Node.js 20
*/
const hubspot = require('@hubspot/api-client');
/* — constants — */
const PRIVATE_APP_TOKEN = process.env.JOB_MATCH_RANKER; // from Secrets
const JOB_MATCH_OBJECT_TYPE_ID = '2-45931025'; // ✔️ update for your portal
const TOP_PERCENT = 0.20; // 20 %
const MIN_COUNT = 2; // at least two flagged
exports.main = async (event, callback) => {
const api = new hubspot.Client({ accessToken: PRIVATE_APP_TOKEN });
/* 1. current Job-Match ID */
const jobMatchId =
event.inputFields['hs_object_id'] ||
(event.object && event.object.objectId);
if (!jobMatchId) {
console.log('❌ No hs_object_id supplied; skipping run.');
return callback({ outputFields: { flaggedCount: 0 } });
}
/* 2. look up its job_id */
const thisMatch = await api.crm.objects.basicApi.getById(
JOB_MATCH_OBJECT_TYPE_ID,
jobMatchId,
['job_id']
);
const jobId = thisMatch.properties.job_id;
if (!jobId) {
console.log(`❌ Job-Match ${jobMatchId} missing job_id; aborting.`);
return callback({ outputFields: { flaggedCount: 0 } });
}
/* 3. gather ALL matches for that job */
const searchBody = {
filterGroups: [{
filters: [{ propertyName: 'job_id', operator: 'EQ', value: jobId }]
}],
properties: ['score', 'is_top_applicant']
};
const { results } = await api.crm.objects.searchApi.doSearch(
JOB_MATCH_OBJECT_TYPE_ID,
searchBody
);
if (!results.length) {
console.log(`ℹ️ No matches found for job ${jobId}.`);
return callback({ outputFields: { flaggedCount: 0 } });
}
/* 4. rank & choose the cut-off */
results.sort((a, b) => Number(b.properties.score) - Number(a.properties.score));
const cutoff = Math.max(MIN_COUNT, Math.ceil(results.length * TOP_PERCENT));
/* 5. batch-update the top records */
const updates = results.map((rec, idx) => ({
id: rec.id,
properties: { is_top_applicant: idx < cutoff }
}));
while (updates.length) {
const batch = updates.splice(0, 100);
await api.crm.objects.batchApi.update(JOB_MATCH_OBJECT_TYPE_ID, { inputs: batch });
}
callback({ outputFields: { flaggedCount: cutoff } });
};
4. Trigger Next-Step Automation
- “Top applicant?” →
is_top_applicant
equals Yes → Notify recruiters, create tasks, or move the candidate to an Interview pipeline. - Else → keep the match in the regular review queue.
5. Test the Workflow
- Create three Job-Match records for the same Job with varying
score
values. - Manually enroll them (or trigger via score updates).
- Verify that the top 20 % (at least two) have
is_top_applicant = true
, and the rest are false. - Check the workflow history to confirm the code action processed and the branch logic fired.
Results & Benefits
- Objective short-listing — your best candidates are surfaced automatically.
- Recruiter efficiency — teams focus on interviewing instead of spreadsheet jockeying.
- Flexible logic — tweak
TOP_PERCENT
orMIN_COUNT
for any use case. - Cross-industry ready — swap “Job” and “Job-Match” for any parent-child scoring model.
Wrapping Up
HubSpot alone can store scores, but turning those scores into actionable tiers requires custom logic. With a concise Node.js custom code action, you can rank child records, flag the winners, and let workflows take it from there. Whether you’re matching people to jobs, partners to programs, or leads to campaigns, this pattern keeps your team laser-focused on the top opportunities.
Have questions or want to explore other ranking recipes? Let’s elevate your Ops Hub game!