Skip to content

CAP workflow abstraction

Audience: backend, product
Status: specced
Owns: backend
Depends on: CAP overview, RPL lifecycle, NSQ lifecycle, Data model, Architecture handover

Protects against changing stage order and ownership without pretending every stage behavior is a JSON parameter.

Problem Configurable? Approach
A — Stage sequencing Yes Which stages exist, order, assigning authority, entry conditions
B — Stage behavior No (closed set of strategies in code) Single approval vs panel+signatures vs evidence review vs form submission are different interaction patterns

Do not build a full BPMN engine. Platform admin may later reorder/add stages or swap role ownership; how a panel completes stays in code.

workflow_templates (id, name, application_type, version)
stage_definitions (workflow_template_id, key, order,
handler_strategy, -- see closed set below
actor_role, -- reviewer | facilitator | panel | iv | ev | unit_assessor | ...
assigning_authority,-- centre | awarding_body | platform
entry_conditions) -- e.g. payment_complete
application_workflow_state (application_id, workflow_template_id, current_stage_key, entered_at)
stage_history (application_id, stage_key, entered_at, exited_at, outcome)

Cross-cutting (any stage):

feedback (application_id, stage_key, version_no, author_id, content, created_at)
appeals (application_id, stage_key, raised_by, against_stage_history_id, status)
resolutions (appeal_id, resolved_by, decision, comment, resulting_stage_key)

Appeals reference (application_id, stage_key) / stage_history — they do not need to know the handler strategy. Resolver derives from that stage’s assigning_authority.

Version-lock: application_workflow_state.workflow_template_id points at the template version active when the application started so editing future templates does not break in-flight apps.

Seed RPL and NSQ as rows via migration (no platform-admin config UI yet).

Path Mid-pipeline (simplified) Shared tail
RPL application form → payment → folder arrangement → interview (panel_evaluation) IV → EV → certification
NSQ application form → payment → induction (form_submission) → regular assessment (evidence_review) IV → EV → certification

For the single-application view, Application Form and Payment become real stage_definitions rows (not only derived UI state), so GET /applications/{id}/stages reads one uniform stage_history source for:

  1. Application Form
  2. Payment
  3. Folder Arrangement
  4. Interview
  5. IV
  6. EV
  7. Certification

Relative-time labels (“3 days left”) are client-computed from raw status + dates — not returned by the API.

interface StageHandlerStrategy {
readonly key: string;
assign(ctx: StageContext, actors: Actor[]): Promise<void>;
submitEvaluation(ctx: StageContext, input: EvaluationInput): Promise<void>;
evaluate(ctx: StageContext): Promise<StageOutcome>;
}
const strategyRegistry: Record<string, StageHandlerStrategy> = {
single_approval: /* reviewer, facilitator final, IV, EV */,
panel_evaluation: /* 3 panelists + lead; signatures; candidate sign-off — RPL */,
evidence_review: /* NSQ NOS evidence / DO+PRF; persists AssessorAssignment */,
form_submission: /* NSQ induction — candidate self-service, no external approver */,
// TODO (§28): self_service — Application Form draft/submit as a real stage
// TODO (§28): external_gate — Payment stage whose "approval" comes from webhook, not an actor
};

Engine: look up handler_strategy from stage_definitions → dispatch → persist outcome to stage_history → advance current_stage_key on approve, or write feedback / bump application_versions on reject.

Planned strategies (TODO — not implemented)

Section titled “Planned strategies (TODO — not implemented)”
Strategy Intended use Status
self_service Application Form as a tracked stage (draft/submit) TODO — need described; seeded stage_definitions rows not written yet
external_gate Payment stage completed by payment.completed webhook, not an actor TODO — same

Until these land, payment unlock and application submit continue to work via existing entry conditions / consumers; the seven-stage API surface expects the fuller model once CAP-36 (implementation checklist) completes.

UNIT_ASSESSOR / AssessorAssignment (NSQ-only)

Section titled “UNIT_ASSESSOR / AssessorAssignment (NSQ-only)”
  • NOS form sign-off roles: LEARNER | UNIT_ASSESSOR | IQA | EQA. UNIT_ASSESSOR is a contextual role (not identity-level AssessorProfile).
  • AssessorAssignment is structurally parallel to IvAssignment / EvAssignment; EvidenceReviewStrategy.assign() persists to it.
  • For NSQ DO, the UNIT_ASSESSOR role is filled by someone holding QAA.
  • RPL has no unit sign-off. RPL uses interview panel evaluation only. UnitAssessmentRecord, PerformanceCriteriaEvidence, UnitSignoff, and AssessorAssignment are never created for RPL (templates never use evidence_review for that path).
  • RPL Folder Arrangement uses the Evidence Vault models instead — see Data model.
  • LEARNER needs no assignment table — always via application.candidateId.
  • ApplicationUnit remains shared by both application types.
Now Later
Tables + four strategies + seeded RPL/NSQ templates Platform-admin workflow editor UI
Candidate self-submit induction Optional centre reviewer on induction (product open)
Document need for self_service / external_gate Implement + seed those strategies (§28 TODO)