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.
| Source | Enum Value | Description |
|---|---|---|
| Manual entry | MANUAL_ENTRY | Recruiter adds a user directly |
| Talent pool import | TALENT_POOL_IMPORT | Imported into a talent pool |
| CV upload | CV_UPLOAD | Created during a single CV upload |
| AI CV upload | AI_CV_UPLOAD | Single CV parsed by the AI extraction service |
| AI email upload | AI_EMAIL_UPLOAD | CV parsed from an inbound email (draft-first intake) |
| AI bulk upload | AI_BULK_UPLOAD | Multiple CVs parsed from a batch by the AI service |
| AI agent upload | AI_AGENT_UPLOAD | CV sourced by an AI sourcing agent |
| Direct application | APPLICATION_DIRECT | User self-applies to a job |
| Referral | REFERRAL | Referred by another user |
| Recruitment agency | RECRUITMENT_AGENCY | Supplied by a recruitment agency |
| LinkedIn import | LINKEDIN_IMPORT | Imported from LinkedIn |
| Bulk import (ZIP) | BULK_IMPORT | ZIP archive import of multiple CVs |
| API integration | API_INTEGRATION | Created via an external API integration |
| Internal transfer | INTERNAL_TRANSFER | Moved between companies |
| Invited | INVITED | Employer accepted a job seeker's collaboration/profile invitation |
| Invited member | INVITED_MEMBER | An invited team member's own CV upload created a company talent |
| Shared | SHARED | Extracted from a collaborator's shared talent |
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.
INVITED vs INVITED_MEMBERThese 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 throughensureCompanyTalentsForInvitedMember→addUserToTalentPoolWithTransaction(TalentSource.INVITED_MEMBER). Only members who joined via an invitation are included; company founders/owners — role rows with noinvitationId— 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.
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).
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 state | Behaviour |
|---|---|
Inactive owner (user.isActive !== true), any source | Direct 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
| Location | Computed value |
|---|---|
app/company/resume/talents/[id]/edit/page.tsx | userIsActive = talent.user.isActive === true; suggestMode = talent && userIsActive |
TalentDetails.tsx | canEdit = user && user.isActive !== true (direct edit; source not considered) |
TalentDetails.tsx | canProposeEdit = source === "MANUAL_ENTRY" && user.isActive === true |
- The talents list's
onEditClickroutes straight to/editfor every talent — there is no detail-page detour and no warning toast. The edit page itself decides direct-save vs suggest-mode fromuserIsActive. - 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.
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:
| Level | Model | Scope |
|---|---|---|
| User share | TalentPoolUserShare | Individual user with VIEW/EDIT/MANAGE |
| Company share | TalentPoolCompanyShare | Entire company with VIEW/EDIT/MANAGE |
| External share | TalentPoolExternalShare | Token-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 } }
}
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