Pipeline System
Pipelines are Kanban-style boards for managing recruitment workflows. Each pipeline belongs to a company and an owner, contains jobs via PipelineItem records, and tracks candidates through status stages.
The pipeline manage board (/company/pipeline/manage/[id]) reflects and edits each job's status — the single source of truth shared with the Jobs module and every other pipeline. It is not driven by PipelineItem.status. See The Manage Board below.
Pipeline Structure
Core Enums
| Enum | Values |
|---|---|
PipelineType | ACTIVE, PRIVATE, ARCHIVED |
ShareableType | SHARABLE, NOT_SHARABLE |
PipelineItemStatus | NEW, IN_PROGRESS, IN_FOCUS, PENDING_FEEDBACK, CLOSED, FEATURE_REQUEST |
CandidateStatus | NEW, IN_REVIEW, INTERVIEW, APPROVED, REJECTED, WITHDRAWN |
Candidate Status Flow
Candidates move through these stages on the pipeline board:
Withdrawals can happen from any active stage. When a candidate withdraws, the status changes to WITHDRAWN and a withdrawal reason is persisted.
The Manage Board (Job-Status Kanban)
The pipeline manage board at /company/pipeline/manage/[id] is a job-status Kanban. Its columns come from JobStatus (JOB_KANBAN_COLUMNS), not from PipelineItemStatus:
| Column | Backing JobStatus |
|---|---|
DRAFT | DRAFT |
NEW | NEW |
IN_PROGRESS | IN_PROGRESS |
PENDING_FEEDBACK | PENDING_FEEDBACK |
FEATURE_REQUEST | FUTURE_REQUESTS (display alias) |
CLOSED | CLOSED |
Cards are built with mapItemsToCards(items, { useJobStatus: true }), so a card's column is derived from job.status, never from PipelineItem.status.
Status changes persist to the Job
Any status change — dragging a card between Kanban columns or using the dropdown in the grid/list view — persists through updateJob. The board maps between its column labels and the canonical JobStatus values:
| Direction | Mapping |
|---|---|
Board → Job (kanbanStatusToJobStatus) | FEATURE_REQUEST → FUTURE_REQUESTS, IN_FOCUS → NEW |
Job → Board (mapJobStatusToKanban) | FUTURE_REQUESTS → FEATURE_REQUEST |
Because the board edits the job's status — the single source of truth shared with the Jobs module and every other pipeline — Apollo normalizes the result by job id, so the Jobs page and every other pipeline containing that job update in place. If the mutation fails, the board refetches to resync.
The view mode (kanban / grid / list) is persisted in the page settings.
PipelineItem.status is vestigial for this boardThe board no longer reads or writes PipelineItem.status. The field still exists on the model and the updatePipelineItem mutation still accepts it, but it is not the board's source of truth — the job's status is. Treat PipelineItem.status as legacy for pipeline-board purposes.
Adding an existing job
The add-existing-job modal lists candidate jobs with each job's current status shown, dedupes the list, and excludes jobs already in the pipeline so you can't add a duplicate PipelineItem.
Scoring Configuration
Each pipeline can have a PipelineScoringConfig that controls how AI match scores are weighted:
| Weight | Default | Purpose |
|---|---|---|
skillWeight | 0.40 | Skill-based matching |
semanticWeight | 0.35 | Semantic similarity |
contextWeight | 0.25 | Contextual relevance |
The innerWeights JSON field allows fine-grained sub-weight tuning.
Key GraphQL Operations
Pipeline CRUD
# Create pipeline with scoring config
mutation {
createPipeline(input: {
name: "Q1 Engineering Hires"
description: "Backend and frontend roles"
companyId: "company-uuid"
pipelineType: ACTIVE
shareableType: SHARABLE
scoringConfig: {
skillWeight: 0.45
semanticWeight: 0.30
contextWeight: 0.25
}
}) { id name pipelineType }
}
# List pipelines for current user
query {
pipelines {
id name pipelineType
owner { firstName lastName }
pipelineItems { id job { title } status }
}
}
Pipeline Items (Jobs)
# Add job to pipeline
mutation {
createPipelineItem(input: {
pipelineId: "pipeline-uuid"
jobId: "job-uuid"
}) { id status }
}
# Update pipeline item status
mutation {
updatePipelineItem(input: {
id: "item-uuid"
status: IN_PROGRESS
}) { id status }
}
Candidate Management
# Add talent as candidate to a job
mutation {
addTalentAsCandidate(addTalentAsCandidateInput: {
talentId: "talent-uuid"
jobId: "job-uuid"
cvId: "cv-uuid"
}) { id status }
}
# Self-apply as candidate
mutation {
applyAsCandidate(applyAsCandidateInput: {
jobId: "job-uuid"
motivation: "Excited about this role"
}) { id status }
}
# Update candidate status
mutation {
updateCandidate(updateCandidateInput: {
id: "candidate-uuid"
status: INTERVIEW
}) { id status }
}
applyAsCandidate prevents duplicate applications per user/job. It auto-provisions a talent pool entry and generates default motivation text when not supplied.
Pipeline Sharing
Pipelines support four sharing mechanisms:
| Type | Model | Permission Levels |
|---|---|---|
| User share | PipelineUserShare | VIEW, EDIT, MANAGE |
| Company share | PipelineCompanyShare | VIEW, EDIT, MANAGE |
| External share | PipelineExternalShare | VIEW only (token-based) |
| Collaborator share | CollaboratorPipelineShare | VIEW, EDIT, MANAGE |
Access Control Matrix
| Role | Create | Read | Update | Delete | Share | Manage Items |
|---|---|---|---|---|---|---|
| Pipeline Owner | Y | Y | Y | Y | Y | Y |
| Company Admin | Y | Y | Y | Y | Y | Y |
| Company Member | Y | Y | - | - | - | Y |
| Shared (MANAGE) | - | Y | Y | - | Y | Y |
| Shared (EDIT) | - | Y | Y | - | - | Y |
| Shared (VIEW) | - | Y | - | - | - | - |
| External Token | - | Y | - | - | - | - |
# Share pipeline with user
mutation {
createPipelineUserShare(input: {
pipelineId: "pipeline-uuid"
userId: "user-uuid"
permissionLevel: EDIT
}) { id permissionLevel }
}
# External share (token-based, with expiry)
mutation {
createPipelineExternalShare(input: {
pipelineId: "pipeline-uuid"
email: "partner@example.com"
expirationHours: 168
}) { id token expiresAt }
}
Sharing from the UI: existing collaborator or invite by email
SharePipelineModal has a mode toggle with two paths:
- Share with an existing collaborator — pick a current collaborator and choose a permission level (VIEW / EDIT / MANAGE).
- Invite by email — invite someone who isn't a collaborator yet. This path is View only and calls
createCollaborationInvitationwithpipelineIds, so once the invitee accepts, the pipeline is already visible to them. The modal validates the email format and blocks inviting yourself.
Pending (not-yet-accepted) collaborators
The Share tab surfaces invitees whose collaboration invitation is still PENDING:
- A PENDING badge is shown next to the entry.
- The permission is read-only in the UI (it can't be changed until acceptance).
- The row title reads "Invitation pending · <permission>".
Once the invitee accepts, they graduate from a pending entry to an active collaborator share.
Authorization
The PipelineAuthorizationService provides centralized access checks:
canAccessPipeline(userId, pipelineId)-- read access (returns boolean)hasWriteAccess(userId, pipelineId)-- write access (returns boolean)getAccessiblePipelineIds(userId)-- all pipelines the user can see
Access is resolved by checking, in order: ownership, company admin/member status, user/company shares, and collaborator relationships (only ACTIVE collaborators).