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
| Method | Path | Permission |
|---|---|---|
POST | /api/decision-packet-workflows/assemble | REQUEST_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}/approve | REQUEST_TIER_REVIEW, APPROVE_TIER_3_ACTIVATION, or RUN_BACKOFFICE (+ in-handler specialist gate) |
POST | /api/decision-packet-workflows/{id}/reject | REQUEST_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
↘ rejectedapplied 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). A403covers 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-org404), no LLM (V-5), and never student-facing.
Related guides
- RTI Decisions & Tiers: the engine that owns the tier write and the
TM_*recommendation - Data Room: the packet shell this workflow assembles on top of
- Intervention Fidelity Tracker: the gate
TM_BLOCK_01reads before an escalation