Loading documentation

Access required

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

Sign out
Skip to main content

Check Nudges

Status: stable Category: trinityassist Added: 2026-05-26 Last 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

FieldValue
MethodPOST
Path/api/nudge/check_resolution
Full URLhttps://engine.tripod-health.com/api/nudge/check_resolution
AuthenticationAPI key (required)
Request bodyapplication/json
Response bodyapplication/json
IdempotentNo (each call asks the model afresh)
Rate-limitedYes

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 a resolve_nudge envelope on the WebSocket.
  • system_resolved -- consumer-side logic (a co-pilot, a clinician app heuristic) marks the card resolved; the consumer sends a resolve_nudge envelope.

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

HeaderRequiredNotes
apikeyYesYour engine API key.
Content-TypeYesapplication/json.
X-Request-IDNoOptional 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

FieldTypeRequiredConstraintsDescription
encounter_idstringNo1-64 charsOptional 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_nudgesarrayYes1-64 entriesCurrently-open nudges to evaluate. The endpoint returns one verdict per entry; the order is preserved.
active_nudges[].card_idstringYes1-32 charsThe 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[].categorystringYes1-64 chars, ^[a-z0-9-]+$Kebab-case slug for the nudge category.
active_nudges[].bodystringYes1-280 charsThe imperative copy the clinician saw. The model uses this to decide what would count as resolution.
transcript_windowarrayYes1-256 segmentsFinalized transcript segments to evaluate against. Same shape as the transcript_window on POST /api/nudge/suggest.
transcript_window[].segment_idstringYes1-256 charsConsumer-supplied id.
transcript_window[].speakerstringYes1-32 charsSpeaker label.
transcript_window[].textstringYes1-8,000 charsUtterance text.
transcript_window[].started_at_msintegerYes>=0Offset (ms) from session start.
transcript_window[].ended_at_msintegerYes>=0Offset (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

FieldTypeAlways presentDescription
resolutionsarrayYesOne entry per active_nudges input, in the same order.
resolutions[].card_idstringYesEcho of the input card_id.
resolutions[].resolvedbooleanYestrue when the model decided the transcript window addressed the nudge; false otherwise.
resolutions[].reasonstring | nullYesOne-line natural-language explanation. Non-null when resolved is true; typically null when resolved is false.
request_idstring | nullYesEcho of the inbound X-Request-ID if present, otherwise null.
timestampstringYesISO 8601 UTC server timestamp at response emission.

Status codes

CodeMeaningWhen
200OKVerdicts generated for every supplied card.
400ValidationErrorBody shape failure -- empty active_nudges, empty transcript_window, malformed entry, category not kebab-case, body too long. field names the offender.
400DeIdentificationViolationBody carries an identifier-shaped value.
401UnauthenticatedMissing or invalid API key.
413PayloadTooLargeBody exceeds 512 KB.
415UnsupportedMediaTypeContent-Type is not application/json.
422UnprocessableEntityA downstream Pydantic constraint rejected the body.
429RateLimitedPer-consumer rate limit exceeded.
500InternalErrorUnexpected failure.
502UpstreamErrorUpstream model call failed.
503ServiceUnavailableService 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 reason on a resolved: false row 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_nudge envelope on the WS for the same card. The streaming inferencer treats a card that is already engine-resolved as resolved; a follow-up resolve_nudge is 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

  1. Capture the request_id from the response (success or error) or the X-Request-ID response header.
  2. 400 ValidationError on active_nudges[N].category -- the regex ^[a-z0-9-]+$ enforces kebab-case. Reject anything with uppercase, underscores, or spaces on your side before sending.
  3. 400 ValidationError on transcript_window -- empty arrays are rejected. The minimum is one segment; the maximum is 256.
  4. 502 UpstreamError -- the model call failed. The reasons are the same as on POST /api/nudge/suggest.
  5. All verdicts come back false and you expected true -- the model is conservative about claiming resolution. Make sure the body you pass on each active_nudges entry 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 reason is scanned for identifier shapes; a match fails the call with 502 UpstreamError.

See conventions.md.

Rate limits

Per-consumer limits are enforced at the gateway. See rate-limits.md.

Change history

DateChange
2026-05-26Initial publication. Companion to the engine-owned nudge lifecycle that replaced the retired prompt registry.
2026-05-26Resource 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.