GraphQL operations for the recruitment domain -- talents, talent pools, CVs, candidates, jobs, pipelines, and match scores. All operations require JWT authentication unless marked as Public.
CV parsing is handled by the background-tasks service. After uploading a CV document to S3, subscribe to the cvExtraction subscription on the AI service endpoint to receive parsed fields in real time.
Typed mutations for editing CV data directly on the 11 structured tables. These replace the deprecated updateCvSchema / updateMyCvSchema raw JSON mutations. All mutations require JWT authentication. Job seeker mutations verify CV ownership; company-scoped mutations verify write access via TalentAuthorizationService.
Company-scoped mutations for all CV sections, not just skills. These verify write access via TalentAuthorizationService.checkWriteAccessToCV() — the user must own the CV or have write access to the talent (via company talent pool membership or sharing with write permission).
Authorization
Company-scoped mutations use the same input types and logic as the "My" variants. The only difference is the access control check — ownership vs. company write access.
Use the "My" variants when the job seeker is editing their own CV. Use the company-scoped variants (no "My" prefix) when an employer is editing a candidate or talent CV they have write access to.
The useStructuredCvMutations hook handles both modes automatically:
All structured CV mutations (both job seeker and company-scoped) emit a cv.updated event after the transaction commits, via the @EmitCvUpdatedAfter method decorator on CvStructuredMutationService. This triggers match score invalidation so job-candidate matching scores are recalculated within the next ~5-second worker cycle whenever CV data changes.
Each typed mutation writes directly to the normalized structured CV tables. Prefer these typed mutations over the deprecated updateCvSchema / updateMyCvSchema raw JSON mutations.
Candidates represent a talent applied or added to a specific job. They are tracked through a pipeline with statuses: NEW -> IN_REVIEW -> INTERVIEW -> APPROVED / REJECTED / WITHDRAWN.
When building the employer-facing candidate view, fetch CV/preference data from candidate.cv (the application snapshot). Do not add talent.activeCV { ... } or the live preference fields on user (expectedSalary, salaryCurrency, salaryFrequency, preferredJobTypes, preferredWorkSite, preferredLocation, desiredJobTitles, willingToRelocate, availableIn) — those bleed the seeker's live edits through the frontend fallback chains, which is exactly what the snapshot is meant to prevent. See Candidate Management — Frontend read path for the full rule and rationale.
The Candidate type includes a convenience responsibleUser field that resolves the responsible user from the associated job:
typeCandidate{ id:ID! userId:ID! jobId:ID! source:CandidateSource! status:CandidateStatus! talentId:ID! cvId:ID # ... other fields responsibleUser:User# Resolved from job.responsibleUser user:User job:Job talent:Talent cv:CV matchScore:Float# Numeric score (0-100) matchReport:MatchScore# Full match report with details JSON interviews:[Interview] notes:[CandidateNote] }
tip
responsibleUser is a convenience field that reads from candidate.job.responsibleUser. It avoids the need to traverse the job relation in frontend queries.
mutationAddTalentAsCandidate($input:AddTalentAsCandidateInput!){ addTalentAsCandidate(input:$input){ id talent{idfirstNamelastName} job{idtitle} responsibleUser{idfirstNamelastName} status createdAt } }
Public job listings — hide flags applied by the public serializer (SUP-00121/122/125/126/106)
contactsByLabel(label)
JWT
[Contact!]!
Contacts on the caller's company filtered by a ContactType label (e.g. "Client", "Partner"). Used by the Job form's Hiring Company and Supplied By dropdowns (SUP-00121/122).
Create a new job posting. dueDate rejects past dates via @IsFutureDate (SUP-00051). Accepts hideHiringCompany/hideSuppliedBy/hideRateOut Boolean flags (SUP-00121/122/125/126/106).
updateJob
Update job fields or status. Rejects IN_PROGRESS → NEW when dueDate is already past (SUP-00129).
removeJob
Soft-archive a job — sets Job.archivedAt instead of hard-deleting; emits job.archived event (SUP-00083). Pipeline items / candidates / skill requirements are preserved.
mutationCreateJob($input:CreateJobInput!){ createJob(input:$input){ id title status # SUP-00125/126/106 — public-view visibility toggles hideHiringCompany hideSuppliedBy hideRateOut archivedAt# null unless removeJob was called (SUP-00083) company{idname} createdAt } }
Public view masking (SUP-00121/122/125/126/106)
When a job is fetched via the public jobUnrestricted / allJobs resolvers, a public-job.serializer.ts applies the hide* flags:
hideHiringCompany=true → hiringCompanyContactId is nulled and hiringCompanyId is rewritten to the posting company's id (external viewers see "Posted by <your company>")
hideSuppliedBy=true → suppliedById / suppliedByContactId are nulled
hideRateOut=true → rateOut / rateOutFrequency / rateOutCurrency are nulled
The underlying Job row is never mutated — authenticated company queries (opportunities, job(id)) still return the complete record.
Use candidateDetailed(id).matchReport to fetch the full MatchScore object (including details) directly from a candidate, instead of looking up the score separately by job + CV.
warning
Match scores are generated asynchronously by background-tasks. They may not exist immediately after creating a candidate. Use the userJobMatching or candidateJobMatching subscriptions on the AI service endpoint to trigger and monitor matching in real time.