---
title: "Decision Packets"
url: https://docs.growamal.com/en/guides/decision-packets/
language: en
---

# 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.

> **Note:** 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.

```bash
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",
    "triggerSource": "system",
    "triggerRuleId": "DECISION_PACKET_ASSEMBLE"
  }'
```

`reviewType` is one of `escalation` / `de_escalation` / `maintenance` / `insufficient_data_review`.

The request body accepts exactly these four fields. `evidence` and `bundleAssignmentId` are not
accepted from the client. The server derives the `TM_*` evidence from real DB rows
(`tm-evidence-reader.ts`) so a client cannot self-assert the statistics that gate a tier move
(V-5: statistics measure). The response carries the
`workflowId`, `decisionPacketId`, `tierReviewRequestId`, the `approvalState`, and the
`recommendation`:

```json
{
  "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.

```bash
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"
  }'
```

As with `/assemble`, `evidence` and `bundleAssignmentId` are server-derived and are not accepted
from the client. The server re-derives `TM_*` evidence at approve time before calling the RTI
engine (V-5 gate-hardening).

`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**.

## Related guides

- [RTI Decisions & Tiers](https://docs.growamal.com/en/guides/rti-decisions/): the engine that owns the tier write and the `TM_*` recommendation
- [Data Room](https://docs.growamal.com/en/guides/data-room/): the packet shell this workflow assembles on top of
- [Intervention Fidelity Tracker](https://docs.growamal.com/en/guides/fidelity-tracker/): the gate `TM_BLOCK_01` reads before an escalation
