Skip to content

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

FromAllowed next
PLANNINGAWAITING_CLIENT, RUNNING, FAILED, CANCELLED
AWAITING_CLIENTRUNNING, CANCELLED, FAILED
RUNNINGAWAITING_CLIENT, AWAITING_REVIEW, APPLYING, CLOSED, FAILED, CANCELLED
AWAITING_REVIEWAPPLYING, RUNNING, CANCELLED, FAILED
APPLYINGRUNNING, CLOSED, FAILED
CLOSED(terminal)
FAILEDRUNNING
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:

SeqStepstepTypeWhat it does
1classifyLLMRecords the pass type / applicant / employer for the run.
2collectApplicationFormFORMPauses the run at AWAITING_CLIENT: issues a FormInstance + magic link (real mode only).
3collectDocumentsTOOLDerives document completeness from the uploaded KycDocument rows (real mode) or the mock adapter.
4screenTOOLPersists the applicant KycCase + ScreeningResult (idempotent per run + party).
5evalEligibilityRULERuns the SG IMM rule pack; throws → run FAILED if any BLOCK rule fails.
6assembleApplicationLLMBuilds the assisted MOM submission pack (mappedFields + HTML artifact).
7submitEXTERNALProposes 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:

  1. Issues a FormInstance (idempotent per runId) with the prefilled applicant / pass-type / employer / salary fields and a 48-hour magic-link token whose resourceType is IMM_FORM_INSTANCE.
  2. Returns awaitClient: true, which the orchestrator translates into RUNNING → AWAITING_CLIENT — the run stops here and persists its resume point.
  3. The full run input is 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 (registry MOM, type PASS_APPLICATION, status ACKNOWLEDGED) 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) (default filedAt + 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

Internal use only — BreezyCorp