Immigration application run lifecycle
An immigration pass application is one AgentRun of workflow IMM.PREPARE_APPLICATION (packages/agent-runtime/src/modules/imm.ts). Unlike the module-specific status machines (payroll cycle, XBRL filing), an IMM application has no bespoke status enum — it rides the platform AgentRun lifecycle (packages/agent/src/lifecycle.ts, RunStatus), which every workflow on the tenant spine shares. What is IMM-specific is the shape of the walk through that lifecycle: a client FORM pause and a tier-4 human gate.
RunStatus values (platform-wide)
RunStatus = PLANNING | AWAITING_CLIENT | RUNNING | AWAITING_REVIEW | APPLYING | CLOSED | FAILED | CANCELLED.
The transition map (packages/agent/src/lifecycle.ts):
| From | Allowed next |
|---|---|
PLANNING | AWAITING_CLIENT, RUNNING, FAILED, CANCELLED |
AWAITING_CLIENT | RUNNING, CANCELLED, FAILED |
RUNNING | AWAITING_CLIENT, AWAITING_REVIEW, APPLYING, CLOSED, FAILED, CANCELLED |
AWAITING_REVIEW | APPLYING, RUNNING, CANCELLED, FAILED |
APPLYING | RUNNING, CLOSED, FAILED |
CLOSED | (terminal) |
FAILED | RUNNING |
CANCELLED | (terminal) |
API: canTransition(from, to) and getNextStatuses(from); assertTransition throws InvalidRunTransitionError on an illegal edge.
The seven workflow steps
The IMM.PREPARE_APPLICATION workflow (IMM_MODULE.workflows[0]) runs these steps in order:
| Seq | Step | stepType | What it does |
|---|---|---|---|
| 1 | classify | LLM | Records the pass type / applicant / employer for the run. |
| 2 | collectApplicationForm | FORM | Pauses the run at AWAITING_CLIENT: issues a FormInstance + magic link (real mode only). |
| 3 | collectDocuments | TOOL | Derives document completeness from the uploaded KycDocument rows (real mode) or the mock adapter. |
| 4 | screen | TOOL | Persists the applicant KycCase + ScreeningResult (idempotent per run + party). |
| 5 | evalEligibility | RULE | Runs the SG IMM rule pack; throws → run FAILED if any BLOCK rule fails. |
| 6 | assembleApplication | LLM | Builds the assisted MOM submission pack (mappedFields + HTML artifact). |
| 7 | submit | EXTERNAL | Proposes the tier-4 IMM.SUBMIT_APPLICATION action → routes to the review-inbox gate. |
producedActions: IMM.SUBMIT_APPLICATION at riskTier: 4.
The AWAITING_CLIENT pause (FR-IMM-002/003)
When the runtime is built with collectForm: true (the worker path — mock/dev auto-satisfies the step), the collectApplicationForm executor:
- Issues a
FormInstance(idempotent perrunId) with the prefilled applicant / pass-type / employer / salary fields and a 48-hour magic-link token whoseresourceTypeisIMM_FORM_INSTANCE. - Returns
awaitClient: true, which the orchestrator translates intoRUNNING → AWAITING_CLIENT— the run stops here and persists its resume point. - The full run
inputis embedded in the FORM step output so the resume bridge can rebuild the remaining steps.
The client opens /portal/imm/:token, confirms fields, uploads the required documents (save-and-resume via PATCH), and submits. POST /portal/imm/:token/submit validates every required-doc slot is present, flips the FormInstance to SUBMITTED, and enqueues agent.run-form-resume. The handler (apps/worker/src/handlers/agent-run-form-resume.ts) confirms the run is still AWAITING_CLIENT (defensive no-op otherwise), rebuilds the full workflow from the embedded input, and resumes: AWAITING_CLIENT → RUNNING, then steps 3–7 execute.
This is the only workflow on the spine with a FORM step today; the lifecycle permits RUNNING → AWAITING_CLIENT precisely so a step may pause an already-advancing run to collect client input.
The tier-4 submission gate (FR-IMM-007)
Step 7 (submit) never files anything itself — it only proposes the tier-4 IMM.SUBMIT_APPLICATION action, which routes the run to AWAITING_REVIEW in the shared review inbox. Submission to the Ministry of Manpower is human-performed: the operator keys the application into EP Online / WPOL using the assembled pack, obtains the real acknowledgement, and approves the gate via POST /ops/review-inbox/:reviewTaskId/approve with applyInput.filing.acknowledgementNumber (optional filedAt, reference).
On approval the run moves AWAITING_REVIEW → APPLYING and the apply executor (imm-apply.ts, the only writer) runs in one tenant-scoped transaction:
- creates a
FilingTransaction(registryMOM, typePASS_APPLICATION, statusACKNOWLEDGED) with the operator's acknowledgement number; - persists the assembled pack as a
Document(IMM_APPLICATION)(the W3 artifact bytes land in S3 when a storage writer is wired); - upserts a
StatutoryDeadline(PASS_RENEWAL)(defaultfiledAt + 24 months, or the operator-supplied renewal date); - emits
FILING_OUTCOME(+DEADLINE_STATE).
The executor is idempotent: a retry that finds the run's filing already written is a no-op, so a mid-apply crash never double-submits. The run then closes APPLYING → CLOSED.
Eligibility BLOCK → FAILED
evalEligibility evaluates the SG IMM rule pack (packages/rules/src/packs/sg-imm.ts, four BLOCK rules: salary threshold, required documents present, screening cleared, employer eligible). The per-pass-type salary threshold is injected at eval time from the global Jurisdiction.config.immThresholds[passType] (fallback 5000 SGD). If any BLOCK rule fails the executor throws ImmEligibilityBlockedError, which the orchestrator records as a step failure leaving the run FAILED; once the blocker is resolved the run can be re-driven (FAILED → RUNNING).
Implementation
- Lifecycle:
packages/agent/src/lifecycle.ts - Workflow + executors:
packages/agent-runtime/src/modules/imm.ts - Apply (filing + renewal):
packages/agent-runtime/src/modules/imm-apply.ts - Form-resume bridge:
apps/worker/src/handlers/agent-run-form-resume.ts