Reflection Questions
Status:
stableCategory:trinityassistAdded:2026-05-11Last reviewed:2026-07-01
Summary
Generate 3 to 10 typed reflection questions tailored to a clinical
case -- the longitudinal episode of care across one or more encounters.
Every question carries a required kind (yes_no, numeric_scale,
date, single_select, multi_select, or short_text) that tells the
consumer how to render its input and validate its answer. The engine
returns a relevant mix, not a flat list of free-text prompts.
Use this when the consumer's case has reached a natural close point --
"summarize what worked", "what would you do differently", "what stayed
uncertain" -- and the answers are about the case's full arc, not the
last visit alone. The signed answers are forwarded to
/api/case_reflection/ingest.
Endpoint
| Field | Value |
|---|---|
| Method | POST |
| Path | /api/case_reflection/questions |
| Full URL | https://engine.tripod-health.com/api/case_reflection/questions |
| Authentication | API key (required) |
| Request body | application/json |
| Response body | application/json |
| Idempotent | No (each call may return a new question mix) |
| Rate-limited | Yes |
Generated text passes the engine's outbound de-identification scan
before returning; identifier-shaped output fails the call with
502 UpstreamError rather than scrubbing.
Authentication
Standard API-key. See authentication.md.
Request
Headers
| Header | Required | Notes |
|---|---|---|
apikey | Yes | Your engine API key. |
Content-Type | Yes | application/json. |
X-Request-ID | No | Echoed back; lets you correlate logs. |
Body
{
"case_meta": {
"case_id": "cas-xyz789",
"case_type": "work_injury",
"case_subtype": null,
"encounters_count": 4
}
}
| Field | Type | Always present | Description |
|---|---|---|---|
case_meta | object | Yes | Wrapper. |
case_meta.case_id | string | Yes | Consumer-supplied opaque correlation token (1-64 chars). The engine treats it as opaque. |
case_meta.case_type | enum | Yes | One of non_injury_consult, work_injury, non_work_injury, prevention. |
case_meta.case_subtype | string | null | No | Optional free-form subtype (max 64 chars). Only meaningful when case_type='prevention'. |
case_meta.encounters_count | integer | Yes | Number of encounters under the case at call time. Allowed 0..1000. Frames "across N visits" language. |
The body MUST NOT contain identifier-shaped values. Inbound
de-identification is enforced on every request and rejects
identifier-shaped payloads with 400 DeIdentificationViolation. See
conventions.md.
Response
Success -- 200 OK
{
"questions": [
{
"question_id": "outcome_matched_expectation",
"prompt_text": "Did the clinical outcome across the case match what you expected when the injury first presented?",
"hint": "Consider the case as a whole, not any single visit.",
"kind": "yes_no"
},
{
"question_id": "confidence_in_care_plan",
"prompt_text": "How confident are you that the care plan you pursued was the right call for this case?",
"kind": "numeric_scale",
"scale": {
"min": 0,
"max": 10,
"min_label": "not at all confident",
"max_label": "completely confident"
}
},
{
"question_id": "recovery_trajectory_clarity",
"prompt_text": "How clear was the recovery trajectory to you across the encounters?",
"kind": "numeric_scale",
"scale": {
"min": 0,
"max": 10,
"min_label": "very unclear",
"max_label": "very clear"
}
},
{
"question_id": "primary_recovery_barrier",
"prompt_text": "What was the primary barrier to recovery on this case?",
"kind": "single_select",
"options": [
{ "value": "persistent_pain", "label": "Persistent pain" },
{ "value": "psychosocial", "label": "Psychosocial factors" },
{ "value": "access_to_care", "label": "Access to care" },
{ "value": "none", "label": "No significant barrier" }
]
},
{
"question_id": "case_summary_what_worked",
"prompt_text": "Anything else worth capturing about what worked on this case?",
"kind": "short_text"
}
],
"request_id": "req-xyz",
"timestamp": "2026-05-11T14:23:00.000Z"
}
The example above is a real production response for a work_injury case
spanning four encounters: a yes_no judgment, two numeric_scale
questions (the analytical backbone), a single_select, and one
short_text. The exact mix and the question_id values vary with the
metadata you submit and are regenerated on every call -- persist
whatever the engine returned and pair it verbatim with the clinician's
answers on the ingest call.
Response fields
| Field | Type | Always present | Description |
|---|---|---|---|
questions | array | Yes | Between 3 and 10 entries. |
questions[].question_id | string | Yes | Stable opaque key (lowercase snake_case, max 64 chars). Correlates the answer the clinician provides. |
questions[].prompt_text | string | Yes | The question text the clinician answers. Max 2000 chars. |
questions[].hint | string | No | Optional supplemental guidance the UI may render under the prompt. Max 500 chars; absent when the engine supplied none. |
questions[].kind | enum | Yes | One of yes_no, numeric_scale, date, single_select, multi_select, short_text. Drives input rendering and answer validation. |
questions[].scale | object | No | Present only on numeric_scale questions. Absent on every other kind. See scale. |
questions[].options | array | No | Present only on single_select and multi_select questions. Absent on every other kind. See options. |
scale
Present only when kind is numeric_scale. The range is always the
fixed 0-10 integer scale; the engine authors only the two end-anchor
labels in the case's language.
| Field | Type | Always present | Description |
|---|---|---|---|
scale.min | integer | Yes | Always 0. The low end of the scale. |
scale.max | integer | Yes | Always 10. The high end of the scale. |
scale.min_label | string | Yes | Human-readable label for 0 (for example "not at all confident"). Max 120 chars. |
scale.max_label | string | Yes | Human-readable label for 10 (for example "completely confident"). Max 120 chars. |
options
Present only when kind is single_select or multi_select. Always at
least two entries (2-6), each a value/label pair. The clinician's answer
references option values, never labels.
| Field | Type | Always present | Description |
|---|---|---|---|
options[].value | string | Yes | Stable machine token (lowercase snake_case, max 64 chars). This is what the answer carries. |
options[].label | string | Yes | Human display text (max 200 chars). |
The typed question mix
The engine returns a relevant mix rather than a fixed set. The yes_no
and numeric_scale kinds are the always-present backbone; the remaining
kinds appear only when the engine judges them relevant to the specific
case.
| Kind | Answer value type | How many | Notes |
|---|---|---|---|
yes_no | boolean | 1-3 (always ≥ 1) | A binary judgment. Carries no scale and no options. |
numeric_scale | integer 0..10 | 2-3 (always ≥ 2) | The analytical backbone. Fixed 0-10 scale; carries scale anchor labels. |
date | "YYYY-MM-DD" string | 0-1 | A single calendar date (for example an expected return-to-duty milestone). Included only when relevant. |
single_select | one option value string | 0-1 | Pick exactly one. Carries options (2-6). Included only when relevant. |
multi_select | array of option value strings | 0-1 | Select all that apply. Carries options (2-6). Included only when relevant. |
short_text | string | 0-1 | The one optional free-narrative question. Carries no scale/options. |
The total is always between 3 and 10 questions. Per-kind counts are guidance the engine follows, not a caller input, and are not returned in the response.
Status codes
| Code | Meaning | When |
|---|---|---|
200 | OK | 3-10 typed questions returned. |
400 | ValidationError | Body missing case_meta, an enum is unknown, encounters_count is out of range, or an unknown field is present. |
400 | DeIdentificationViolation | Body carries an identifier-shaped value. |
401 | Unauthenticated | Missing or invalid API key. |
413 | PayloadTooLarge | Body exceeds 512 KB. |
415 | UnsupportedMediaType | Content-Type is not application/json. |
429 | RateLimited | Per-consumer rate limit exceeded. |
500 | InternalError | Unexpected failure. |
502 | UpstreamError | Upstream model call failed, or generated text failed the outbound de-id pass. |
503 | ServiceUnavailable | Service temporarily unhealthy. |
504 | UpstreamTimeout | Upstream model call exceeded the engine's question-generation budget. |
All non-2xx responses use the uniform error envelope; see errors.md.
Examples
curl
curl -sS -X POST "https://engine.tripod-health.com/api/case_reflection/questions" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"case_meta": {
"case_id": "cas-xyz789",
"case_type": "work_injury",
"case_subtype": null,
"encounters_count": 4
}
}'
Python (requests)
# Uses the call() helper from quickstart.md.
result = call("POST", "/api/case_reflection/questions", {
"case_meta": {
"case_id": "cas-xyz789",
"case_type": "work_injury",
"case_subtype": None,
"encounters_count": 4,
},
})
for q in result["questions"]:
print(q["kind"], q["question_id"], "->", q["prompt_text"])
Node.js (native fetch)
// Uses the call() helper from quickstart.md.
const result = await call("POST", "/api/case_reflection/questions", {
case_meta: {
case_id: "cas-xyz789",
case_type: "work_injury",
case_subtype: null,
encounters_count: 4,
},
});
for (const q of result.questions) {
console.log(q.kind, q.question_id, "->", q.prompt_text);
}
Intended use
- Case-close reflection capture once all encounters under the case are finalized.
- Dispatch the input widget on each question's
kind-- a yes/no toggle, a 0-10 slider anchored byscale.min_label/scale.max_label, a date picker, a single- or multi-select fromoptions, or a free-text box -- and collect a typed answer per question. - Case-level retrospective when revising the case-handling process; the typed answers make the captured reflections comparable across cases.
Mistaken use
- Rendering every question as a free-text box. The
kindis required for a reason -- dispatch the input on it so the answer value matches the type the ingest endpoint validates. - Inventing a scale range other than 0-10, or reading
scale.min/scale.maxas anything but0and10. The scale is always fixed; only the anchor labels vary. - Treating questions as a clinical assessment. The engine's clinical
assessment surface is
/api/cdri/compute,/api/phenotype/classify, and/api/acuity/compute. - Sending raw patient identifiers in
case_id. Use an opaque consumer- side surrogate.
De-identification contract
This endpoint enforces both directions of the engine's de-id contract:
- Inbound: the request body is walked for identifier-shaped strings
and blocked field names. Failures return
400 DeIdentificationViolation. - Outbound: generated question text, hints, option labels, and anchor
labels are scanned for identifier shapes before return. A match fails
the call with
502 UpstreamError; the engine never silently scrubs.
See conventions.md.
Rate limits
Per-consumer limits are managed at the gateway. See rate-limits.md.
Related
POST /api/case_reflection/ingest-- forward the signed typed answers to the learning corpus.- conventions.md -- de-identification contract, request IDs, timestamp shape.
- errors.md -- error envelope and code list.
Change history
| Date | Change |
|---|---|
| 2026-05-11 | Initial publication. |
| 2026-07-01 | Renamed from /api/cases/outcome/questions to /api/case_reflection/questions; questions are now typed (kind per question, fixed 0-10 numeric_scale, single_select/multi_select options) and return a 3-10 mix instead of a flat 5-7 free-text set. |