Loading documentation

Access required

Your account doesn't have access to the documentation. Contact your administrator.

Sign out
Skip to main content

Adaptive Intake

Status: stable Category: composite Added: 2026-06-03 Last reviewed: 2026-07-12

Summary

Engine-driven adaptive intake, the collection counterpart to /cdri/plan. You send the full set of answers collected so far; the engine ranks the remaining question batches by value-of-information and returns the next questions to ask — dripped from the single highest-value instrument, in order — until the plan is exhausted. Every turn also returns a deterministic, AI-free risk_snapshot (CDRI score, the seven phenotype magnitudes, and confidence) so you can show a live, moving-as-they-answer risk readout without running the full CDRI on each turn.

/cdri/plan answers "what should I ask?" as a whole ranked map; /intake/next drives the asking and the collection one turn at a time and adds the live risk feedback. Both sit on the same deterministic scoring math.

How it works (for integrators)

  • Stateless. There is no per-subject session. On every call you re-send every answer collected so far in answers; the engine re-evaluates the whole set. Store answers on your side (see Storing answers).
  • The loop. Call /intake/next → render questions → collect answers → append them to answers → call again. Repeat until the response comes back phase: "complete". In a waiting-room flow you simply stop early whenever the session ends (e.g. a clinician picks up) — the answers are already stored.
  • Value-of-information order. Questions come from the highest-value instrument first (the same greedy math as /cdri/plan), so a limited number of questions covers the most decision-relevant ground.
  • Item drip. A turn returns questions from one instrument at a time (never mixed). ?limit caps how many of that instrument's items come back per turn; the default is the whole remaining batch.
  • Live risk snapshot. risk_snapshot reflects the answers so far. Because an instrument only contributes to the CDRI once all its required items are answered, the snapshot moves when an instrument completes, not on every keystroke — single-item instruments move immediately; a long instrument moves when its last required item lands.
  • Completion. The engine returns phase: "complete" when no instrument is left worth asking (reason: "plan_exhausted"), or when a filter matches nothing (see Filters).
  • Read-only. Intake does not run the AI layers and does not persist a training record. Persist the scored record later by submitting the collected answers to /cdri/compute.

Endpoint

FieldValue
MethodPOST
Path/api/intake/next
Full URLhttps://engine.tripod-health.com/api/intake/next
AuthenticationAPI key (required)
Request bodyapplication/json
Response bodyapplication/json
IdempotentYes (deterministic; no model-generated fields)
Rate-limitedYes

Authentication

Every request must include a valid API key in the apikey header; see authentication. A missing or unknown key returns 401 Unauthenticated at the gateway.

Request

Query parameters

All optional. They narrow or pace the turn; omit them for the default flow.

ParameterTypeDefaultDescription
limitintegerwhole batchMax questions to return this turn (the drip size). Questions always come from a single instrument, so limit caps items within that instrument. Must be a positive integer.
domainstringRestrict to one CDRI domain: pain_load, psychosocial_load, function_load, rtw_load, or iatrogenic_load.
phenotypestringRestrict to instruments that inform a phenotype (e.g. iatrogenic_escalation), via the phenotype matrix.
response_typestringRestrict to instruments whose items render as this type. Supported today: number, single_select. boolean, multi_select, and free_text are reserved — no instrument currently emits them, so filtering on one returns phase: "complete" with reason: "reserved_response_type".

Body

FieldTypeRequiredDescription
answersarrayYesEvery answer collected so far. Empty array [] on the first call. Each entry is { "instrument_id", "item_id", "value" }. Re-send the full list on every call.
contextobject | nullNoStructural descriptors for the subject (age_bracket, sex, occupation_class, naics_code, region) — the same shape as demographics elsewhere. Improves cohort context for the confidence/snapshot math.
{
"answers": [],
"context": { "age_bracket": "45-54", "sex": "male", "occupation_class": "construction", "region": "OH" }
}

A later turn re-sends prior answers and appends new ones:

{
"answers": [
{ "instrument_id": "pain_scale", "item_id": "item_1", "value": 8 }
],
"context": { "age_bracket": "45-54", "sex": "male", "occupation_class": "construction", "region": "OH" }
}

De-identification

/intake/next is subject to the same de-identification filter as every endpoint — an identifier-shaped value anywhere in the body returns 400 DeIdentificationViolation. Send structural descriptors only.

Response

The response always carries a phase discriminator: "ask", "complete", or "error". Branch on it. progress and risk_snapshot are present on both ask and complete.

phase: "ask"200 OK

Verbatim production capture of the first call (empty answers, ?limit=3). The engine selected pain_scale (the cheapest way to cover the highest-value uncovered domain) and returned its single item. risk_snapshot is all-zero because nothing is scored yet.

{
"phase": "ask",
"batch": {
"instrument_id": "pain_scale",
"instrument_version": "1.0.0",
"domain": "pain_load",
"rank": 1,
"response_type": "number",
"marginal_confidence_gain": 0.3571,
"projected_confidence_after": 0.5714,
"projected_tier_after": "medium",
"informs_phenotypes": ["established_chronicity", "multi_domain_catastrophic", "pain_dominant"],
"items_remaining_in_instrument": 1,
"drip_limit": 3
},
"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,
"instrument_rank": 1,
"item_index": 1,
"item_count_required": 1
}
],
"progress": {
"domains_required": ["pain_load", "psychosocial_load", "function_load", "rtw_load"],
"domains_covered": [],
"instruments_complete": [],
"instruments_partial": [],
"plan_remaining_instruments": 4
},
"risk_snapshot": {
"cdri_score": 0,
"overall_magnitude": 0,
"magnitudes": { "pain_dominant": 0, "psychosocial_dominant": 0, "fear_avoidance_dominant": 0, "rtw_barrier": 0, "multi_domain_catastrophic": 0, "iatrogenic_escalation": 0, "established_chronicity": 0 },
"confidence": { "overall": 0.2143, "tier": "low", "breakdown": { "input_completeness": 0, "imputation_variance": 0.5, "model_certainty": null } },
"matrix_version": "2026-06-02-first-pass"
},
"catalog_version": "0.22.0",
"matrix_version": "2026-06-02-first-pass",
"request_id": "385e24e7-948f-441a-a4c5-f1b7f4330868",
"timestamp": "2026-07-12T23:12:14.983027+00:00"
}

Watch the snapshot move. Answering that one pain_scale item (value: 8) and re-posting completes the pain instrument, so the next turn advances to recovery_expectation and the risk bars jump — verbatim batch + risk_snapshot from that turn:

{
"batch": {
"instrument_id": "recovery_expectation",
"domain": "rtw_load",
"rank": 1,
"response_type": "number",
"marginal_confidence_gain": 0.1429,
"projected_confidence_after": 0.7143,
"projected_tier_after": "medium",
"items_remaining_in_instrument": 1
},
"progress": { "domains_covered": ["pain_load"], "instruments_complete": ["pain_scale"], "plan_remaining_instruments": 3 },
"risk_snapshot": {
"cdri_score": 80,
"overall_magnitude": 0.8,
"magnitudes": { "pain_dominant": 0.8, "psychosocial_dominant": 0, "fear_avoidance_dominant": 0, "rtw_barrier": 0, "multi_domain_catastrophic": 0.8, "iatrogenic_escalation": 0, "established_chronicity": 0.8 },
"confidence": { "overall": 0.5714, "tier": "medium", "breakdown": { "input_completeness": 0.25, "imputation_variance": 1, "model_certainty": null } }
}
}

Response fields — ask

FieldTypeDescription
phasestring"ask".
batchobjectMetadata for the instrument this turn's questions belong to (see below). All questions in a turn belong to this one instrument.
questionsarrayRender-ready items: instrument_id, item_id, prompt, required, response_type, min/max (number) or options (single_select), plus instrument_rank, item_index, item_count_required. Same item shape as /questions.
progressobjectdomains_required, domains_covered, instruments_complete, instruments_partial, plan_remaining_instruments. Drive a progress bar from this.
risk_snapshotobjectLive deterministic risk over the answers so far (see Risk snapshot).
catalog_version, matrix_versionstringVersions served.
request_idstringCorrelation ID.
timestampstring (ISO 8601 UTC)Server time.

batch carries: instrument_id, instrument_version, domain, rank (its VOI rank this turn — always 1, since the drip is from the top instrument), response_type, marginal_confidence_gain and projected_confidence_after / projected_tier_after (the confidence completing this instrument buys), informs_phenotypes, items_remaining_in_instrument, and drip_limit (echo of ?limit).

The risk snapshot

risk_snapshot is the deterministic, AI-free CDRI readout over the answers collected so far — the same math /cdri/compute runs, minus the AI layers, so it is fast enough to return on every turn.

FieldTypeDescription
cdri_scorenumber0–100 peak headline (100 × the top phenotype magnitude).
overall_magnitudenumberThe peak magnitude, 0–1.
magnitudesobjectThe seven phenotype magnitudes, each 0–1 — the "bars."
confidenceobjectoverall + tier + per-term breakdown. model_certainty is always null here (an AI-layer signal, not run during intake).
matrix_versionstringPhenotype-matrix version behind the magnitudes.

It reflects the actual answers, so it only moves when an instrument completes (the scoring cliff above). A partially-answered instrument contributes nothing until its last required item lands. The engine never fabricates per-item movement.

phase: "complete"200 OK

Returned when the plan is exhausted (every worthwhile instrument answered). Verbatim production capture after all four required domains were covered:

{
"phase": "complete",
"reason": "plan_exhausted",
"progress": {
"domains_required": ["pain_load", "psychosocial_load", "function_load", "rtw_load"],
"domains_covered": ["function_load", "pain_load", "psychosocial_load", "rtw_load"],
"instruments_complete": ["anxiety_screen", "oswestry", "pain_scale", "recovery_expectation"],
"instruments_partial": [],
"plan_remaining_instruments": 0
},
"risk_snapshot": {
"cdri_score": 80,
"overall_magnitude": 0.8,
"magnitudes": { "pain_dominant": 0.8, "psychosocial_dominant": 0.5, "fear_avoidance_dominant": 0, "rtw_barrier": 0.8, "multi_domain_catastrophic": 0.737, "iatrogenic_escalation": 0, "established_chronicity": 0.8 },
"confidence": { "overall": 1, "tier": "high", "breakdown": { "input_completeness": 1, "imputation_variance": 1, "model_certainty": null } }
},
"catalog_version": "0.22.0",
"matrix_version": "2026-06-02-first-pass",
"request_id": "48ea4115-49a0-47f0-a7c1-457bddbd76f7",
"timestamp": "2026-07-12T23:12:51.827428+00:00"
}

reason is one of: plan_exhausted (nothing left worth asking), no_candidates_for_filter (an active filter matched no instrument), or reserved_response_type (filtered on a reserved type — carries a notes line). On plan_exhausted, submit the collected answers to /cdri/compute for the full scored result.

A reserved_response_type completion (verbatim, ?response_type=boolean):

{
"phase": "complete",
"reason": "reserved_response_type",
"notes": ["response_type 'boolean' is reserved; no instrument currently emits it."],
"progress": { "domains_required": ["pain_load", "psychosocial_load", "function_load", "rtw_load"], "domains_covered": [], "instruments_complete": [], "instruments_partial": [], "plan_remaining_instruments": 0 },
"risk_snapshot": { "cdri_score": 0, "overall_magnitude": 0, "magnitudes": { "pain_dominant": 0, "psychosocial_dominant": 0, "fear_avoidance_dominant": 0, "rtw_barrier": 0, "multi_domain_catastrophic": 0, "iatrogenic_escalation": 0, "established_chronicity": 0 }, "confidence": { "overall": 0.2143, "tier": "low", "breakdown": { "input_completeness": 0, "imputation_variance": 0.5, "model_certainty": null } }, "matrix_version": "2026-06-02-first-pass" },
"catalog_version": "0.22.0",
"matrix_version": "2026-06-02-first-pass",
"request_id": "528c7c0a-5a04-4279-a971-13ca7d49633b",
"timestamp": "2026-07-12T23:12:16.334288+00:00"
}

phase: "error"200 OK

Returned with a 200 when the engine cannot produce a question — for example an answers entry references an instrument the engine never issued. The caller halts; it does not retry blindly.

{
"phase": "error",
"error_code": "unknown_instrument",
"message": "Unknown instrument: 'frobnitz'",
"catalog_version": "0.22.0",
"matrix_version": "2026-06-02-first-pass",
"request_id": "…",
"timestamp": "…"
}

error_code is a stable machine code (unknown_instrument, instrument_unavailable). Malformed JSON, a missing apikey, an invalid response_type, or a de-identification violation still produce the standard HTTP error envelopes (400/401), not a phase: "error" body.

Storing answers (the contract)

The engine is stateless — you store the answers and re-send them each turn, and you keep them long-term so a scored CDRI can be reproduced and audited. An answer is always the triple { instrument_id, item_id, value }. The value type is governed by the question's response_type, so store it accordingly:

response_typeStore value asRuleStatus
numberintegerWithin the item's min..max; no decimals.live
single_selectintegerThe chosen option's value (not its label).live
booleanbooleantrue / false.reserved
multi_selectarray of integersThe chosen option values.reserved
free_textstringFree text (still subject to de-identification).reserved

A minimal consumer-side storage shape (adapt to your database):

CREATE TABLE intake_answer (
correlation_token text NOT NULL, -- YOUR opaque subject/session key; the engine never sees it
instrument_id text NOT NULL, -- e.g. "pain_scale"
item_id text NOT NULL, -- e.g. "item_1"
value jsonb NOT NULL, -- typed per response_type above
response_type text NOT NULL, -- captured from the question, for replay validation
instrument_version text NOT NULL, -- provenance: which rubric version was answered
answered_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (correlation_token, instrument_id, item_id) -- last write wins per item
);

Capture response_type and instrument_version from the questions / batch you rendered. They are not required to re-submit, but they let you prove later that a stored answer was valid for the instrument version the subject actually saw, and detect when a rubric has since changed.

Replaying answers. To resume intake or to score, read your rows back into the triple list:

  • resume /intake/next: send them as the answers array.
  • score via /cdri/compute: reassemble into its instruments map — { instrument_id: { responses: { item_id: value } } }.

Because the engine is deterministic and de-identified, the same stored answers re-submitted later reproduce the same result (for a given catalog + matrix version), which is what makes stored answers audit-grade.

De-identification. Store only these structural values plus your own opaque correlation_token. Never store names, DOBs, MRNs, or other identifiers against these rows — the engine rejects them on the way in and they must not live in the answer store either.

Examples

curl — first call

curl -sS -X POST "https://engine.tripod-health.com/api/intake/next?limit=3" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "answers": [], "context": { "age_bracket": "45-54", "sex": "male", "occupation_class": "construction", "region": "OH" } }'

Python — the intake loop with live risk feedback

# Uses the call() helper from quickstart.md.
answers = []
context = {"age_bracket": "45-54", "sex": "male", "occupation_class": "construction", "region": "OH"}

while True:
turn = call("POST", "/api/intake/next?limit=3", {"answers": answers, "context": context})

render_risk_bars(turn["risk_snapshot"]) # your gamified UI: bars + confidence meter

if turn["phase"] == "complete":
break # or stop early when the clinician arrives
if turn["phase"] == "error":
raise RuntimeError(f"intake halted: {turn['error_code']} - {turn['message']}")

for q in turn["questions"]: # phase == "ask"
value = prompt_user(q) # store {instrument_id,item_id,value} on your side
answers.append({"instrument_id": q["instrument_id"], "item_id": q["item_id"], "value": value})

# On completion, score the stored answers via /api/cdri/compute.

Intended use

  • A patient- or clinician-facing intake flow (e.g. a virtual waiting room) that asks the most decision-relevant questions first and shows live risk feedback as the subject answers.
  • Collecting the answer set that a later /cdri/compute call will score, without hard-coding which instruments to administer.
  • Pacing questions (?limit) or scoping them (?domain / ?phenotype / ?response_type) to fit a specific screen or budget.

Mistaken use

  • Do not keep state on the engine's behalf and send only new answers — the endpoint is stateless; always send the full answers list.
  • Do not treat the risk_snapshot as a scored result — it is the deterministic floor over what's answered so far, with no AI phenotype refinement. The authoritative read is /cdri/compute.
  • Do not expect the snapshot to move on every answer — it moves when an instrument completes (the scoring cliff).
  • Do not treat phase: "error" as retryable — fix the offending answer and resubmit, or stop.
  • Do not filter on a reserved response_type and expect questions — you get a reserved_response_type completion, by design.

Status codes

CodeMeaningWhen
200OKAny of phase: "ask", "complete", or "error".
400ValidationErrorBody not valid JSON, or an invalid query param (limit, response_type).
400DeIdentificationViolationBody contained an identifier-shaped value.
401UnauthenticatedMissing or invalid API key.
413PayloadTooLargeBody exceeds 512 KB.
429RateLimitedPer-consumer rate limit exceeded.
503ServiceUnavailableA component is unhealthy.

Change history

DateChange
2026-06-03Initial publication.
2026-07-12Rebuilt on the value-of-information core: VOI-ordered item drip (?limit), ?domain/?phenotype/?response_type filters, phase: "complete" reasons, a per-turn deterministic risk_snapshot, and the answer-storage contract. Response reshaped (batch + risk_snapshot); no per-turn corpus write.