---
title: "Queue System"
slug: queue-system
section: Globus Core
status: stable
description: "The operational control center for sponsor teams — entity-agnostic, rule-driven, lifecycle-aware, zero hardcode"
last-updated: 2026-10-07
---

# Queue System

The operational control center for sponsor teams — entity-agnostic, rule-driven, lifecycle-aware, zero hardcode

> **What is the Queue System?**
>
> The Queue System provides a real-time, actionable view of entities requiring attention. Queues are entity-agnostic — they support applicants, partner operations, job orders, housing assignments, and any future entity type. Queues are derived views — computed from rules engine outputs, workflow step completion, document status, and compliance signals. They are never manually assigned or stored as statuses.

The Queue System sits at the intersection of the Rules Engine, Workflow Studio, and Applicant Lifecycle. It transforms raw system state into team-specific workspaces aligned to operational responsibilities across all entity types.

## Core Principles

### Entity-Agnostic Architecture

The queue system is built on a unified, entity-agnostic foundation. Every queue definition declares an entity_type that determines what kind of record appears in the queue. The same infrastructure — rules, memberships, SLA tracking, bulk actions, and dashboards — works identically regardless of entity type.

> **One system, many entity types**
>
> Adding a new entity type to the queue system requires only a database configuration change — create new queue_definitions rows with the desired entity_type, attach rules, and the full queue infrastructure (views, counts, SLA, bulk actions, export) activates automatically.

### Lifecycle ≠ Queue

| Concept | Definition | Example |
| --- | --- | --- |
| Lifecycle Phase | Where the entity is in the journey | Phase 3 — Placement (applicant), Review (job order) |
| Status / Position | Exact step within a phase | Invited, Submitted, Approved |
| Queue | Operational grouping of entities needing action | Ready for SEVIS Pool ✅, Pending Approval 📋 |

> **Queues are derived, not stored**
>
> An entity's queue membership is computed on-the-fly from system state — rules engine outcomes, workflow completion, document verification, and compliance flags. There is no "queue status" field on the entity record. Queues are views, not data.

### Zero Hardcode

All queues must be defined via database configuration — never in application code. This ensures:

New queues can be added without a code deployment / Queue rules can be modified by admin users / Each brand/program can have different queue configurations / New entity types can be onboarded without code changes / Queue definitions are auditable and version-trackable
### Rule-Driven Readiness

Queue membership is powered by five signal sources:

## Queue Types

Five categories of queues, each serving a different operational purpose. All categories support any entity type.

### Lifecycle Queues (Primary Navigation)

Aligned to lifecycle phases per entity type. Each queue is an entry point — clicking into it reveals the readiness sub-queues.

#### Applicant Queues

├── My Work

├── Application

├── Validation

├── Interview

├── Placement

├── Visa & SEVIS

├── Pre-Departure

├── In Program

├── Completion

└── Exceptions ⚠️

#### Partner Ops Queues

├── My Work

├── Onboarding

├── Agreement Renewals

├── Host Company Verification

├── Quota Review

├── Compliance

└── Exceptions ⚠️

#### Job Order Queues

├── My Work

├── Pending Review

├── Pending Approval

├── Active / Matching

└── Exceptions ⚠️

#### Housing Queues

├── My Work

├── Inspection Due

├── Assignment Pending

├── Active Assignments

└── Exceptions ⚠️

### Readiness Queues (Core of the System)

> **The most important queue type**
>
> Readiness queues answer one question: "Who is ready for the next step?" They are nested inside lifecycle queues and powered by the rules engine. Each entity type has its own set of readiness sub-queues.

#### Intake & Review

#### Placement

Pre-showcase

Post-showcase

Placement readiness

#### Visa & SEVIS

#### In Program

####  Compliance Review Queue

A dedicated readiness queue scoped to compliance officers and super admins. Created using the entity-agnostic queue_definitions model. Items enter this queue when the Rules Engine produces a compliance rule failure whose fail action carries a readiness dimension that routes to it (the dimension is the routing marker; there is no `failure_behavior` field).

> **Ready for SEVIS Pool & Ready to be Showcased**
>
> These two key queues are derived queues — not lifecycle statuses. They are computed by the Rules Engine using structured rule definitions (typed conditions, configurable parameters, compliance gates) based on application approval, document verification, compliance clearance, and profile completeness. Phase 3 readiness is subdivided into pre-showcase, post-showcase, and placement readiness stages.
>
> **Live evaluator, not the Compliance Rules tile.** The authoritative signal for Ready for SEVIS Pool membership is a live per-applicant blocker evaluator (sevisReadiness.blockers[]), computed fresh on read. The "Compliance Rules" tile shown elsewhere on the applicant record is a separate, point-in-time snapshot and can be stale relative to the live evaluator — treat the live blockers list, not the tile, as ground truth when triaging a specific applicant. The live evaluator only recomputes on specific applicant-lifecycle events (stage transitions, hire) — not on unrelated edits such as a host-record change — and recompute can lag the triggering event by several minutes.

> **Interview queues are behind-the-scenes staff/host work**
>
> The Interview lifecycle queue and the Sponsor Interview / Host Interview readiness sub-queues surface staff- and host-facing work, not an applicant-facing step. A sponsor or host interview is a step-tree node flagged behind-the-scenes by role assignment: it never renders on the applicant stepper and is never sent to the Applicant Portal as an access="hidden" step. It still produces an outcome fact that gates the phase, which is exactly what these queues track and assign.

> **Registered, Never Started intake queue**
>
> A seeded child of the Intake lifecycle queue (slug `intake-never-started`, category `stage`, entity type application) lists applicants registered over a week ago who have not completed a single workflow step, so staff-registered applicants who never open step 1 have their own worklist instead of being mixed into `intake-initial`. Its entry rule is the `never_started` SQL condition. The row is inserted by migration in dev, QA and production, because deployed environments boot with SKIP_SEEDS=true and would otherwise never receive the seed.

> **Entered Market stage retired; program-scoped stage names**
>
> The Entered Market (`market_ready`) lifecycle stage is retired as a user-facing stage: the forward transition from Sponsor Interview and the rollback edge from Reserved to Host were removed, so applicants advance directly from `sponsor_interview` to `reserved_to_host` when a reservation is created (rollback to Sponsor Interview requires a reason and the `stage.transition.rollback_sensitive` permission). Legacy records at `market_ready` are preserved. Separately, the stage catalogue is shared by all programs but a program can carry its own display wording for a stage (for example the Wild Packs programs show `first_document_batch` as "Application Submitted"). The override is naming only: stage keys, transitions, queue membership and rule bindings are untouched, and a program with no override falls back to the shared catalogue name.

### Exception Queues (Critical)

High-visibility, prioritized queues for entities requiring immediate attention. Exception queues support a sub-category filter strip — a horizontal pill bar at the top of the exception view that lets staff filter by exception sub-category (e.g., "Documents", "Compliance", "SEVIS", "Placement") without leaving the exception queue.

| Exception Queue | Trigger | Priority | Auto-Action |
| --- | --- | --- | --- |
| Missing Documents | Required documents not uploaded by deadline | 🟠 Warning | Notification to applicant + partner |
| Needs Manual Review | Rules engine flags ambiguous eligibility | 🟠 Warning | Routed to review team queue |
| Compliance Hold | Failed compliance check (background, medical) | 🔴 Critical | Block placement, notify compliance team |
| SEVIS Check-In Overdue | 4+ days since arrival, no SEVIS check-in (configurable: sevis_checkin_due_days = 3) | 🟠 Warning | Reminder to applicant + SEVIS team notification |
| SEVIS Risk Critical (Day 20) | 20+ days since arrival, no SEVIS check-in (configurable: sevis_critical_escalation_day = 20) | 🔴 Critical | Urgent escalation to SEVIS team + program manager + compliance lead |
| Missed Monthly Check-ins | 2 consecutive check-ins missed | 🔴 Critical | Create support ticket, compliance queue |
| Address Mismatch | Monthly check-in reports address change ("No") | 🟠 Warning | Flag for follow-up, update records |
| Second Job Declared | Participant reports second job in check-in | 🟠 Warning | Compliance review, program rule check |
| Re-placement Required | Placement cancelled or host withdrew | 🔴 Critical | Return to placement queue, notify partner |
| Partner Suspension Impact | Partner suspended with active applicants | 🔴 Critical | Reassign applicants, freeze nominations |

#### Sub-Category Filter Strip

Horizontal pill bar at the top of the exception queue view:

All / Documents / Compliance / SEVIS / Placement / Check-ins / Partner
### Exception Queue Definitions

*Status: the table below is the original design. Exception queues are queue_definitions rows with category exception under the parent slug exceptions (server/seed/queueDefinitions.exceptions.ts). queue_definitions has no sub_category or brand_scope columns (it has brand_id, program_id, parent_queue_id, display_family and owner), and entity_type defaults to application. The seeded set differs, for example Document Expired, SEVIS Overdue, No-Show, Compliance Failed and the Issues queues.*

All 10 exception queues are stored as queue_definitions records with category: exception and entity_type: applicant. Each definition includes priority level, icon, sub-category, and optional brand/program scoping.

| name | icon | color | priority | sub_category | brand_scope |
| --- | --- | --- | --- | --- | --- |
| Missing Documents | file-warning | amber | 🟠 Warning | Documents | null (all) |
| Needs Manual Review | eye | amber | 🟠 Warning | Compliance | null (all) |
| Compliance Hold | shield-alert | red | 🔴 Critical | Compliance | null (all) |
| SEVIS Check-In Overdue | clock-alert | amber | 🟠 Warning | SEVIS | null (all) |
| SEVIS Risk Critical (Day 20) | alert-triangle | red | 🔴 Critical | SEVIS | null (all) |
| Missed Monthly Check-ins | calendar-x | red | 🔴 Critical | Check-ins | null (all) |
| Address Mismatch | map-pin-off | amber | 🟠 Warning | Check-ins | null (all) |
| Second Job Declared | briefcase-business | amber | 🟠 Warning | Compliance | null (all) |
| Re-placement Required | rotate-ccw | red | 🔴 Critical | Placement | null (all) |
| Partner Suspension Impact | building-2 | red | 🔴 Critical | Partner | null (all) |

### Exception Queue Rules

*Status: the table below is the original design. queue_rules columns are queue_definition_id, rule_type, conditions, logical_operator, group_index, sort_order, is_entry and is_active, and each seeded exception queue carries a queue_definitions.rule_config with a type such as sevis_checkin_overdue rather than SQL condition strings.*

Each exception queue has a corresponding queue_rules entry defining its trigger condition as a JSONB rule_config. The rules engine evaluates these against live entity state.

| Exception Queue | rule_type | rule_config (trigger condition) |
| --- | --- | --- |
| Missing Documents | sql_condition | "condition": "required_documents_pending > 0 AND document_deadline < NOW()" |
| Needs Manual Review | rules_engine | "rule_outcome": "needs_review", "eligibility_result": "ambiguous" |
| Compliance Hold | rules_engine | "compliance_check": "failed", "check_types": ["background", "medical"] |
| SEVIS Check-In Overdue | sql_condition | "condition": "days_since_arrival > sevis_checkin_due_days AND sevis_checkin_status = 'not_completed'" |
| SEVIS Risk Critical | sql_condition | "condition": "days_since_arrival >= sevis_critical_escalation_day AND sevis_checkin_status = 'not_completed'" |
| Missed Monthly Check-ins | sql_condition | "condition": "consecutive_missed_checkins >= 2" |
| Address Mismatch | event_trigger | "event": "checkin_submitted", "field": "address_confirmed", "value": false |
| Second Job Declared | event_trigger | "event": "checkin_submitted", "field": "second_job_declared", "value": true |
| Re-placement Required | event_trigger | "events": ["placement_cancelled", "host_withdrew"] |
| Partner Suspension | event_trigger | "event": "partner_suspended", "has_active_applicants": true |

### Escalation & Auto-Routing

Exception queues support enhanced escalation rules with auto-routing, manual override, and configurable escalation chains. When an entity enters an exception queue, the system evaluates escalation rules and can automatically route to the appropriate team or individual.

#### Auto-Routing

When an entity enters an exception queue, it is automatically routed to the team or individual defined in the escalation rule. Routing considers team load balancing, skill matching, and availability.

#### Manual Override

Staff with appropriate permissions can manually override auto-routing to reassign exception items to a different team or individual. All overrides are audit-logged.

#### Escalation Chains

Multi-level escalation: if the initial assignee does not act within the SLA window, the item escalates to the next level (team lead, then department head, then operations manager).

### SEVIS 4-Tier Progression

#### sevis-readiness-blocked Queue

> **sevis-readiness-blocked workbench queue**
>
> Applicants who fail the global `ready_for_sevis` 11-rule gate — or a program add-on gate such as CAMP-001 / CAMP-002 / CAMP-003 — route to the sevis-readiness-blocked workbench queue. The gate is **fail-closed**: an applicant stays in this queue until every failing rule passes (or is overridden). See the SEVIS Readiness page for the full gate architecture and rule list.
>
> **Seeded reason codes and SLA.** The queue-definition seed step also creates five global (brand-independent) reason codes for this queue, each with severity hint `blocking` and owner role `compliance`: `SEVIS_PASSPORT_NOT_UPLOADED` (passport document absent or rejected/archived), `PASSPORT_VALIDITY_INSUFFICIENT` (the `passport_validity` compliance rule is failing or pending), `AGE_MIN_NOT_MET` (the `age_min` rule is failing), `MINIMUM_AGE_NOT_MET` (the program-specific minimum-age variant) and `SEVIS_PREREQUISITES_NOT_MET` (catch-all). They are display metadata; no new document checks are introduced, and a code that already exists globally is skipped on re-seed. The same seed adds an SLA policy of 480 business minutes (one business day, 09:00 to 17:00) with a severity-upgrade escalation rule and no auto-lapse, so the block persists until the readiness data changes.

### Auto-Actions (Dual Trigger)

Each exception queue can fire two independent action triggers — on queue entry and on SLA breach:

| Exception Queue | On Queue Entry | On SLA Breach |
| --- | --- | --- |
| Missing Documents | notify (applicant + partner) | add_flag + notify (team lead) |
| Needs Manual Review | reassign (review team queue) | notify (team lead) + create_ticket |
| Compliance Hold | block_placement + notify (compliance team) | dashboard_alert + create_ticket + notify (operations lead) |
| SEVIS Check-In Overdue | notify (applicant + SEVIS team) | reassign (SEVIS escalation queue) |
| SEVIS Risk Critical (Day 20) | notify (SEVIS team + PM + compliance lead) + add_flag | dashboard_alert + create_ticket + reassign |
| Missed Monthly Check-ins | create_ticket + add_flag | notify (compliance lead) + dashboard_alert |
| Address Mismatch | notify (assigned staff) | notify (team lead) |
| Second Job Declared | notify (compliance team) | add_flag + create_ticket |
| Re-placement Required | reassign (placement queue) + notify (partner) | dashboard_alert + notify (operations lead) |
| Partner Suspension Impact | reassign (all applicants) + add_flag | dashboard_alert + create_ticket |

> **Event Architecture**
>
> Queue-entry actions are triggered by the queue.exception.entered event, emitted by the queue membership resolver when an entity is newly added to an exception queue. SLA-breach actions are triggered by the existing escalation_rules infrastructure when a queue_sla_events timer transitions to breached state. Both mechanisms are independent and can be configured separately per exception queue. For the full notification catalog, channel routing, and escalation chains, see the Notification & Communication Model .

### Team Queues (RBAC-Driven)

Queues filtered by who owns the work. Driven by RBAC role assignments and program scope. Team queues apply across all entity types.

Compliance Team / Placement Team / SEVIS Team / Operations Team / Partner Team
Team queue membership is determined by assigned_team, assigned_role, and program_scope from the RBAC system.

### My Work Queue (Personal)

A personalized view showing only what the logged-in user needs to act on. The My Work queue includes instant count refresh — counts update in real-time as items are assigned, completed, or escalated without requiring a page reload.

Entities assigned to this user across all entity types / SLA-overdue items requiring immediate attention / High-priority flags from exception queues / Recently touched items (last 7 days) / Grouped by entity type with collapsible sections
> **Instant Count Refresh**
>
> The My Work queue sidebar count badge refreshes by polling (React Query refetchInterval of 30 seconds); there is no WebSocket push channel for queue counts. When a queue membership changes, the badge catches up on the next poll or on refetch after a mutation.

## Queue Features

The queue system includes a comprehensive set of operational features that work identically across all entity types.

### Bulk Actions

Multi-select rows in any queue view to perform batch operations. Select individual items via checkbox or use "Select All" to select all visible (or all matching filter) items.

| Action | Description | Example |
| --- | --- | --- |
| Batch Assign | Assign selected items to a staff member or team | Select 15 applicants → Assign to SEVIS Team |
| Batch Transition | Move selected items to a different queue or status | Select 8 job orders → Transition to Approved |
| Batch Reassign | Reassign selected items from one owner to another | Reassign 12 items from User A → User B |
| Batch Unassign | Remove assignment from selected items | Unassign 5 items back to team pool |

All bulk actions are audit-logged with the acting user, timestamp, and affected entity IDs. Bulk actions respect RBAC — users can only perform actions they have permission for.

**Queue and review mutation routes (BackOfficePrototype).** The handlers, not the mutation registry, define the gates. `POST /api/queues/bulk-approve` and `POST /api/queues/bulk-move` take `applicationIds` (at most 100; more returns `400 BAD_REQUEST`, and no matching application returns `404 NOT_FOUND`) and drop ids outside the caller's RBAC scope, reporting the count as `skippedByScope`; when none are in scope the response is `403 FORBIDDEN`. Bulk approve sets `readinessState` to `on_track` and adds an internal note per application (response `approved`, `skippedByScope`). Bulk move takes `targetQueueSlug` (an active queue, otherwise `404 NOT_FOUND`) and an optional `sourceQueueSlug` that must differ from the target, and returns `moved`, `skippedAlreadyInTarget` and `skippedByScope`. `POST /api/queues/inline/change-readiness` takes `applicationId`, a `readinessState` of `on_track`, `needs_attention`, `blocked` or `paused`, and a required `note`; `POST /api/queues/inline/escalate` takes `applicationId` with an optional `note` and `assignToUserId`, sets `needs_attention`, and assigns through the canonical assignment service when the target user is active. These four routes require a session and `applicant.manage`, and errors use `{ error: { code, message, details, traceId } }`. `POST /api/work-items/:id/schedule` (API key scope `applications.write`, permission `applications.manage`) requires `scheduledAt`, sets the item to `scheduled`, and optionally sets `assignedToUserId` and `notes`. `POST /api/review/items/:source/:id/decision` (API key scope `dashboard.read`, permission `document.verify`) records `approved`, `rejected` or `resubmission_requested` on the applicant document `:id` (the handler does not read `:source`) with an optional `reason`. It returns `400` for a missing or invalid action, `404` for a document that is missing or outside the caller's scope, `403` when the document type's reviewer permission or assignment is missing, and `409` with the top-level `code` `INVALID_STATE_TRANSITION` when the state change is not allowed.

### CSV / Excel Export

*Status: not built for queue item views. There is no export route under /api/queues and no export action in the queues page; the CSV and Excel sections below are a design spec. The only queue-related export is the aggregate Queue Aging & SLA report behind GET /api/reports/queue-metrics, which carries no participant rows.*

Export the current queue view (with active filters applied) as a CSV or Excel file. Export is available from any queue view via the export button in the toolbar.

#### CSV Export

All visible columns included / Active filters applied / Supports up to 10,000 rows / UTF-8 encoding with BOM for Excel compatibility
#### Excel Export

Formatted headers and column widths / Color-coded priority and status columns / Separate sheet per entity type (if mixed) / Timestamp in filename for versioning
### Inline Row Actions

Each row in a queue table includes a contextual action menu (three-dot icon or hover-reveal buttons) that allows staff to take action on a single item without navigating away from the queue view.

Per-item workbench actions are gated by four permission keys. `queue.item.manage` covers `POST /api/queues/:slug/items/:applicationId/priority` (set priority without changing status). `queue.item.resolve` covers `.../resolve` (terminal; `overridden: true` records `resolutionType` overridden) and `.../pass` (resolves the item, marks its open reasons manually resolved and advances the applicant). `queue.item.dismiss` covers `.../dismiss` (terminal, `resolutionType` dismissed). Resolve and dismiss require a `note` of 1 to 2000 characters. `queue.item.override` is seeded as restricted with a required reason (resolve a blocked item through an eligibility override), but no route handler currently checks it: an override resolve only needs `queue.item.resolve`. An item that is missing, outside the caller's scope or not a queue member answers `404` `QUEUE_ITEM_NOT_FOUND`.

### Facet Filtering

Queue views support multi-dimensional facet filtering. Facets update available options and counts dynamically as filters are applied (progressive disclosure).

| Facet | Type | Applies To |
| --- | --- | --- |
| Entity Type | Single select | All views (when mixed entity queues) |
| Brand | Multi-select | All entity types |
| Program | Multi-select (filtered by brand) | Applicant, Job Order |
| Season | Multi-select (filtered by program) | Applicant, Job Order |
| Assigned To | Multi-select (user picker) | All entity types |
| Priority | Multi-select (Critical, Warning, Normal) | Exception queues |
| SLA Status | Multi-select (On Track, Warning, Breached) | All queues with SLA |
| Date Range | Date picker (entered queue) | All entity types |
| Partner | Multi-select | Applicant, Partner Ops |
| Host | Multi-select | Job Order, Housing |

On the Queues page, the worklist's search, priority, reason code, action-required, unassigned-only and SLA-breached-only filters, its activity sort direction and its page number are bound to the URL query parameters `search`, `priority`, `reasonCode`, `actionRequired`, `unassigned`, `slaBreached`, `sortDir` and `page`. The worklist renders Ops advanced applicant filters, an assignee filter and a classification filter at its top, and the Queues page shows applicant filter chips at its top. When the worklist query returns an error (`isError`), the worklist shows an alert reading "This worklist could not be loaded. Please retry; this is not an empty search result." instead of the empty state. In the split detail panel, when `position` is null the panel shows a status message reading "This application is not in the current results. The selected profile remains open; select a row to resume queue navigation." The worklist derives the open application's row position from its identity on every refresh, and does not treat a different application of the same person as the selected row.

### Reassignment & Unassignment

Queue items can be reassigned to a different staff member or unassigned back to the team pool. Both operations are available as inline row actions or bulk actions.

#### Reassignment

User picker shows team members with current load counts / Reassignment preserves SLA timer (does not reset) / Previous assignee receives notification of reassignment / New assignee receives notification with context / Audit log records from/to/reason/timestamp
#### Unassignment

Returns item to the team pool for claim / SLA timer continues running (not paused) / Item visible in team queue, not personal queues / Optional reason field for context / Auto-routing may re-assign based on rules
#### Bulk Assign from Unassigned Work

Queues whose rule configuration type is `unassigned_work` support a safe bulk claim through `POST /api/queues/:slug/items/bulk-assign` (`applicant.manage`, API key scope `applications.write`). The body takes `applicationIds` (1 to 100) and an optional `targetUserId` that defaults to the caller, so the page selection can be assigned to me or to an eligible staff member. Other queues return 400 `BULK_ASSIGN_NOT_SUPPORTED`. The target must be an active, non-superadmin user holding an assignable role, otherwise 400 `INVALID_TARGET`. Queue membership and full Ops scope are re-checked at mutation time for every selected application. Each applicant is then claimed with an atomic compare-and-set on the assigned staff field (only while still unassigned), which keeps the application owner, assignment history and audit trail in step, so a concurrent claim is never overwritten. Applicants claimed in the meantime are returned as skipped rather than failing the batch. The response carries the `assigned` and `skipped` counts plus `assignedApplicantIds` and `skippedApplicantIds` (counted per applicant, not per application), and one `queue.unassigned.bulk_assigned` audit entry is written for the batch.

**Queue list endpoints.** `GET /api/queues/:slug/entities` (`dashboard.view`; `limit` 1 to 100, default 25, `offset`, `search`) lists the rows of an entity-backed queue (host verification, job orders, site visits, housing leads) and falls back to the applicant list for applicant-backed queues. For entity-backed queues it also accepts optional UUID `programId` and `hostOrgId` filters, applied inside the caller's scope, and the response includes `scopeEmpty` when the caller holds the permission but has no brand or program grant. `GET /api/queues/:slug/items` (`dashboard.view`) is the operational worklist: filters `programId`, `seasonId`, `search`, `assignedToUserId`, `ruleId` (or `reason.rule_id`), `priority` (comma-separated), `reasonCode`, `sourceType`, `actionRequired`, `unassigned`, `slaBreached`, `sortBy`, `sortDir`, `page` and `pageSize` (at most 200); it returns `items`, `total`, `summary`, reason `groups` and `facets`, and strips note text for callers without `applicant.profile.view`. An unknown queue slug is not an error on these two reads, which return an empty result; the queue mutation routes answer 404 `QUEUE_NOT_FOUND` instead.

The queue-item mutation routes (`resolve`, `dismiss`, `priority` and `pass` under `/api/queues/:slug/items/:applicationId/`, and `bulk-assign`) authorize each application id against the caller's full Ops visibility scope and the queue's membership predicate before any state, audit or event write. A missing id, an id outside the caller's scope and an id that is not a current member of the queue all return the same `404` with the code `QUEUE_ITEM_NOT_FOUND` and the message `Queue item not found in this queue`, so the response never reveals whether the application exists. When a request carries more than one id (bulk assign), the whole batch is rejected if any id is denied, and `details.deniedApplicationIds` echoes the submitted ids that were denied; single-item requests carry `details` null. `GET /api/swt/applications/:applicationId/review` (permission `compliance.overrides.review`) returns the same denial for a `queue` value that is not an SWT review queue or for a non-member application.
### Instant Count Refresh

Queue sidebar counts and badge indicators refresh by polling (30-second React Query refetchInterval) and after queue mutations. There is no WebSocket push channel for queue counts.

## Data Model

> **Zero Hardcode — Database-Driven Configuration**
>
> All queue definitions, rules, and display properties are stored in the database. Application code reads configuration — it never defines queues. This allows admin users to create, modify, and deactivate queues without code changes. The entity_type field on queue_definitions and queue_memberships enables a unified data source approach across all entity types.

### queue_definitions

| Column | Type | Description |
| --- | --- | --- |
| id | UUID | Primary key |
| name | VARCHAR(100) | Display name (e.g., "Ready for SEVIS Pool") |
| entity_type | VARCHAR(50) | Entity type this queue applies to: applicant, partner, job_order, housing |
| category | ENUM | lifecycle \| readiness \| exception \| team |
| sub_category | VARCHAR(50) (nullable) | Sub-category for exception filter strip (e.g., Documents, Compliance, SEVIS) |
| parent_queue_id | UUID (nullable) | Parent queue for nesting (e.g., readiness queue under lifecycle queue) |
| icon | VARCHAR(50) | Icon identifier for UI rendering |
| color | VARCHAR(20) | Color code for visual differentiation |
| sort_order | INTEGER | Display order within category |
| is_active | BOOLEAN | Soft delete / disable flag |
| brand_scope | VARCHAR[] (nullable) | Restrict queue to specific brands (null = all brands) |
| program_scope | VARCHAR[] (nullable) | Restrict queue to specific programs (null = all programs) |

### queue_rules

| Column | Type | Description |
| --- | --- | --- |
| id | UUID | Primary key |
| queue_id | UUID (FK) | Reference to queue_definitions |
| rule_type | ENUM | rules_engine \| sql_condition \| event_trigger |
| rule_config | JSONB | Rule configuration (conditions, thresholds, field checks) |
| priority | INTEGER | Rule evaluation order (lower = first) |
| is_active | BOOLEAN | Enable/disable individual rules |

### queue_memberships

*Status: there is no queue_memberships table; the table below is the original design. Membership history is stored in queue_transitions (entity_type, entity_id, application_id, queue_definition_id, entered_at, exited_at, exit_reason), see Queue Transition History below.*

| Column | Type | Description |
| --- | --- | --- |
| id | UUID | Primary key |
| entity_type | VARCHAR(50) | Entity type: applicant, partner, job_order, housing |
| entity_id | UUID (FK) | Reference to the entity record (polymorphic via entity_type) |
| queue_id | UUID (FK) | Reference to queue_definitions |
| assigned_to | UUID (FK, nullable) | Staff member currently assigned to this item |
| entered_at | TIMESTAMP | When the entity entered this queue |
| exited_at | TIMESTAMP (nullable) | When the entity left this queue (null = still active) |
| computed_at | TIMESTAMP | When membership was last computed / refreshed |

> **Unified Data Source**
>
> The entity_type + entity_id pair in queue_memberships creates a polymorphic reference. The queue system resolves the correct entity table at query time based on entity_type. This enables a single membership table to serve all entity types while maintaining referential integrity through application-level validation.

### Queue Transition History

Every queue entry and exit is tracked with timestamps, enabling full audit trails and operational analytics. The transition history powers throughput metrics, bottleneck detection, and compliance reporting.

| Field | Description |
| --- | --- |
| entered_at | Timestamp when the entity entered the queue (set on membership creation) |
| exited_at | Timestamp when the entity left the queue (set on exit, null while active) |
| duration | Computed: exited_at - entered_at (total time in queue including pauses) |
| active_duration | Computed: total time minus SLA pause periods (actual working time) |
| exit_reason | Why the entity left: completed, reassigned, escalated, cancelled, expired |
| transitioned_by | User who triggered the exit (null for automated transitions) |

#### Transition History Use Cases

Audit & Compliance

Full trail of every queue an entity passed through, with timestamps and actors

Performance Analytics

Average time-in-queue per queue, per team, per entity type

Bottleneck Detection

Identify queues where entities spend disproportionately long

## Queue Logic Examples

Each queue's rule_config is a JSONB document describing the conditions an entity must meet. The rules engine evaluates these conditions against live system state. Examples span multiple entity types.

Applicant

#### Ready for SEVIS Pool

entity_type / applicant / application_status / approved / placement_status / placed / documents / compliance 
Applicant

#### Ready to be Showcased

entity_type / applicant / application_status / approved / profile_complete / skills_completed / showcase_video / documents_verified 
Job Order

#### Ready for Matching

entity_type / job_order / status / approved / host_compliance / cleared / positions_available / > 0 / season_active 
Partner Ops

#### Agreement Expiring

entity_type / partner / agreement_end_date / < NOW() + 30 days / renewal_status / not_started / has_active_applicants 
Housing

#### Inspection Due

entity_type / housing / last_inspection_date / < NOW() - 365 days / has_active_assignments / inspection_status / not_scheduled 
Applicant

#### SEVIS Risk Critical (Day 20+)

entity_type / applicant / days_since_arrival / >= sevis_critical_escalation_day / sevis_checkin_status / not_completed 
Triggers: Urgent escalation — SEVIS team + program manager + compliance lead

## UI / UX Design

The queue interface follows a master-detail layout consistent with the rest of Globus. The entity type selector at the top of the sidebar switches between entity-specific queue trees.

#### Left Sidebar

Entity type selector (Applicant, Partner, Job Order, Housing) / Queue categories (collapsible) / Icons + live counts per queue (instant refresh) / Active queue highlighted / Badge indicators for critical items
#### Main Panel

Table view (sortable columns) / Facet filters (program, season, partner, status, SLA) / Bulk assignment toolbar / Inline row actions (three-dot menu) / Multi-select checkboxes with Select All
Table columns:

| Column | Description | Sortable |
| --- | --- | --- |
| Entity Name | Full name / title with link to profile | Yes |
| Entity Type | Applicant, Partner, Job Order, Housing | Yes |
| Program / Season | Active program and season (where applicable); applicant worklist rows also render a participation classification display | Yes |
| Phase | Current lifecycle phase | Yes |
| Current Position | Exact step within the phase | Yes |
| Queue Tags | Colored chips showing all active queue memberships | No |
| Flags | Warning/critical indicators from exception queues | Yes (by priority) |
| SLA Status | Timer indicator with time remaining or breach duration | Yes |
| Last Activity | Timestamp of most recent state change | Yes |
| Assigned To | Staff member or team responsible | Yes |

The Queues page worklist table has separate Program and Placement columns and a Screening column.

### Queue Tags & Priority

Each entity can appear in multiple queues simultaneously. Queue tags are displayed as colored chips:

Example applicant tags:

Ready for SEVIS 
Compliance OK 
Placement Complete 
Priority indicators:

## Queue Dashboard

The queue dashboard provides operational leaders with a bird's-eye view of all queue activity across entity types. It surfaces pipeline health, bottlenecks, throughput, and workload distribution.

### Pipeline Visualization

A horizontal pipeline strip shows queue stages with entity counts at each stage. Stages with high counts or breached SLAs are highlighted to draw attention. The pipeline adapts to the selected entity type.

Example: Applicant Pipeline

### Bottleneck Detection

The dashboard automatically identifies bottlenecks — queues where entities are accumulating faster than they are being processed. The Top Bottlenecks widget (GET /api/ops/top-bottlenecks) ranks unresolved queue reasons grouped by rule_id. Each bucket scores the sum of severity weight (blocker 3, warning 2, info 1) times an aging factor (1 plus days since the reason was generated divided by 7), and at most 6 buckets are shown. The percentile, SLA-overrun, inflow/outflow and breach-rate alert triggers described in the original design are not implemented as automatic alerts.
Bottleneck Alert Example

Bottleneck Detected: Placement Queue

67 entities waiting (avg: 28) · Avg time in queue: 4.2 days (SLA: 3 days) · 23% breach rate

### Throughput Metrics

Throughput metrics track the rate at which entities move through queues over time. These metrics are powered by the queue transition history (entered_at / exited_at timestamps).

`GET /api/admin/queue-definitions/:slug/metrics` (API key scope `dashboard.read`, permission `queues.manage`) returns `{ open, averageAgeDays, breaches }` for one queue, computed in a single aggregate pass over the caller's scoped membership; the optional `programId` and `seasonId` query parameters narrow it. `GET /api/applications/:id/current-queues` (API key scope `stages.read`; any of `applications.view`, `applications.edit`, `applicant.profile.view` or `applicants.list.view`) lists every active queue the application currently appears in, evaluated with the same membership predicates as the Queues page rather than read from `queue_transitions`; the applicant profile Current Queue rail uses it. The queue-item routes that resolve a queue by slug (bulk assign, resolve, dismiss, priority and pass) answer an unknown slug with `404` and the code `QUEUE_NOT_FOUND` in the envelope `{ error: { code, message, details, traceId } }`.

### Workload Distribution

The workload view shows how queue items are distributed across team members. It helps managers identify overloaded staff and rebalance assignments.

| Staff Member | Assigned | Breached | Completed Today | Load Status |
| --- | --- | --- | --- | --- |
| Sarah K. | 24 | 1 | 8 | 🟢 Normal |
| Mike R. | 38 | 4 | 3 | 🔴 Overloaded |
| Jane L. | 15 | 0 | 12 | 🟢 Normal |
| Unassigned | 22 | 3 | 0 | ⚠️ Pool |

## Event-Driven Updates

Queues update automatically — no manual refresh required. The following events trigger queue recomputation across all entity types:

| Event Source | Events | Queue Impact |
| --- | --- | --- |
| Application Steps | step_completed, step_reset | Readiness queue recalculation |
| Document System | document_uploaded, document_verified, document_rejected | Document-dependent queues + exception queues |
| Rules Engine | eligibility_evaluated, compliance_checked | All readiness + exception queues |
| SEVIS | sevis_checkin_recorded, ds2019_issued, sevis_status_changed | Visa & SEVIS queues + SEVIS exception queues |
| Monthly Check-in | checkin_submitted, checkin_overdue | In Program queues + missed check-in exceptions |
| Placement | placement_confirmed, placement_cancelled, reservation_expired | Placement queues + re-placement exception |
| Partner | partner_suspended, partner_reactivated, agreement_expiring | Partner ops queues + partner suspension impact |
| Job Order | job_order_submitted, job_order_approved, job_order_closed | Job order queues + matching readiness |
| Housing | inspection_scheduled, inspection_completed, assignment_changed | Housing queues + housing exception queues |

## Integration Points

#### Applicant Portal

Portal actions drive queue updates:

Step completion → readiness recalc / Document uploads → document queues / Check-in submission → compliance queues / Travel requests → on-program queues
#### SEVIS

SEVIS events update compliance queues:

Check-in status → SEVIS overdue queue (Day 4+) or critical queue (Day 20+) / DS-2019 issuance → visa queue progression / Status changes → compliance flags / Arrival tracking → 4-tier SEVIS compliance (Arrival → Standard Days 0–3 → Overdue Day 4+ → Critical Day 20+)

Deadline-driven exception reconciliation (document expiry, SEVIS check-in regulatory deadlines) is persisted in the durable `queue_deadline_jobs` table rather than in-process timers, so pending deadlines survive restarts and there is no scheduling horizon beyond which a deadline is dropped. Jobs are unique per (application, reason, due time); a polling worker running inside the SLA recalculation worker's advisory-locked cycle claims due rows, re-runs exception-queue reconciliation, and parks a job as `failed` after 5 attempts. A journey transition into a terminal position (withdrawn, rejected, completed, cancelled, alumni) that leaves the application in the SEVIS Ready Pool now revokes it from the pool (audit log and `sevis_readiness.revoked` event, no readiness evaluation).
#### n8n / Workers

Automation triggers from queue state:

Notifications from queue transitions / Queue count-based alerts / Support ticket creation / Scheduled queue snapshots
## Queue-Powered Reporting

Queue counts, throughput metrics, and transition history power the executive and operational dashboards:

Ready for SEVIS / Ready for Placement / Compliance Risk Count / Check-in Completion Rate / Exception Trends / Pipeline Throughput / SLA Compliance Rate / Bottleneck Alerts / Workload Balance / Entity Type Breakdown
## SLA Management

> **Queues show work. SLA shows urgency.**
>
> Without SLAs, queues are passive lists. With SLAs, they become real management tools — measurable service standards, operational accountability, escalation paths, and compliance-critical deadline tracking. SLA is a separate configuration layer on top of queues.

### Design Principle

SLA rules are not embedded in queue logic. They are a separate configuration layer:

#### Queue Responsibility

Determines who belongs in the queue (membership rules)

#### SLA Responsibility

Determines how quickly that queue must be worked (time targets)

This separation is critical because the same queue may later have different SLA rules by brand, program, season, priority, entity type, or visa type. A single queue definition supports multiple SLA policies with different scopes.

### Policy Configuration

Super Admin configures SLA policies at Admin → Queue Configuration → SLA Policies. Each policy defines timing, behavior, and escalation for a specific queue and scope. The policy form includes timezone selection and a scope picker that allows narrowing the policy to a specific brand, program, season, or entity type.

| Setting | Description | Example |
| --- | --- | --- |
| Response Target | Time to first action on the queue item | 4 business hours |
| Resolution Target | Time to fully resolve / move entity out of queue | 2 business days |
| Working Hours | Business hours vs 24/7 calendar | Mon–Fri 8am–6pm EST |
| Timezone | Timezone for business hours calculation | America/New_York |
| Scope Picker | Narrow policy to brand, program, season, or entity type | Brand: Alliance Abroad, Program: Camp |
| Warning Threshold | % of SLA elapsed before warning triggers | 80% (amber highlight) |
| Breach Threshold | Grace period after SLA expires before escalation | 30 minutes |
| Escalation Rule | What happens when SLA breaches | Notify team lead + create ticket |
| Pause Conditions | Events that pause the SLA timer | Waiting on applicant document upload |
| Priority Override | Different SLA for different priority levels | Critical = 2hr, Normal = 2 days |

Example SLA policies:

| Queue | Response SLA | Resolution SLA | Hours |
| --- | --- | --- | --- |
| Ready for SEVIS Pool | — | 2 business days | Business |
| SEVIS Check-In Overdue (Day 4+) | 4 hours | 1 business day | Business |
| SEVIS Risk Critical (Day 20+) | 1 hour | 4 hours | 24/7 |
| Ready to be Showcased | — | 3 business days | Business |
| Missing Documents | 4 hours | 1 business day | Business |
| Compliance Hold | 2 hours | 8 business hours | Business |
| Missed Monthly Check-ins | 4 hours | 1 business day | 24/7 |

### Scope Controls

SLA policies can be scoped at multiple levels. More specific scopes override broader ones.

Scope override example:

Default: All Compliance Hold queues = 8 business hours globally

Override: Camp Counselor SEVIS queues = 4 business hours in peak season (Jun–Aug)

### Timer Behavior

SLA timers attach to the queue membership event, not the entity. One entity may move through many queues, each with its own independent timer. Timers respect the configured timezone and business hours calendar.

| Timer Event | When It Happens | Example |
| --- | --- | --- |
| Starts | Entity enters queue | Applicant joins Ready for SEVIS Pool → 2-day timer starts |
| Pauses | Waiting on external dependency | Waiting on applicant document upload → timer frozen |
| Resumes | External dependency resolved | Document arrives → timer resumes from where it paused |
| Stops | Entity leaves queue or marked complete | SEVIS processed → timer ends as Completed |
| Warns | Warning threshold reached (e.g., 80% elapsed) | 1 day 14hr of 2-day SLA → amber warning badge |
| Breaches | SLA target exceeded (+ grace period) | 2 days + 30min grace → red breach indicator + escalation |

Timer statuses visible on every queue item:

### Pipeline Strip Indicators

The pipeline strip on the queue dashboard shows SLA health per pipeline stage. Each stage displays a color-coded bar indicating the proportion of items that are on-track, warning, or breached.

Pipeline SLA Health Strip

On Track 
Warning 
Breached 
### SLA Data Model

Three tables support the SLA system — policies, live timer events, and escalation rules.

#### queue_sla_policies

| Column | Type | Description |
| --- | --- | --- |
| id | UUID | Primary key |
| name | VARCHAR(100) | Policy name (e.g., "SEVIS Pool — Standard") |
| queue_definition_id | UUID (FK) | Reference to queue_definitions |
| first_response_minutes | INTEGER (nullable) | Target time for first staff response (null = track resolution only) |
| resolution_minutes | INTEGER | Target time to fully resolve / exit queue |
| use_business_hours | BOOLEAN | true = business hours only (default); false = 24/7 calendar |
| business_hours_start / business_hours_end | INTEGER | Business-day start and end hour (defaults 9 and 17) |
| business_hours_timezone | VARCHAR(64) | IANA timezone for business hours (default America/New_York) |
| include_weekends | BOOLEAN | When true, weekends count as working days (default false) |
| warning_percent | INTEGER | % of SLA elapsed before warning (default: 75) |
| breach_grace_minutes | INTEGER | Grace period after SLA expires before escalation |
| pause_conditions_json | JSONB | Events that pause the timer (e.g., awaiting document) |
| holiday_country / holiday_region / holiday_calendar_json, capacity_target / capacity_warning_threshold / capacity_critical_threshold / stop_assign_when_critical, reminder_ladder_json, health_score_config | various | Holiday calendar, queue capacity, reminder ladder and health-score configuration |
| priority | INTEGER | For scope conflict resolution (higher = wins) |
| is_active | BOOLEAN | Enable/disable without deleting |
| created_by | UUID (FK) | Admin who created the policy |

#### queue_sla_events

| Column | Type | Description |
| --- | --- | --- |
| id | UUID | Primary key |
| application_id | UUID (FK) | Reference to applications |
| queue_definition_id | UUID (FK) | Reference to queue_definitions |
| sla_policy_id | UUID (FK) | Reference to queue_sla_policies |
| started_at | TIMESTAMP | When entity entered the queue |
| paused_at | TIMESTAMP (nullable) | When timer was last paused |
| resumed_at | TIMESTAMP (nullable) | When timer was last resumed |
| completed_at | TIMESTAMP (nullable) | When entity left queue or was resolved |
| breached_at | TIMESTAMP (nullable) | When SLA was breached |
| current_state | ENUM | on_track \| warning \| breached \| paused \| completed |
| elapsed_minutes | INTEGER | Total active time elapsed (excluding pauses) |
| remaining_minutes | INTEGER | Time remaining before SLA target |

#### escalation_rules

| Column | Type | Description |
| --- | --- | --- |
| id | UUID | Primary key |
| name | VARCHAR(100) | Rule name (e.g., "Compliance Escalation") |
| escalation_type | ENUM | notify \| reassign \| create_ticket \| add_flag \| dashboard_alert |
| escalation_level | INTEGER | Level in the escalation chain (1 = first responder, 2 = team lead, 3 = manager) |
| auto_route_config | JSONB | Auto-routing configuration (target team, load balancing, skill matching) |
| config_json | JSONB | Type-specific configuration (who to notify, which queue to reassign to, etc.) |
| is_active | BOOLEAN | Enable/disable the rule |

Two further tables support escalation. `escalation_tickets` holds the ticket created by a `create_ticket` escalation rule when an SLA breaches: it links the SLA event, application, policy, escalation rule and queue definition, snapshots the policy and queue names, and records `breached_at`, `elapsed_minutes`, `resolution_minutes`, an optional `assigned_to_user_id`, a `status` (default `open`) and the resolution (`resolved_at`, `resolved_by_user_id`, `resolution_note`). `escalation_executions` is the atomic dedupe lock: one row per (`sla_event_id`, `escalation_rule_id`), enforced by a unique index, with the `escalation_type` and `executed_at`, so a rule runs at most once for a given SLA event; if its action fails, the claim is released so it can be retried.

### Escalation Rules

When an SLA breaches, Globus can trigger one or more escalation actions:

| Escalation Type | Action | Example |
| --- | --- | --- |
| Notify | Send alert to assigned owner or team lead | Email + in-app notification to SEVIS team lead |
| Reassign | Move item to escalation queue or different team | Compliance Hold → Senior Compliance Officer queue |
| Create Ticket | Auto-create support or compliance ticket | New compliance ticket with entity details and SLA breach info |
| Add Flag | Add red risk flag to entity profile | "SLA Breach" flag visible on entity profile and dashboards |
| Dashboard Alert | Surface item on leadership dashboard | Breached items appear in Operations Manager overview panel |

**SLA milestones.** A policy can carry a schedule of time-based actions. `queue_sla_milestones` has one row per (`policy_id`, `milestone_day`) with an `action_type`, a `template_key` (the notification template, for the reminder actions), a `reason_code` (for the lapse and cancel actions), a `label` and an `is_active` flag. Action types: `SEND_REMINDER` (notify the applicant), `ESCALATE_TO_APPLICANT` (ask the applicant to act, for example to replace a referee, without changing status or completing the timer), `AUTO_LAPSE` (transition the application to lapsed and close the queue item) and `AUTO_CANCEL` (the same flow, transitioning to cancelled). `queue_sla_milestone_events` is the idempotency log: one row per (SLA event, milestone) with `fired_at`, an `outcome` of `sent`, `skipped` or `failed`, and an `error_message`. Sent and skipped milestones never fire again; failed ones are retried on the next run. The executor runs hourly under an advisory lock and skips completed SLA events, so nothing fires after the applicant resolves the condition.

**Milestone ladder.** Each policy can carry a day-based milestone ladder, managed under `/api/admin/sla-policies/:policyId/milestones` (permission `sla_policies.manage`; `POST` creates, `PUT` on `.../:milestoneId` updates, `DELETE` removes). Writable action types are `SEND_REMINDER` (needs the key of an active notification template), `AUTO_LAPSE` and `AUTO_CANCEL` (each needs an active global reason code); a missing or invalid one returns `400 VALIDATION_ERROR`. Only one milestone may exist per policy and `milestoneDay`: creating a second, or moving one onto an occupied day with `PUT`, returns `409` with the top-level code `DUPLICATE_MILESTONE_DAY` in the envelope `{ error: { code, message } }`. The check runs before the write, and a database unique-index violation is mapped to the same response.

### SLA in the UI

SLA visibility appears in three places:

#### Super Admin Config

Admin → Queue Configuration → SLA Policies

Create / Edit / Disable SLA / Clone SLA for new scope / Timezone + scope picker on form / Table: Name, Queue, Scope, Response, Resolution, Escalation, Status
#### Queue Table View

Per-row SLA indicators

2h 14m left 
On Track 
Warning 
80% elapsed 
Breached 6h 
Overdue 
#### Entity Profile

Operational SLA panel

Queue: Ready for SEVIS Pool

Entered: 18 Mar 2026 09:42

SLA Target: 2 business days

Remaining: 6h 12m

Status: Warning

> **SLA & the Entity Profile**
>
> The entity profile shows lifecycle phase and position (where they are). The SLA panel shows operational urgency (how quickly they need to be worked). These are complementary — phase is the entity's journey, SLA is the team's accountability.

## Document Review

Document Review is a focused, type-scoped workflow for verifying applicant-submitted documents. It is designed for simplicity: a temp reviewer hired for one month should be able to open the Review Hub, see only their assigned document types, and take one of three actions — approve, reject, or request resubmission. The architecture has two layers: the Review Hub (entry point showing what needs attention) and Review Mode (the sequential review workflow for a single document type).

> **Document Review ≠ Queue Engine**
>
> Document Review is a simple, reviewer-facing workflow for approving and rejecting documents. It is not part of the Queue Engine (P16). The Queue Engine is the broader work-routing infrastructure that powers lifecycle queues, readiness queues, exception queues, and SLA tracking. Document Review is a standalone feature that feeds signals (e.g., “all documents verified”) into the queue system but does not use the queue engine’s rule definitions, membership tables, or auto-actions.


### Specialist Document Review Workbench — pre-release implementation

> **Architecture investigation — release blockers, 12 November 2026.** The subsequent independent source review found that claims only prevent another claim, not another authorized decision or read. Separate transactional evidence-version fences remain in place; exclusive review ownership is not implemented. Workbench document/package rejection is not equivalent to applicant workflow-step rejection: reopening/editability, reasons and correction-notification paths differ. Passport correction recovery enqueues events without the direct applicant inbox notification used by the legacy resubmission wrapper; CBC rejection also has a seeded/requested template-key mismatch risk requiring live verification. Escalation creates an unassigned work item and audit visible in the workbench lane, but assigns and notifies nobody. Existing internal staff notification infrastructure is present but is not connected to this action. These are source-traced findings, not new runtime tests. AC4, AC5 and AC7 are not accepted; no runtime correction has yet been made. The walkthrough below describes controls currently present, not proof that exclusive ownership, full applicant correction parity or delivered escalation works.


**Status checked 12 November 2026:** implemented on the isolated BackOfficePrototype branch feature/1219177235115066-document-review-workbench, revision b6a1304a073fcd0a6e72c04b8500107db1da7df4. Development workflow 43188802343 completed successfully. Dev-15 is the reserved target; this entry does not assert deployed endpoint acceptance, Karin's UAT acceptance, QA promotion or production approval. Dev-6 retains the separate minor-fixes stream. The existing Review Hub remains the entry point, not a replacement dashboard.

#### Scope and ownership

The specialist workbench handles passport and criminal-background-check (CBC) evidence across programmes and operational cycles without opening each applicant profile. Sam retains multiple-CBC/contract upload and replacement-handling mechanics. The workbench's governed request-replacement decision is included, but does not replace Sam's upload lifecycle work. Reference-source accounting and linked-reference decision consistency are included; reference decisions remain in their owning reference review surface. Host evidence retains a separately authorized lane. No blind approve-all, automatic gallery approval, new eligibility policy or production backfill is included.

#### Preview walkthrough

The isolated Replit preview uses local port 8000, exposed as port 8080. The bare workspace preview runs a different branch. Choose the isolated document-workbench preview, then open /review-hub. This preview uses a disposable local PostgreSQL database and synthetic three-page documents through a read-only storage emulator; it does not use real participant documents. Test account credentials are deliberately excluded from this documentation. Fixture locators regenerate on restart.

1. Open **Document Review** and select the **Passport Review** card. The workbench opens at /document-workbench with passport selected.
2. Choose programme and operational cycle. Set **Disposition / exception lane** to **Ready for review**, and **Submitted** to **Oldest first** or **Newest first**. Select a row to load its current evidence and applicant/application context. Lists fetch 25 rows by default, capped at 50, with server-side scope, stable cursor pagination and matching totals.
3. Inspect every document page. **Claim evidence** coordinates reviewers for two minutes; **Renew claim** extends it and **Release claim** releases it. A claim is not an approval and never replaces the atomic evidence-version check.
4. Select **Fullscreen**, reload, and use **Exit fullscreen** or Escape. Category, programme/cycle, lane, sort, cursor and selected evidence identity/version remain in the URL. Do not share evidence-bearing session URLs as attachments.
5. Use **Skip** to move without recording a decision. Use **Approve and next** only after reviewing the document. Navigation follows a confirmed save and successful refreshed list; failed or unknown saves must not claim success or silently advance.
6. For passport replacement, select an approved reason under **Replacement or rejection reason**, optionally enter **Review notes**, then choose **Request replacement and next**. Notes are staff-only; the governed reason supplies the applicant-facing message. **Reject and next** is a separate refusal action.
7. Enter an escalation reason and choose **Escalate and next**. Find the durable request in **Escalated for specialist review**. Escalation creates a work item and audit, not approval.
8. Switch **Evidence category** to **Criminal background check**. Inspect the current original and translation together, all pages, country context, provenance and validation warnings. The package reviewer uses **Approve** and **Request Replacement**. Missing, inaccurate or blocked required translation cannot produce a successful package approval. An approval must be based on the exact current artifacts.
9. Inspect **Processing**, **Processing failed**, **Missing evidence**, **Reviewed**, **Superseded**, **Not reviewable** and **Reconciliation exception** as appropriate. An empty lane is not proof of complete source coverage. Source-accounting diagnostics are administrator-only observations, not a combined pending count, and cover applicant documents/signed evidence, workflow media and reference submissions.
10. Return to the Hub or a second open review/profile tab. Refresh-on-focus and a 30-second polling interval provide bounded convergence. A changed version requires reselection; a failed access refresh hides previously loaded rows and previews.

#### Security and decision contracts

Every list, count, detail, proxied-byte read, claim and action intersects current reviewer document-type assignments, category permissions and resource scope. A passport-only reviewer cannot obtain CBC metadata, counts or bytes through direct endpoints; general dashboard access is not sufficient. API keys are refused for the specialist session. Files are served only by authorized Globus backend proxy endpoints, never raw portal or GCS URLs.

Canonical passport and CBC writers fence decisions inside transactions against the expected evidence basis. Claims expire independently. Exact retries recover the same canonical result rather than duplicate decisions, audit or notifications. Generic legacy endpoints refuse independent decisions on passport/CBC/linked reference evidence. Durable recovery receipts use the existing outbox; a saved decision confirms durable enqueueing, not external email/webhook delivery. Reference submission and explicitly linked document status change together; identity is not inferred from matching paths.

#### Engineering evidence and remaining acceptance

Local production build, type checking, lint, repository skill-enforcement checks, targeted route/security tests, and guarded disposable-PostgreSQL concurrency/replay proofs passed before branch integration. The development pipeline subsequently passed for the revision above. One critical browser pass covered Hub entry, passport paging/fullscreen, skip, claims, escalation, replacement, approval-and-next, restricted CBC access and side-by-side CBC PDFs. It found reload and CBC-authority failures; fixes were subsequently verified by unit tests and real HTTP, not a second complete browser pass.

Synthetic HTTP benchmarks used 100 and 1,000-item datasets, three-page PDFs, three warmups and 30 samples per operation. At 100 items, p50/p95 milliseconds were list 15.57/25.11, preview 19.69/44.46 and save-to-refetch 45.66/59.37. At 1,000 items they were 23.25/29.05, 18.06/31.78 and 51.12/77.49. These passed the documented local budgets; they are not deployed-GCS latency, browser-paint or multi-user stress results.

Remaining release acceptance includes deployed endpoint verification, full cross-tab convergence and live-revocation browser evidence, country/replaced-artifact scenarios integrated with Sam's work, complete supported-source browser coverage, and Karin's manual acceptance. Keep engineering results, Karin acceptance and production approval separate. Reproducible fixtures, per-AC evidence, performance limitations and ten manual UAT scenarios are in docs/document-review-workbench on the isolated feature branch. Do not label all nine acceptance criteria passed.

### Review Hub

The Review Hub is the sole entry point for document reviewers. It displays a card for each document type the reviewer is assigned to, showing the count of pending documents or an “all clear” state when no documents need attention.

### Review Mode

Review Mode is the core workflow. When a reviewer selects a document type from the Hub, they enter a sequential review experience focused on one document at a time.

### Role-Based Document Assignment

Reviewers can be scoped to specific document types using role-based access control. This enables hiring temporary staff who only see the document types they are qualified to review.

| Role Example | Assigned Types | Hub Visibility |
| --- | --- | --- |
| Passport Reviewer | passport_scan | Only Passport cards appear in the Hub |
| Background Check Reviewer | background_check | Only Background Check cards appear |
| Full Document Reviewer | All types | All document type cards are visible |
| Temp Reviewer (scoped) | passport_scan, reference_letter | Only assigned type cards appear |

> **RBAC Integration**
>
> Document type assignments are managed through the RBAC system. Permissions are checked at the API level — even if a reviewer guesses a URL, they cannot access documents outside their assigned types.

> **Host documents & brand scope (QA, 2026-07 — Task #1947)**
>
> Host documents (`host_documents`) are organization-scoped and carry no brand, so brand filtering cannot apply to them. `GET /api/document-review/queue` now always fetches host items for any caller passing the `dashboard.view` gate — previously host items only appeared for callers with unrestricted brand scope. `POST /api/document-review/action` likewise drops the unrestricted-brand-scope requirement on its `source: "host"` branch; the `document.verify` permission gate remains. Applicant-document brand filtering is unchanged — this authorization relaxation is scoped to host documents only. A host document id sent to `POST /api/document-review/action` without `source: "host"` is refused with `400` and code `HOST_SOURCE_REQUIRED` instead of falling through to the applicant branch and returning 404; the accepted `source` values are `local`, `host` and `host_offer_contract`.

### Document State Machine

Every document follows a strict state machine. State transitions are audit-logged with the reviewer, timestamp, and review notes.

| State | Description | Transitions To | Triggered By |
| --- | --- | --- | --- |
| pending | Document uploaded, awaiting staff review | verified, rejected | Portal sync or staff upload |
| verified | Document reviewed and accepted by staff | expired (if expiry date passes) | Staff action (single or bulk) |
| rejected | Document failed review. Applicant notified with reason | pending (resubmission) | Staff action with mandatory review_notes |
| expired | Document validity period has lapsed | pending (resubmission) | Automated (expiry date check) |

> **Rejection Always Notifies**
>
> When a document is rejected, three things happen automatically: (1) a notification is created for the applicant with the rejection reason, (2) an ops_event is emitted for n8n workflows, and (3) an n8n webhook push triggers email delivery. Staff cannot reject without providing a reason.

### Review Workflow

Documents enter the system through two channels and flow through a structured review pipeline.

Emitted Events

document.verified — Feeds readiness signals into the queue system

document.rejected — Triggers applicant notification and resubmission request

document.resubmission_requested — Applicant notified to correct and resubmit

### Secure Storage & Access

All document files are stored in Google Cloud Storage (GCS), and raw GCS paths are never exposed to clients — every read is brokered through a short-lived signed URL after an authorization and audit check. The full storage contract (bucket layout, the signed-URL upload flow, versioning, the proxied-download access model, retention, and encryption) now lives on the dedicated Document Storage spec, which is the source of truth for how files are persisted and served. The Queue System page keeps only the review-side concerns below.

> **Downstream Integration**
>
> Document review feeds signals into the broader queue system. Verified documents clear the “Missing Documents” exception queue and contribute to readiness calculations. Rejected documents trigger applicant notifications and resubmission requests. SLA timers can be attached to review turnaround tracking.

### Document Completeness Signal

The Document Completeness Signal is a computed readiness indicator that aggregates individual document verification states into a single boolean per applicant. It answers the question: “Has this applicant submitted and had all required documents verified?”

The signal feeds into three downstream consumers:

| Consumer | When Signal = true | When Signal = false |
| --- | --- | --- |
| Queue System (Readiness Queues) | Applicant exits the Missing Documents exception queue and becomes eligible for the next processing queue | Applicant remains in or re-enters the Missing Documents exception queue |
| Workflow Engine (Phase Gates) | The document-completeness gate for the current phase is satisfied, enabling auto-advance if all other gates are also met | Phase gate remains blocked; applicant cannot advance to the next phase |
| Applicant Profile (Completeness Ring) | Document segment of the profile completeness ring shows 100%. Green indicator on the staff-facing applicant detail page | Document segment shows partial completion. Amber/red indicator with a list of missing or pending document types |

Signal Data Model

| Field | Type | Description |
| --- | --- | --- |
| application_id | UUID (FK) | Reference to the application record |
| is_complete | BOOLEAN | true if all required documents are verified and non-expired |
| total_required | INTEGER | Count of required document types from the workflow template |
| total_verified | INTEGER | Count of document types with at least one verified, non-expired record |
| missing_types | TEXT[] | Array of document type keys that are not yet satisfied |
| last_computed_at | TIMESTAMP | When the signal was last recomputed |
| last_event_id | UUID | The document event that triggered the most recent recomputation |

> **Eventual Consistency**
>
> The completeness signal is recomputed asynchronously after each document event. There is a brief window (typically <1 second) where the signal may not reflect the latest document state. Queue membership changes triggered by the signal use an idempotent upsert pattern to handle duplicate or out-of-order event deliveries safely.

## Queue Categories (Orthogonal Model)

The Applicant Processing Workspace epic introduces a second axis on top of the existing lifecycle-phase queues. Lifecycle phase answers where in the journey an applicant sits (Intake, Review, Placement, …); the orthogonal category axis answers what state of attention the work needs right now. The two axes compose — a single applicant is always in exactly one lifecycle phase and simultaneously in one or more categories.

> **Categories are derived, not stored**
>
> Like every other queue concept on this page, queue categories are computed views over existing system state (queue rules, SLA timer, assignment state, exception buckets). There is no queue_category column on the applicant or membership rows — the category an applicant appears under is the output of the derivation rules below, evaluated at read time.

### The Six Categories

| Category | Meaning | Tone |
| --- | --- | --- |
| Ready | An action is available now and no blockers stand in the way. Pick it up and move it forward. | Action |
| Waiting | Forward motion depends on a party outside the processor (applicant reply, host decision, partner upload, government response). | Hands-off |
| Blocked | An internal precondition has failed (rules-engine compliance failure, missing required field/document, document rejected). Cannot proceed until cleared. | Internal |
| Aging | Membership in the current queue has crossed an SLA threshold without breach yet — the timer is running yellow. | SLA |
| Exceptions | An exception rule has fired (compliance flag, integrity issue, host-cancel after placement). Mirrors the existing Exception Queue catalog. | Risk |
| Escalated | A supervisor or compliance officer has been pulled in — either auto-escalated by SLA breach or manually escalated by the assigned processor. | Leadership |

### Derivation Rules

Each category is computed from already-existing signals. No new storage is introduced.

| Category | Derivation |
| --- | --- |
| Ready | Primary queue is a readiness queue with status “Ready for …” and no Blocked / Exception / Waiting reasons attached. |
| Waiting | Primary or any attached reason has source_system ∈ applicant_portal, host_portal, partner_portal, sevis, external AND no Blocked reason is present. |
| Blocked | Any attached reason has severity = blocker (rules-engine compliance failure, missing required field/document, document rejected). |
| Aging | SLA timer for the primary queue membership is past the warning threshold but before the breach threshold (see SLA Pipeline Strip Indicators). |
| Exceptions | Applicant is a member of any queue whose definition_type = exception (mirrors the Exception Queue Definitions section). |
| Escalated | SLA timer has breached AND auto-escalation rule has fired, OR the assigned processor has manually flagged escalation_required = true. |

### Composition with Lifecycle Phase

The processor UI lets staff filter by either axis independently or both together. The category axis sits on the left rail of the workspace; the lifecycle-phase filter sits in the facet bar. Selecting Blocked + Placement returns applicants who are blocked while in the Placement phase — not a separate stored bucket, just the intersection.

> **Categories never replace lifecycle phase**
>
> A category answers “how does this work item feel right now?”. A lifecycle phase answers “where in the journey is the applicant?”. The two questions are independent. An applicant can be Blocked in Placement today and Ready in Visa & Compliance next week — same applicant, same row in the database, different derived categories at different times.

### Worked Example

Applicant: Maria Lopez — Camp Counselor, Summer 2026

- Lifecycle phase: Placement (host interview completed, awaiting Wave 2 documents)
- Primary readiness queue: “Awaiting Wave 2 Documents”
- Attached reasons: doc.wave2.health_form.missing (severity: blocker), sla.placement.aging (severity: warning)
- SLA timer: 9 days in queue, warning threshold 7d, breach 14d
- Derived categories: Blocked + Aging
Maria appears in both the Blocked and Aging category lists, but her lifecycle phase remains a single value: Placement. Clearing the missing health form drops her out of Blocked; she stays in Aging until either she advances out of the queue or the SLA timer is reset.

## Queue Reason Object

Today an applicant’s queue membership tells staff which queue they’re in, but not why. Processors operating from Hanover-style exports rely on tribal knowledge to answer “why is this applicant in this queue”. The Queue Reason object is the structured contract that lets the platform answer that question itself — one row per active reason, machine-readable, auditable, and explainable in the UI.

### Field Contract

| Field | Type | Source of truth |
| --- | --- | --- |
| id | uuid | Generated on insert. Stable identifier referenced by audit log rows and override records. |
| applicant_id | uuid | Foreign key into the applicant aggregate. Required. |
| queue_id | uuid | Foreign key into queue_definitions — the queue this reason is attached to. |
| title | string (≤ 80 char) | Human-readable short label rendered as the chip text (e.g. “Health form missing”). Comes from the rule definition that emitted the reason. |
| description | string (markdown) | Longer explanation rendered in the reason popover. Templated by the rule with applicant-context interpolation. |
| severity | enum info \| warning \| blocker | Drives both visual treatment and the Blocked-category derivation. Set by the emitting rule. |
| generated_at | timestamptz | When the rule evaluation first emitted this reason. Used for sort and for SLA Aging derivation when the queue SLA is per-reason. |
| source_system | enum rules_engine \| applicant_portal \| host_portal \| partner_portal \| sevis \| hubspot \| external \| manual_staff | Which subsystem produced the reason. Drives the Waiting-category derivation when the value is one of applicant_portal, host_portal, partner_portal, sevis, external — i.e. the reason is gated on a party outside the processor. |
| linked_workflow_step_id | uuid (nullable) | If the reason relates to a specific workflow step (e.g. Wave 2 document upload), this points at the step so the UI can deep-link into Workflow Studio. |
| rule_id | uuid (nullable, FK to queue_rules.id) | Id of the queue_rules row that emitted this reason. |
| resolved_at | timestamptz (nullable) | Set when the reason is cleared. Once non-null the reason no longer participates in derivation. |
| resolution_kind | enum auto_cleared \| manual_resolved \| superseded (nullable) | How the reason was resolved. Set together with resolved_at. |

### Multiplicity & Primary Reason

An applicant can carry multiple simultaneous reasons against the same queue, and reasons across different queues coexist independently. The UI promotes a single primary reason to the Queue Row’s Primary Queue Reason column — the remainder render as a stacked chip count (“+3 more”) and expand in the row popover.

Primary reason is selected deterministically by: severity (blocker &rarr; warning &rarr; info), then generated_at ascending (oldest first). No new column — computed on read.

### Rule-Driven Generation Path

Reasons are emitted by the Rules Engine as a structured output of a rule evaluation. When a rule whose fail action carries a readiness dimension (the routing marker; there is no `failure_behavior` field) evaluates to a failing outcome, the engine writes a Queue Reason row in addition to placing the applicant into the target queue. The reason carries the rule_id back-pointer so the processor UI can render the underlying rule, its parameters, and the evaluation evidence.

> **Two ways reasons enter the system**
>
> Rules-engine emission (primary path): any failing rule outcome whose fail action routes to a queue attaches the reason. SEVIS, HubSpot, and portal-submission sources hand off through the rules engine via the same emission API so the contract stays uniform. Manual staff entry (escape hatch): manual_staff is an accepted source_system value, but no staff-facing route attaches a free-text reason yet. Staff with the queues.manage permission can resolve an open reason through POST /api/queue-reasons/:id/override, which is always written to audit.

### Resolution & Override Path

A reason has two ways to leave the active set:

- Auto-clear: the next rules-engine evaluation finds the underlying condition no longer fails. The engine writes resolved_at = now(), resolution_kind = auto_cleared, and emits queue.reason.resolved.
- Staff override: a user with the right permission supplies a written justification, which is stored on the audit row. The reason is written with resolution_kind = staff_override and emits queue.reason.overridden. The underlying rule outcome is unchanged — only this applicant’s reason is cleared.
Auto-clear and staff override use the same row — reasons are never deleted, only resolved. This keeps the timeline complete.

### Audit Vocabulary

Three new actions are appended to the Audit Logging Contract:

| Action | Entity type | When it fires |
| --- | --- | --- |
| queue.reason.created | queue_reason | A new reason row is inserted. Actor is the emitting subsystem (e.g. system:rules-engine) or the staff user for manual entries. |
| queue.reason.resolved | queue_reason | Auto-clear fires from a passing rule re-evaluation. Actor is system:rules-engine. before/after captures the resolution timestamp transition. |
| queue.reason.overridden | queue_reason | A staff user overrides the reason. Actor is the staff user (real userId via resolveActor). The justification text is written into the audit row’s after.justification field. |

## Queue Row Contract (Processor UI)

The processor workspace renders one row per primary queue membership. The column-by-column contract below extends the generic Table Columns described under UI / UX Design with the processor-specific fields the new workspace needs. Every value is derived — no new stored column is introduced.

| Column | Source of truth |
| --- | --- |
| Applicant | applicants.first_name + applicants.last_name + link to applicant detail page. |
| Program | applications.program_id → programs.name (the program of the active application row, not the applicant identity). |
| Season | applications.season_id → seasons.name. |
| Lifecycle Stage | Computed lifecycle phase + status (e.g. “Placement — Awaiting Wave 2 Documents”). Same value the lifecycle filter operates on. |
| Primary Queue Reason | Resolved primary reason chip per the Multiplicity & Primary Reason rule; stacks remaining reasons as “+N more”. |
| Also In | |
| SLA Status | Pill driven by the SLA Pipeline Strip Indicators rules (on-track / aging / breached). Tooltip shows time remaining or breach duration. |
| Days In Stage | now() − queue_transitions.entered_at for the primary membership, rounded to whole days. Independent of the SLA pill so a non-SLA queue still surfaces age. |
| Assigned Processor | applicants.assigned_staff_id → user.display_name. “Unassigned” when null. Pulled from the Applicant Assignment model. |
| Priority | Max severity across all attached reasons, expressed as the Priority Indicator chip (critical / warning / ready / informational). |
| Next Action Hint | Short imperative phrase derived from the primary reason’s rule definition (e.g. “Review Wave 2 health form”). Falls back to the queue’s default action label when no reason is attached. |
| Risk Indicators | Inline icons for active Exception-category memberships and Escalated category. Hover reveals which exception(s) fired and by which rule. |

> **Cross-links, not duplication**
>
> The Also In multi-bucket surfacing, the Applicant Assignment model, and the Activity Timeline / last-touch staleness pills are all defined on the Applicant Lifecycle page. The Queue Row contract references those definitions rather than restating them.

## Saved Views — Seed Presets

Saved Views are the existing facet-filter + grouping primitive already supported by the queue workspace. The processor workspace ships with three canonical seed presets to give new users a productive starting point. They are normal saved views over existing filter facets — no new infrastructure.

| Preset name | Filter composition |
| --- | --- |
| My Jamaica applicants | assigned_processor = current_user AND applicant.country_of_origin = “Jamaica” AND lifecycle.phase ∈ Application, Validation, Placement, Visa & SEVIS . |
| SEVIS Risk | lifecycle.phase = “Visa & SEVIS” AND ( category ∈ Blocked, Aging, Escalated OR reason.rule_id startswith “sevis.” ). |
| Aging > 14 Days | days_in_stage > 14 AND sla_status ∈ aging, breached . No phase filter — surfaces aging items anywhere in the lifecycle. |

## Consumers of this Contract

> **Where this spec is consumed**
>
> Two downstream docs pages render against the Queue Categories and Queue Reason Object contracts defined above. Neither is required reading to understand the contract itself, but both depend on it: Operations Command Center — the cross-queue leadership view that aggregates the six categories into pipeline health, bottleneck, and escalation panels. (Spec page pending; route reserved.) Split-View Applicant Workspace & Next-Action Engine — the per-applicant processor screen that renders the Queue Row contract on the left and consumes Primary Queue Reason + Next Action Hint on the right. (Spec page pending; route reserved.)

## Implementation Follow-up — P16 Asana Tickets

This page is a docs-site spec extension only. Runtime ownership of the work below sits with the Globus core P16 — Queue System & SLA Management project. The four tickets below are drafted in the house format documented in docs/asana-ticket-standard.md so a P16 owner can copy-paste straight into Asana when work is picked up. None of these tickets should be filed against this docs-site repo.

*Status: draft ticket text. Its references to `failure_behavior = "route_to_queue"` describe a proposed rule field that does not exist in code; queue routing today is by the readiness dimension on a fail-class rule action (ADR 0004).*

P16 Ticket 1

Globus: queue-category derivation service for orthogonal Ready / Waiting / Blocked / Aging / Exceptions / Escalated axis

FOR PRODUCT OWNERS / STAKEHOLDERS
Invisible to end users, but it is the engine behind the new left-rail category filter
in the Applicant Processing Workspace. Without it the workspace falls back to lifecycle-phase
only and processors cannot ask "show me everyone who is Blocked right now, regardless of phase".
----------------------------------------
BACKGROUND
Queues today are derived per-membership from "queue_definitions" + "queue_rules". The only
axis available to filter on is lifecycle phase (Intake, Review, Placement, ...). There is
no shared notion of "what state of attention does this work item need", which means the new
Applicant Processing Workspace cannot offer a Ready / Waiting / Blocked / Aging / Exceptions /
Escalated filter without each consumer reinventing the derivation locally.
WHAT THIS TICKET DOES
Adds a "deriveQueueCategories(applicantId)" service module that returns the set of orthogonal
categories an applicant currently belongs to. Inputs are the existing primary queue membership,
the SLA timer for that membership, the attached queue reasons, and exception-queue membership.
Output is consumed by the workspace left rail and by the "Operations Command Center" downstream
page. No new persistence — purely a derived read.
ACCEPTANCE CRITERIA (Gherkin)
Feature: Orthogonal queue-category derivation
Scenario: deriveQueueCategories module exists with the documented signature
Given the package "@globus/queue-system"
Then it exports "deriveQueueCategories(applicantId: string): Promise "
And "QueueCategory" is the union "'ready' | 'waiting' | 'blocked' | 'aging' | 'exceptions' | 'escalated'"
Scenario: Blocked category fires when any attached reason has severity "blocker"
Given applicant "A1" has a primary queue membership in "Awaiting Wave 2 Documents"
And a queue reason with "severity" = "blocker" is attached to that membership
When "deriveQueueCategories('A1')" runs
Then the result includes "blocked"
Scenario: Waiting category fires when the only attached reasons originate outside the processor
Given applicant "A2" has a queue reason with "source_system" = "host_portal"
And no reason with "severity" = "blocker" is attached
When "deriveQueueCategories('A2')" runs
Then the result includes "waiting"
And the result does not include "blocked"
Scenario: Aging fires from SLA timer crossing the warning threshold
Given applicant "A3" has a primary queue membership 9 days old
And the queue SLA warning threshold is 7 days and breach is 14 days
When "deriveQueueCategories('A3')" runs
Then the result includes "aging"
And the result does not include "escalated"
Scenario: Build-time guard against duplicating the derivation in consumers
Given a lint rule "no-inline-queue-category-derivation"
When a consumer file references "queue_memberships" together with the literal "blocked"
Then the lint rule fails the build
And the only allowed call site is "deriveQueueCategories"
P16 Ticket 2

Globus: queue_reasons table + rules-engine emission path for structured queue reasons

FOR PRODUCT OWNERS / STAKEHOLDERS
Processors will, for the first time, see a written reason next to every applicant explaining
why they are in the queue — health form missing, awaiting host decision, SEVIS error, and so on.
Today this knowledge lives in spreadsheets and tribal memory.
----------------------------------------
BACKGROUND
"queue_memberships" tells us which applicants are in which queues but not why. Rule evaluations
in the Rules Engine that fail with "failure_behavior" = "route_to_queue" silently place the
applicant into the target queue without leaving a structured trace. SEVIS, HubSpot, and portal
submissions emit similar "this is why" signals into ad-hoc fields with no shared schema.
WHAT THIS TICKET DOES
Adds a "queue_reasons" table with the field contract documented in the docs page section
"Queue Reason Object". Extends the rules-engine evaluation pipeline so that any outcome with
"failure_behavior" = "route_to_queue" also inserts one "queue_reasons" row carrying the rule_id
back-pointer. Exposes a single internal "emitQueueReason()" helper that SEVIS, HubSpot, and
portal-submission handlers call through — no second emission path. Companion ticket:
"Globus: queue-reason audit vocabulary extension for queue.reason.created / .resolved / .overridden".
ACCEPTANCE CRITERIA (Gherkin)
Feature: Structured queue reasons emitted by the rules engine
Scenario: queue_reasons table has the documented columns
Given the database schema
Then table "queue_reasons" has columns "id", "applicant_id", "queue_id", "title",
"description", "severity", "generated_at", "source_system", "linked_workflow_step_id",
"rule_id", "resolved_at", "resolution_kind"
And "severity" is enum "info" | "warning" | "blocker"
And "source_system" is enum "rules_engine" | "applicant_portal" | "host_portal" | "partner_portal" | "sevis" | "hubspot" | "external" | "manual_staff"
Scenario: Rules-engine outcome emits a reason
Given a rule "R1" (a queue_rules row) with "failure_behavior" = "route_to_queue"
And applicant "A4" fails that rule
When the rules engine writes its outcome
Then a "queue_reasons" row exists with "rule_id" = the id of "R1"
And the row has "source_system" = "rules_engine"
And the row has the queue_id of the rule's target queue
Scenario: Multiple reasons coexist against the same membership
Given applicant "A5" has two failing rules attached to the same primary queue
Then "queue_reasons" contains two rows for "A5" with that queue_id
And neither has "resolved_at" set
Scenario: Auto-clear on next passing evaluation
Given applicant "A6" has an active "queue_reasons" row with "rule_id" = "doc.wave2.health_form.missing"
When the rules engine re-evaluates "doc.wave2.health_form.missing" and it passes
Then that row has non-null "resolved_at"
And the row has "resolution_kind" = "auto_cleared"
Scenario: Build-time guard against bypassing emitQueueReason
Given the package "@globus/rules-engine"
Then a lint rule "no-direct-queue-reason-insert" prevents any "INSERT INTO queue_reasons"
outside the "emitQueueReason" helper
P16 Ticket 3

Globus: queue-reason audit vocabulary extension for queue.reason.created / .resolved / .overridden

FOR PRODUCT OWNERS / STAKEHOLDERS
Invisible to end users, but it is what lets compliance see who overrode which reason and why.
The Audit Console gains three new action types so a queue-reason override leaves the same
quality of trail as a partner-overview edit or a SEVIS submission.
----------------------------------------
BACKGROUND
The shared "recordAudit()" helper writes every mutating endpoint to "audit_logs" with a dotted
action vocabulary scoped by subsystem ("asana.project.created", "mcp.heartbeat.sent",
"host_organization.note.created"). The Queue Reason object introduced by the companion ticket
"Globus: queue_reasons table + rules-engine emission path for structured queue reasons" has no
action names yet — overrides and auto-clears currently leave no audit trail.
WHAT THIS TICKET DOES
Registers three new action names — "queue.reason.created", "queue.reason.resolved",
"queue.reason.overridden" — against "entityType" = "queue_reason" in the shared audit vocabulary.
Wires emission into the three call sites: rules-engine reason insert, rules-engine auto-clear
on passing re-evaluation, and the staff override endpoint. The override path requires a
justification string which is written into the audit row's "after.justification" field.
ACCEPTANCE CRITERIA (Gherkin)
Feature: queue.reason.* audit actions
Scenario: queue.reason.created fires on every reason insert
Given applicant "A7" fails a rule with "failure_behavior" = "route_to_queue"
When the rules engine emits the reason
Then an "audit_logs" row exists with "action" = "queue.reason.created"
And "entityType" = "queue_reason"
And "entityId" = the new "queue_reasons.id"
And "userId" is null and the actor label is "system:rules-engine"
Scenario: queue.reason.resolved fires on auto-clear
Given an active reason row for applicant "A8"
When the rules engine re-evaluates and the underlying rule now passes
Then an "audit_logs" row exists with "action" = "queue.reason.resolved"
And "before.resolved_at" is null
And "after.resolved_at" is non-null
Scenario: queue.reason.overridden requires a justification
Given a staff user "U9" with permission "queues.manage"
When "U9" POSTs to the override endpoint with body "justification\\ / Confirmed via phone, waiver attached\\"
Then an "audit_logs" row exists with "action" = "queue.reason.overridden"
And "userId" = "U9" via "resolveActor"
And "after.justification" = "Confirmed via phone, waiver attached"
Scenario: Override without justification is rejected
Given a staff user "U10"
When "U10" POSTs to the override endpoint with an empty body
Then the response is HTTP 400
And no "audit_logs" row is written
P16 Ticket 4

Globus: saved-view seed presets for the Applicant Processing Workspace

FOR PRODUCT OWNERS / STAKEHOLDERS
New processors will land in the Applicant Processing Workspace with three ready-to-use
saved views — "My Jamaica applicants", "SEVIS Risk", "Aging > 14 Days" — instead of an empty
filter bar. Existing saved-views infrastructure does the heavy lifting; this ticket only seeds
the presets.
----------------------------------------
BACKGROUND
The queue workspace already supports user-defined saved views built on the existing facet-filter
+ grouping primitive. There is no seed data — every new tenant starts with zero saved views,
forcing each processor to rebuild the same three views by hand on day one.
WHAT THIS TICKET DOES
Adds a seed migration that inserts three system-owned saved views with the exact filter
composition documented in the docs page section "Saved Views — Seed Presets". The views are
marked as "system_owned" so users cannot delete or rename them, only clone. Reuses the existing
saved-views schema — no new tables, no new filter facets.
ACCEPTANCE CRITERIA (Gherkin)
Feature: Seed saved-view presets
Scenario: Three seed views exist after migration
Given the seed migration has run
Then saved views named "My Jamaica applicants", "SEVIS Risk", "Aging > 14 Days" exist
And each is marked "system_owned" = true
And each has "owner_id" = null
Scenario: "Aging > 14 Days" uses the documented filter
Given the seed view "Aging > 14 Days"
Then its filter JSON contains "days_in_stage > 14"
And it contains "sla_status \u2208 "
And it has no "lifecycle.phase" filter
Scenario: System-owned views cannot be deleted
Given a user "U11" attempts to DELETE saved view "SEVIS Risk"
Then the response is HTTP 403
And the view still exists
Scenario: Users can clone a system-owned view
Given a user "U12" clones "My Jamaica applicants"
Then a new saved view exists with "owner_id" = "U12"
And "system_owned" = false
And the filter JSON matches the source view
## Future Enhancements

See Build Status for current build status and project tracking.
