---
title: "Applicant Lifecycle"
slug: applicant-lifecycle
section: Applicant Journey
status: stable
description: "A structured lifecycle built on a fixed 8-phase reporting spine, program-specific Stages, Readiness, Queues, and compliance gates"
last-updated: 2026-10-07
---

# Applicant Lifecycle

# Applicant Lifecycle Model

A structured lifecycle built on a fixed 8-phase reporting spine, program-specific Stages, Readiness, Queues, and compliance gates

> **From Status Labels to Lifecycle Phases**
>
> Legacy systems often use status labels that mix operational, compliance, and placement states into a flat list. Globus introduces a structured lifecycle where each phase has a clear focus, defined actors, explicit transition triggers, and compliance gates between phases.

> **Lifecycle is per-application, identity is per-applicant**
>
> Every phase, status, and transition on this page belongs to a single application row — one attempt at one program in one season. The person’s identity (name, email, nationality, brand, external ID) lives separately on the applicant row and is reused across every attempt. For the canonical boundary, the field-level rule of thumb, the document split, and the per-portal visibility matrix, see Applicant Data vs Application Data .

## Operating Model

The applicant lifecycle is built on a small set of distinct layers. The core architecture is Phase + Stage, Readiness, and Queues. Two further layers — Processing Areas (diagnostic) and Progress Milestones (presentation) — sit alongside the core to answer “why is the applicant stuck?” and “what is their progress summary?” without ever becoming the source of truth. Each layer serves a different purpose and avoids the status explosion that plagues flat systems.

| Layer | Purpose | Definition | Example |
| --- | --- | --- | --- |
| Lifecycle Phase | Reporting spine (fixed) | Where the applicant is, for reporting. 8 platform-defined phases, the same for every program; cannot be customized. Derived from the current Stage — never set manually. | Phase 4 — Placement |
| Stage | Milestone (program-specific) | Furthest verified program milestone. Configurable per program via Stage Management; each Stage belongs to exactly one Phase. | Hired (owned by Placement) |
| Work Type | Activity (operational) | Actual operational activities happening underneath. Interviews, document waves, compliance tasks. | Host Interview, Document Wave 2 |
| Readiness | Can they move? (computed) | System-calculated read-model of whether the applicant can advance or is blocked. Computed from 5 signal sources; never manually edited, never the source of truth. | Ready to be Showcased ✅ |
| Queue | What needs action? (views) | How work is organized for staff. Derived from Readiness. No stored queue field. | Host Interviews queue |
| Processing Areas | Why stuck? (diagnostic) | Pre-SEVIS work running concurrently and out of order. Never overrides the current Stage or Phase. | Documentation area blocked |
| Progress Milestones | Progress summary (presentation) | Applicant Progress Summary showing which major requirements are complete/incomplete. A presentation layer — explicitly not Readiness. | 4 of 6 milestones complete |

> **Interview = Work Type, not Status**
>
> Sponsor interviews and host interviews are modeled as Work Types, not statuses. This avoids status explosion while still providing scheduling, tracking, queues, and SLA enforcement. An applicant in “Under Review” status may have a sponsor interview work item — the interview doesn’t change the lifecycle status.

## Phase & Stage Derivation

The relationship between Phase and Stage is the load-bearing rule of the whole model. A Lifecycle Phase is platform-defined — the same 8 phases for every program, never customizable. A Stage is a program-specific milestone defined per program. The two are bound by one rule: every Stage belongs to exactly one Phase, and the applicant’s Phase is simply the Phase that owns their current Stage.

> **Phase is derived from Stage — forward-only, never manual**
>
> Phase is never stored or edited directly. It is derived from the applicant’s current Stage, where the current Stage is the furthest verified milestone the applicant has reached. Derivation is forward-only: a Stage advances when a milestone is verified and is not automatically pulled back because some other piece of work is still outstanding. Because Stages are ordered and each maps to a Phase, the reporting spine moves forward monotonically.

Derivation chain

verified milestones / \u2192 / furthest = current Stage / owning Phase / reporting spine position
> **Stage is the source of truth — completion percentage never drives it**
>
> Workflow completion percentage is a progress and readiness signal only. It may light up progress bars and incomplete-work warnings, but it must never set current_stage_id, journey_position, or the Lifecycle Phase. Stage advances only when a verified milestone is achieved — a submitted step is not an approved milestone — and journey_position is a derived mirror of the current Stage, not an independent field. A 55%-complete applicant who is Hired resolves to Stage Hired / Phase Placement / Readiness blocked, never back to Interview.

> **One writer, always logged — application_stage_history**
>
> Every Stage change — manual, reconciliation, bulk action, rules-triggered, or system/supersession — flows through a single shared writer (transitionApplicationStage) owned by Globus-core. No code path writes current_stage_id or journey_position directly (migrations and seeds excepted, with a comment). The writer derives the Phase from the target Stage, prevents backwards movement unless an admin override is supplied, and writes an application_stage_history row (from_stage_id, to_stage_id, change_source, actor, reason, timestamp) for every transition — history that cannot be reconstructed after the fact. This writer is the seam a full Lifecycle Transition Service grows into later.

> **Two signals move the Stage — verified milestones and live evidence**
>
> Automatic advancement resolves candidate target Stages from two sources and applies them only through the shared writer above, forward-only. (1) Verified milestones: a completed phase-completion event whose target is the status-change target authored on the workflow phase. (2) Live evidence, read from the same records the parallel pre-SEVIS tracks already trust: an interview track that has left not_started targets Sponsor Interview; a live (active or confirmed) reservation hold targets Reserved to Host; a live canonical placement (confirmed or active) targets Hired directly, and a confirmed placement never fabricates Host Interview. Candidates are tried forward-most first. The writer still owns placement readiness, authored edges, phase-gate rules and forward-only ordering, so when it refuses a move the applicant stays at the last legitimate Stage, and the refused or pending target is visible in the reconciliation outcome. Because targets are derived from live state, the periodic reconcile sweep converges applicants that are behind their own evidence. The sanctioned backwards moves are narrow. First, when the last live reservation ends (declined, cancelled, released or expired) and the applicant is still at Reserved to Host, an audited system transition with an explicit override returns them to Sponsor Interview, recorded in application_stage_history. Second, when the last live placement ends and the application is at Hired, with no live placement remaining, a transition through `transitionApplicationStage` with an explicit override and change source `system` moves it to Reserved to Host while a live hold remains and otherwise to Sponsor Interview; the move is written to stage history. The release accepts a `strict` option so that durable cancellation workers retry failures rather than silently succeed: when it is set, any failure, including a cancellation target stage that is not configured, is thrown instead of swallowed. The transition is requested with `skipReadinessRecompute` set to true.
> 
> Placement (Reserved to Host, Host Interview, Hired) is a durable segment: an automatic move may enter or continue through it but may never start before it and land after it. A milestone target that would leapfrog it is a workflow configuration defect, not applicant state — the move is refused, the originating phase is told the applicant must enter the Placement stage first, and it is retried on later reconciliations once evidence has carried the applicant into the segment. Self-arranged placement programs are exempt, since placement is work they never do.

Milestone target validation. A workflow phase's status-change target (`milestoneStatusChange`) is checked whenever a phase is created or updated, and an illegitimate target is refused with 400 `VALIDATION_ERROR` whose `details.code` names the problem (with `phaseKey`, `authoredTarget` and, where relevant, `relatedPhaseKey`): `UNKNOWN_STAGE_TARGET` (not a lifecycle stage), `INACTIVE_STAGE_TARGET` (a retired stage), `TERMINAL_STAGE_TARGET` (a terminal outcome is recorded by a staff decision, never by completing a phase), `TARGET_BEHIND_PREVIOUS_PHASE` (not further along than the earlier phase's target) and `TARGET_AHEAD_OF_NEXT_PHASE` (not before the later phase's target). Clearing a target is always allowed. The same rules feed the publish-time warnings, the staff coverage report and the stalled-applicant explanation; for an application workflow they also report `NO_MILESTONE_TARGETS` (no phase has a usable target, so completing phases can never move an applicant past the first stage) and `STAGES_UNREACHABLE` (phase targets stop short of later stages, which are listed).

### Worked Example — Hired before documents are clear

A Camp Counselor applicant is matched and accepts a host offer. The program’s Hired Stage is owned by the Placement Phase, so the moment Hired is verified the applicant’s Phase derives to Phase 4 — Placement — even though their passport (Documentation) is still unverified and their sponsor interview (Screening) has not been conducted.

| Signal | Value for this applicant |
| --- | --- |
| Current Stage (furthest verified) | Hired |
| Derived Lifecycle Phase | Phase 4 — Placement |
| Documentation Processing Area | Incomplete — passport not yet verified |
| Screening Processing Area | Incomplete — sponsor interview not conducted |
| Readiness | Blocked (cannot reach SEVIS until areas clear) |
| Surfaced via | Queues, Blocking Reasons, Processing Diagnostics — not by changing Phase |

> **The outstanding work does not move the Phase backward**
>
> The missing passport and un-conducted interview do not drag the applicant back to Validation or Interview. They surface as Readiness = blocked, as memberships in the relevant Queues, as explicit Blocking Reasons, and in the Processing Diagnostics view — the Phase stays at Placement because that is the furthest verified Stage. This is exactly why Phase alone is never enough to operate from: it is the reporting spine, and Readiness, Queues, and Processing Areas carry the operational detail.

## Stage Management — Program Lifecycle Builder

Because Phases are fixed but programs differ, the program-specific variation lives entirely in Stages. Stage Management (the Program Lifecycle Builder) is the configuration surface where a program administrator defines the ordered list of Stages for a program and binds each one to a Phase. This is how a Camp Counselor program and an H-2B program can share the same 8-phase reporting spine while running very different milestone sets underneath.

### Stage Attributes

| Attribute | Definition |
| --- | --- |
| Name | Human-readable milestone label shown to staff (e.g. “Hired”, “Contract Signed”). |
| Owning Phase | Exactly one of the 8 fixed Lifecycle Phases. Determines where the Stage lands on the reporting spine. |
| Program / workflow applicability | Which program(s) and workflow version(s) this Stage applies to. Stage definitions are global rows in `stage_definitions` (optionally limited by `applies_to_program_ids` and visa types); a program overrides a stage's name, description, sort order and active flag in `program_stage_overrides`. |
| Sort order | Ordinal position within the program’s Stage list. Establishes which Stage is “further” for forward-only derivation. |
| Active / inactive | Whether the Stage is currently in use. Inactive Stages are retained for historical applications but not assignable to new ones. |
| Allowed transitions | The set of Stages this Stage may advance to. Constrains the milestone graph without re-encoding it as Phases. |

Supporting tables (BackOfficePrototype): `program_stage_overrides` holds per-program overrides of a stage's name, description, sort order and active flag, unique on program and `stage_key`. `application_track_stages` holds one current stage per application and pre-SEVIS track (`application`, `validation`, `interview`, `placement`), advanced by the reconciliation stage projector, with `changed_by_user_id`, `change_source` and `reason` on manual moves. `requirement_gates` holds one row per application and `gate_type` with `state` (default `missing`), `notes` and override fields (`is_overridden`, `override_reason`, `overridden_by`, `overridden_at`). `application_classification_assertions` is an append-only record of who asserted a classification and on what basis: `field` is `j1_participation_classification`, `placement_source`, `proposed_employer_known` or `camp_same_camp_return`, `provenance` is `programme_backfill`, `staff_attestation` or `applicant_declaration`, and a correction is a new row that supersedes the old one, never an update. `applicant_alerts` holds internal-only, person-level banner cards for the applicant profile (`title`, `body`, `category`, `severity` of `info`, `warning` or `critical`, `is_active`). `applicant_contact_history` records changes to email, phone and preferred name with `previous_value`, `new_value` and `source` (`staff`, `portal`, `api_key` or `system`). `application_notes` holds staff notes per application with `visibility` (default `internal`), optional `category` and `is_pinned`.

**Program stage overrides.** `GET /api/admin/program-stage-overrides?programId=` lists a program's overrides (400 VALIDATION_ERROR when `programId` is missing), `POST /api/admin/program-stage-overrides` upserts one (create or update, 201), and `DELETE /api/admin/program-stage-overrides/:id` removes one (404 NOT_FOUND when absent). All three need `programs.manage`; the reads use API key scope `admin.read` and the writes `admin.write`.

### Program Examples

The same 8 Phases, three different Stage sets. Each Stage binds to exactly one Phase; the Phase column is the reporting spine, the Stage column is the program-specific milestone.

| Program | Example Stage | Owning Phase |
| --- | --- | --- |
| Camp Counselor | Showcased → Hired | Placement |
| Camp Counselor | DS-2019 Issued | Visa & SEVIS |
| Teach USA | Credential Review Passed | Validation |
| Teach USA | School Interview Accepted | Interview |
| H-2B | Employer Contract Signed | Placement |
| H-2B | Consular Appointment Booked | Visa & SEVIS |

> **Builder runtime lives in Globus-core**
>
> This page is the specification for Stage Management. The Stage Builder UI runtime and the Stage–Phase reconciliation engine that derives Phase from the furthest verified Stage are owned by the Globus-core platform, not the docs-site. Configuration changes are made there; this page defines the contract those systems implement.

## Processing Areas (Pre-SEVIS Concurrency)

The 8-phase spine above is a reporting order, not a march. Before SEVIS, the underlying work does not run in a strict line: an applicant can land a host offer (Placement complete) while their pre-showcase documents are still being cleared. Globus models this pre-SEVIS work as four Processing Areas — Intake, Documentation, Screening, and Placement — that progress concurrently and out of order. The Applicant Queue's “Also in” column exists precisely because a single applicant routinely sits in multiple Processing Areas at the same time.

> **Processing Areas are diagnostic — they never override Stage or Phase**
>
> Processing Areas answer “why is this applicant stuck?”. They are a diagnostic view over the same work items, document waves, and placement records that already exist — not a second source of truth. An applicant’s Stage (and the Phase derived from it) is set by the furthest verified milestone and is never pulled back because one Processing Area is still in flight. A participant can be in Phase 4 (Placement) while their Documentation area is still incomplete.

> **Concurrent until SEVIS, sequential after**
>
> Pre-SEVIS (Processing Areas): Intake, Documentation, Screening, Placement run in parallel with independent state machines. Order is not enforced — only completion is. SEVIS (gate): A single convergence point. SEVIS entry requires every Processing Area to be in a terminal-pass state. Post-SEVIS (phases): Pre-Departure, In Program, Completion run sequentially — they depend on regulatory state and arrival, which are inherently ordered.

### Per-Area State Model

Each Processing Area has its own trigger-in, its own terminal-pass state, and its own gate-out condition. An area can be in flight, blocked, or terminal independently of the others.

| Processing Area | Trigger-in | In-flight states | Terminal-pass (gate-out) |
| --- | --- | --- | --- |
| Intake | Account created (direct or partner invite) | Invited, In Progress, Submitted | Submitted & required fields complete |
| Documentation | Wave 1 document uploaded or required field saved | Awaiting Docs, In Review, Additional Info Required | All Wave 1 docs verified & background check cleared |
| Screening | Sponsor or host requests interview (work item) | Scheduled, Rescheduled, Conducted, Pending Outcome | Outcome = passed (sponsor) / accepted (host) |
| Placement | Applicant flagged eligible-for-placement | Showcased, Awaiting Host Decision, Reserved | Confirmed placement against an active job order |

Processing Areas are not new entities — they are views over the same work items, document waves, and placement records that already exist. The Processing Area abstraction names what each bucket of work owns and when it is “done enough” for SEVIS, without forcing the buckets into a serial order.

### SEVIS as the Convergence Gate

SEVIS is the single point in the lifecycle where every Processing Area must converge. The rules engine evaluates sevis_ready by AND-ing the four area-terminal flags above with the regulatory readiness checks (passport on file, SEVIS fee receipt, DS-160 stub). An applicant can't enter the SEVIS pool with three areas green and one in flight — the gate is hard.

&rarr; SEVIS gate
Four Processing Areas in any order &rarr; one gate &rarr; Pre-Departure / In Program / Completion in sequence.

### Step Locking & Area Convergence

Step locking inherits this Processing Area model directly. Whether a workflow step is editable, view_only, locked, or hidden is derived by Globus and rendered by the Applicant Portal — the full contract lives in Step Access & Lock Contract . The lifecycle rule that governs when a downstream step unlocks is the one on this page:

- Before SEVIS — concurrent. Steps inside the Intake, Documentation, Screening, and Placement Processing Areas lock only on their own intra-area prerequisites (and on compliance holds). A step in one area must not be locked merely because another area is unfinished — a strict “previous phase complete” gate would wrongly serialize work that is designed to run in parallel.
- At the gate — convergence. Steps that depend on sevis_ready stay locked until all four area-terminal flags AND the regulatory readiness checks resolve. This is the hard gate described above.
- After SEVIS — sequential. Pre-Departure, In Program, and Completion steps unlock in order, so post-gate locking is a linear phase chain.
Access is always evaluated against the application’s pinned workflow version, so re-publishing a workflow never retroactively re-locks an in-flight applicant; recomputation rides on application.lifecycle_reconciled and is pushed to the portal as step.lock_changed .

Workflow records (BackOfficePrototype): `compliance_snapshots` stores point-in-time compliance results per application, with optional workflow instance, program and season, `trigger` (for example `sevis_pool_entry`, `issuance`, `manual`, `scheduled`, `gate_check`), `overall_status`, pass, fail, pending and needs-review counts, and the `checks` and `inputs_used` JSON. `phase_completion_events` records each detected phase completion per application, workflow definition and `phase_key`, with `status`, flags for the milestone notification, status change and auto-fire, the previous and new application status, `manual_confirmation_required` with confirmer, and a `dedupe_key`. `workflow_audit_events` is the audit trail of a workflow definition (`action`, actor, `before_json`, `after_json`, `diff_json`, `correlation_id`). `workflow_reconciliation_runs` records each reconciliation of a program's workflow definition (`definition_version`, `trigger_source`, applications affected, steps added, removed and moved, `results_json`). `workflow_linked_objects` links a workflow definition to the rule definitions and rule bundles imported with it (`linked_type` of `rule_definition` or `rule_bundle`, natural `linked_key`); the publish gate refuses to publish the workflow while a linked object is unpublished.

## Progress Milestones — Applicant Progress Summary

Progress Milestones are a presentation layer: an Applicant Progress Summary that shows, at a glance, which major requirements are complete and which are still outstanding. They make a complex application legible to staff, partners, and the applicant — a “4 of 6 complete” read rather than a wall of statuses. Progress Milestones are explicitly not Readiness: they describe what is done, not whether the applicant can move.

| Layer | Answers | Nature |
| --- | --- | --- |
| Progress Milestones | “How much of the application is done?” — a completeness summary across major requirements. | Presentation (read-only summary) |
| Readiness | “Can the applicant advance, or are they blocked?” — a single computed move/blocked verdict. | Computed read-model |

> **Readiness is a read-model — never manually set**
>
> Readiness is system-calculated and is never edited by hand. Its source of truth is the underlying operational signal set: workflow step completions, document reviews, Rules Engine evaluations, interview outcomes, placement records, and integration events. Staff never “set an applicant to ready” — they complete the underlying work, and Readiness recomputes. This is what keeps the Phase / Stage spine, the Queues, and the Progress Summary all consistent with one another.

## Work Types & Document Waves

Work Types represent the actual operational activities that happen within each phase. They are tracked as work items attached to an applicant record — separate from the lifecycle status.

#### Interviews

Sponsor Interview (Phase 3)

Conducted by sponsor staff during the Interview phase. Used for compliance validation, cultural fit assessment, or program-specific requirements. Outcome: passed / failed / reschedule.

Host Interview (Phase 3)

Conducted by the host employer after the applicant is showcased. Required before placement confirmation for most programs. Outcome: accepted / rejected / reschedule. See Interview Result Capture for how the outcome is recorded, gated, and fed to placement.

#### Document Waves

Wave 1 — Pre-Showcase (Phases 1–2)

Documents needed to present the applicant to hosts: passport, profile photo, background check consent, skills assessment, application forms. Reviewed during Application and Validation.

Wave 2 — Post-Showcase (Phase 4)

Documents required after placement is identified but before SEVIS submission: medical clearance, financial disclosures, SEVIS fee receipt, housing agreements, insurance documentation.

> **Work Item Data Model**
>
> Each work type instance is stored as a work item linked to the applicant. Key fields: work_type, status (pending / in_progress / completed), scheduled_at (for interviews), outcome, assigned_to, and notes. For interviews, additional fields: interview_type (sponsor / host), interviewer_id, job_order_id (host interviews).

Lifecycle Flow

## Portal Phase Mapping

The Globus lifecycle defines 8 fixed operational phases — the full truth of how an applicant moves from invitation to program completion. However, the Applicant Portal simplifies this into 3 phases, showing only what the applicant needs to act on and hiding back-office work such as sponsor interviews, host matching, SEVIS processing, and DS-2019 generation.

| Portal Phase | Globus Phases | What the Applicant Sees | What's Hidden (Back-Office) |
| --- | --- | --- | --- |
| | Phase 1 (Application) + Phase 2 (Validation) + Phase 3 (Interview) | Personal Information About Me Skills & Hobbies ( Camp SWT / Camp Counselor taxonomy) Showcase Yourself Application (submit) Terms & Conditions | Sponsor Review Sponsor Interview Rules Engine auto-screening |
| | Phase 4 (Placement) + Phase 5 (Visa & SEVIS) | Camp Contract (program-specific) Criminal Background Check Passport Information Reference Details | Host Matching Host Interview Reservation Management SEVIS Processing DS-2019 Generation |
| | Phase 6 (Pre-Departure) + Phase 7 (In Program) + Phase 8 (Completion) | VISA Information Flight Arrival Details Online Orientation | SEVIS Check-In enforcement Compliance Monitoring Completion Processing SEVIS Closure Incident Handling |

Note: Once on-program, the applicant interacts with the portal for monthly check-ins, SEVIS check-in, and travel requests — but these actions occur outside the 3-phase stepper. The stepper tracks pre-arrival progression; on-program engagement is handled through separate portal screens.

### Behind the Scenes

The following Globus work types are invisible to the applicant in the portal. They are executed by sponsor staff, host organizations, or automated systems — but the applicant is never asked to take action on them. Which placement milestones *should* become participant-visible communications — and which remain hidden — is specified in [Participant Placement Journey & Notifications](/docs/participant-placement-notifications).

> **Interviews are behind-the-scenes step nodes — not hidden applicant steps**
>
> Sponsor and host interviews remain real nodes in the program’s Workflow Studio step tree — admins author and see them in the builder. They are flagged behind-the-scenes by role assignment (staff-only or host-only), so they are excluded from the applicant payload entirely and never render on the applicant stepper. This is different from a conditionally hidden applicant step (a data-driven hide of an applicant-facing step): a behind-the-scenes interview is never sent to the portal at all — it is not transmitted as access = "hidden". The interview still produces an outcome fact that gates the phase (see Interview Result Capture ).

### Program-Specific Variation

> **Steps Vary by Program — Structure Does Not**
>
> The specific steps shown within each portal phase vary depending on the program. For example, Camp Counselor applicants see a "Camp Contract" step in Phase 2, while Teach USA applicants would see different program-specific steps in the same position. However, the 3-phase structure is consistent across all programs — every applicant experiences Apply → Documents & Compliance → Visa & Travel regardless of their program.

The portal stepper as seen by a Camp Counselor applicant. Phase 1 collects application data, Phase 2 gathers compliance documents, and Phase 3 handles visa and travel logistics. The specific steps within each phase are program-configured.

Camp programs use a Camp-specific Personal Information step (`personal_information_camp_v1`). It carries the shared identity fields, with the name labels reworded to "Given name(s) (as shown on passport)" and "Surname (as shown on passport)", plus two camp sections: Program Availability (required earliest start and latest finish dates at camp) and Camp Placement (a required "Do you already have a position at a camp for this season?" yes/no question; answering Yes reveals the required "previously worked at this same camp" and camp name questions, and answering No hides them without validating them). The migration repoints only Camp Counselor (`camp_counselor_%`) application workflow drafts to it; published and archived workflow history and other programs' drafts are unchanged. The Camp applicant step seeds also define a Passport step version (`passport_information_v1_1`) that opens with a four-step photo guidance panel with illustrations (open and place flat, take photo, check, upload), adds a required "Date of birth shown on passport" field after the surname, and labels the name field "Given name(s) (as shown on passport)".

### Stepper Fallback (Over-Cap Scroll)

Workflow Studio enforces structural caps of 7 steps per phase and 6 phases per workflow at publish time (see Workflow Studio — Publish-Time Cap Validation ). The applicant portal is not the enforcement point. It must still render legacy data, override paths, and edge cases without ever wrapping, truncating, or hiding steps.

Step row — horizontal scroll

If the active phase contains more than 7 rendered steps, the step-circles row stays in a single horizontal line and scrolls horizontally within the phase row. No step is wrapped, hidden, condensed, or truncated.

Phase tabs — horizontal scroll

If the workflow contains more than 6 phases, the phase-tab row above the stepper follows the same rule: single horizontal line, scrolls horizontally, never wraps.

Active item is the scroll anchor

On initial render, both rows are scrolled so the applicant’s active step (and active phase) are visible without manual scrolling. The applicant lands looking at where they are, not at step 1.

Compliant workflows render with no scroll

When step and phase counts are within the caps, both rows fit without scroll and no scroll affordance is shown.

> **Portal does not enforce caps**
>
> The applicant portal renders defensively but never logs, warns about, surfaces, or modifies a workflow because of a cap violation. Enforcement lives entirely in Workflow Studio (publish-time) and the staff-targeted workflow_cap_violation_detected notification. See also the Workflow Studio Guide — Limits & Guardrails for the admin-facing summary. The drafted Asana ticket for this portal behavior lives in the Implementation Tickets — Step & Phase Caps appendix on the Workflow Studio page (Ticket B).

### Lifecycle Stepper — Current-Step Pulse

The portal stepper continuously pulses the applicant’s current step so the active position is unmistakable at a glance. The active step circle animates with a soft pulse while completed and upcoming steps remain static, anchoring the applicant to “where I am now” the moment the stepper renders.

Single active indicator

Exactly one step pulses at a time — the current step derived from the applicant’s phase and readiness. Completed steps show the done state; future steps stay neutral.

Pairs with the scroll anchor

The pulse reinforces the over-cap scroll behavior above: the active step is both scrolled into view and visually pulsing, so the applicant lands on their current step rather than step 1.

> **Shipped — On-Program Dashboard & Stepper**
>
> The current-step pulse is implemented and shipped under the On-Program Dashboard & Stepper project, which owns the program lifecycle stepper. Runtime ownership is the On-Program Lifecycle delivery area; this docs page is the spec source.

## Transition Triggers & Compliance Gates

Every phase transition is governed by a specific trigger and a compliance gate. The Rules Engine evaluates readiness at each gate by running the 5-step evaluation pipeline (Collect &rarr; Resolve &rarr; Evaluate &rarr; Decide &rarr; Act) against structured rule definitions from the rule_definitions table. Each transition produces an immutable rule_evaluation_records entry. No manual override can bypass a compliance gate without producing an audit-logged rule_overrides exception record.

> **Payment Gates at Phase Transitions**
>
> The Placement &rarr; Visa & SEVIS and Visa & SEVIS &rarr; Pre-Departure transitions include payment gates enforced by the Rules Engine. These gates check application_finance_summary.financeStatus and block progression until payment conditions are met. For full payment gate details, see Finance & NetSuite — Compliance Payment Gates .

**Readiness gates and terminal status errors.** A stage transition (`services/stages.ts`) runs the Rules Engine phase gate; when it blocks, the transition is refused with 422 `PHASE_GATE_BLOCKED`, and `details` carries `applicationId`, `fromPhase`, `toPhase`, `blockingRules`, `reviewRequiredRules`, `indeterminateRules` and `aggregatedRequiredActions`. Entering the stage keys `visa`, `pre_departure`, `on_program` or `program_completion` also requires a pass on the matching global operational gate (Ready for Visa, Departure, Arrival, Completion), each of which depends on the prior gate; an explicit admin override skips this readiness check. A gate that is not ready, or has an unevaluated dependency, refuses the move with 422 `VISA_NOT_READY`, `DEPARTURE_NOT_READY`, `ARRIVAL_NOT_READY` or `COMPLETION_NOT_READY`; `details` holds `reasonCode` (`VISA_PREREQUISITES_NOT_MET`, `DEPARTURE_PREREQUISITES_NOT_MET`, `ARRIVAL_PREREQUISITES_NOT_MET` or `COMPLETION_PREREQUISITES_NOT_MET`), `queueKey` `blocked`, `readinessImpact` `blocked` and `failedDependencies`. Staff end an application with `POST /api/applications/:id/end` (permission `stage.transition.terminate`; body `outcome` of `reject`, `disqualify`, `withdraw` or `cancel`, required `reasonCodeId`, optional `notes`); an application already in a terminal status (`withdrawn`, `rejected`, `lapsed`, `cancelled`, `completed`) is refused with 409 `CONFLICT` whose `details.code` is `APPLICATION_STATUS_TERMINAL`.

## Exception Paths

Not every applicant follows the happy path. Globus defines structured exception paths for common disruptions. Every exception produces an audit record and triggers the appropriate downstream actions.

`application_status_history` is the append-only trail of every application status transition: `from_status`, `to_status`, `reason_code_id` (required when the new status is terminal), `actor_id` (null when the system made the change), `actor_kind` (`system`, `staff` or `applicant`), `notes` and `created_at`. It preserves the prior terminal reason code when an application is reactivated and the live row's reason is cleared. The staff End application action (`POST /api/applications/:id/end`) writes to it through the status transition service.

> **Override Authority**
>
> Sponsor Staff can override certain business rules for edge cases (e.g., expedited processing, government exceptions). Compliance rules can only be overridden by users with compliance.rules.manage permission. Every override is stored in rule_overrides with the original rule outcome, justification, authority level, and before/after state. See Rules Engine — Override Mechanism .

A `compliance_case_item` represents an independently governed rule request within a Compliance Case. A case can group several rules, where each item retains its own decision, evidence and lifecycle. The status of a `rule_overrides` record can include `superseded`.

Lifecycle exceptions are stored in `lifecycle_exceptions` (application, `exception_type`, `reason`, `triggered_by`, `trigger_source` default `manual`, `previous_stage_key`, `resulting_stage_key`, `affected_entities`, `resolution_status` default `pending`, resolver, `resolved_at`, `resolution_notes`) and managed at `/api/admin/lifecycle-exceptions`, all requiring `compliance.view`. `GET` lists them (filters `applicationId`, `exceptionType`, `resolutionStatus`), `GET /:id` reads one (404 when missing), `POST` creates one (201, `triggered_by` set from the caller), `PATCH /:id` updates any field, and `POST /:id/resolve` (required body `notes`) sets `resolution_status` to `resolved` with `resolved_by`, `resolved_at` and `resolution_notes`. Create, update and resolve are audited as `lifecycle_exception.create`, `lifecycle_exception.update` and `lifecycle_exception.resolve`.

**Orphaned applicants (operator check).** `GET /api/admin/orphaned-applicants` (permission `applicant.notes.edit`) lists applicants with no program association that were created more than a grace window ago. The window defaults to 24 hours and the `olderThanHours` query parameter overrides it; a non-numeric value returns 400 `VALIDATION_ERROR`. The response has `count`, `sample` (up to 25 applicants) and `olderThanHours`, and the count is capped at 500.

## DS-2019 Resubmission Flow

When an applicant’s DS-2019 has already been issued (isIssued = true) and certain biographical or site-of-activity fields subsequently change, the application is flagged for DS-2019 resubmission. This triggers a structured flow from detection through SEVIS amendment resolution.

### Triggering Fields

Two categories of field changes trigger a DS-2019 resubmission requirement:

Biographical Changes 
firstName / lastName / dateOfBirth / nationality / passportNumber
Site of Activity Changes 
placementJobOrderId / placementLocationCountry
> **Reason Concatenation**
>
> If a previous resubmission reason already exists (e.g., a biographical change was flagged and then a site change also occurs), the new reason is appended with a semicolon separator. The full reason trail is preserved for audit purposes.

### Resubmission Lifecycle

The resubmission follows a four-stage lifecycle tracked on the application record:

| Stage | Field | Value | Description |
| --- | --- | --- | --- |
| Flagged | ds_resubmission_required | true | Field change detected on an issued DS-2019. Reason text generated automatically (e.g., "Biographical information changed: First Name, Passport Number") |
| Queued | ds_resubmission_requested_at | timestamp | Application appears in the DS-2019 Resubmission Queue for staff review |
| Amendment | (SEVIS batch) | batch created | Staff creates a SEVIS amendment batch to process the resubmission through government systems |
| Resolved | ds_resubmission_resolved_at | timestamp | New DS-2019 issued. ds_resubmission_required cleared to false. Application exits the resubmission queue |

> **Automatic Detection, Manual Resolution**
>
> The system automatically detects field changes that require resubmission — staff does not need to manually flag them. However, resolution requires explicit staff action: creating a SEVIS amendment batch and confirming the new DS-2019 was issued. This prevents premature closure.

## J-2 Dependent Tracking

J-2 dependents (spouses and minor children of J-1 participants) require their own SEVIS records, DS-2019 issuance, and lifecycle tracking. Globus manages the full J-2 dependent lifecycle end-to-end, linked to the primary J-1 applicant record. J-2 rules are evaluated as a distinct application_type: 'j2' in the Rules Engine , with cascade dependency logic that auto-terminates J-2 records when the primary J-1 is terminated.

J-2 Dependent Record

application_id, enrollment_id (link to the J-1 application and enrollment) / dependent_type (spouse | child), relationship / first_name, last_name, date_of_birth, nationality / passport_number, passport_expiry_date / sevis_id (assigned upon SEVIS creation) / ds2019_number / status (default active) / workflow_instance_id
Enforcement Rules

J-2 cannot be created without an approved J-1 applicant / J-2 DS-2019 issuance requires J-1 DS-2019 to be issued first / J-2 SEVIS record is terminated when J-1 record is terminated / J-2 biographical changes trigger independent DS-2019 resubmission / J-2 dependents appear on the J-1 applicant profile in Globus
### J-2 Lifecycle Stages

Created / \u2192 / Documents Uploaded / SEVIS Record Created / DS-2019 Issued / Active / Completed / Terminated
> **J-2 dependents follow the primary J-1 applicant lifecycle but maintain independent SEVIS records and DS-2019 documents.**
>
> When the J-1 applicant’s program ends or is terminated, all linked J-2 dependent records are automatically flagged for closure. J-2 dependent management is visible on the J-1 applicant’s profile in Globus with a dedicated dependents tab.

J-2 Finance Implications

*Status: design spec, not built. The charge catalogue (`financeChargeCatalog.ts`) has no `J2_*` charge keys (its keys are `PROGRAM_FEE`, `SEVIS_FEE`, `INSURANCE_FEE`, `HOUSING_DEPOSIT`, `EXTENSION_FEE`, `RENEWAL_FEE`), and no J-2 payment gate or 3+ dependent budget flag exists. The `J2_*` charges, events and gates below are not implemented.*

Each J-2 dependent incurs their own financial charges separate from the primary J-1 applicant. These charges are tracked on the J-1 applicant’s finance summary but are itemized as distinct charge entries in application_charges.

J-2 SEVIS Fee (J2_SEVIS_FEE) — government fee for creating the J-2 SEVIS record, triggered by J2_SEVIS_SUBMISSION_INITIATED event / J-2 Insurance Fee (J2_INSURANCE_FEE) — insurance coverage for each J-2 dependent, triggered when the primary J-1 placement is confirmed / Payment gate: J-2 DS-2019 issuance is blocked until J2_SEVIS_FEE is PAID — prevents issuing J-2 immigration documents for unpaid dependents / Budget tracker requirement: applicants with 3 or more J-2 dependents are flagged for finance review and require budget verification before charges are finalized / All J-2 charges are linked to the primary J-1 application_finance_summary — there is no separate finance summary per dependent
See Finance & NetSuite — Charge Trigger Events for J2_SEVIS_FEE and J2_INSURANCE_FEE trigger details.

### J-2 Workflow Evidence Scoping

Shipped Sep 15 2026. Each J-2 dependent runs its own pinned workflow instance (spouse or child definition), and document evidence for a dependent is scoped to that exact dependent:

- **Ownership is application-local and durable.** A new dependent is created transactionally from one exact `application_id`, a compatible enrollment, and the published spouse/child definition. Its workflow instance keeps `external_portal_application_id` null — that field is external sync metadata and is never used to associate a dependent checklist or document.
- **Evidence stays in its own lane.** A document uploaded for a dependent is evaluated against that dependent's pinned workflow version and that dependent's document lane. It can complete only a step authored for that dependent — never a principal-applicant step and never a sibling dependent's. Every required document mapped to the dependent step must be accepted before the step completes.
- **Fail closed on identity.** If the dependent cannot be proven to belong to the application's applicant/program enrollment, or has no pinned workflow, no status or snapshot is written.
- **Evidence completion is not staff approval.** A qualifying document decision records a system `completed` step through the canonical workflow-step writer and synchronises the portal snapshot; manual staff approval, completion or rejection remains authoritative. Rejection or resubmission reopens only a step this projection completed; repeats are idempotent. Dependent status and snapshot identity include the dependent id, so two people may safely share a step key.
- **Canonical passport mapping.** Both the spouse and child seeds author `j2_passport_copy` mapped to the `passport` document type, for draft and published definitions; seed repair and migration backfill preserve it without creating principal program-matrix requirements.
- **Deletion preserves evidence.** An empty, unstarted dependent can be hard-deleted (its unused workflow instance is cancelled). Once documents, review history, or a dependent workflow status/snapshot projection exist, deletion is rejected with a conflict; staff must use the retention or cancellation process. The rejection is lane-scoped and never touches the principal or a sibling.

**Dependent routes.** `POST /api/j2-dependents` (permission `applications.edit`, audited as `j2_dependent.create`) creates a dependent from the `applicationId` in the body and returns 201; a body without `applicationId` returns 409 `CONFLICT`. `POST /api/applications/:id/j2-dependents` does the same with the application in the path. `PATCH /api/j2-dependents/:id` (`applications.edit`) refuses `applicationId`, `enrollmentId` and `workflowInstanceId` in the body with 409 `CONFLICT`. `DELETE /api/j2-dependents/:id` (`applications.edit`) returns 404 for an unknown dependent; the evidence rule above returns 409 `CONFLICT` with `details.reason` `DURABLE_EVIDENCE_OR_PROJECTION_EXISTS` plus `dependentId`, `documentCount`, `statusCount` and `snapshotCount`.

## SEVIS Batch Processing

SEVIS operations are processed in batches to align with Department of State submission requirements. Each batch contains one or more participant records for creation, amendment, or status update. Globus tracks individual participant results within each batch.

| Batch Type | Purpose | Trigger |
| --- | --- | --- |
| Initial Creation | Create new SEVIS records for approved applicants entering the SEVIS pool | Applicant reaches Ready for SEVIS Pool readiness |
| Amendment | Update existing SEVIS records (bio changes, site of activity, program extension) | DS-2019 resubmission flagged or program extension approved |
| Status Update | Report check-in, completion, termination, or no-show to SEVIS | Lifecycle event (arrival confirmed, program ended, early termination) |
| J-2 Dependent | Create or update dependent SEVIS records linked to primary J-1 | J-2 dependent added or biographical data changed |

Batch selection requires more than readiness: a current Ready verdict and reviewed operational material are required before an applicant can be selected for a batch. An applicant is selectable only when it is ready and its SEVIS operational review status is `reviewed`; a ready applicant whose review is not complete cannot be selected and is shown with the prompt to complete the SEVIS operational review before selecting.

### Participant Results

Each participant in a batch receives an individual result status after SEVIS processing:

| Result | Meaning | Next Action |
| --- | --- | --- |
| Success | SEVIS accepted the record. SEVIS ID assigned or update confirmed. | Proceed to DS-2019 issuance or mark amendment resolved |
| Validation Error | SEVIS rejected due to data quality issue (e.g., duplicate, missing field) | Queued for staff review with error details. Staff corrects and resubmits. |
| Partial Success | Some participants in the batch succeeded, others failed | Successful records proceed; failed records queued individually |
| System Error | SEVIS API unreachable or timeout during submission | Entire batch retried automatically. Staff alerted after 3 failures. |

## Certificate Management

SEVIS communication requires valid digital certificates for API authentication. Globus manages certificate upload, validation, status tracking, and rotation to ensure uninterrupted SEVIS connectivity.

Certificate upload via Globus admin interface with drag-and-drop support / Automatic extraction of certificate metadata (issuer, subject, validity period) / Expiry monitoring with 30/14/7 day advance warning notifications / Active certificate indicator — only one certificate is active at a time per environment / Certificate history maintained for audit trail (previous certificates archived, not deleted)
### Fingerprint Validation

Every uploaded certificate is validated using its SHA-256 fingerprint to prevent duplicates and ensure integrity:

> **Certificate Expiry**
>
> If the active SEVIS certificate expires without a replacement, all SEVIS batch processing is automatically paused. Staff receives critical alerts via in-app notification and Slack. Batches are queued (not discarded) and resume once a valid certificate is activated.

## Biographical Data Sync

Globus synchronizes participant biographical data with SEVIS to ensure records match government systems. When biographical fields change in Globus (name, date of birth, nationality, passport), the system detects the delta and queues a SEVIS amendment.

Field-level change detection compares current values against last-synced SEVIS snapshot / Only changed fields are included in the SEVIS amendment payload (delta sync, not full sync) / Sync history is maintained per-participant showing field, old value, new value, sync timestamp, and batch ID / Failed syncs are retried in the next batch cycle with the cumulative delta
**Portal profile sync into Globus.** When the Applicant Portal syncs an applicant that already exists in Globus (a non-session caller, an API key, of `POST /api/applicants` that matches an existing applicant by id, Globus id or email; a staff session gets `409 DUPLICATE` instead), the portal's self-reported values win: `phone`, `nationality`, `dateOfBirth`, `gender`, `countryOfBirth` and `passportNumber` are overwritten whenever the incoming value is non-empty and differs from the stored one, and an empty incoming value never clears a field. The profile photo is the exception and stays fill-only: it is fetched and stored only when the applicant has no photo yet. Route detail is on [Globus Portal API](/docs/globus-portal-api).

### Environment Derivation

The SEVIS API environment (production vs. test/staging) is derived from the active certificate rather than a manual configuration toggle. This prevents accidental submissions to the wrong SEVIS environment.

Active certificate issued by the Department of State for the production SEVIS endpoint. All batch submissions affect real government records.

Test certificate pointing to the SEVIS test endpoint. Used for development, QA, and pre-production validation. No impact on real SEVIS records.

> **Certificate determines environment. No manual environment toggle exists.**
>
> This design eliminates the risk of accidentally submitting test data to production SEVIS or production data to the test endpoint. The certificate itself is the source of truth for which SEVIS environment Globus communicates with.

## Program Extension & Program Renewal Requests

> **Terminology: Program Extension vs Reservation Extension**
>
> Program Extension (this section) extends a participant’s authorized program duration on an active DS-2019. It is a lifecycle event that modifies the existing application timeline. Reservation Extension is unrelated — it refers to a partner extending a time-limited hold on a job position (default 7-day hold; the hold window `holdDurationDays` and extension cap `maxHoldExtensions` are rule-library parameters) and is documented in Partner Governance — Reservation Rules .

Program Extension and Program Renewal requests allow participants to continue beyond their original program end date. Program Extension modifies the existing application’s duration (same DS-2019, amended end date). Program Renewal creates a new linked application for a subsequent program season (new DS-2019). This subsystem handles duration validation, SEVIS amendment triggers, portal visibility, review queues, and carry-forward logic for multi-season scenarios. Both are evaluated as distinct application types (program_extension and program_renewal) in the Rules Engine , with rule resolution flattening the Brand &rarr; Program &rarr; Season hierarchy to determine which duration limits, placement requirements, and compliance gates apply.

### Program Extension — Duration Validation

Program Extension requests are validated against program-specific duration rules before entering the review queue:

| Validation Rule | Description | Enforcement |
| --- | --- | --- |
| Maximum duration | Total program duration (original + program extension) cannot exceed program-defined maximum | Hard block — request rejected with reason |
| Minimum remaining | Program extension must be requested before the program end date (configurable lead time per program) | Hard block — late requests flagged for manual review |
| Season boundary | Program extension cannot cross into a closed or non-existent season without explicit carry-forward approval | Soft block — requires supervisor override |
| Active placement | Participant must have an active, confirmed placement to request a program extension | Hard block — no active placement, no program extension |
| Compliance status | Participant must be in good compliance standing (no open compliance issues) | Soft block — queued for compliance review first |

### Program Extension — SEVIS Amendment Triggers

When a Program Extension is approved, a SEVIS amendment is automatically triggered to update the participant’s program end date in government records. The amendment follows the same batch processing pipeline as other SEVIS updates.

Program Extension Approved / \u2192 / SEVIS Amendment Queued / Batch Submitted / SEVIS Confirmed / New DS-2019 Issued
### Program Extension — Review Queue & Eligibility

Program Extension requests enter a dedicated review queue with eligibility pre-screening:

Portal visibility — applicants and partners can see Program Extension eligibility status on their dashboard when the program allows program extensions / Request submission — applicants submit Program Extension requests through the Applicant Portal with desired end date and justification / Pre-screening — Rules Engine evaluates duration limits, compliance standing, placement status, and EXTENSION_FEE payment status before queue entry. Payment status is checked against application_finance_summary or the specific program extension charge status (enforcement: hard block — program extension cannot proceed without EXTENSION_FEE in PAID or INVOICED status, configurable per program). / Review queue — approved requests enter the Program Extension Review queue with SLA tracking and priority based on program end date proximity / Staff decision — reviewer can approve, reject (with reason), or request additional information / Season validation — system verifies the program extension period falls within an active season boundary

**Approval failure.** Approving through `POST /api/admin/extension-requests/:id/transition` (permission `extension_requests.manage`) first extends the reservation hold and only then marks the request approved. If the hold cannot be extended the call returns 409 with top-level `error.code` `RESERVATION_EXTENSION_FAILED` (`details` is null) and the request stays unapproved. The message says what failed (the request has no linked application, `extensionDurationDays` is not a positive integer, no active reservation hold was found, the hold update failed, or the hold was no longer active when the extension was applied); the internal reason code is not returned to the caller.
### Program Extension — Carry-Forward Logic

*Status: cross-season carry-forward is not built. Approving a Program Extension (`extensionApprovalService.ts`) creates an `extension` application in the original application's season, linked back to the originating request through `sourceExtensionRequestId` (the prior application id is recorded in the `application.created` audit metadata), and extends the reservation hold. There is no `carry_forward_from_id` and no season-boundary logic.*

When a Program Extension crosses a season boundary, carry-forward logic determines how the participant’s record transitions between seasons:

The original season record is marked as completed with a program-extension carry-forward flag / A new application record is created in the target season, linked to the original via carry_forward_from_id / Placement, compliance history, and SEVIS linkage are preserved across the season boundary / The participant maintains their existing SEVIS ID — no new SEVIS record is created / Quota impact is calculated in the target season (program-extension participants count against target season quotas) / Partner attribution is preserved from the original application — no re-attribution on carry-forward
> **Program Extensions are lifecycle events, not new applications.**
>
> A Program Extension modifies the participant’s existing program timeline. It does not create a new application, restart the lifecycle, or require re-placement. The carry-forward mechanism handles season boundary transitions while preserving continuity of the participant’s record.

### Program Renewals

Unlike Program Extensions (which modify an existing application), Program Renewals create a new linked application for a subsequent program year or cycle. The Program Renewal workflow handles the transition between the completing application and the new application while preserving the participant’s history and SEVIS linkage.

Program Renewal Eligibility Check / \u2192 / Program Renewal Request Submitted / Program Renewal Approved / RENEWAL_FEE Triggered / New Application Created / SEVIS Continuation Linked
Program Renewal creates a new application record linked to the prior application (the prior application id is passed at creation and recorded in the `application.created` audit metadata; `renewal_requests.renewal_application_id` points back to the new application) — it is not a modification of the existing application / The new application enters the lifecycle at the Intake phase with pre-populated data carried forward from the original application / Program Renewal workflow type (RENEWAL) is distinct from standard applications — it follows a streamlined intake path since the participant is already known / RENEWAL_FEE is triggered upon Program Renewal approval (`renewal.approved` event) and must be paid before the new application can progress past the Review phase / The original application transitions to Completion with a program-renewal carry-forward flag / SEVIS continuity is maintained — the new application references the same SEVIS ID when the program and visa category permit it / Program Renewal eligibility is validated against season availability — the target season must be active and accepting program renewals / Partner attribution from the original application is preserved on the program renewal unless explicitly reassigned
> **Program Renewals are new applications, not Program Extensions.**
>
> A Program Renewal creates a new application with its own lifecycle, finance charges, and season assignment. It is linked to the prior application for continuity but follows the full application workflow. See Finance & NetSuite — Charge Trigger Events for the RENEWAL_FEE trigger details.

**Staff routes for extension and renewal requests.** `POST /api/admin/extension-requests/program-extension` (permission `extension_requests.manage`) creates a `program_extension` request with status `pending` and returns 201; the body needs `applicantId`, `applicationId`, `programId`, `hostOrgId`, `currentEndDate`, `requestedEndDate` and `reason`. It returns 400 when the application does not belong to the applicant or program, the program does not allow extensions, the duration is under 1 day or over 365 days, the requested end passes the program `maxDurationMonths` (counted from the placement start date, else the application creation date), or the applicant already has 1 approved program extension. Approval goes through `POST /api/admin/extension-requests/:id/transition` and creates the first-class extension application; a program extension has no reservation hold to advance. For a reservation-hold extension request the same call also advances the active hold, and if the hold cannot be advanced it returns 409 `RESERVATION_EXTENSION_FAILED` (`details.reason` such as `NO_ACTIVE_HOLD`, `MISSING_DURATION`, `HOLD_UPDATE_FAILED` or `MISSING_APPLICATION_LINK`) and the request is not marked approved. Renewal reads under `/api/admin/renewal-requests` need `renewal_requests.view` or `renewal_requests.manage`: `POST /check-eligibility` (body `applicantId`, `applicationId`, `programId`, optional `targetSeasonId`) returns the eligibility result, `GET /available-seasons/:programId` lists the program's seasons that end in the future ordered by start date, and `GET /:id` returns one renewal request.

**Flow gate reason codes.** The shared flow gate, used both by the portal dashboard available-flows read and by application creation, evaluates program capability, in-flight application, window and eligibility for renewal, extension and J-2 (spouse or child) flows. A blocked flow carries one of `PROGRAM_CAPABILITY_DISABLED`, `FLOW_WINDOW_CLOSED`, `APPLICANT_INELIGIBLE` or `DUPLICATE_FLOW_IN_FLIGHT`. `APPLICANT_INELIGIBLE` comes from the renewal or extension eligibility service and lists the failed checks in `eligibility.details`; if the eligibility computation itself errors the gate fails closed with the detail `Eligibility could not be evaluated`. J-2 flows have no eligibility service and pass this check when the program capability is on. Creating an application for a blocked flow returns 400 `VALIDATION_ERROR` with the blocked code as `error.reasonCode` and `details` carrying `flowType`, `inFlightApplicationId`, `eligibility` and `availability`.

## Applicant Lifecycle Phase & Readiness

Every applicant sits in exactly one of the eight fixed Lifecycle Phases. The lifecyclePhase field on the applications table records the current phase. It is never set manually: it is derived forward-only from the furthest verified Stage by the Lifecycle Reconciliation engine and never pulled back (see Phase & Stage Derivation).

application / \u2192 / validation / interview / placement / visa_sevis / pre_departure / in_program / completion
Independent of phase, each application carries a system-calculated readiness read-model that indicates whether the application can move forward or is held up at its current point:

| Readiness | Meaning |
| --- | --- |
| on_track | Application is progressing normally. All required inputs are present and no blockers exist. |
| blocked | Application cannot advance. A hard dependency is missing (e.g., required document, placement, SEVIS submission). |
| needs_attention | Application can advance but has soft issues that should be reviewed (e.g., expiring reservation, pending interview). |
| completed | Application has reached the Completion phase. No further transitions are expected. |

> 📘
>
> `blocked` derives only from **genuine** compliance failures on data the applicant has actually submitted. Fail-closed rules referencing data from steps the applicant has not yet reached count as *awaiting submission* — they surface as attention items for staff but never set readiness to `blocked` (and therefore never lock portal steps). This prevents the catch-22 where an application was blocked for missing data the applicant had not yet had the chance to enter.

## Portal Step Submission Flow

When an applicant completes a step in the Applicant Portal, the following pipeline processes the submission end-to-end — from data capture through lifecycle reconciliation:

Applicant fills step in Portal / POST /api/portal/steps/:stepKey/submit / stepValidation validates stepData against the step\u2019s defined schema / Data saved to portal_step_snapshots / step_status_tracking updated \u2192 status: completed / Event emitted: step.status_changed / Biographical sync extracts key fields \u2192 updates applicants table / Lifecycle reconciliation triggered: gathers all step progress + review decisions as evidence, advances Stage through the shared transitionApplicationStage writer only when a milestone is verified (never from completion percentage), which writes an application_stage_history row and derives lifecyclePhase forward-only from the furthest verified Stage, then sets readiness (on_track / blocked / needs_attention)
### Portal Contract Interface

The portal contract is declared as endpoint schemas, not as a single shared interface file. ~~shared/portalContract.ts~~ (the former shared interface file) was **deleted on 2026-04-14** and no longer exists; do not import from it or cite it.

| Component | Purpose |
| --- | --- |
| `server/routes/endpointSchemas/portal-*.ts` | Declarative request/response/param/header schemas and stability tier for every `/api/portal` endpoint — the authoritative contract surface. |
| `server/versioning/portal-contract-snapshot.json` | Committed baseline of that contract. A diff against it is what detects a breaking portal change. |
| `server/versioning/changeRegistry.ts` | Version entries that must accompany any new or altered portal endpoint. |
| `scripts/checkers/portal-contract-guard.cjs` | Build-time guard (run via `scripts/run_skill_enforcement.cjs`) that fails the build when a portal endpoint changes without a snapshot update and a `changeRegistry` entry. |
| `scripts/update-portal-contract-snapshot.js` | Regenerates the snapshot after an intentional contract change. |
| `server/services/portalAdapter.ts` | Maps raw portal step data to compliance inputs. Supports hardcoded fallbacks and dynamic `complianceInputMapping`. |
| `server/services/biographicalSync.ts` | Extracts key biographical fields from portal step snapshots to update the core `applicants` table. |
| `server/services/workflowPublisher.ts` | Pushes workflow configuration to the Portal. |
| `server/services/stepProgressSync.ts` | Local caching and sync of step completion states. |

> **The endpoint schema registry plus its committed snapshot is the single source of truth for Portal integration types.**
>
> Any change to step schemas, submission payloads, or status enums must be made in `server/routes/endpointSchemas/portal-*.ts`, accompanied by a `changeRegistry.ts` version entry, and the snapshot regenerated — otherwise `portal-contract-guard` fails the build.

### Application-Bound Step Serving (Agreement Step Hardening)

Shipped Aug 31 2026 (security correction; portal change-registry effective version `2028-10-24` for `GET /api/portal/applications/:appId/step-config/:stepKey` and `POST /api/portal/steps/:stepKey/submit`). Portal step serving is **application-bound**: the Applicant Portal must send its integration key **and** the signed-in applicant identity (the verified-token-aware portal auth boundary) when it reads or submits any workflow step. Before serving step configuration — including agreement text on an Agreement carrier step — or recording acceptance/snapshot evidence, Globus verifies that the applicant owns the application and that the requested step is a member of that application's exact pinned workflow — the current published definition, or an archived definition that was previously published (one carrying a publication timestamp) after a replacement publish archived it. Drafts and archived definitions that were never published are rejected (422 `WORKFLOW_VERSION_NOT_PUBLISHED`), and the portal never falls forward to the newest published workflow: moving an existing application to another version goes through the reconciliation preview-and-apply flow (corrective change, portal change-registry effective version `2028-11-16` for step config, step submit and the workflow snapshot). Calling with only an application UUID, or requesting a step key that is not in the pinned workflow, is rejected before any read or write. This closes the previous gap where a valid integration key plus a guessable step key could surface agreement text outside the applicant's own workflow.

## Profile Completeness Algorithm

Profile completeness is a computed percentage that indicates how much of an applicant’s required data has been submitted and verified. It is never stored — it is calculated on-the-fly from the current state of workflow steps, documents, and biographical fields.

| Signal Source | Weight | Logic |
| --- | --- | --- |
| Required workflow steps | Primary | Count of completed required steps / total required steps for the active workflow version. Hidden/conditional steps excluded unless their branch is active. |
| Required documents | Primary | Count of approved documents / total required document types for the current wave. Documents in Pending Review count as submitted but not verified. |
| Biographical fields | Secondary | Count of populated mandatory bio fields / total mandatory bio fields as defined by the program configuration. |
| Compliance gates | Modifier | If a compliance gate is failed (e.g., background check rejected), completeness is capped at the pre-gate maximum regardless of other progress. |

> **Program-Specific Weighting**
>
> Each program can configure custom weighting for completeness signals. For example, Camp Counselor programs may weight document completion higher than biographical fields, while Teach USA programs may prioritize credential verification steps. The default is equal weighting across all primary signals.

## Monthly Check-In System

During the In Program phase, J-1 participants are required to complete monthly check-ins as part of ongoing compliance monitoring. The check-in system manages the full lifecycle from schedule generation through compliance enforcement.

### Cadence-Agnostic Check-In Contract

The check-in system is built on a cadence-agnostic contract: Globus emits the set of check-in periods for a participant, and the Applicant Portal renders whatever periods it receives without assuming a fixed monthly rhythm. The portal never derives the schedule itself — it is a pure consumer of Globus-emitted periods, each carrying its own window, due date, and grace.

Globus owns the cadence

Schedule generation, period spacing, window/grace, and reminder timing are computed server-side from the season configuration. The cadence is data emitted to the portal, not portal logic.

Portal renders periods generically

The portal iterates the emitted periods and renders each as a check-in card. Introducing a new cadence (biweekly, quarterly, ad-hoc) requires no portal change — only a different emitted period set.

v1 emits monthly-only

For the first release Globus emits a monthly cadence (configurable due day, default the 15th). The contract supports other cadences without re-work, but only monthly is emitted today.

> **Shipped — On-Program Monthly Check-In**
>
> This contract is implemented and shipped under the On-Program Monthly Check-In project, which owns the cadence-agnostic check-in contract. Runtime ownership is the On-Program Lifecycle delivery area; this docs page is the spec source.

### Check-In Compliance Reporting

The check-in system generates compliance reports used for SEVIS oversight and sponsor accountability:

## Travel Request Flow

During the In Program phase, applicants may submit travel requests through the Applicant Portal. The travel request flow is a Globus-controlled approval workflow — the portal collects the request but never approves or rejects it autonomously.

Applicant submits travel request via portal (destination, dates, travel type, supporting documents) / Portal validates dates and displays holiday period warnings as UX guidance / POST /api/travel-requests/receive — request pushed to Globus / Request appears in staff Travel Request review queue / Staff reviews: travel type, date conflicts, program policy compliance, supporting documentation / Staff approves or rejects with written justification / Decision communicated back to portal via webhook event (travel_request.approved / travel_request.rejected) / Portal updates applicant view with decision and any conditions
| Endpoint | Method | Purpose |
| --- | --- | --- |
| /api/travel-requests/receive | POST | Receive travel request from the Applicant Portal |
| /api/travel-requests/application/:applicationId | GET | List travel requests for a specific application (staff view) |
| /api/travel-requests/:id/review | POST | Staff approve/reject with justification |

> **Approval Authority**
>
> Travel request approval is always a Globus staff decision. The Applicant Portal provides date validation and holiday warnings as guidance only. No travel request is auto-approved regardless of portal-side validation.

## Early Termination Flow

Early termination occurs when a participant’s program ends before the scheduled completion date. This triggers a structured flow covering approval, SEVIS reporting, and downstream cleanup.

## No-Show Detection

A no-show is detected when a participant fails to arrive at their program site by the expected arrival date. Detection is automated with configurable thresholds and escalation paths.

| Detection Signal | Threshold | Action |
| --- | --- | --- |
| No flight arrival confirmation | Program start date + 2 days (configurable) | Flag as potential no-show, notify assigned staff |
| No SEVIS check-in initiated | Program start date + 5 days | Escalate to compliance team, enter No-Show review queue |
| Host reports non-arrival | Any time after program start | Immediate no-show flag, staff review required |
| No activity after critical period | Program start + 20 days (configurable) | Auto-trigger SEVIS no-show reporting, compliance escalation |

> **SEVIS No-Show Reporting**
>
> No-show detection feeds directly into the SEVIS batch processing system. When a participant is confirmed as a no-show, Globus generates the required SEVIS termination record with the no-show reason code. This is a compliance-critical flow with strict regulatory deadlines.

## Program Completion Flow

Program completion is the structured process for transitioning a participant from In Program to Completed status. It encompasses exit surveys, final compliance checks, SEVIS closure, and record finalization.

Program end date approaches — system generates pre-completion checklist (configurable: 14 days before end) / Applicant completes exit survey and final check-in via the portal / Staff verifies employment end confirmation from host organization / System validates all compliance requirements met (check-in history, no outstanding flags) / SEVIS closure record queued — program completion reason code added to next batch / Financial reconciliation — final charges processed, refunds calculated if applicable / Application status transitions to Completed — readiness set to completed / Post-program grace period begins (30-day travel window for J-1 participants)
## Step Submission API

The step submission API defines the contract between the Applicant Portal and Globus for submitting workflow step data. These endpoints handle the full lifecycle of step data — from initial submission through staff review.

| Endpoint | Method | Purpose | Auth |
| --- | --- | --- | --- |
| /api/portal/steps/:stepKey/submit | POST | Submit step data from the Applicant Portal (body: `applicationId`, `stepData`). Triggers validation, snapshot, bio-sync, and lifecycle reconciliation. | Portal token or integration key |
| /api/portal/steps/:stepKey/validation-schema | GET | Retrieve the validation schema descriptor for a step (404 if the step has none). | Integration key |
| /api/portal/applications/:appId/step-config/:stepKey | GET | Retrieve the configuration of a single step for an application (ownership-checked). | Portal token or integration key |
| /api/applicants/:applicantId/steps/:stepKey/complete | POST | Staff completes a step for an applicant (body: `applicationId`, optional `stepData`). | Staff token (permission `applicant.journey.update`) |
| /api/applications/:id/multi-year-steps | GET | Aggregate portal step snapshots for the application applicant and program across enrollment years. Requires the `stepKeyPattern` query parameter (400 `VALIDATION_ERROR` if missing), matched as a substring of the step key; 404 if the application is missing. Returns `totalYears` and `entries` (latest snapshot per enrollment year and step key, dependents excluded, each with `enrollmentYear`, season, `applicationId`, `stepKey`, `stepData`, `isCompleted`, `submittedAt`, `snapshotAt`). | Staff token (permission `applicant.profile.view`) or API key scope `applications.read` |

### Staff Back-Office View

Staff members access submitted step data through the back-office applicant detail view. The interface displays all submitted steps with their data, review status, and version history. Staff with the appropriate RBAC permissions can edit or correct step data, which creates a new versioned snapshot attributed to the staff member.

### Step Completion Percentage

The step completion percentage is computed per node type in the workflow tree:

| Node Type | Calculation |
| --- | --- |
| Leaf Step | Binary — 0% (not started / in progress) or 100% (completed) |
| Hub (group) | Average of child node percentages. Only visible children counted (respects visibility rules). |
| Phase | Average of all hub and direct-child step percentages within the phase. |
| Overall | Weighted average of phase percentages. Weighting follows program configuration or defaults to equal weight per phase. |

## Applicant Queue — Multi-Bucket Surfacing

Because pre-SEVIS work runs across concurrent Processing Areas, a single applicant frequently has open work in more than one queue bucket at the same time (e.g. document review and host interview scheduling). The staff Applicant Queue surfaces this by listing the applicant once per primary bucket but exposing every secondary bucket they belong to in an “Also in” column — so staff never lose track of the parallel work and a bucket is never “hidden” just because the applicant happens to be visible somewhere else. Bucket definitions, SLA timers, and per-queue exception routing live in the Queue System spec; this section names only the multi-bucket surfacing contract.

### Also In Column & Primary Bucket Resolution

| Column | Definition |
| --- | --- |
| Primary bucket | The queue the current view is rendering. The applicant row is sorted, SLA-coloured, and assigned-to-styled by this bucket only. |
| Also in | Comma-separated chips of every other open bucket the applicant currently satisfies. Each chip is a link that re-opens the queue scoped to that bucket with the same applicant focused. |
| Bucket count badge | Numeric badge next to the applicant name when bucket count > 1. Hovering reveals the full list; click jumps to the applicant detail page with the queue context preserved. |
| De-dupe rule | An applicant is never listed twice within the same primary-bucket render. Across distinct queue views (e.g. switching from Document Review to Interview Scheduling), the same applicant will appear in each view that owns them. |

Bucket membership is computed live from the per-area state model — there is no persisted queue_membership table. A Processing Area entering a terminal-pass state silently removes the applicant from that bucket on the next refresh.

### Screening Action Required

The Screening Action Required queue (membership rule `screening_action_required`) lists applications with outstanding staff-owned screening activity, and each row explains itself per activity rather than with a generic blocked reason. Screening-category work items are classified by what is missing: no outcome and no scheduled time — **needs scheduling**; scheduled but no outcome — **needs an outcome**; outcome recorded as no-show — **needs no-show follow-up**. Completed or cancelled items drop out unless they ended as a no-show. The row's reason label is the first outstanding activity, the action text lists every outstanding activity, and the priority starts at high and is raised one step when the queue SLA is breached.

### Per-Bucket SLA, Sort & Filter

Sort order, SLA colouring, and the default filter set are per-bucket, not per-applicant. Two staff looking at the same applicant from two different queues see different urgency cues because each bucket has its own SLA timer (e.g. document-review SLA starts at upload; interview-scheduling SLA starts at host request). The applicant detail page reconciles the picture by showing every bucket the applicant is in with its own SLA pill — never a single “overall” SLA.

SLA timers are driven through `/api/sla`. `POST /api/sla/start` (body `applicationId` and `queueDefinitionId`) returns `eventId` and `started`; `POST /api/sla/pause/:eventId` (optional `reason`), `POST /api/sla/resume/:eventId` and `POST /api/sla/complete/:eventId` act on one timer event. These four need `dashboard.view` and are audited as `sla_timer.started`, `sla_timer.paused`, `sla_timer.resumed` and `sla_timer.completed`. `POST /api/sla/bulk-recalculate` recalculates every active timer, needs `admin.access`, and emits `sla.bulk_breach_detected` when any timer is breached. `PUT /api/admin/escalation-tickets/:id/resolve` (permission `admin.access`, optional `resolutionNote`) marks an escalation ticket resolved with the resolving user and time, and answers 404 `NOT_FOUND` when the ticket does not exist. The mutation registry lists `applicants.view` for the timer routes, but the handlers in `routes/slaPolicies.ts` check `dashboard.view`.

## Applicant Assignment Model

Every application carries one assigned Globus staff owner at a time — the recruiter / programme coordinator responsible for moving that applicant's pre-SEVIS work forward. Assignment is decoupled from queue bucket: a single staff owner can own an applicant across all four concurrent Processing Areas, even when different operational queues surface different pieces of the work. Assignment is a back-office concept only and is never exposed to applicants or partner portals.

### Assignment States

> **Data model (additive)**
>
> Assignment is person-level: `assigned_staff_id` (nullable user id), `assigned_at` and a stored `assignment_state` column (`pending`, then `assigned`, then `reassigned`) live on the applicants table and are written only through the assignment service (`applicantAssignment.ts`); `applications.assignedToUserId` is kept in lock-step for back-compat. A separate `applicant_assignment_history` table appends one row per change with `action` (created | reassigned | auto_assigned | unassigned), `from_staff_id`, `to_staff_id`, `actor_user_id`, `reason` and `created_at`.

### Manual Assign Flow (Default)

- Staff opens the applicant detail page (or selects rows in the queue) and clicks Assign / Reassign.
- A picker lists eligible staff scoped to the applicant's brand & programme, sorted by current open-applicant count (lightest load first) with a search field.
- An optional reason note is captured (free text, mandatory for Reassigned, optional for first assignment).
- On confirm: assigned_staff_id and assigned_at update atomically, a history row appends with source = manual, and an audit log row writes (vocabulary below).
- The new owner receives a single in-app notification: “You were assigned applicant by .”
### Optional Load-Balanced Auto-Assign

Auto-assign is OFF by default. When enabled per programme × season by an administrator, any application transitioning from Pending assignment state runs the auto-assign rule:

- Filter the staff pool to active recruiters scoped to the applicant's brand & programme and not flagged Out-of-Office.
- For each candidate, compute open applicant count — applications where they are assigned_staff_id and the application has not yet entered Phase 7 (In Program).
- Select the candidate with the lowest open count. Tiebreaker: least-recently-assigned (oldest MAX(assigned_at)); if still tied, deterministic hash of staff id — never random, so re-runs are reproducible.
- Write the assignment with source = auto and emit the dedicated auto_assigned audit action.
- If the staff pool is empty, the application remains Pending and surfaces in the Unassigned bucket; no fallback to the previous owner, no silent drop.
> **Auto-assign never overrides a human**
>
> Auto-assign only fires on transitions out of Pending. It never reassigns an already-assigned application — reassignment is always a manual action so workload changes have a named human actor.

Staff routes: `POST /api/applicants/:id/unassign` (permission `applicant.manage`; optional `reason`) returns the applicant to Pending and answers 404 when the applicant does not exist. `GET` and `PUT /api/auto-assign-settings` (permission `applicant.manage`) read and set the per programme and season toggle: `GET` needs the `programId` and `seasonId` query parameters (400 `BAD_REQUEST` otherwise) and reports `enabled` false when no row exists; `PUT` takes `programId`, `seasonId` and boolean `enabled`, upserts the row, and is audited as `auto_assign.setting_changed`. `POST /api/applications/reconcile-lifecycle` runs lifecycle reconciliation with body `persist` (default true) and optional `applicationIds`; without ids it reconciles every application, with ids it returns `total`, `processed`, `changed`, `errors` and per-application `results`. The reconcile route checks `applications.manage` in `routes/applications/lifecycle.ts`, although the mutation registry lists `applicant.manage`.

### Assignment History & Audit Vocabulary

Every assignment change writes one row to applicant_assignment_history and one row to the platform-wide audit_logs via the shared recordAudit() helper documented on the Audit Console. The named action vocabulary for the audit channel is:

| Action | Emitted when |
| --- | --- |
| applicant.assignment.created | First time an application moves from Pending to Assigned via manual action. |
| applicant.assignment.reassigned | Manual ownership change on an already-assigned application. before/after both populated; reason note carried in the audit payload. |
| applicant.assignment.auto_assigned | Auto-assign rule selects an owner. Payload includes the candidate pool size, the open-applicant counts considered, and the tiebreaker that decided the pick — so the decision is reproducible from the log alone. |

The history is read with `GET /api/applicants/:id/assignment-history`, which requires `applicant.view` or `applicant.manage` (so program managers who can assign and reassign but lack `applicant.view` can still read it) and returns the applicant `applicant_assignment_history` rows oldest first.

## Activity Timeline & Last-Touch Signals

The Activity Timeline is the chronological record of who did what, when on an application. It powers the per-applicant timeline tab, the staleness indicators on the Applicant Queue, and the last-touch columns used by recruiter workload reports. It is staff-back-office only — applicants and partners never see it.

### Two Channels: Applicant vs Staff

The application row carries two last-touch timestamps that are maintained independently — collapsing them into one number hides whether the application is silent because the applicant has gone cold or because the staff owner has stopped working it.

| Column | Updated by | Definition |
| --- | --- | --- |
| last_applicant_activity_at | Applicant | Most recent timestamp on any applicant-driven event: portal login, field save, document upload, message reply, check-in submission. |
| last_staff_activity_at | Staff | Most recent timestamp on any staff-driven event on this application: review action, note, assignment change, queue action, outbound message, manual status change. System / rule-engine events do not count. |

Both columns are updated after the originating mutation succeeds, in the same transactional path that writes the audit log row — they cannot drift away from the timeline they index.

A separate `last_staff_review_at` timestamp records when staff last reviewed the application, and it drives the unread dot in the queue and applicants list. `POST /api/applications/:id/acknowledge-review` (permission `dashboard.view`) sets it to now, returns `acknowledged: true` with `lastStaffReviewAt`, and returns 404 for an unknown application. The acknowledging user is recorded as the staff-activity actor.

### Event Vocabulary

Timeline entries are grouped into four channels. The channel determines which last-touch timestamp is bumped and how the entry is rendered.

| Channel | Bumps | Example actions |
| --- | --- | --- |
| Applicant | last_applicant_activity_at | profile_save, profile_photo_upload, step_save, step_submission, document_upload, agreement_acceptance, checkin_submission, registration, application_creation (the `APPLICANT_PORTAL_ACTIVITY_ACTIONS` set) |
| Staff | last_staff_activity_at | applicant.assignment.created, applicant.assignment.reassigned, applicant.document.reviewed |
| System | (neither) | Rule-engine evaluation, queue refresh and SLA events — audited but not counted as activity, so staleness is not artificially reset by background work. |
| Partner | (neither) — system, partner and API-key writes never bump the staff timestamp, which needs a verified human actor | Host comments and partner-portal actions are recorded in the timeline for context but do not reset applicant staleness. |

### Staleness Bands & Queue Surfacing

Staleness is computed at render time from now() &minus; last_*_activity_at. Bands are deliberately coarse — the goal is to make “needs a nudge” visible at a glance, not to encode an SLA (queue-level SLAs continue to live in the Queue System spec).

- The Applicant Queue renders two pills per row: applicant-side band (from last_applicant_activity_at) and staff-side band (from last_staff_activity_at). Either pill can independently turn Stale or Cold.
- A saved view “Stale applicants” defaults to applicant-side &ge; Stale AND staff-side &ge; Stale — both sides quiet at once is the actionable case.
- Bands are derived; no staleness_band column is persisted. Re-tuning the thresholds is a config change, not a backfill.
- Withdrawn, cancelled, and completed applications are excluded from staleness rendering entirely.
### Timeline Source & Audit Bridge

The Activity Timeline tab on the applicant detail page is a filtered view over audit_logs scoped to entity_type = 'application' and the applicant's application id(s) — same row shape as the Audit Console documented under the platform audit contract. There is no parallel activity_timeline table: the audit log is the source of truth, and the timeline is a presentation layer that adds applicant-friendly grouping, channel filters, and the two last-touch summary lines at the top.

## Applicant Detail Page

The Applicant Detail Page is the staff-facing view for a single applicant. It provides a comprehensive operational view including profile data, per-area status, assignment, queue membership, last-touch signals, and full lifecycle history.

> **One spec, multiple surfaces — full-page and split-view**
>
> Every sub-section below applies identically across every surface that opens an applicant. The same blocks render against the same data — there are no detail-only-on-one-surface sections, and a new block added here must appear everywhere. Current entry surfaces: Full-page Applicant Detail at /staff/applicants/ — the standalone surface. Split-View Applicant Workspace right panel — reached from the Applicant Queue. See Split-View Applicant Workspace for the queue-context preservation contract. Host Management &rarr; Applicants tab (row click) — clicking any row on the sponsor-side Host Management Applicants tab opens the full-page detail surface. See Partner Organizations &rarr; Applicants Tab Row Interaction .

### Workflow Step Progress View

The step progress view on the applicant detail page renders the applicant’s workflow as a visual progress tracker. Each phase is expandable to show individual steps with their status, submitted data summary, and review notes. The view is driven by the applicant’s bound workflow version — it always shows the exact workflow the applicant is completing, not the latest published version.

### Lifecycle Phase Filter

The phase filter dropdown on the applicant detail page aligns with the 8-phase Lifecycle spine (application through completion). Staff can filter the activity timeline, compliance panel, and document list to show only items relevant to a specific Lifecycle Phase. The filter also controls which phase of the workflow progress view is expanded by default.

All Phases / Application / Validation / Interview / Placement / Visa & SEVIS / Pre-Departure / In Program / Completion
### Split-View Applicant Workspace

The Split-View Workspace is the default staff entry point into an applicant when navigated from the Applicant Queue. The page is divided into two persistent regions: a left queue panel that keeps the surrounding queue rows (with their current filter, sort, and scroll position intact) and a right detail panel that renders the full Applicant Detail blocks documented in the sub-sections above. Opening an applicant never loses queue context; closing the detail panel returns the user to the exact queue position they came from — same filter, same scroll offset, same focused row.

| Contract | Behavior |
| --- | --- |
| Queue-context preservation | Left panel retains the active queue's filter set, sort order, and scroll position for the full lifetime of the split-view session. Closing the right panel restores focus to the originating row. |
| Deep-linkability | The split view is fully URL-addressable: `/queues?applicant=ID&app=ID` opens the queue with the applicant pre-selected. Pasting the URL into a fresh tab reproduces the exact same split-view state. |
| Keyboard navigation | Left arrow moves to the previous applicant in the current queue order; Right arrow moves to the next. The detail panel updates in place — it never unmounts and remounts — so scroll position within open sub-sections is preserved. |
| Parity with full-page detail | Every block documented in the Applicant Detail Page sub-sections (Header, Per-Area Status, Assignment, Workflow Progress, Compliance, Activity Timeline) renders identically on both surfaces. No detail-only-on-full-page surfaces are permitted. |
| Full-page escape hatch | The detail panel header exposes an "Open in full page" affordance for tasks that need maximum screen real estate (e.g. document review). The full-page URL is also deep-linkable: /staff/applicants/ . |
| Mobile responsiveness | Out of scope for this revision. The split view is a desktop-only surface; mobile staff fall back to the full-page Applicant Detail route. |

> **Forward cross-link**
>
> The left queue panel is the same row contract documented in Queue System — Queue Row Contract and surfaces the multi-bucket “Also In” column from Also In Column & Primary Bucket Resolution without re-rendering them.

**Change digest and view markers.** `POST /api/applicants/:id/workspace-visit` (permission `applicant.profile.view`; 404 for an unknown applicant) upserts the caller row in `applicant_view_markers` and returns `previousVisitAt` and a change `digest`. The table holds one row per staff user and applicant (`staff_user_id`, `applicant_id`, `last_viewed_at`, unique on the pair, no foreign key on the user). The digest is built from audit rows on the applicant and its applications written since the previous visit, scanning at most 500 rows; the first visit returns an empty digest. The marker is viewer-local presence state and drives no domain behavior.

### Operational Timeline

The Operational Timeline is the Activity Timeline tab documented in Activity Timeline & Last-Touch Signals extended to surface the workflow-progression events processors need in order to action an applicant from the split-view detail panel. It remains a filtered view over audit_logs — there is no parallel store, no second write path, no second source of truth.

The additions to the audit vocabulary that the timeline reads from:

| Audit action | Channel | Fires when |
| --- | --- | --- |
| applicant.lifecycle.transitioned | System / Staff | Application's derived Lifecycle Phase advances (Application → Validation, Interview → Placement, …) or a Processing Area moves to a terminal-pass state. Records old_phase / new_phase in the diff. |
| applicant.approval.granted | Staff | A staff approval gate is cleared (sponsor approval, host approval, compliance approval). Records the gate id, approver, and any rationale. |
| applicant.approval.denied | Staff | A staff approval gate is denied. Records the gate id, denier, and required reason. |
| `document.uploaded` (`document.replaced` on re-upload) | Applicant / Staff | A document is uploaded against this applicant. Records document_type_id and source channel; already counted toward last_*_activity_at by the channel rules above. |
| applicant.document.reviewed | Staff | A document review decision is recorded (approved / rejected / needs-info). Records reviewer and decision. |
| queue.reason.created | System | Rules Engine emits a new queue reason against the applicant. See Queue Reason Object. |
| queue.reason.resolved | System / Staff | A queue reason is cleared by a passing re-evaluation (auto_cleared) or by staff override (staff_override). |
| applicant.assignment.created | Staff | First-time staff assignment on this application. Already documented under Assignment History & Audit Vocabulary. |
| applicant.assignment.reassigned | Staff | Existing assignment is moved to a different staff owner. |
| applicant.assignment.auto_assigned | System | Optional load-balanced auto-assign rule selects an owner for an unassigned applicant. |
| applicant.next_action.invoked | Staff | Processor clicks a Next Action recommendation in the split-view detail panel. See Next Action Engine below. |

- The timeline tab adds an event-type filter grouped by the four channels already defined in Event Vocabulary plus a “Workflow progression” pseudo-filter that selects exactly the audit actions in the table above.
- Every row in the operational timeline links to its underlying audit row in the Audit Console for full before/after JSON. The Audit Console's vocabulary block is the canonical list — adding a new action to the operational timeline starts by adding it to the audit contract there.
- The system / partner channel rules from Event Vocabulary continue to apply: rule-engine-driven entries do not bump last_*_activity_at, so staleness bands are not artificially reset by background workflow progression events.
### Next Action Engine

The Next Action Engine renders zero-or-more recommended next actions on the right-side detail panel (and on the full-page Applicant Detail Header Panel). Recommendations are the bridge between rule-engine output, open queue reasons, and the canonical Quick Actions a processor can perform — they turn “the engine knows something is wrong” into “here is the one click that fixes it.”

> **Persisted on the application, recomputed on reconciliation**
>
> There is no next_actions table. The current recommendation is stored on the application row (`next_action_label`, `next_action_kind`, `next_action_deep_link`) and recomputed on every lifecycle reconciliation pass; the detail panel reads the persisted values.

Recommendation shape (per item in the derived set):

| Field | Definition |
| --- | --- |
| id | Stable derivation key — hash of (applicant_id, action_type, source). Lets the UI dedupe and keep keyboard focus across recomputes. |
| action_type | Closed vocabulary (see table below). The detail panel will not render a recommendation whose action_type is outside this set. |
| title | Human-readable label shown on the action button (e.g. "Request passport scan from applicant"). |
| source | Either a rules-engine outcome id (rule_definition.id + evaluation id) or a queue_reason.id — whichever produced this recommendation. Used for click-through to evidence. |
| severity | info \| warning \| blocker — inherits from the producing queue reason or rule outcome. Drives sort order and pill colour. |
| generated_at | Timestamp of the producing evaluation. Used as the deterministic secondary sort (oldest first within a severity). |
| operation | The exact same operation contract the matching Quick Action invokes — see Quick Actions below. The recommendation is a pre-filled invocation of that operation. |

Closed action_type vocabulary:

| action_type | Maps to Quick Action | Typical producer |
| --- | --- | --- |
| request_document | Request document | Rules-engine outcome with failure_behavior = route_to_queue on a missing document; or queue reason with source_system = rules_engine and linked_document_type. |
| follow_up_host | Send message (host) | Queue reason emitted by a stalled host-side action (e.g. host has not responded to placement offer within SLA). |
| follow_up_partner | Send message (partner) | Queue reason emitted by a stalled partner-side action (e.g. partner has not submitted required cohort data). |
| schedule_interview | Schedule interview | Rules-engine outcome requiring sponsor or host interview as a precondition to advancing the Screening Processing Area. |
| escalate_compliance | Escalate | Compliance rule failure with severity = blocker, or a queue reason with source_system = sevis. |
| reassign | Reassign | Assignment-policy rule (e.g. owner away, owner over capacity, programme reassignment override). |
| send_reminder | Send message (applicant) | Queue reason with source_system = portal_submission indicating a stalled applicant-side step past its nudge threshold. |
| request_payment | Request payment | Finance rule outcome flagging an outstanding charge gating the next lifecycle step. |

Invocation contract: clicking a recommendation calls the exact same underlying operation that the processor would invoke by clicking the matching Quick Action manually — no parallel code path, no separate permission gate, no second audit shape.

Every successful invocation writes an audit row with action applicant.next_action.invoked in addition to the underlying operation's own audit row (e.g. the document-request operation still writes its own `document.request` row). The next-action row records action_type, source, and recommendation_id in its after diff so an analyst can later answer “how often did processors take the engine's suggestion?” without joining tables. The Audit Console contract already accepts the action under its applicant.* family — see Audit Contract.

### Quick Actions

Quick Actions are the canonical set of operations a processor can invoke from the right-side detail panel header (and from the full-page Header Panel quick-action row). Each maps to an existing operation contract documented elsewhere; this section names the set, not the operation internals.

| Quick Action | Underlying operation | Notes |
| --- | --- | --- |
| Add note | `add_note` (permission `applicant.notes.edit`) | Free-text staff note; rendered in the Activity Timeline under the Staff channel. `POST /api/applications/:appId/notes` takes `content`, `visibility` (`internal` or `partner`) and `category` (default `general`). A `partner` note is also written to the partner note log in the same transaction; if the application has no partner organization it is refused with `400` and `error.code` `NO_PARTNER_ORG` and nothing is saved. |
| Reassign | `reassign` (permission `applicant.manage`) | Same operation that emits applicant.assignment.reassigned. Requires reason capture per Assignment Panel spec. |
| Request document | `request_document` (permission `document.verify`) | Creates a pending document request for the application (POST /api/applications/:id/document-requests, audit action `document.request`); pre-filled by request_document recommendations. |

Send message, Escalate and Request payment were withdrawn from the quick-action set (see `QUICK_ACTION_KEYS` in `shared/applicantActionModel.ts`).

The profile header also offers two staff flow actions for renewal and extension flows. `GET /api/applications/:appId/available-flows` (permission `applications.view`, `applications.edit` or `applications.manage`) lists a `renewal` entry when the program allows renewals and an `extension` entry when it allows extensions, each with eligibility. `POST /api/applications/:appId/flow-actions/send-reminder` (permission `applications.edit` or `applications.manage`; body `applicationType` of `renewal` or `extension`) emits an `application.flow_reminder` event on every call so the applicant is prompted again, and returns `ok` true; it returns 400 when the program does not allow that flow or the applicant is not eligible for it. `POST /api/applications/:appId/flow-actions/start-on-behalf` (same permissions; body `applicationType`, optional `seasonId`, and a required `reason` of at least 3 characters) creates the renewal or extension application for the applicant, tagged `initiatedBy` `staff:USER_ID`, and returns 201 with `applicationId` and `workflowDefinitionId`. A renewal is created in the server-resolved next season (or `seasonId` when given) and an extension in the prior application's season; 400 covers an ineligible flow and `No upcoming season is configured for this program`. The actions are audited as `application.flow_reminder_sent` and `application.start_on_behalf`.

> **One operation, two entry points**
>
> Every Quick Action is the same underlying operation regardless of whether it was invoked manually from the panel header or by clicking a Next Action recommendation. The recommendation path is just a pre-filled invocation — it does not introduce a parallel write path, a parallel permission gate, or a parallel audit shape.

### Implementation Follow-up — P16 Asana Tickets

The following tickets carry the runtime work for the split-view detail surface and the next-action engine. They follow the house Asana standard documented in docs/asana-ticket-standard.md. Runtime ownership is P16 (Queue System & SLA Management); this docs page is the spec source.

Title

Globus: split-view applicant workspace layout + keyboard navigation

FOR PRODUCT OWNERS / STAKEHOLDERS

Staff processors will be able to action an applicant without losing their place in the queue: opening an applicant slides their full detail panel in next to the queue list, arrow keys move to the prev / next applicant in place, and closing the panel returns to the exact same scroll position. Replaces the “click out, click back, lose your filter” trip that legacy systems force on processors today.

----------------------------------------

BACKGROUND

The current /staff/applicants/ route is a full-page detail surface. Navigating from the Applicant Queue replaces the queue entirely; on back-navigation the queue re-renders from scratch, losing filter facet state and scroll position. There is no shared component between the queue rows and the detail surface.

WHAT THIS TICKET DOES

- Introduces `/queues?applicant=ID&app=ID` as the deep-linkable split-view URL.
- Renders the existing Applicant Detail blocks (Header, Per-Area Status, Assignment, Workflow, Compliance, Activity Timeline) inside a right-side panel without remounting between applicant navigations.
- Preserves the left queue panel's active filter, sort, and scroll position for the lifetime of the split-view session.
- Binds Left / Right arrow keys to prev / next applicant in the current queue order.
- Adds an “Open in full page” affordance that routes to /staff/applicants/ without losing the queue context.
ACCEPTANCE CRITERIA (Gherkin)

Feature: Split-view applicant workspace
Scenario: Opening an applicant from the queue preserves queue context
Given a staff user has filtered the "Ready for Review" queue and scrolled to row 47
When they click the row for applicant "A-1234"
Then the URL becomes "/queues?queue=ready-for-review&applicant=A-1234&app=ID"
And the left queue panel still shows the "Ready for Review" filter
And the queue is still scrolled to row 47 with row "A-1234" focused
Scenario: Right arrow key moves to the next applicant in queue order
Given the split view is open on applicant "A-1234" at queue position 47
When the user presses the "ArrowRight" key
Then the right panel re-renders against applicant at queue position 48
And the detail panel component is not unmounted
And the URL updates to reflect the new "applicant" query parameter
Scenario: Closing the detail panel returns to the originating queue position
Given the split view is open on applicant "A-1234" at queue position 47
When the user closes the detail panel
Then the URL becomes "/queues?queue=ready-for-review"
And the queue is still scrolled to row 47 with row "A-1234" focused
Scenario: Deep-link reproduces split-view state in a fresh tab
Given a staff user pastes "/queues?queue=ready-for-review&applicant=A-1234&app=ID" into a new tab
When the page finishes loading
Then the left panel renders the "Ready for Review" queue
And the right panel renders the Applicant Detail blocks for "A-1234"
Scenario: Parity guard — full-page Applicant Detail renders the same blocks
Given the same applicant "A-1234"
When the user opens "/staff/applicants/A-1234" in full-page mode
Then the rendered set of detail blocks equals the set rendered in the split-view right panel
And a build-time test fails if a new detail block is added to only one surface 
Title

Globus: operational timeline audit-vocabulary extension

FOR PRODUCT OWNERS / STAKEHOLDERS

Invisible to end users, but processors will see a richer Activity Timeline tab on the applicant detail panel — lifecycle transitions, approvals, document uploads, queue-reason activity, and assignment changes will all flow into the same chronological view they already use, instead of being scattered across separate logs.

----------------------------------------

BACKGROUND

The Activity Timeline tab is already a filtered view over audit_logs scoped to entity_type = 'application'. Today the assignment family (applicant.assignment.*), document-upload family, and queue-reason family already write through the shared recordAudit() helper, but lifecycle phase transitions and approval-gate decisions do not yet emit a dedicated audit action — they are inferred from a mix of status-column diffs and downstream notification rows.

WHAT THIS TICKET DOES

- Adds applicant.lifecycle.transitioned, applicant.approval.granted, applicant.approval.denied, and applicant.document.reviewed to the audit vocabulary, written through the existing recordAudit() helper.
- Extends the operational-timeline filter on the applicant detail panel to include a “Workflow progression” pseudo-channel selecting the new actions plus the existing queue-reason and assignment families.
- Updates the Audit Console applicant.* contract entry to list the new actions.
- No new tables; no parallel write path; the timeline remains a read-side view.
ACCEPTANCE CRITERIA (Gherkin)

Feature: Operational timeline audit-vocabulary extension
Scenario: Lifecycle phase transition writes a dedicated audit row
Given application "A-1234" is in lifecycle phase "Interview"
When the rules engine transitions it to "Placement"
Then an audit_logs row exists with action "applicant.lifecycle.transitioned"
And its "after" diff records old_phase "Interview" and new_phase "Placement"
Scenario: Approval gate cleared writes a dedicated audit row
Given application "A-1234" requires "sponsor_approval"
When staff user "u-9" grants sponsor approval
Then an audit_logs row exists with action "applicant.approval.granted"
And its "after" diff records gate "sponsor_approval" and approver_user_id "u-9"
Scenario: Operational timeline tab surfaces the new rows
Given application "A-1234" has both a transition and an approval row from the above scenarios
When a staff user opens the Activity Timeline tab and selects the "Workflow progression" filter
Then both rows are visible in chronological order
And each row links to the matching Audit Console entry
Scenario: System-channel events do not bump staff staleness
Given application "A-1234"
When the rules engine writes "applicant.lifecycle.transitioned" without a staff actor
Then "last_staff_activity_at" on the application is unchanged
Scenario: Build-time guard — vocabulary list stays in sync
Given the operational-timeline filter source list and the Audit Console "applicant.*" contract list
When a developer adds a new "applicant.*" action to one list without adding it to the other
Then the vocabulary-sync test fails 
Title

Globus: next-action derivation service for applicant detail panel

FOR PRODUCT OWNERS / STAKEHOLDERS

Processors opening an applicant will see a small set of suggested next actions at the top of the detail panel (“Request passport scan”, “Reassign — current owner over capacity”, etc.). Suggestions come from rules the engine already evaluates, so processors stop having to translate a queue reason into the right click.

----------------------------------------

BACKGROUND

The Rules Engine already evaluates applicants and emits Queue Reasons against them; the canonical Queue Reason shape is documented in queue-system.mdx. Today there is no derivation layer that translates those reasons into actionable UI items — processors read the reason text and then hunt for the matching control on the detail page.

WHAT THIS TICKET DOES

- Adds a read-side deriveNextActions(applicationId) service that returns the recommendation set defined in the “Next Action Engine” spec section.
- Enforces the closed action_type vocabulary; recommendations with an unknown type are dropped server-side.
- Does not introduce a next_actions table — the function is pure over (latest rule outcomes, open queue reasons).
- Ships with the companion ticket “Globus: next-action invocation routing & audit”.
ACCEPTANCE CRITERIA (Gherkin)

Feature: Next-action derivation service
Scenario: Open queue reason for a missing document produces a request_document recommendation
Given application "A-1234" has an open queue reason with source_system "rules_engine"
And the reason references document_type "passport_scan"
When deriveNextActions("A-1234") is called
Then the result contains a recommendation with action_type "request_document"
And its "source" references the queue_reason.id
And its "operation" matches the `request_document` contract
Scenario: Resolving a queue reason removes its recommendation on next derivation
Given a recommendation derived from queue_reason "qr-7"
When "qr-7" is resolved (auto_cleared or staff_override)
And deriveNextActions("A-1234") is called again
Then the result no longer contains a recommendation with source "qr-7"
Scenario: Unknown action_type is dropped server-side
Given a rules-engine outcome that would map to an action_type outside the closed vocabulary
When deriveNextActions runs
Then no recommendation is returned for that outcome
And a server-side warning log is emitted naming the rejected action_type
Scenario: Severity drives sort order, generated_at breaks ties
Given three recommendations with severities "blocker", "warning", "warning"
And the two "warning" items have different generated_at timestamps
When the derivation result is returned
Then the "blocker" item is first
And the older "warning" item precedes the newer "warning" item
Scenario: Recommendation id is stable across recomputes
Given the same (applicant_id, action_type, source) inputs
When deriveNextActions runs twice in a row with no state change in between
Then the returned "id" values are identical for matching recommendations 
Title

Globus: next-action invocation routing & audit

FOR PRODUCT OWNERS / STAKEHOLDERS

Invisible to end users, but every time a processor clicks a suggested action, the system runs the same operation they would have run by hand and records the click so we can later report on how often the suggestions are useful.

----------------------------------------

BACKGROUND

Each Quick Action listed in the “Quick Actions” spec section is already a real, permission-gated, audited operation. The derivation service from the companion ticket produces recommendations that already carry a fully-formed operation field. What is missing is the click-handler glue that invokes the underlying operation through the existing call site — not a parallel one — and stamps an additional audit row so recommendation usage is measurable.

WHAT THIS TICKET DOES

- Routes Next Action button clicks through the same operation handler that the matching Quick Action button uses — no second permission check, no second write path.
- Writes an applicant.next_action.invoked audit row in addition to the underlying operation's own audit row, capturing action_type, source, and recommendation_id in the diff.
- Updates the Audit Console applicant.* contract entry to list applicant.next_action.invoked.
- Surfaces a quiet error state on the panel if the underlying operation refuses (permission, validation) — the recommendation is not auto-dismissed; it remains visible until the next derivation drops it.
ACCEPTANCE CRITERIA (Gherkin)

Feature: Next-action invocation routing and audit
Scenario: Clicking a recommendation invokes the same underlying operation as the matching Quick Action
Given a recommendation with action_type "request_document" on application "A-1234"
When a staff user clicks the recommendation
Then the `request_document` operation handler is invoked exactly once
And the resulting audit row "document.request" has the same shape as a manual invocation
Scenario: A second audit row records the recommendation usage
Given the same successful click as above
When the operation completes
Then an audit_logs row exists with action "applicant.next_action.invoked"
And its "after" diff records action_type "request_document", source, and recommendation_id
Scenario: Permission denial does not bypass the standard gate
Given a staff user without permission `document.verify`
When they click a "request_document" recommendation
Then the underlying operation rejects with a permission error
And no "applicant.next_action.invoked" row is written
And the recommendation remains visible on the panel
Scenario: Recommendation is removed by the next derivation, not by the click
Given a recommendation produced by queue_reason "qr-7" is clicked and the underlying operation succeeds
When the panel re-derives recommendations
And queue_reason "qr-7" is now resolved
Then the recommendation is no longer rendered
And no client-side cache override was needed to hide it 
Title

Globus: quick-actions bar on applicant detail panel

FOR PRODUCT OWNERS / STAKEHOLDERS

The right-side applicant detail panel gets a small bar of one-click actions (add note, send message, reassign, escalate, request document, request payment) so processors can act on an applicant without leaving the queue context.

*Status: ticket text as filed. The shipped quick-action set is Add note, Reassign and Request document (`add_note`, `reassign`, `request_document`); Send message, Escalate and Request payment were withdrawn, so the six-action wording here and the `applicant.finance.request_payment` scenario below do not apply as written.*

----------------------------------------

BACKGROUND

The same set of operations is already invocable from the full-page Applicant Detail header. They are individually permission-gated and individually audited. The detail panel inside the split-view workspace currently has no equivalent control row.

WHAT THIS TICKET DOES

- Renders the canonical Quick Actions set (the “Quick Actions” spec table above) on the right-side detail panel header.
- Reuses the existing per-operation permission gating — no duplicated permission logic.
- Pre-fills inputs when invoked by a Next Action recommendation (see companion ticket “Globus: next-action invocation routing & audit”).
- Quick-actions are identical on the full-page and split-view surfaces — the parity rule from the detail page header callout is enforced by a shared component.
ACCEPTANCE CRITERIA (Gherkin)

Feature: Quick-actions bar on applicant detail panel
Scenario: The canonical action set renders on the split-view detail panel
Given a staff user opens the split-view workspace on application "A-1234"
Then the detail panel header shows actions "Add note", "Reassign", "Request document"
Scenario: The same action set renders on the full-page detail surface
Given the same applicant
When the user opens "/staff/applicants/A-1234"
Then the Header Panel renders the same three quick actions
And a build-time test fails if the two surfaces diverge
Scenario: Per-action permission gating
Given staff user "u-9" lacks permission "applicant.notes.edit"
When the detail panel renders for "A-1234"
Then the "Add note" action is rendered disabled with a tooltip naming the missing permission
And the other two actions remain enabled
Scenario: Recommendation pre-fills the action invocation
Given a Next Action recommendation with action_type "request_document" and document_type "passport_scan"
When the user clicks the recommendation
Then the "Request document" quick action opens with document_type pre-selected to "passport_scan" 
## Structured Lifecycle Model

Clear separation of operational vs compliance states / No overloaded status labels \u2014 interviews and document waves are Work Types, not statuses / Derived readiness based on rules \u2014 5 signal sources, never stored / Two document waves (pre-showcase & post-showcase) with distinct timing / Interview tracking without status explosion / Each phase has a single focus area / Supports rules engine gating between phases / Defined actors at every phase \u2014 no ambiguity about who does what / Structured exception paths for withdrawals, cancellations, and disruptions / Every transition is auditable with before/after state
> **Principle**
>
> An applicant’s position in the lifecycle is determined by their state in Globus — not by a manually set label. The rules engine evaluates readiness, and the lifecycle phase follows from that evaluation. Work Types track what’s happening. Queues surface what needs attention.

## Profile Completeness Algorithm

Profile Completeness is a derived score that shows how much of an applicant’s required information has been submitted and verified. It is computed in real-time from the workflow template’s required fields and documents cross-referenced against the applicant’s current data. The score is never stored — it is always derived.

### Completeness Segments

*Status: the weighted segments below are a design spec, not built. The built score (`profileCompleteness.ts`) is filled required fields divided by total required fields for the applicant's program (`BASE_REQUIRED_FIELDS` plus per-program defaults, or the `required_fields` of the program's `documents_verification` rule), shown as one percentage with a missing-field list.*

The overall completeness score is composed of weighted segments:

| Segment | Weight | What Counts | Complete When |
| --- | --- | --- | --- |
| Biographical Data | 20% | Name, DOB, nationality, passport number, contact info, emergency contacts | All required biographical fields are populated and validated |
| Documents | 35% | All required document types from the workflow template | Every required document type has at least one verified, non-expired record |
| Workflow Steps | 25% | Applicant-facing workflow steps (forms, uploads, confirmations) | All required applicant steps are in “completed” state |
| Compliance Gates | 10% | Background check, health clearance, insurance verification | All compliance gate steps are cleared |
| Placement Data | 10% | Employer match, job order acceptance, housing assignment (if applicable) | Placement confirmed and all required placement fields populated |

### Scoring Model

The scoring model is phase-aware — the required fields and weights adjust based on the applicant’s current lifecycle phase:

> **Completeness Is Not Readiness**
>
> Profile completeness measures how much data has been provided. Readiness (used by phase gates and queues) measures whether the data passes validation. An applicant can have 100% completeness but still fail readiness if a compliance check is rejected.

## Travel Request Flow

On-program participants who need to travel outside their placement area must submit a travel request through the Applicant Portal. This flow captures travel details and triggers compliance checks.

## Monthly Check-In Lifecycle

Monthly check-ins are a Department of State requirement for all J-1 participants during the In Program phase. The lifecycle is managed by the system with configurable timing per season.

| State | Description | Transition |
| --- | --- | --- |
| Scheduled | Check-in window created for the upcoming month | Window opens → Pending |
| Pending | Window is open, participant has not submitted yet | Participant submits → Submitted |
| Submitted | Participant completed the check-in form | If flags exist → Flagged; else → Reviewed |
| Flagged | Check-in contains data requiring staff review (address change, well-being concern) | Staff reviews → Reviewed |
| Reviewed | Check-in acknowledged by staff (or auto-acknowledged if no flags) | Terminal state for this month |
| Missed | Window closed without submission (after grace period) | Triggers compliance escalation chain |

> **Check-In Help**
>
> For a participant-facing guide to the monthly check-in form and process, see the Monthly Check-In help page .

## On-Program Dashboard

The On-Program Dashboard is the staff-facing view for managing participants in the active program phase. It provides at-a-glance status across all on-program participants.

## Early Termination Detail

Early termination occurs when a participant exits the program before the scheduled end date. The process involves multiple systems and has compliance implications.

| Termination Reason | Initiated By | SEVIS Action | Grace Period |
| --- | --- | --- | --- |
| Voluntary Withdrawal | Participant | End program status, update reason code | 30 days (standard) |
| Employer Termination | Host employer / Staff | End program, shortfall reason code | 30 days to find new placement |
| Compliance Violation | Compliance officer | Terminate with violation reason | None — immediate termination |
| No-Show | System (automated) | No-show termination code | 14-day confirmation window |
| Medical / Emergency | Staff | Suspend status, medical reason | Varies (case-by-case) |

Termination record created with reason, effective date, and supporting documentation / SEVIS amendment batch queued with appropriate termination reason code / All active workflow steps for the participant are cancelled / J-2 dependents (if any) are auto-terminated with the same effective date / Partner and host employer notified via the notification system / Financial reconciliation triggered (pro-rated charges if applicable) / Grace period timer starts for departure tracking
## No-Show Detection

No-show detection identifies participants who were expected to arrive at their placement but failed to report. The system uses automated signals and configurable thresholds.

> **No-Show Confirmation Window**
>
> A no-show flag does not immediately terminate the participant. Staff have a 14-day confirmation window to investigate (contact the participant, check with the employer, verify travel delays). If not resolved within 14 days, the system auto-transitions to early termination with the no-show reason code.

## Program Completion Flow

Program completion is the standard exit path when a participant finishes their program on the scheduled end date. The flow ensures all compliance requirements are met before closing.

## Check-In Compliance Reporting

Check-in compliance reporting provides aggregate analytics across all participants for audit and operational oversight:

## Applicant Detail Page

The Applicant Detail Page is the staff-facing view for a single applicant. It consolidates all information about the applicant into a single screen with tabbed sections.

### Workflow Step Progress View

The Workflow tab on the Applicant Detail Page shows a visual representation of the applicant’s progress through their assigned workflow:

Phase headers show completion percentage and step count (e.g., "Intake \u2014 4/5 steps complete") / Each step shows its status, assigned role, completion date (if done), and SLA indicator / Conditional steps show the rule that controls their visibility and whether the condition is met / Hub nodes expand to show their child subtasks with individual completion states / Clicking a step opens its detail panel: submission data, attachments, review history, and staff notes
### Profile Completeness UI

The profile completeness ring is a circular progress indicator on the Applicant Detail Page overview tab. It provides instant visual feedback on the applicant’s data status.

Completeness Ring Segments

Overall percentage displayed in the center of the ring / Each segment is color-coded and shows its individual completion percentage on hover / Incomplete segments list the specific missing items (e.g., \u201cMissing: Background Check, Insurance Proof\u201d) / The ring updates in real-time as documents are verified or steps are completed / Staff can click any segment to jump to the relevant tab with the incomplete items highlighted
### Lifecycle Phase Filter

The Lifecycle Phase Filter is a dropdown on applicant list views that lets staff filter by Lifecycle Phase rather than raw status codes. It provides a human-readable way to find applicants based on where they are in their lifecycle.

*Status: the curated option set below is a design spec, not built. The built applicants list filter (`journeyFilter` in `applicants.tsx`) is a multi-select over journey positions (`useJourneyPositions`).*

| Filter Option | Matches | Use Case |
| --- | --- | --- |
| Just Started | Application phase, < 25% complete | Find new applicants who may need onboarding support |
| In Review | Validation or Interview phase | Track applicants waiting for staff decisions |
| Awaiting Placement | Placement phase, no confirmed placement | Identify applicants who need employer matching |
| Visa Processing | Visa & SEVIS phase | Monitor SEVIS and DS-2019 progress |
| In Program | Active in-program participants | View currently placed and active participants |
| Completing Soon | Program end date within 30 days | Manage upcoming program completions |
| Needs Attention | Any active compliance hold, missed check-in, or SLA breach | Priority triage for operational staff |
| All Active | All non-completed, non-withdrawn applications | Full pipeline view |

### Nationality Filter

The Nationality Filter sits alongside the Lifecycle Phase Filter on applicant list views and lets staff narrow the list to applicants of one or more nationalities. It is multi-select: an applicant matches if their nationality is any of the selected values (logical OR within the facet). The filter is available for all program types — the need surfaced from Teach staff, but it is not gated to Teach.

| Property | Behavior |
| --- | --- |
| Selection model | Multi-select. An applicant matches when their nationality equals any one of the selected values (logical OR within the facet). |
| Program-type scope | Available on every program type (Work & Travel, Teach, Intern/Trainee, etc.). Not gated to Teach — Teach was only the trigger for adding it. |
| Default state | Empty by default. With no nationality selected the list applies no nationality narrowing — every applicant is eligible on this facet. |
| Composition | Combines with the Lifecycle Phase Filter and any other active facets via AND. Selecting "Awaiting Placement" plus nationalities BR + AR returns applicants in the placement phase who are Brazilian or Argentine. |
| Option source | The selectable values are the distinct nationality codes present on the applicant rows in scope, so staff only ever see nationalities that can actually return results. |

> **Data source: nationality lives on the applicant row**
>
> Nationality is an applicant-row field (part of the person’s identity, reused across every application) — not an application-row field. See Applicant Data vs Application Data . This is the same nationality value the matcher reads and the host-facing Talent Pool Nationality Filter filters on (exposed there as citizenship), so the applicant-list filter must reuse it rather than introduce a parallel field — one source of truth keeps the terminology consistent across surfaces. Field caveat for the runtime team: the request describes this as “country of birth,” but the existing applicant-row nationality field models nationality / citizenship, which is not always the same as country of birth. Confirm which field the Teach staff actually need before wiring the P16 queue filter, and bind the filter to that field explicitly rather than assuming the two are interchangeable.
