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 candidates section is restricted to owner, tenant admin, hr_admin. Manager roles get 403 with reason candidates_require_hr_admin.
  • Linear workflow: New → Screening → Interview → Offered → Hired, with exits Rejected, 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 previous current one.
  • Archive vs Delete: two distinct actions. Archive is a workflow state (Archived, reactivatable by changing status). Delete moves 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.sql applied on the tenant Supabase project
  • bucket hr-candidate-cvs exists (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

  1. Open /workspace/hr/candidates
  2. Click the + New candidate CTA in the top right
  3. Fill the modal: only Full name is required. Email, phone, headline, source and notes are optional — they can be filled during screening.
  4. Click Create candidate. Initial status is New.

2. Update the candidate profile

  1. 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).

  1. On the candidate detail, Candidate CV section, find the uploaded CV row (Status column shows Uploaded)
  2. Click Extract CV data (shown until status is Data extracted)
  3. 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.
  4. Fail-soft: if parsing or the AI call fails, the CV stays Uploaded and 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.

  1. 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 up profiles, organization_memberships (with the chosen role) and organization_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 in hr_cv_files (version 1). The candidate CV is marked migrated.
  • 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 auto links (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: Archive button (or manually change status to Archived)
  • The candidate disappears from the default list view; to see archived ones again, select Archived from 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: Delete button (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; click Restore on 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 sourcing
  • referral — internal referral
  • gara — emerged during an RFP / tender
  • cv-upload — direct CV upload (e.g. via public form, on the roadmap)
  • website — spontaneous website application
  • agency — external agency
  • other — 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_candidates and hr_candidate_cv_files have RLS fully blocked: access is ONLY via service-role admin client server-side, with applicative canManageCandidates check.
  • CVs live in the private hr-candidate-cvs bucket, with temporary signed URLs (default TTL 5 minutes).
  • Audit fields: created_by_user_id, uploaded_by_user_id, deleted_by_user_id on CV files. On hr_candidates: created_by_user_id + updated_at autotouch trigger + deleted_at/deleted_by_user_id for 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 release
  • HORIS_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

Related guides