Check Nudges
Status:
stableCategory:trinityassistAdded:2026-05-26Last reviewed:2026-05-26
Summary
Given a list of currently-open clinical-decision nudges and a window
of finalized transcript segments, decide which nudges have been
satisfied by the conversation. Returns one verdict per supplied
card -- resolved: true plus a one-line reason when the
conversation addressed the nudge, resolved: false when it did not.
This is a stateless HTTP endpoint: the consumer supplies its currently open nudges plus a transcript window and gets back a per-nudge verdict. The consumer owns cadence -- a live-visit integration typically calls this on a recurring window to clear nudges the conversation has since addressed.
Endpoint
| Field | Value |
|---|---|
| Method | POST |
| Path | /api/nudge/check_resolution |
| Full URL | https://engine.tripod-health.com/api/nudge/check_resolution |
| Authentication | API key (required) |
| Request body | application/json |
| Response body | application/json |
| Idempotent | No (each call asks the model afresh) |
| Rate-limited | Yes |
Purpose
The streaming nudge lifecycle has three resolution paths:
engine_resolved-- the engine's own 60-second sweep over the transcript window decides an open nudge has been answered.user_resolved-- the clinician dismisses the card; the consumer sends aresolve_nudgeenvelope on the WebSocket.system_resolved-- consumer-side logic (a co-pilot, a clinician app heuristic) marks the card resolved; the consumer sends aresolve_nudgeenvelope.
This endpoint backs the engine_resolved path on the streaming
side. The HTTP surface exists for adjacent use cases:
- Running the engine's resolution logic synchronously over HTTP -- for non-streaming consumers, batch processing, or eval harnesses.
- Re-deciding resolution outside a live session (e.g. a recorded transcript review tool that wants to know which open nudges a later window would have closed).
Both paths produce identical verdicts for identical input.
Authentication
Standard API-key. Set apikey: $ENGINE_API_KEY on the request. See
authentication.md.
Request
Headers
| Header | Required | Notes |
|---|---|---|
apikey | Yes | Your engine API key. |
Content-Type | Yes | application/json. |
X-Request-ID | No | Optional client correlation ID. Echoed back in the response. |
Body
{
"encounter_id": "visit-2026-05-26-shoulder-01",
"active_nudges": [
{
"card_id": "01KSJTM039H6H6Z5PJQSDD2V30",
"category": "pain-assessment-missing",
"body": "Ask about pain severity on a 0-10 scale."
},
{
"card_id": "01KSJTM03G3BQ299M77N8TQ017",
"category": "functional-impact-missing",
"body": "Ask how the back pain is affecting daily activities, work, or sleep."
}
],
"transcript_window": [
{
"segment_id": "s10",
"speaker": "provider",
"text": "On a scale of zero to ten what is your pain right now?",
"started_at_ms": 0,
"ended_at_ms": 3000
},
{
"segment_id": "s11",
"speaker": "patient",
"text": "Right now sitting here it is about a five.",
"started_at_ms": 3000,
"ended_at_ms": 6000
},
{
"segment_id": "s12",
"speaker": "patient",
"text": "When I drive in this morning it was more like an eight.",
"started_at_ms": 6000,
"ended_at_ms": 10000
},
{
"segment_id": "s13",
"speaker": "provider",
"text": "Are you still able to do your normal activities, working out, sleeping?",
"started_at_ms": 10000,
"ended_at_ms": 14000
},
{
"segment_id": "s14",
"speaker": "patient",
"text": "I have been okay at work, just lighter duty since lifting is what flares it up.",
"started_at_ms": 14000,
"ended_at_ms": 19000
}
]
}
Body fields
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
encounter_id | string | No | 1-64 chars | Optional opaque correlation key. When you sent an encounter_id on the matching POST /api/nudge/suggest call, send the same value here so both calls stay grouped under one visit in your own logs. Treated as opaque by the engine. |
active_nudges | array | Yes | 1-64 entries | Currently-open nudges to evaluate. The endpoint returns one verdict per entry; the order is preserved. |
active_nudges[].card_id | string | Yes | 1-32 chars | The card_id you got back on POST /api/nudge/suggest (26-char ULID), or any non-empty consumer-supplied id when calling this endpoint standalone. |
active_nudges[].category | string | Yes | 1-64 chars, ^[a-z0-9-]+$ | Kebab-case slug for the nudge category. |
active_nudges[].body | string | Yes | 1-280 chars | The imperative copy the clinician saw. The model uses this to decide what would count as resolution. |
transcript_window | array | Yes | 1-256 segments | Finalized transcript segments to evaluate against. Same shape as the transcript_window on POST /api/nudge/suggest. |
transcript_window[].segment_id | string | Yes | 1-256 chars | Consumer-supplied id. |
transcript_window[].speaker | string | Yes | 1-32 chars | Speaker label. |
transcript_window[].text | string | Yes | 1-8,000 chars | Utterance text. |
transcript_window[].started_at_ms | integer | Yes | >=0 | Offset (ms) from session start. |
transcript_window[].ended_at_ms | integer | Yes | >=0 | Offset (ms) from session start. |
The body MUST NOT contain identifier-shaped values. See conventions.md.
Response
Success -- 200 OK
{
"resolutions": [
{
"card_id": "card-a1",
"resolved": true,
"reason": "Patient reported pain severity: 5/10 while sitting, 8/10 during driving."
},
{
"card_id": "card-a2",
"resolved": true,
"reason": "Patient described functional impact: modified work duties with lighter duty due to pain with lifting."
}
],
"request_id": "48700f18-afac-471a-a7da-0799bddfe408",
"timestamp": "2026-05-26T18:20:01.817595+00:00"
}
The example above is a real production response where two open
nudges -- a pain-severity gap and a functional-impact gap -- were
both resolved by a follow-up window where the provider asked and the
patient answered each question. The reason strings summarize what
in the transcript drove each verdict.
Response fields
| Field | Type | Always present | Description |
|---|---|---|---|
resolutions | array | Yes | One entry per active_nudges input, in the same order. |
resolutions[].card_id | string | Yes | Echo of the input card_id. |
resolutions[].resolved | boolean | Yes | true when the model decided the transcript window addressed the nudge; false otherwise. |
resolutions[].reason | string | null | Yes | One-line natural-language explanation. Non-null when resolved is true; typically null when resolved is false. |
request_id | string | null | Yes | Echo of the inbound X-Request-ID if present, otherwise null. |
timestamp | string | Yes | ISO 8601 UTC server timestamp at response emission. |
Status codes
| Code | Meaning | When |
|---|---|---|
200 | OK | Verdicts generated for every supplied card. |
400 | ValidationError | Body shape failure -- empty active_nudges, empty transcript_window, malformed entry, category not kebab-case, body too long. field names the offender. |
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. |
422 | UnprocessableEntity | A downstream Pydantic constraint rejected the body. |
429 | RateLimited | Per-consumer rate limit exceeded. |
500 | InternalError | Unexpected failure. |
502 | UpstreamError | Upstream model call failed. |
503 | ServiceUnavailable | Service temporarily unhealthy. |
Error envelope
{
"timestamp": "2026-05-26T18:20:18.187Z",
"error": "ValidationError",
"message": "active_nudges[0].category must match /^[a-z0-9-]+$/",
"field": "active_nudges[0].category",
"request_id": "3ac6869f-154d-47df-864f-294ce1632e6f"
}
See errors.md for the full error code list.
Examples
curl
curl -sS -X POST "https://engine.tripod-health.com/api/nudge/check_resolution" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"encounter_id": "visit-2026-05-26-shoulder-01",
"active_nudges": [
{"card_id": "01KSJTM039H6H6Z5PJQSDD2V30", "category": "pain-assessment-missing", "body": "Ask about pain severity on a 0-10 scale."},
{"card_id": "01KSJTM03G3BQ299M77N8TQ017", "category": "functional-impact-missing", "body": "Ask how the back pain is affecting daily activities, work, or sleep."}
],
"transcript_window": [
{"segment_id": "s10", "speaker": "provider", "text": "On a scale of zero to ten what is your pain right now?", "started_at_ms": 0, "ended_at_ms": 3000},
{"segment_id": "s11", "speaker": "patient", "text": "Right now sitting here it is about a five.", "started_at_ms": 3000, "ended_at_ms": 6000},
{"segment_id": "s12", "speaker": "patient", "text": "When I drive in this morning it was more like an eight.", "started_at_ms": 6000, "ended_at_ms": 10000},
{"segment_id": "s13", "speaker": "provider", "text": "Are you still able to do your normal activities, working out, sleeping?", "started_at_ms": 10000, "ended_at_ms": 14000},
{"segment_id": "s14", "speaker": "patient", "text": "I have been okay at work, just lighter duty since lifting is what flares it up.", "started_at_ms": 14000, "ended_at_ms": 19000}
]
}'
encounter_id is optional. Send the same value you sent on the
matching suggest call to keep both calls grouped under one visit.
Omit it for one-shot resolution checks.
Python (requests)
import os, requests
resp = requests.post(
"https://engine.tripod-health.com/api/nudge/check_resolution",
headers={"apikey": os.environ["ENGINE_API_KEY"]},
json={
"encounter_id": "visit-2026-05-26-shoulder-01",
"active_nudges": [
{"card_id": "01KSJTM039H6H6Z5PJQSDD2V30", "category": "pain-assessment-missing",
"body": "Ask about pain severity on a 0-10 scale."},
{"card_id": "01KSJTM03G3BQ299M77N8TQ017", "category": "functional-impact-missing",
"body": "Ask how the back pain is affecting daily activities, work, or sleep."},
],
"transcript_window": [
{"segment_id": "s10", "speaker": "provider",
"text": "On a scale of zero to ten what is your pain right now?",
"started_at_ms": 0, "ended_at_ms": 3000},
{"segment_id": "s11", "speaker": "patient",
"text": "Right now sitting here it is about a five.",
"started_at_ms": 3000, "ended_at_ms": 6000},
],
},
timeout=30,
)
resp.raise_for_status()
for r in resp.json()["resolutions"]:
state = "RESOLVED" if r["resolved"] else "open"
print(f"{r['card_id']} -> {state}: {r['reason']}")
Node.js (fetch)
const resp = await fetch(
"https://engine.tripod-health.com/api/nudge/check_resolution",
{
method: "POST",
headers: {
apikey: process.env.ENGINE_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
encounter_id: "visit-2026-05-26-shoulder-01",
active_nudges: [
{ card_id: "01KSJTM039H6H6Z5PJQSDD2V30", category: "pain-assessment-missing",
body: "Ask about pain severity on a 0-10 scale." },
{ card_id: "01KSJTM03G3BQ299M77N8TQ017", category: "functional-impact-missing",
body: "Ask how the back pain is affecting daily activities, work, or sleep." },
],
transcript_window: [
{ segment_id: "s10", speaker: "provider",
text: "On a scale of zero to ten what is your pain right now?",
started_at_ms: 0, ended_at_ms: 3000 },
{ segment_id: "s11", speaker: "patient",
text: "Right now sitting here it is about a five.",
started_at_ms: 3000, ended_at_ms: 6000 },
],
}),
},
);
if (!resp.ok) throw new Error(`Engine ${resp.status}: ${await resp.text()}`);
for (const r of (await resp.json()).resolutions) {
console.log(`${r.card_id} -> ${r.resolved ? "RESOLVED" : "open"}: ${r.reason}`);
}
Intended use
- One-shot resolution sweeps over a transcript window outside a live WebSocket session.
- Eval harnesses that need to drive the same resolution primitive the streaming inferencer uses.
- Consumer-side logic that wants to ask the engine "did this window resolve any of my open cards?" before deciding whether to dismiss them on its UI.
Mistaken use
- Polling this endpoint inside an open WS session alongside the engine's own resolution sweep. The streaming inferencer already calls this primitive every 60 seconds when there are open nudges; adding a parallel HTTP poll doubles the LLM cost without producing different verdicts.
- Treating an empty
reasonon aresolved: falserow as an error. The model omits a reason when nothing in the window addressed the nudge -- that is the expected shape. - Resolving a card on the consumer side based on this endpoint's
verdict and also sending a
resolve_nudgeenvelope on the WS for the same card. The streaming inferencer treats a card that is already engine-resolved as resolved; a follow-upresolve_nudgeis a silent no-op (idempotent) but the corpus row will reflect the first resolution, not the second. - Sending raw patient identifiers in segment text or nudge body.
Debugging
- Capture the
request_idfrom the response (success or error) or theX-Request-IDresponse header. 400 ValidationErroronactive_nudges[N].category-- the regex^[a-z0-9-]+$enforces kebab-case. Reject anything with uppercase, underscores, or spaces on your side before sending.400 ValidationErrorontranscript_window-- empty arrays are rejected. The minimum is one segment; the maximum is 256.502 UpstreamError-- the model call failed. The reasons are the same as onPOST /api/nudge/suggest.- All verdicts come back
falseand you expectedtrue-- the model is conservative about claiming resolution. Make sure thebodyyou pass on eachactive_nudgesentry is the same body the model originally generated (or a faithful representation of it); paraphrasing the body changes what the model treats as resolution criteria.
De-identification contract
- Inbound: the request body is walked for identifier-shaped
strings; failures return
400 DeIdentificationViolation. - Outbound: every generated
reasonis scanned for identifier shapes; a match fails the call with502 UpstreamError.
See conventions.md.
Rate limits
Per-consumer limits are enforced at the gateway. See rate-limits.md.
Related
POST /api/nudge/suggest-- companion endpoint that proposes new nudges for a transcript window.- conventions.md -- de-identification contract, request IDs, timestamp shape.
- errors.md -- error envelope and code list.
Change history
| Date | Change |
|---|---|
| 2026-05-26 | Initial publication. Companion to the engine-owned nudge lifecycle that replaced the retired prompt registry. |
| 2026-05-26 | Resource version bumped to 0.3.0. Request body gains an optional encounter_id (max 64 chars) so callers chaining suggest -> check_resolution can keep both calls grouped under the same correlation key in their own logs. Non-breaking. |