---
title: "Intervention Bundles"
url: https://docs.growamal.com/en/guides/intervention-bundles/
language: en
---

# Intervention Bundles

When a student has an active educational profile, the `modules/bundle` module
takes a single step: it looks the profile up in a **21-row deterministic map**
and returns either one named intervention bundle **or** a non-bundle
routing/action outcome. Per R12 Tier-D, **7 of the 21 MAP rows**
(MAP-000/001/002/003/018/019/020) resolve to a non-bundle object with
`recommended_bundle_id = NULL` — a monitoring process, a maintenance/enrichment
option, a light-support action, a support module, a routing guard, or a
future-inactive object. Only MAP-004..017 return a real bundle that writes a
`bundle_assignment`. The teacher then decides whether to activate it. Nothing
reaches a student until the teacher acts (V-12).

## The catalog

The bundle catalog is **14 intervention bundles + 7 reclassified non-bundle
objects = 23 physical rows** in `bundle_definition` (16 bundle-type rows —
including the 3 `VOCAB_SYNTAX_MORPH` variant rows — plus 7 non-bundle objects).
Each row carries a `BundleObjectType` enum (9 values); only
`INTERVENTION_BUNDLE` / `COMPOSITE_SEQUENCE_PLAN` /
`COMPOSITE_INTERVENTION_BUNDLE` rows write a `bundle_assignment` on activation.
Bundles are closed catalog items identified by a string `bundleId`. They are
not generated, not freeform, and not per-student. What changes per student is
the scaffold tier assigned to each skill within that bundle.

The catalog is versioned and publish-then-freeze: once a catalog version is
published it is never mutated. A V-6 dry-run replay endpoint lets you preview
how a catalog change would affect existing recommendations before publishing.

## From profile to bundle: the map

The 21-row `profile_bundle_map` is keyed on
`(primary_profile_code, access_driver | comprehension_subprofile | modifier_driver)`.
The extra driver column narrows which bundle is selected for profiles that have
multiple instructionally distinct sub-types.

Row `MAP-000` is the halt case. The `NO_BUNDLE_SYSTEM_GUARD` sentinel-bundle row
is **retired** (D-015, GAP-021); `MAP-000` is now a `ROUTING_GUARD` object. When
the student has no active profile, a `DATA_INCOMPLETE` guard state, or a
`do_not_decide_yet` anchor skill, the map returns `recommended_bundle_id = NULL`,
`object_type = ROUTING_GUARD`, `routing_state = BLOCKED_MISSING_EVIDENCE`, and
`recommendation_status = blocked_insufficient_evidence`, plus a
`next_recommended_data_action` that names the specific data action needed. No
bundle is recommended and there is no override path around this.

> **Warning:** `MAP-000` is a hard halt. It is no longer a placeholder bundle row — it is a
> nullable-bundle routing outcome. It surfaces only the next data action, never a
> bundle. No override code bypasses it. The teacher's action is to complete the
> named data collection step, not to activate an alternative plan.

## The Threaded Path Model

Every recommended bundle has three parts:

| Part | Role | Rule |
|---|---|---|
| Anchor | Primary focus of the cycle | Addresses the primary skill group; always the largest allocation band |
| Supporting threads (1–2) | Linked to the anchor | Address skills related to the anchor; maximum 2 |
| Asset bridge | Strength-based bridge | Builds on a documented strength; `not_enough_evidence` when support data is absent |

> **Note:** **R12 Tier-D (D-003) — dosage model changed.** Per-session fixed-percentage
> splits are **retired**. Dosage is now expressed as **cumulative-cycle allocation
> ranges** across the intervention cycle (2 master recipes in Q11 sheet 07), stored
> via the `cycle_allocation_model` column on `bundle_definition`. The old fixed
> 40–60% / 25–35% / 10–15% percentages are the **old model — illustrative only**.

The old `dosageSplit` JSONB (`sum == 100` + anchor-strictly-largest) DB CHECK
constraint is **dropped**. The engine (`threaded-path.ts`) now asserts the anchor
*band* is the largest (range-based `dosageSplitWellFormed`), not a fixed-percentage
strict-largest CHECK. The CI test `test:ibc:weights` (= `test:qa:020`) asserts the
cumulative-cycle anchor-band-largest invariant, not `sum == 100`.

## Scaffold tier

The scaffold tier is assigned per (student × skill × bundle × task_type),
not per student and not per bundle. A student may have `Level_1_Foundation` on
one skill within a bundle and `Level_2_Targeted` on another.

| Tier | Identifier |
|---|---|
| Foundation | `Level_1_Foundation` |
| Targeted | `Level_2_Targeted` |
| Independence | `Level_3_Independence` |

The scaffold tier is never shown to the student. It drives which level of
scaffolded task the task-delivery layer selects. Teacher and admin routes can
see tier distribution (the cluster preview and the scaffold adjustment endpoint
expose it), but no student-facing route does.

Scaffold tier adjustments use override rule `OVR_SCAFFOLD` with a `reasonCode`
from the 9-code catalog. RTI tier is in every override's `forbidden_fields` and
is never written through any bundle endpoint.

## Bundle assignment lifecycle

A `bundle_assignment` is written **only** for the bundle-type object types
(`INTERVENTION_BUNDLE` / `COMPOSITE_SEQUENCE_PLAN` /
`COMPOSITE_INTERVENTION_BUNDLE` — MAP-004..017). The 4 non-bundle outcome types
are recorded by TAW as routing/action codes with no `bundle_assignment` row.

Once a teacher activates a bundle, the assignment follows a **17-status state
machine (including `paused` as a transient hold state)** reflecting the monitoring
loop:

```
recommended → in_review → activated → monitoring
                                      ├─ paused
                                      ├─ reviewed → continued | adjusted | modified
                                      ├─ escalated → immediate_review
                                      ├─ data_completion_required
                                      ├─ met_criteria → exited
                                      ├─ teacher_modified
                                      ├─ regression_review
                                      └─ rescinded
```

The `BundleAssignmentStatus` enum is the authoritative list returned in
`ActivateBundleResponse` and exposed on the class profile distribution endpoint.

## Teacher adjustments (override codes)

Teachers adjust bundle parameters (delivery mode, schedule, context flags)
through controlled override operations. Every adjustment picks a `reasonCode`
from the 9-code `OverrideCodeCatalog`:

| Code | Applicable scope |
|---|---|
| `Override_Contextual_01` | All |
| `Override_Procedural_02` | All |
| `Override_Scheduled_03` | All |
| `Override_DataGap_04` | All |
| `Gam_Award_Invalid_05` | Gamification rescind only |
| `Bundle_Met_Criteria_06` | Bundle exit |
| `Bundle_Teacher_Modify_07` | Bundle exit |
| `Bundle_Acute_Regression_08` | Bundle exit |
| `Bundle_Data_Incomplete_09` | Bundle exit |

No free-text reason fields exist anywhere in the bundle module. All
adjustment endpoints accept only a `reasonCode` FK into this catalog. This is
enforced at the CI lint level (`pnpm lint:bundle`).

Context flags (e.g. `CTX_SESSION_INTERRUPTED`) may optionally accompany an
adjustment. The 7 allowed `ContextFlagId` values are the only representable
flags; the 3 blocked ones are not constructible through the API.

## Key endpoints

| Endpoint | Purpose |
|---|---|
| `POST /api/bundles/recommend` | Compute and write a bundle recommendation for a student |
| `GET /api/bundles/catalog` | All 22 catalog rows (14 intervention bundles + 7 non-bundle objects; 23 physical rows in `bundle_definition` total, the internal `NO_BUNDLE_SYSTEM_GUARD` sentinel is excluded from this response) |
| `GET /api/bundles/:id/definition` | One bundle with delivery options and cluster rule |
| `GET /api/bundles/profile-map` | All 21 map rows and integration rules |
| `POST /api/students/:id/activate-bundle` | Teacher activates a recommendation |
| `PATCH /api/students/:id/scaffold-assignment` | OVR_SCAFFOLD adjustment |
| `PATCH /api/assignments/:id/delivery` | OVR_DELIVERY adjustment |
| `PATCH /api/assignments/:id/schedule` | OVR_SCHEDULE adjustment |
| `POST /api/assignments/:id/context-flag` | OVR_CONTEXT_FLAG (7 allowed flag ids) |
| `POST /api/admin/bundle-config-sets` | Publish new catalog version |
| `POST /api/admin/bundle-replay` | V-6 dry-run; writes nothing |

Smart Cluster + bulk-activation endpoints are in [Smart Clustering](https://docs.growamal.com/en/guides/smart-clustering/).

## Connections

- **Upstream:** [Student Profiles](https://docs.growamal.com/en/guides/student-profiles/): the active profile
  is the input to the profile-to-bundle map.
- **Downstream:** [Smart Clustering](https://docs.growamal.com/en/guides/smart-clustering/): cluster logic
  groups students who share bundle, anchor, tier, and delivery mode.
- **Monitoring:** [Progress Monitoring](https://docs.growamal.com/en/guides/progress-monitoring/): the 5 GSR
  safety rules and 19 per-bundle success criteria that govern when a bundle transitions.
- **Workflow:** [Teacher Activation Workflow](https://docs.growamal.com/en/guides/teacher-activation/): the
  17-step UI workflow that drives teacher confirmation, adjustment, and review.
- **Task delivery:** [Task Delivery](https://docs.growamal.com/en/guides/task-delivery/): once a bundle is
  active, task delivery selects and serves stored items using the scaffold tier.
