CDRI Plan (Value of Information)
Status:
betaCategory:compositeAdded:2026-07-12Last reviewed:2026-07-12
Summary
Given a partial CDRI input, /cdri/plan tells you which questions to ask
next, in what order, and how much each one raises the reliability of the CDRI
result. It accepts the same body as /cdri/compute
but inverts the output: instead of the risk readout, it returns a ranked plan
of the remaining question batches (instruments) most worth asking, each
annotated with the marginal confidence it buys and the projected confidence and
tier after completing it.
It is the value-of-information inverse of /cdri/compute, and the quantified
counterpart to /intake/next: where /intake/next hands
back a single next batch, /plan returns the whole ranked map so you can decide
"ask the top 3" or "ask until we reach medium confidence" up front.
How it works (for integrators)
- Read-only and deterministic. The plan is computed from coverage, not from the subject's answers, so the same input always yields the same plan. No AI call is made.
- The unit of value is the instrument, not the item. A question batch earns its confidence only when all of its required items are answered — a half-finished batch scores nothing. There is no defensible independent per-question probability, and the endpoint does not pretend otherwise (see Mistaken use).
- Gains are cumulative and submodular — never additive. Each row's
marginal_confidence_gainassumes the rows above it are completed. The first instrument covering an uncovered domain delivers a large jump; a second instrument in an already-covered domain delivers almost nothing. Do not sum the gains — readprojected_confidence_afterfor the cumulative value. - Coverage drives the plan. CDRI confidence rises as you cover the four
required domains (
pain_load,psychosocial_load,function_load,rtw_load). The plan front-loads the cheapest batch that covers each missing domain.
Endpoint
| Field | Value |
|---|---|
| Method | POST |
| Path | /api/cdri/plan |
| Full URL | https://engine.tripod-health.com/api/cdri/plan |
| Authentication | API key (required) |
| Request body | application/json — identical to /cdri/compute |
| Response body | application/json |
| Idempotent | Yes (deterministic; no model-generated fields) |
| Rate-limited | Yes |
Purpose
A caller who wants a reliable CDRI must decide which instruments to administer
and in what order — under a real question budget. /cdri/compute scores
whatever you send and reports a confidence tier after the fact.
/cdri/plan answers the question before you spend a single question: given
what you have so far, what is the highest-value thing to ask next, and what
reliability will it buy?
Typical use: drive a questionnaire UI that shows "answering these raises our confidence from low to medium," or pre-compute a fixed battery for a cohort.
Authentication
Every request must include a valid API key in the apikey header. Keys are
issued per consumer; see authentication. A missing or
unknown key returns 401 Unauthenticated at the gateway before the request
reaches the engine.
Request
Headers
| Header | Required | Description |
|---|---|---|
apikey | Yes | Consumer API key. |
Content-Type | Yes | Must be application/json. |
X-Request-ID | No | Optional client-generated correlation ID; echoed in the response. |
Query parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
include_optional | boolean | No | false | When true, also plans optional domains (e.g. iatrogenic_load → opioid_risk). Optional domains do not raise completeness, so their confidence gain is negligible; they are surfaced for their phenotype coverage (see informs_phenotypes). |
unit | string | No | instrument | instrument (one row per question batch) or domain (collapse to one row per uncovered domain, using its highest-ranked instrument). |
budget | integer | No | catalog max | Cap on plan length. Must be a positive integer. |
Body
Byte-identical to /cdri/compute: a partial
instruments map, optional demographics, and optional situation/onset.
An empty body {} is valid and returns the full cold-start plan (every required
domain uncovered).
{
"instruments": {
"pain_scale": { "responses": { "item_1": 6 } }
},
"demographics": { "age_bracket": "45-54", "occupation_class": "construction", "region": "OH" }
}
Body fields
| Field | Type | Required | Description |
|---|---|---|---|
instruments | object | No | Map of instrument_id → { "responses": { item_id: value } }. Any subset, any partial responses. Omit or {} for a cold-start plan. |
demographics | object | null | No | Structural descriptors (age_bracket, sex, occupation_class, naics_code, region). Selects the cohort used for the imputation term. |
situation | object | null | No | Accepted for input parity with /cdri/compute. Does not influence the plan (acuity does not feed confidence); a note is added to the response when present. |
onset | string | null | No | Same as situation — accepted for parity, not used in planning. The raw date never enters the planner. |
De-identification requirement
/cdri/plan is subject to the same de-identification filter as every endpoint.
Send structural descriptors only; an identifier-shaped value anywhere in the
body returns 400 DeIdentificationViolation.
Response
Success — 200 OK
A verbatim cold-start capture (empty body {}). The questions arrays are
trimmed to the first item of each batch for readability — the real response
carries every item, in the same shape as /questions.
{
"composite_id": "cdri",
"current": {
"confidence": {
"overall": 0.2143,
"tier": "low",
"breakdown": { "input_completeness": 0.0, "imputation_variance": 0.5, "model_certainty": null }
},
"domains_required": ["pain_load", "psychosocial_load", "function_load", "rtw_load"],
"domains_covered": [],
"instruments_complete": [],
"instruments_partial": []
},
"plan": [
{
"rank": 1,
"instrument_id": "pain_scale",
"instrument_version": "1.0.0",
"domain": "pain_load",
"cost_items": 1,
"missing_required_items": ["item_1"],
"questions": [
{ "instrument_id": "pain_scale", "item_id": "item_1", "prompt": "Pain intensity right now (0 = no pain, 10 = worst imaginable)", "required": true, "response_type": "number", "min": 0, "max": 10 }
],
"marginal_confidence_gain": 0.3571,
"projected_confidence_after": 0.5714,
"projected_tier_after": "medium",
"value_per_item": 0.3571,
"informs_phenotypes": ["established_chronicity", "multi_domain_catastrophic", "pain_dominant"],
"rationale": "First instrument covering pain_load; 1 item; raises required-domain coverage to 1 of 4."
},
{
"rank": 2,
"instrument_id": "recovery_expectation",
"instrument_version": "1.0.0",
"domain": "rtw_load",
"cost_items": 1,
"missing_required_items": ["item_1"],
"questions": [
{ "instrument_id": "recovery_expectation", "item_id": "item_1", "prompt": "How likely is it that you will be able to do your usual work 6 months from now?", "required": true, "response_type": "number", "min": 0, "max": 10 }
],
"marginal_confidence_gain": 0.1429,
"projected_confidence_after": 0.7143,
"projected_tier_after": "medium",
"value_per_item": 0.1429,
"informs_phenotypes": ["established_chronicity", "multi_domain_catastrophic", "rtw_barrier"],
"rationale": "First instrument covering rtw_load; 1 item; raises required-domain coverage to 2 of 4."
},
{
"rank": 3,
"instrument_id": "anxiety_screen",
"instrument_version": "1.0.0",
"domain": "psychosocial_load",
"cost_items": 7,
"missing_required_items": ["item_1", "item_2", "item_3", "item_4", "item_5", "item_6", "item_7"],
"questions": [
{ "instrument_id": "anxiety_screen", "item_id": "item_1", "prompt": "Feeling nervous, anxious, or on edge", "required": true, "response_type": "single_select", "options": [{ "value": 0, "label": "Not at all" }, { "value": 1, "label": "Several days" }, { "value": 2, "label": "More than half the days" }, { "value": 3, "label": "Nearly every day" }] }
],
"marginal_confidence_gain": 0.1428,
"projected_confidence_after": 0.8571,
"projected_tier_after": "high",
"value_per_item": 0.0204,
"informs_phenotypes": ["multi_domain_catastrophic", "psychosocial_dominant"],
"rationale": "First instrument covering psychosocial_load; 7 items; raises required-domain coverage to 3 of 4."
},
{
"rank": 4,
"instrument_id": "oswestry",
"instrument_version": "1.0.0",
"domain": "function_load",
"cost_items": 10,
"missing_required_items": ["item_1", "item_2", "item_3", "item_4", "item_5", "item_6", "item_7", "item_8", "item_9", "item_10"],
"questions": [
{ "instrument_id": "oswestry", "item_id": "item_1", "prompt": "Pain intensity", "required": true, "response_type": "single_select", "options": [{ "value": 0, "label": "No problem" }, { "value": 1, "label": "Mild problem" }, { "value": 2, "label": "Moderate problem" }, { "value": 3, "label": "Fairly severe problem" }, { "value": 4, "label": "Very severe problem" }, { "value": 5, "label": "Complete disability" }] }
],
"marginal_confidence_gain": 0.1429,
"projected_confidence_after": 1.0,
"projected_tier_after": "high",
"value_per_item": 0.0143,
"informs_phenotypes": ["established_chronicity", "multi_domain_catastrophic", "pain_dominant"],
"rationale": "First instrument covering function_load; 10 items; raises required-domain coverage to 4 of 4."
}
],
"confidence_formula": {
"weights": { "completeness": 0.4, "model_certainty": 0.3, "imputation_variance": 0.3 },
"active_terms": ["completeness", "imputation_variance"],
"note": "model_certainty is held out at CONTEXTUAL depth; the two active weights renormalize to sum to 1."
},
"notes": [
"Gains are the deterministic, coverage-driven confidence floor and are independent of the subject's answers.",
"An instrument earns its value only when ALL its required items are answered; partial batches score nothing.",
"Gains are cumulative and submodular -- each row's gain assumes the rows above it are completed. Do not sum them.",
"value_per_item is amortized (gain / missing items), not an independent per-question probability.",
"model_certainty (answer-dependent) is held out at CONTEXTUAL depth; realized confidence after scoring may be higher."
],
"catalog_version": "0.22.0",
"matrix_version": "2026-06-02-first-pass",
"request_id": "9b02322b-0ff9-4df6-a8e3-3ed3c1c67b97",
"timestamp": "2026-07-12T23:12:14.500180+00:00"
}
Response fields
| Field | Type | Always present | Description |
|---|---|---|---|
composite_id | string | Yes | Always "cdri". |
current | object | Yes | The state before any planned batch — see below. |
plan | array | Yes | Greedy-ranked plan rows — see below. Empty when nothing is worth asking. |
confidence_formula | object | Yes | The confidence weights, the terms active at this depth, and a note. Makes the numbers auditable. |
notes | array | Yes | Plain-English statements of how to read the gains (submodular, coverage-driven, value_per_item amortized). |
catalog_version | string | Yes | Catalog version served. |
matrix_version | string | null | Yes | Active phenotype-matrix version behind informs_phenotypes. |
request_id | string | Yes | Correlation ID. |
timestamp | string (ISO 8601 UTC) | Yes | Server time at response emission. |
current
| Field | Type | Description |
|---|---|---|
confidence | object | Confidence of the input as-is. Same shape as CDRI confidence; model_certainty is null here (see below). |
domains_required | array | The four required CDRI domains. |
domains_covered | array | Required domains already covered by a complete instrument. |
instruments_complete | array | Instruments whose required items are all answered. |
instruments_partial | array | Instruments present but below their required-items bar (they currently score nothing). |
plan[]
| Field | Type | Description |
|---|---|---|
rank | integer | 1-based rank. Ask in this order. |
instrument_id | string | The instrument (question batch) to administer. |
instrument_version | string | Rubric version. |
domain | string | The CDRI domain this instrument maps to. |
cost_items | integer | Number of still-missing required items — the burden of completing this batch. |
missing_required_items | array | The specific item ids still needed. |
questions | array | Render-ready items, same shape as /questions. |
marginal_confidence_gain | number | Confidence gained by completing this batch, given the batches ranked above it are completed. Cumulative — do not sum across rows. |
projected_confidence_after | number | Cumulative overall confidence after completing this batch and all above it. |
projected_tier_after | string | Tier (low/medium/high) at that projected confidence. |
value_per_item | number | Amortized marginal_confidence_gain / cost_items. A UI-ranking convenience, not an independent per-question probability. |
informs_phenotypes | array | Phenotypes this instrument feeds in the active matrix. Reference metadata, not part of the gain math. |
rationale | string | One-line explanation of why the batch is ranked here. |
Why model_certainty is null
CDRI confidence normally combines three terms: completeness,
imputation_variance, and model_certainty. The first two depend only on
which instruments are present and complete and on the demographic cohort —
never on the answer values — so they can be projected before the answers exist.
model_certainty depends on the answers themselves and is therefore not
knowable in advance. /cdri/plan holds it out and reports the honest,
coverage-driven confidence floor; the remaining two weights renormalize to sum
to 1. Realized confidence after scoring via /cdri/compute may be higher.
Partial input
Sending one complete instrument covers its domain and raises current
confidence. The plan then targets only the still-uncovered domains. Verbatim
capture for {"instruments":{"pain_scale":{"responses":{"item_1":6}}}, ...}
(plan[].questions omitted here for brevity):
{
"current": {
"confidence": { "overall": 0.5714, "tier": "medium", "breakdown": { "input_completeness": 0.25, "imputation_variance": 1.0, "model_certainty": null } },
"domains_required": ["pain_load", "psychosocial_load", "function_load", "rtw_load"],
"domains_covered": ["pain_load"],
"instruments_complete": ["pain_scale"],
"instruments_partial": []
},
"plan": [
{ "rank": 1, "instrument_id": "recovery_expectation", "domain": "rtw_load", "cost_items": 1, "marginal_confidence_gain": 0.1429, "projected_confidence_after": 0.7143, "projected_tier_after": "medium" },
{ "rank": 2, "instrument_id": "anxiety_screen", "domain": "psychosocial_load", "cost_items": 7, "marginal_confidence_gain": 0.1428, "projected_confidence_after": 0.8571, "projected_tier_after": "high" },
{ "rank": 3, "instrument_id": "oswestry", "domain": "function_load", "cost_items": 10, "marginal_confidence_gain": 0.1429, "projected_confidence_after": 1.0, "projected_tier_after": "high" }
]
}
Note that current.confidence.overall here (0.5714) equals the cold-start
plan's rank-1 projected_confidence_after — the projection matched the real
subsequent state.
include_optional=true
Adds optional-domain instruments after the required plan. They carry negligible
confidence gain but are surfaced for their unique phenotype coverage — e.g.
opioid_risk is the sole feeder of iatrogenic_escalation, so skipping it
leaves that phenotype permanently at 0. Verbatim tail row:
{
"rank": 5,
"instrument_id": "opioid_risk",
"domain": "iatrogenic_load",
"marginal_confidence_gain": 0.0,
"informs_phenotypes": ["iatrogenic_escalation", "multi_domain_catastrophic"],
"rationale": "Optional domain iatrogenic_load; negligible confidence gain -- its value is the phenotype coverage in informs_phenotypes."
}
Status codes
| Code | Meaning | When |
|---|---|---|
200 | OK | Plan computed (including an empty plan when nothing is worth asking). |
400 | ValidationError | Malformed JSON, an unknown instrument_id, or a bad query param (unit, budget). |
400 | DeIdentificationViolation | Request body contained an identifier. |
401 | Unauthenticated | Missing or invalid API key. |
413 | PayloadTooLarge | Body exceeds the 512 KB limit. |
415 | UnsupportedMediaType | Content-Type is not application/json. |
429 | RateLimited | Per-consumer rate limit exceeded. |
500 | InternalError | Unexpected server-side failure. |
503 | ServiceUnavailable | A required dependency is temporarily unhealthy. |
Error envelope
All non-2xx responses use the uniform envelope. Verbatim capture for
?unit=bogus:
{
"timestamp": "2026-07-12T22:12:12.439439+00:00",
"error": "ValidationError",
"message": "unit must be 'instrument' or 'domain'",
"request_id": "docs-plan-err"
}
See errors for the full list.
Examples
curl — cold-start plan
curl -sS -X POST "https://engine.tripod-health.com/api/cdri/plan" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
curl — plan from a partial input
curl -sS -X POST "https://engine.tripod-health.com/api/cdri/plan?budget=3" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "instruments": { "pain_scale": { "responses": { "item_1": 6 } } }, "demographics": { "age_bracket": "45-54", "occupation_class": "construction", "region": "OH" } }'
Python — ask the top N, then score
# Uses the call() helper from quickstart.md.
plan = call("POST", "/api/cdri/plan", {"instruments": {}, "demographics": {"age_bracket": "45-54"}})
budget_items, asked = 8, []
for step in plan["plan"]:
if step["cost_items"] > budget_items:
break
budget_items -= step["cost_items"]
asked.append(step["instrument_id"])
# render step["questions"], collect answers into your instruments map...
print("ask these batches:", asked)
print("projected confidence if all asked:", plan["plan"][len(asked) - 1]["projected_confidence_after"])
# Later, submit the collected answers to /api/cdri/compute for the scored result.
Intended use
- Planning a questionnaire battery under a question budget: "ask the top 3."
- Showing a user "answering these raises our confidence from low to medium" before they start.
- Deciding whether an optional instrument is worth its burden (via
include_optional+informs_phenotypes).
Mistaken use
- Do not sum
marginal_confidence_gainacross rows. The gains are submodular and cumulative — readprojected_confidence_afterinstead. - Do not treat
value_per_itemas an independent per-question probability. It is an amortized ranking convenience; a batch buys its confidence only when fully completed. - Do not use
/cdri/planto score. It never returns a CDRI score, magnitudes, or a phenotype distribution — that is/cdri/compute. - Do not expect
situation/onsetto change the plan. They are accepted for input parity only; acuity does not feed confidence. - Do not confuse this with
/intake/next./intake/nextreturns a single next batch in a stateful-feeling loop;/planreturns the whole ranked value map in one shot.
Rate limits
Per-consumer limits are enforced at the gateway; see rate limits.
Related
- composite-cdri-compute.md — the scored counterpart; identical request body.
- intake-next.md — the single-next-batch adaptive-intake cousin.
- patterns-questions.md — the render-ready item shape reused in
plan[].questions. - credits.md, errors.md, conventions.md — cross-cutting contract rules.
Change history
| Date | Change |
|---|---|
| 2026-07-12 | Initial publication. |