Skip to Content

Decision Packets

The decision-packet workflow (WP-DECISION-PACKET-BE) is the closing piece of the RTI decision wave: the review-and-approval workflow that turns a tier-movement recommendation into an audited decision, and only then moves the child’s tier. It assembles a packet on top of the Data Room’s packet shell, computes the deterministic recommendation, routes it through the approval gate, and on approval executes the tier move through the RTI engine’s single tier-write path. It never writes a tier_decision_record itself.

This module adds no tier write of its own. It calls the RTI engine to move the tier and the Data Room to open and close the packet shell. The only tier write path stays in the RTI engine.

Routes

MethodPathPermission
POST/api/decision-packet-workflows/assembleREQUEST_TIER_REVIEW, APPROVE_TIER_3_ACTIVATION, or RUN_BACKOFFICE
GET/api/decision-packet-workflows/{id}REQUEST_TIER_REVIEW, VIEW_OWN_CLASSES, VIEW_SCHOOL, VIEW_ORG, or RUN_BACKOFFICE
POST/api/decision-packet-workflows/{id}/approveREQUEST_TIER_REVIEW, APPROVE_TIER_3_ACTIVATION, or RUN_BACKOFFICE (+ in-handler specialist gate)
POST/api/decision-packet-workflows/{id}/rejectREQUEST_TIER_REVIEW, APPROVE_TIER_3_ACTIVATION, or RUN_BACKOFFICE

The workflow id is a positive integer. There is deliberately no student endpoint; students never see any of this. A cross-org or out-of-scope id returns 404.

Assemble

POST …/assemble opens the Data Room packet (evidence snapshotted by reference, every rule and config version pinned for replay), computes the deterministic TM_* recommendation, writes the workflow row in the assembled state, and sets the packet to under_review. A 409 is returned if a live workflow already exists for the student.

curl -X POST https://localhost:3000/api/decision-packet-workflows/assemble \ -H "Authorization: Bearer <accessToken>" \ -H "Content-Type: application/json" \ -d '{ "studentId": 42, "reviewType": "escalation", "bundleAssignmentId": 8124, "triggerSource": "system", "triggerRuleId": "DECISION_PACKET_ASSEMBLE", "evidence": { "hasCbmProgressProbe": true, "hasInterventionFidelity": true, "comparablePmPoints": 4, "positiveSlope": false, "hasActiveMonitoringPlan": true } }'

reviewType is one of escalation / de_escalation / maintenance / insufficient_data_review. The evidence object carries the controlled TM_* booleans and counts the recommendation engine reads (the same review context the RTI decision flow consumes). The response carries the workflowId, decisionPacketId, tierReviewRequestId, the approvalState, and the recommendation:

{ "workflowId": 17, "decisionPacketId": 31, "tierReviewRequestId": 22, "approvalState": "assembled", "recommendation": { "matchedRuleId": "TM_BLOCK_01", "movementResult": "defer", "recommendedAction": "defer", "blockingConditions": ["fidelity_below_threshold"], "movementRuleVersion": 3 } }

matchedRuleId is one of the four movement rules (TM_ESC_01 / TM_MAINT_01 / TM_DEC_01 / TM_BLOCK_01). movementResult is escalate / de_escalate / maintain / defer / needs_more_data.

Approve and reject

GET …/{id} is the reviewer’s surface: the assembled packet, the workflow state, and the pinned recommendation snapshot.

POST …/{id}/approve routes the recommendation through the approval gate. A Tier-3 move requires the high-intensity specialist (APP_HIGH_INTENSITY, admin-seated for Wave 1); the in-handler specialist gate returns 403 for a non-specialist escalation. When the verdict permits and the gate passes, it calls the RTI engine’s submitTierReviewDecision (which runs applyDecision, the sole tier write path), then closes the packet.

curl -X POST https://localhost:3000/api/decision-packet-workflows/17/approve \ -H "Authorization: Bearer <accessToken>" \ -H "Content-Type: application/json" \ -d '{ "decisionReasonCode": "Override_Contextual_01", "reviewType": "escalation", "bundleAssignmentId": 8124, "evidence": { "hasInterventionFidelity": true, "comparablePmPoints": 4 } }'

POST …/{id}/reject records a controlled-reason decline; no movement, no close, terminal. Both decision responses carry the applied state, whether the tier moved, the tierDecisionRecordId (or null), the matchedRuleId, and any blockingConditions.

The decisionReasonCode is a controlled code from the override-code catalog (Override_Contextual_01 / Override_Procedural_02 / Override_Scheduled_03 / Override_DataGap_04 / Bundle_Met_Criteria_06 / Bundle_Teacher_Modify_07 / Bundle_Acute_Regression_08 / Bundle_Data_Incomplete_09). There is no free-text reason field anywhere on the workflow.

The approval state machine

assembled → pending_approval → approved → applied ↘ rejected

applied and rejected are terminal. A second approve or reject on a terminal workflow returns 409.

Invariants

  • No movement without approval. A sentinel halt (do_not_decide_yet), an unapproved Tier-3 move, or an incomplete dossier all block the movement (fail-closed). A 403 covers a sentinel block or a failed specialist gate.
  • The decision reason is a controlled code, never free text.
  • Immutable on close (V-6). A closed packet and an applied decision are immutable forever; the recommendation snapshot pins every version it read, so a replay reproduces the same verdict.
  • No tier write here. The workflow calls the RTI engine to move the tier and the Data Room to open/close the packet shell.
  • Append-only, tenant-isolated (organizationId; cross-org 404), no LLM (V-5), and never student-facing.
Last updated on