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-10-11

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.

Each card is judged against its own text, which already carries what any directive asked for, so this call takes no directives and no assist_context. That keeps it small and fast enough to run every 30 seconds. It is the third member of the nudge family, with the nudge helper and suggest.

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​

A nudge card has three ways to leave the rail:

  • engine_resolved -- this endpoint. A recurring sweep over the transcript window decides an open nudge has been answered by the conversation.
  • user_resolved -- the clinician clears the card themselves.
  • system_resolved -- consumer-side logic (a co-pilot, an app heuristic) retires the card.

Only the first is the engine's business; the other two are entirely yours. A typical live-visit integration calls this every ~30 seconds, offset from suggest so the two never share a tick, with every open card and the same window it feeds suggest.

When a nudge counts as resolved​

The engine marks a card resolved when:

  • the clinician asked the question the nudge prompted, OR
  • the patient volunteered the information the nudge was trying to elicit, OR
  • the clinician did the thing the nudge suggested -- coached the self-examination, named the decision rule, said they were escalating.

For non-safety categories only, a card is also resolved when the conversation has clearly moved past the clinical context it applied to and acting on it later would no longer be useful.

Partial does not count. If a card asked for a 0-10 pain score and the patient said "it hurts a lot", the verdict is false.

:::warning Safety cards are never auto-resolved

Cards in these categories are only ever cleared by the clinician:

emergency-activation, escalation-indicated, red-flag-screening, patient-location-unconfirmed, otc-contraindication-check

This is absolute, and stronger than "the conversation moved on does not count." A safety card sent in active_nudges always comes back resolved: false, even when the window plainly answers it. Ask about neurovascular symptoms, get a clean denial, and the card still returns false.

That is deliberate. Asking once and getting a "no" is not the same as the concern being closed -- the patient in front of the clinician can deteriorate over the next twenty minutes, and the card is the reminder to look again. The asymmetry decides it: wrongly keeping a red-flag card costs a line of screen space, wrongly retiring one silently removes the only prompt to re-screen.

The rule is enforced in code, not requested of the model, so it holds regardless of what the underlying model returns. Do not build a client-side timeout that clears these either.

:::

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.

Body​

{
"encounter_id": "visit-2026-05-26-shoulder-01",
"active_nudges": [
{
"card_id": "01KSJTM039H6H6Z5PJQSDD2V30",
"category": "pain-characterization",
"body": "Ask about pain severity on a 0-10 scale."
},
{
"card_id": "01KSJTM03G3BQ299M77N8TQ017",
"category": "functional-limitation",
"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": "01KSJTM039H6H6Z5PJQSDD2V30",
"resolved": true,
"reason": "Patient reported pain severity: 5/10 while sitting, 8/10 during driving."
},
{
"card_id": "01KSJTM03G3BQ299M77N8TQ017",
"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"
}

Two open cards, both closed by a window where the clinician asked and the patient answered. Note that the verdicts come back keyed by the same card_id values the request sent -- matching them up is your job, and the engine will never invent an id you did not supply.

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_idstringYesEngine-assigned request ID.
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, or assist_context sent (not accepted here). 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.
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-characterization", "body": "Ask about pain severity on a 0-10 scale."},
{"card_id": "01KSJTM03G3BQ299M77N8TQ017", "category": "functional-limitation", "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-characterization",
"body": "Ask about pain severity on a 0-10 scale."},
{"card_id": "01KSJTM03G3BQ299M77N8TQ017", "category": "functional-limitation",
"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-characterization",
body: "Ask about pain severity on a 0-10 scale." },
{ card_id: "01KSJTM03G3BQ299M77N8TQ017", category: "functional-limitation",
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​

  • The recurring resolution sweep of a live in-call rail: every ~30 seconds, send every open card plus the current window and retire whatever comes back resolved: true.
  • One-shot sweeps over a recorded transcript window.
  • Eval harnesses driving the same resolution primitive a live integration uses.

Mistaken use​

  • Auto-clearing a safety card on a timer. The engine will never return resolved: true for one on the "conversation moved on" rule; a client-side timeout that clears it anyway defeats the entire point of the category.
  • Sending only some open cards. The endpoint returns one verdict per card you send and knows nothing about the ones you withheld; a card omitted from the sweep simply never gets a chance to close.
  • Calling this without also calling it again. A single sweep is nearly useless -- resolution is a cadence, not an event.
  • Treating an absent 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.
  • Re-sending a card you already retired locally. Harmless (the engine is stateless and will just re-evaluate it) but it costs a metered call's worth of prompt for a card nobody can see.
  • Sending raw patient identifiers outside transcript_window[].text.

Debugging​

  1. Capture the request_id from the response body, success or error (or, if the response has no body from the engine, 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 verdict whose reason fails the scan comes back resolved: false with a null reason; it does not fail the call, and the card stays open until a later sweep clears it.

See conventions.md.

Rate limits​

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

Change history​

DateChange
2026-10-11Breaking. assist_context is no longer accepted: each card is judged against its own text. Sending it returns 400. Corrected the documented outbound de-id behaviour: a failing reason downgrades that verdict to unresolved rather than failing the call.
2026-10-11Accepts assist_context (instructions and always-loaded references; no on-demand references).
2026-08-09Corrected the safety-category rule, which was understated. A safety card always returns resolved: false, including when the window plainly answers it -- not merely when the conversation has moved on. The rule is now enforced in engine code rather than requested of the model, so it holds regardless of model output. Behaviour change for any consumer that was seeing safety cards resolve.
2026-08-08Documented the safety-category rule: cards in the safety and escalation categories are never resolved by the conversation moving on, only by the clinician. No wire change.
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.
2026-10-14The engine assigns every request ID; an X-Request-ID request header is not used. The X-Request-ID response header is the gateway's trace ID for the request and does not match request_id; quote request_id to support.