Skip to main content

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 manage board is a job-status Kanban

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

EnumValues
PipelineTypeACTIVE, PRIVATE, ARCHIVED
ShareableTypeSHARABLE, NOT_SHARABLE
PipelineItemStatusNEW, IN_PROGRESS, IN_FOCUS, PENDING_FEEDBACK, CLOSED, FEATURE_REQUEST
CandidateStatusNEW, IN_REVIEW, INTERVIEW, APPROVED, REJECTED, WITHDRAWN

Candidate Status Flow

Candidates move through these stages on the pipeline board:

warning

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:

ColumnBacking JobStatus
DRAFTDRAFT
NEWNEW
IN_PROGRESSIN_PROGRESS
PENDING_FEEDBACKPENDING_FEEDBACK
FEATURE_REQUESTFUTURE_REQUESTS (display alias)
CLOSEDCLOSED

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:

DirectionMapping
Board → Job (kanbanStatusToJobStatus)FEATURE_REQUESTFUTURE_REQUESTS, IN_FOCUSNEW
Job → Board (mapJobStatusToKanban)FUTURE_REQUESTSFEATURE_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.

note
PipelineItem.status is vestigial for this board

The 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:

WeightDefaultPurpose
skillWeight0.40Skill-based matching
semanticWeight0.35Semantic similarity
contextWeight0.25Contextual 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 }
}
info

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:

TypeModelPermission Levels
User sharePipelineUserShareVIEW, EDIT, MANAGE
Company sharePipelineCompanyShareVIEW, EDIT, MANAGE
External sharePipelineExternalShareVIEW only (token-based)
Collaborator shareCollaboratorPipelineShareVIEW, EDIT, MANAGE

Access Control Matrix

RoleCreateReadUpdateDeleteShareManage Items
Pipeline OwnerYYYYYY
Company AdminYYYYYY
Company MemberYY---Y
Shared (MANAGE)-YY-YY
Shared (EDIT)-YY--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 createCollaborationInvitation with pipelineIds, 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).