ATS turns a HubSpot portal into a careers site and a hiring pipeline. Candidates apply on the customer's own domain; the application lands as a CRM record; recruiters work it from cards on the records they already use. There is no second login, no separate candidate database, and no export step between recruiting and the rest of the business.

The design decision underneath all of it: a candidate is a Contact. Not a row in a recruiting product that syncs to a Contact — the Contact itself, with recruiting properties on it. So a candidate who later becomes a customer is one record with one history, and every HubSpot list, workflow, report and marketing email works on hiring the same way it works on sales.

KindPublic HubSpot app (OAuth), installed per portal
Built forTeams whose hiring has outgrown a mailbox but does not justify an enterprise ATS
Stores data inCustom CRM objects and Contact properties on your own portal. No external database holds candidate data
Candidate surfacesA careers listing, a job page per posting, an application form, and a passwordless status portal
Recruiter surfaces5 CRM cards, a full-screen recruiting app page inside HubSpot, and 4 workflow actions
Matching10 weighted dimensions, 39 published settings, a dry run that changes nothing — ats-matching.md
How to get itNot distributed. Ask for a walkthrough: w@reus.ie

Important

Maturity, stated plainly. ATS is feature-complete against its build plan and runs on our demo portals, with the matching engine covered by a unit suite that runs on every change. It has not been deployed against a real customer's live hiring. Two consequences are worth knowing before planning around it: resume parsing does not exist — a CV uploads and attaches, but nothing reads it, so matching only sees data somebody entered as properties — and the public application routes authenticate the portal, not the caller, which is appropriate for a careers page and is stated exactly in ats-integration.md. Ask for the hardening pass as part of any real deployment.

What it actually does#

A careers site on the customer's own domain#

A listing page with search and department, location, employment-type and remote filters; a detail page per posting on a permanent slug; an application form that upserts the Contact and creates the application. The embed inherits the site's existing theme, so there is no separate design exercise. Each job page is individually indexable and carries Google Jobs–compatible structured data.

Application intake that produces a record, not an email#

Name, email, phone, cover letter and an optional CV upload. Duplicate applications to the same posting are refused rather than silently doubled. What lands is an ats_application associated to a Contact and a posting — a record a workflow can act on the moment it exists.

Candidate self-service without accounts#

A candidate asks for a status link by email and receives a one-time link. No password, no account creation. The portal deliberately shows a simplified status — never the internal pipeline stage name, never a rejection reason — because the same record is the recruiter's working surface and those two audiences must not see the same field.

A recruiter workspace inside the CRM#

5 cards on the records that already exist: a posting command centre with the funnel and candidate ranking, an application workspace with the match breakdown and interviews, a candidate 360 view on every Contact, an interview panel, and a document inspector. Plus a full-screen app page — dashboard, pipeline board, postings, create-posting and reports — for the work that is not about one record.

Candidate scoring that shows its working#

The part most worth understanding, and the reason this app has a page of its own for it. A candidate is scored against a posting across 10 dimensions, each producing a 0–100 sub-score that is combined by weight. Every rule is a stored setting a recruiter can change from the settings page, and every score returns the per-dimension breakdown that produced it — which skills matched, which were missing, what the seniority gap was.

Two properties make it usable rather than merely present. A dry run scores real candidates and saves nothing, so the effect of a weight change is visible before it is committed. And a dimension the posting says nothing about drops out of the denominator rather than scoring zero — a posting with no language requirement is not a posting every candidate fails on languages.

Full treatment, for recruiters and developers alike: ats-matching.md.

Automation from HubSpot's own workflow builder#

4 workflow actions — set application stage, recalculate the match score, close a posting, create an interview — usable in any workflow the customer builds, with no extra tooling. This is where the CRM-native model pays: "when an application scores above 80, notify the hiring manager" is a workflow, not a feature request.

What it does not do#

Stated up front, because these are the ones that reshape a plan if you find them late.

  • It does not read CVs. Upload, storage, metadata and a re-parse hook all exist; the extraction step behind them does not. Matching sees only structured properties, so a portal that captures nothing but a CV file has nothing to score. This is the single biggest gap and the one to raise first.
  • It does not match on meaning. Skill comparison is literal, widened by a configurable synonym map (so Node matches Node.js). The semantic option appears in the settings and currently behaves as synonym matching — there are no embeddings behind it. See ats-matching.md § Skill matching.
  • It does not push to job boards. There is a pull feed — one XML URL a board polls on its own schedule, alongside the structured data that makes each page indexable by Google Jobs (ats-integration.md § The job-board feed). What does not exist is an authenticated push integration that posts into Indeed's or LinkedIn's own systems and reports back a posting id.
  • It does not author scorecards or offer letters. The objects exist at the higher tiers; the authoring experience is record-level property editing.
  • It is not a compliance product. Compliance objects exist at full tier as data. Legal review of a regulated hiring process is not something a data model performs.
  • It has no candidate accounts. The status portal is a link, by design. If a customer specifically wants candidates to log in, this is not that.

What people see#

WhoSurfaceWhat it is
CandidateCareers listingSearch and filters over live postings, on the customer's domain and in their theme. Filtered views are shareable links
CandidateJob pageOne posting on a permanent slug, individually indexable, with Google Jobs structured data and related roles
CandidateApplication formContact upsert plus an application record; CV optional; duplicate applications refused
CandidateStatus portalA passwordless emailed link showing a simplified status, and the ability to withdraw
CandidateJob alertsOpt-in digest of new postings matching a stated preference
Recruiter5 CRM cardsPosting command centre, application workspace, candidate 360, interview panel, document inspector
RecruiterRecruiting app pageFull-screen inside HubSpot: dashboard, pipeline board, postings list, create posting, reports
OperatorSettings pageStatus and install tier, the 39 matching settings, the dry-run calibrator, sample data
Automation4 workflow actionsSet stage · recalculate match · close posting · create interview

Data model#

Everything is a CRM record, and the schema installs in tiers because most HubSpot portals cap custom objects at around 10 — a 19-object recruiting model does not fit on a standard portal, so it is not the default.

TierObjectsWhat it is for
minimal2Only what the public job board needs at runtime. This is what the OAuth callback provisions
lean (default)4ats_job_posting, ats_application, ats_interview, ats_candidate_document — a working ATS
standard6Adds a candidate profile and an offer object
full19The enterprise model: requisitions, compliance, scorecards, skill junctions. Requires a raised custom-object allowance and is installed deliberately, never from the OAuth callback
ObjectWhat it holds
ats_job_postingA role: its public content, its slug, its status, and the matching criteria a candidate is scored against
ats_applicationOne person applying to one posting: pipeline stage, timestamps, and the persisted match score with its full breakdown
ats_interviewA scheduled interview and its outcome
ats_candidate_documentAn uploaded CV or attachment, its metadata, and a parse-status field awaiting a parser
contact (extended)Recruiting properties on the standard Contact — seniority, skills, languages, education, experience, salary expectation, remote preference, open-to-work
app_settingKey-value configuration, using our shared app-settings pattern; the matching settings are rows here

Three things about this model are deliberate and worth copying whatever you build on:

  • The candidate is the Contact. Recruiting properties live in their own property group on the standard object. No parallel person record, no sync, no reconciliation.
  • The tier is a decision, not a default. Provisioning 19 objects on a portal that allows 10 fails at install time, on the customer's portal, in a way that is tedious to unwind. Installing the smallest thing that works and upgrading additively is the safe direction, and the upgrade never deletes.
  • The match score is stored with its explanation. match_score is a number; match_details_json alongside it holds the per-dimension breakdown and a snapshot of the settings that produced it. A score you cannot explain six months later is a score nobody trusts.

Associations use labels rather than raw type ids, and object type ids are resolved from the schema list at runtime — they differ per portal, so hardcoding one is a bug that only appears on the second install.

Integration seams#

WithWhat crosses
The customer's websiteThe careers embed is a Vite bundle on the customer's own pages, calling the app's public routes. The portal is authenticated and licence-checked; the caller is not — see ats-integration.md
HubSpot workflows4 custom actions, and the ordinary CRM surface. Everything ATS writes is a property or a record, so anything HubSpot can automate against a record it can automate against a hire
Marketing email and listsCandidates are Contacts with a property group. Talent-pool nurture, alert digests and re-engagement are ordinary HubSpot marketing, not an ATS feature

There is no outbound webhook to arbitrary URLs and no third-party job-board push. A system that wants ATS data reads it from the portal.

Where to go next#

You areRead
Deciding how candidates should be ranked, or tuning itats-matching.md — every dimension's arithmetic, all 39 settings, and how to change them safely
Calling it over HTTP, or building against the embedats-integration.md — the auth model, the generated endpoint index, the match API and the persisted score shape
Installing or operating it on a portalats-setup.md — tiers, what install provisions, the properties matching needs, verification and the schema migration
Evaluating whether it fitsThis page, then ask for a walkthrough