Skip to main content

Talent Management

Talents are user profiles organized into company-scoped talent pools. The system supports multiple creation paths, CV management with AI parsing, multi-level sharing, and skill-based recommendations.

Talent Sources

The Talent.source field records how a talent entered a company's pool. It maps to the backend TalentSource enum.

SourceEnum ValueDescription
Manual entryMANUAL_ENTRYRecruiter adds a user directly
Talent pool importTALENT_POOL_IMPORTImported into a talent pool
CV uploadCV_UPLOADCreated during a single CV upload
AI CV uploadAI_CV_UPLOADSingle CV parsed by the AI extraction service
AI email uploadAI_EMAIL_UPLOADCV parsed from an inbound email (draft-first intake)
AI bulk uploadAI_BULK_UPLOADMultiple CVs parsed from a batch by the AI service
AI agent uploadAI_AGENT_UPLOADCV sourced by an AI sourcing agent
Direct applicationAPPLICATION_DIRECTUser self-applies to a job
ReferralREFERRALReferred by another user
Recruitment agencyRECRUITMENT_AGENCYSupplied by a recruitment agency
LinkedIn importLINKEDIN_IMPORTImported from LinkedIn
Bulk import (ZIP)BULK_IMPORTZIP archive import of multiple CVs
API integrationAPI_INTEGRATIONCreated via an external API integration
Internal transferINTERNAL_TRANSFERMoved between companies
InvitedINVITEDEmployer accepted a job seeker's collaboration/profile invitation
Invited memberINVITED_MEMBERAn invited team member's own CV upload created a company talent
SharedSHAREDExtracted from a collaborator's shared talent
Job Seeker Invitation Flow

When a job seeker sends a profile invitation and an employer accepts it, a talent record is automatically created with source: INVITED and assigned to the employer's Default talent pool. This ensures invited talents are immediately visible on the company's talents page. The INVITED source also controls automatic notification delivery — only invited candidates receive automatic emails for pipeline status changes and interview scheduling.

info
INVITED vs INVITED_MEMBER

These two look similar but describe different flows and must not be conflated:

  • INVITED — a job seeker sent a collaboration/profile invitation and the employer accepted it. The accepted invite creates a company talent for that external job seeker.
  • INVITED_MEMBER — an invited team member uploads their own CV, which creates a company talent for them. This runs through ensureCompanyTalentsForInvitedMemberaddUserToTalentPoolWithTransaction(TalentSource.INVITED_MEMBER). Only members who joined via an invitation are included; company founders/owners — role rows with no invitationId — are excluded. This source renders as "Invited Member" in the UI.

Draft intake lifecycle

CVs that arrive through the AI/intake sources (CV_UPLOAD, AI_CV_UPLOAD, AI_EMAIL_UPLOAD, AI_BULK_UPLOAD, AI_AGENT_UPLOAD) are a draft-first flow: the parsed talent is created with TalentStatus.DRAFT and only promoted to READY once the profile is completed (via updateUser). This lets recruiters review and correct machine-parsed data before the talent becomes a fully active pool member.

Inbound-email CVs specifically land in a review inbox rather than going straight into the pool. See Draft CV Inbox and Inbound Email Processing for the intake pipeline.

warning

BULK_IMPORT is the ZIP-archive import source — it is not the inbound-email source. Inbound-email CVs use the AI/intake sources above and follow the draft lifecycle.

Talent Pools

Each company has a Default talent pool created automatically. Talents belong to a pool via the @@unique([userId, talentPoolId]) constraint -- a user can appear only once per pool.

A special Public Talent Pool (ID: a0000000-0000-0000-0000-000000000001) exists for independent job seekers with no company association.

Pool statuses: ACTIVE, INACTIVE, ARCHIVED, DRAFT

Key Queries

# List company talent pools
query {
talentPools {
id
name
status
isPublic
company { id name }
}
}

# Get talents in a pool
query {
talentPool(id: "pool-uuid") {
talents {
id
user { firstName lastName email }
activeCv { id url }
}
skills { skill { name } required proficiency }
}
}

# Get all company talents (owned + shared)
query {
companyTalents(companyId: "company-uuid") {
id
source
user { firstName lastName }
}
}

Key Mutations

# Add talent to pool
mutation {
addTalent(createTalentInput: {
userId: "user-uuid"
talentPoolId: "pool-uuid"
}) { id }
}

# Create a new talent pool with skill requirements
mutation {
createTalentPool(createTalentPoolInput: {
name: "Senior Engineers"
description: "Experienced backend developers"
companyId: "company-uuid"
skills: [
{ skillId: "skill-uuid", required: true, proficiency: ADVANCED }
]
}) { id name }
}

CV Management

Each talent can have multiple CVs with one marked as the active CV (talent.activeCvId). CVs are stored in S3 and parsed by the AI service into 11 structured tables (CvProfile, CvWorkExperience, CvEducation, CvCertification, CvLanguage, CvProject, CvPublication, CvAward, CvVolunteer, CvJobPreference, CvMetadata).

info

CV ownership is dual: the CV table has a FK to Talent set to SET NULL (not CASCADE). When a talent is deleted, the user's CVs are preserved.

Auto-selection logic when creating a candidate: the system prioritizes a provided cvId, then the talent's activeCvId, then the most recent CV.

Editing a Talent

The rule for editing a talent's profile/CV depends only on whether the owner's account is active — the talent's source is not part of the gate.

Owner stateBehaviour
Inactive owner (user.isActive !== true), any sourceDirect edit — the recruiter edits and saves the talent's CV in place
Active owner (user.isActive === true)Suggest-changes flow — edits are proposed to the talent for review rather than saved directly

The reasoning is ownership: once a talent's account is active, they own their own profile data, so a company member can only propose changes; while the account is still inactive (a recruiter-managed placeholder that the person has not yet claimed), the company can edit the CV directly regardless of how the talent was sourced.

Implementation

LocationComputed value
app/company/resume/talents/[id]/edit/page.tsxuserIsActive = talent.user.isActive === true; suggestMode = talent && userIsActive
TalentDetails.tsxcanEdit = user && user.isActive !== true (direct edit; source not considered)
TalentDetails.tsxcanProposeEdit = source === "MANUAL_ENTRY" && user.isActive === true
  • The talents list's onEditClick routes straight to /edit for every talent — there is no detail-page detour and no warning toast. The edit page itself decides direct-save vs suggest-mode from userIsActive.
  • Backend TalentAuthorizationService.hasWriteAccessToCV() enforces the same active/inactive distinction, so a direct save against an active owner's live CV is rejected server-side even if the client attempted it.

Suggest-changes flow

When the owner is active, the edit page opens in suggest mode: instead of mutating the live CV, the recruiter's changes are captured as suggested edits that are surfaced to the talent (the CV owner) for review. The talent reviews the proposal and chooses to apply or discard it, so the company never silently overwrites a claimed profile. Direct-save controls are hidden in this mode; the affordance to open the flow (canProposeEdit) is shown for MANUAL_ENTRY talents whose owner is active.

info

This is a separate gate from the candidate edit rule. Editing a candidate's frozen CV snapshot stays source-based (MANUAL_ENTRY + inactive owner only) — see Candidate Management.

Talent Sharing

Three sharing mechanisms exist, each with different scopes:

1. Company-to-Company Sharing (TalentShare)

Share individual talents between companies with permission control.

mutation {
shareTalent(input: {
talentId: "talent-uuid"
sharedByCompanyId: "company-a"
sharedWithCompanyId: "company-b"
permissionLevel: READ_ONLY
notes: "Strong React candidate"
}) { id permissionLevel }
}

# Bulk share
mutation {
bulkShareTalents(input: {
talentIds: ["t1", "t2", "t3"]
sharedByCompanyId: "company-a"
sharedWithCompanyId: "company-b"
permissionLevel: EDIT
}) { id }
}

Permission levels: READ_ONLY, EDIT, ADMIN

2. Talent Pool Sharing

Pools can be shared at three levels:

LevelModelScope
User shareTalentPoolUserShareIndividual user with VIEW/EDIT/MANAGE
Company shareTalentPoolCompanyShareEntire company with VIEW/EDIT/MANAGE
External shareTalentPoolExternalShareToken-based link with expiration
# External share (generates email with link)
mutation {
createTalentPoolExternalShare(createTalentPoolExternalShareInput: {
talentPoolId: "pool-uuid"
email: "client@partner.com"
expirationHours: 72
}) { id token expiresAt accessCount }
}

# Public access via token (no auth required)
query {
talentPoolByToken(token: "secure-token") {
name
talents { id user { firstName lastName } }
}
}

3. Collaborator Talent Sharing

For companies with active collaboration relationships. Requires both parties to have ACTIVE collaborator status and a valid relationship (direct invitation or common inviter).

mutation {
shareTalentWithCollaborator(input: {
toCollaboratorId: "collab-uuid"
talentId: "talent-uuid"
permissionLevel: READ_ONLY
notes: "Great React developer"
}) { success share { id } }
}
warning

Talents can only be shared from their owner's organization. The system validates that the talent's pool belongs to the sharer's company before allowing the share.

Talent Recommendations

The recommendation engine scores talents against pool skill requirements using weighted proficiency matching:

score = (required ? 1.5 : 1.0) * (talentProficiency / requiredProficiency) * experienceMultiplier

Proficiency scores: BASIC=1, INTERMEDIATE=2, ADVANCED=3, EXPERT=4.

query {
talentRecommendations(input: {
talentPoolId: "pool-uuid"
limit: 20
minScore: 0.3
}) {
recommendations {
talent { id user { firstName lastName } }
matchScore
skillMatches { skill { name } hasSkill scoreContribution }
}
totalCount
averageScore
}
}

Talent Notes

Internal notes can be attached directly to talent profiles, independent of any job candidacy. This fills the gap for sourcing workflows where recruiters need to capture observations before a talent is linked to a specific job.

See Talent Notes for the full API and access control details.

Export

Talents and candidates can be exported to CSV via presigned S3 URLs (1-hour expiry):

mutation {
exportCompanyTalents(companyId: "company-uuid", talentIds: ["t1", "t2"])
}
# Returns a presigned S3 download URL