Process guide
Pre-onboarding candidates (HR)
Candidate pipeline before hiring: screening, interviews, offer, conversion to tenant user.
Pre-onboarding candidates (HR)
Last updated: 2026-05-25 Owner: Product Writer Audience: hr_admin, tenant admin, owner
Purpose
How HR can manage the candidate pipeline before hiring: sourcing, screening, interviews, offer, up to optional conversion into a tenant user. Candidates are dedicated records, separate from auth.users (a candidate is not yet an organization member).
Key concepts
- Candidate vs employee: a candidate is a *prospect* — a work profile you are evaluating. They have no login, see no tenant data, do not occupy a user seat. They become an employee only when converted (status
hired+ link to a tenant user). - HR only scope: the entire
Pre-onboarding candidatessection is restricted toowner,tenant admin,hr_admin.Managerroles get 403 with reasoncandidates_require_hr_admin. - Linear workflow:
New → Screening → Interview → Offered → Hired, with exitsRejected,Withdrawn,Archived. - Dedicated CVs: candidate CVs live in a dedicated bucket (
hr-candidate-cvs), separate from the employee CV bucket (hr-cvs). Uploading a new version demotes the previouscurrentone. - Archive vs Delete: two distinct actions.
Archiveis a workflow state (Archived, reactivatable by changing status).Deletemoves into the trash (deleted_at, recoverable within 30 days from the trash view). - Owner and tags: each candidate can have an assigned *HR owner* (the person handling them) and a list of *tags* (e.g. role, source, focus) — useful for triage and filtering.
Prerequisites
- role:
owner,admin,hr_admin - migrations
0077_hr_candidates.sql+0080_hr_candidates_soft_delete.sqlapplied on the tenant Supabase project - bucket
hr-candidate-cvsexists (created by migration 0077)
Quick access
- Pipeline page:
Workspace › HR › Profile management › 🆕 Pre-onboarding candidates - Direct URL:
/workspace/hr/candidates
Operating steps
1. Create a candidate
- Open
/workspace/hr/candidates - Click the
+ New candidateCTA in the top right - Fill the modal: only
Full nameis required. Email, phone, headline, source and notes are optional — they can be filled during screening. - Click
Create candidate. Initial status isNew.
2. Update the candidate profile
- Changes are saved automatically on blur
3. Extract data from the candidate CV
CV AI extraction is on-demand: you upload the PDF/DOCX, then click a button to populate the structured draft (experiences, skills, languages, identity). It is a prerequisite for profile transfer during conversion (step 4).
- On the candidate detail,
Candidate CVsection, find the uploaded CV row (Statuscolumn showsUploaded) - Click
Extract CV data(shown until status isData extracted) - The system downloads the file, parses it (including OCR fallback for scanned PDFs) and calls Claude Haiku to extract the structured profile. Status transitions to
Data extracted. - Fail-soft: if parsing or the AI call fails, the CV stays
Uploadedand extraction is best-effort — conversion still works but transfers 0 experiences/skills.
4. Convert candidate to employee
When the candidate accepts the offer, you convert them into a tenant employee. The conversion is restricted to owner, admin, hr_admin.
- On confirm the system runs the conversion, marks the candidate as
Hired, and shows the outcome with a link to the employee profile.
Two conversion modes:
- Create new user (
new_user): creates a brand-new auth account, sets upprofiles,organization_memberships(with the chosen role) andorganization_user_details(profile data derived from the candidate), and sends an invite email with a password-set link. Requires an email not already present among users. - Link to existing user (
linked_existing): links the candidate to a member already in the tenant. No new account, no email — just a profile update + resource transfer.
What gets transferred (requires a CV with Data extracted — see step 3):
- CV: storage objects are copied from the candidate bucket (
hr-candidate-cvs) to the employee bucket (hr-cvs), with a new row inhr_cv_files(version 1). The candidate CV is markedmigrated. - Work experiences: the experiences from the CV extraction are inserted on the employee profile.
- Skills: the mapping to the tenant catalog happens at extraction time (entity-linking, links saved in the draft); conversion applies to the profile only the
autolinks (confidence >= 0.85), fuzzy matches 0.70-0.85 remain unapplied suggestions (CV_ENTITY_GRAPH_01 W01).
If the CV extraction is unavailable, the conversion still proceeds but transfers no experiences/skills (counters at 0).
Invite email (new_user mode only): sending is *fail-soft* — if Resend is unreachable the conversion stays valid; the outcome flags the invite as not sent and you can manually send a password reset.
Transactional safety: storage objects are copied before DB writes. If a DB write fails after user creation, a compensating rollback deletes the created auth user and the copied objects. Every conversion (succeeded or rolled back) leaves an audit row in hr_candidate_conversions.
Seat limits: if the tenant plan has a saturated max_users limit, the new_user mode fails with an explicit error (422) before creating the user.
From then on the candidate stays as a historical audit record; the *real* professional profile lives on the employee record.
5. Archive a candidate
- From the detail page:
Archivebutton (or manually change status toArchived) - The candidate disappears from the default list view; to see archived ones again, select
Archivedfrom the status filter. - Archive is a workflow state — the record is visible by switching the filter. For a recoverable removal use
Delete(step 6).
6. Delete into trash (and restore)
- From the detail page:
Deletebutton (ghost, next to Archive). Confirm message:recoverable within 30 days. - The candidate is soft-deleted (
deleted_at) and disappears from all default views — not reachable via status filter. - To recover: in the list, toggle
Show deleted (trash, 30d)to switch to the trash view; clickRestoreon the candidate row. - Automatic hard delete out of scope: trashed candidates remain until an operator (or a future cron) permanently deletes them.
Candidate list filters
The /workspace/hr/candidates list exposes 4 primary filters (auto-applied on change, reset via Clear filters):
- Search by name, email or headline: ILIKE on
full_name,email,headline - Pipeline status: dropdown with all statuses (default: excludes
Archived) - Assigned owner: dropdown populated by tenant users with HR permissions; filters by
assigned_to_user_id - Tag: single-select dropdown derived from the union of tags on currently loaded candidates; filters by
tags && [tag](PG overlap)
Separate Show deleted (trash, 30d) toggle to access the soft-delete view.
Transactional email notifications
On status change, the system automatically sends an email to the candidate (if email is not null) for 4 transitions:
Screening— "application taken in charge"Interview— "interview invitation"Offered— "offer extended"Rejected— "application outcome"
Other statuses (New/Hired/Archived/Withdrawn) do not trigger notifications (the Hired invite is handled separately by the conversion, step 4).
Fail-soft: the Resend call is fire-and-forget; an SMTP error never blocks the status transition. Errors are logged server-side.
Credentials: use platform_email_settings (provider=resend) if configured on the tenant, otherwise RESEND_API_KEY from env. With neither set → silent skip.
Supported sources
The Source field is free-text but the creation modal suggests common presets:
linkedin— direct LinkedIn sourcingreferral— internal referralgara— emerged during an RFP / tendercv-upload— direct CV upload (e.g. via public form, on the roadmap)website— spontaneous website applicationagency— external agencyother— anything else
API endpoints (for integrations)
All endpoints require Authorization: Bearer <jwt> + x-horis-org-id: <org_uuid> headers.
| Method | URL | Description | |--------|-----|-------------| | GET | /api/hr/candidates | paginated list + filters (status, search, assignedTo, tags, includeArchived, includeDeleted, onlyDeleted, page, limit). tags accepts csv. | | POST | /api/hr/candidates | create (JSON body: fullName required + optionals, including tags/assignedToUserId) | | GET | /api/hr/candidates/{id} | detail + cvFiles | | PATCH | /api/hr/candidates/{id} | partial update (including tags/assignedToUserId/status — on a triggerable status + non-null email → sends transactional email) | | DELETE | /api/hr/candidates/{id} | archive (status → archived, NOT soft delete — see /soft-delete) | | POST | /api/hr/candidates/{id}/soft-delete | move to trash (deleted_at), recoverable within 30 days | | POST | /api/hr/candidates/{id}/restore | restore from trash | | GET | /api/hr/candidates/{id}/cv-files | list candidate CVs | | POST | /api/hr/candidates/{id}/cv-files | multipart upload (file) | | POST | /api/hr/candidates/{id}/cv-files/{cvId}/extract | on-demand AI extraction → populates extraction_draft with the profile (experiences/skills/languages/identity). Fail-soft: 200 with warning if parse/LLM fails. | | GET | /api/hr/candidates/{id}/cv-files/{cvId}/signed-url | signed URL TTL 300s (max 3600s via ?ttl=) | | DELETE | /api/hr/candidates/{id}/cv-files/{cvId} | soft delete CV | | POST | /api/hr/candidates/{id}/convert-to-user | convert candidate → employee. Body: mode (new_user\|linked_existing), + targetEmail/existingUserId, role, managerUserId, primaryRole, startDate, transferCv/transferExperiences/transferSkills. Backward-compat: existingUserId only → linked_existing. Errors: 409 email_collision, 422 seat_limit_exceeded, 409 already_hired |
Pipeline statuses
| Status | Meaning | Valid transitions | |--------|---------|-------------------| | new | Just created, not yet screened | any | | screening | Under initial evaluation | any | | interview | In interview phase | any | | offered | Offer sent | hired, rejected, withdrawn | | hired | Hired (post-convert) | only to archived, rejected, withdrawn (no rollback) | | rejected | Discarded after evaluation | terminal (always archivable) | | withdrawn | Self-withdrew | terminal | | archived | Workflow state, NOT deletion | reactivatable by changing status |
Security & GDPR
- Tables
hr_candidatesandhr_candidate_cv_fileshave RLS fully blocked: access is ONLY via service-role admin client server-side, with applicativecanManageCandidatescheck. - CVs live in the private
hr-candidate-cvsbucket, with temporary signed URLs (default TTL 5 minutes). - Audit fields:
created_by_user_id,uploaded_by_user_id,deleted_by_user_idon CV files. Onhr_candidates:created_by_user_id+updated_atautotouch trigger +deleted_at/deleted_by_user_idfor soft delete. - Soft delete (both for trashed candidates and CVs): the record stays for audit, storage object remains in the bucket until an explicit hard delete (future).
- Transactional emails: fail-soft, never blocking. SMTP errors leave no silent residue (logged server-side).
Not (yet) included
- interviewer assignment + interview scheduling
- public form for spontaneous applications
- job postings / ATS pipeline
- background check
- cross-org candidate transfer
- automatic hard delete from trash (cron)
- multi-language and tenant-configurable email templates
Origin program
HORIS_HR_PRE_ONBOARDING_01(2026-05-19) — initial releaseHORIS_HR_PRE_ONBOARDING_02(2026-05-19) — full hire workflow: complete candidate → employee conversion (new-user provisioning, profile/experiences/skills/CV transfer, email invite)HORIS_HR_CANDIDATE_CONSOLIDATION_01(2026-05-25) — consolidation: W01 on-demand AI extractor on candidate CVs (closes the profile-not-transferred gap), W02 UI tags filter + owner picker (list + detail), W03 transactional emails + soft delete with 30d grace period
