Loading documentation

Access required

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

Sign out
Skip to main content

Suggest Nudges

Status: stable Category: trinityassist Added: 2026-05-26 Last reviewed: 2026-10-11

Summary​

Inspect a window of finalized transcript segments and return zero or more clinical-decision nudge candidates for the reader you describe. Each candidate carries a category from a closed vocabulary, a short imperative body, a severity, and the segment ids the model cited as the trigger. Empty arrays are common and correct -- most short windows do not warrant a nudge.

Suggest is the middle member of a three-call family. It works from the latest brief from the nudge helper and the bodies of the documents that brief named, checks for itself whether the conversation has moved on since the brief was written, and says so in brief_check. Pair it with check_resolution to retire satisfied cards.

Nudge bodies are AI-generated within engine guardrails. There is no fixed prompt registry. Categories, by contrast, ARE fixed: see Nudge categories.

:::caution The engine is stateless -- send active_nudges

The engine holds nothing between calls. If you poll this endpoint on a cadence over an overlapping transcript window and do not send active_nudges, you will be handed the same gap again on every cycle with a new card_id each time, and your rail will fill with duplicates.

active_nudges is the dedup key. Send every card currently on the clinician's screen on every call.

:::

Endpoint​

FieldValue
MethodPOST
Path/api/nudge/suggest
Full URLhttps://engine.tripod-health.com/api/nudge/suggest
AuthenticationAPI key (required)
Request bodyapplication/json
Response bodyapplication/json
IdempotentNo (each call may produce a fresh candidate set)
Rate-limitedYes

Generated text passes the engine's outbound de-identification scan before returning. A candidate whose body fails that scan is dropped from the response rather than scrubbed or returned. See De-identification contract.

Purpose​

The consumer drives this endpoint over the course of a clinical encounter -- typically on a recurring window during a live visit, but equally for a one-shot pass over a recorded transcript.

A working live-visit loop:

  1. Buffer finalized transcript segments as they arrive.
  2. Every ~90 seconds, call the nudge helper and keep its brief as the current brief.
  3. Every ~60 seconds, POST the most recent window here with every open card, your audience, the current brief, and the bodies of the documents in brief.directive_refs as assist_context references loaded always. Add the returned candidates to the rail.
  4. Every ~30 seconds, offset from suggest, POST the window and the open cards to check_resolution and retire the cards it marks resolved.
  5. Retire a card immediately when the clinician dismisses it, without waiting for a sweep.

If the helper has not run or failed, call suggest without a brief. It works from the transcript and intake alone and reports brief_check: "no_brief". All three endpoints are billed per call, so cost scales with visit length and cadence; see credits.md.

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-ankle-01",
"audience": {
"label": "Athletic trainer",
"scope": "May give first aid, coach self-examination and set expectations. May not order imaging, prescribe or set work restrictions; routes those to the physician of record.",
"categories": [
"mechanism-of-injury", "pain-characterization", "onset-timeline", "job-demands",
"virtual-exam-opportunity", "clinical-decision-rule", "scope-boundary", "follow-up-plan"
]
},
"intake": {
"description": "I stepped off the edge of the loading dock wrong and my right ankle rolled underneath me. It swelled up fast.",
"body_region": "Lower leg / ankle"
},
"visit_context": {
"visit_type": "intake",
"encounter_purpose": "comprehensive",
"days_since_injury": 2,
"prior_encounter_count": 0
},
"brief": {
"focus": {
"summary": "Acute right ankle inversion injury two days ago, with fast swelling and painful weight bearing.",
"body_region": "right ankle",
"complaint_kind": "acute sprain"
},
"intake_alignment": { "status": "consistent", "note": "The conversation matches the patient's description." },
"shift": { "shifted": false, "note": null },
"directive_refs": ["care:musculoskeletal/ankle"],
"notes": ["Weight bearing immediately after the injury and now has not been established."]
},
"active_nudges": [
{
"card_id": "01KSJTM039H6H6Z5PJQSDD2V30",
"category": "mechanism-of-injury",
"body": "Ask what position the ankle was in when it rolled, and whether the patient felt a pop."
}
],
"assist_context": {
"precedence": ["global.boundaries", "practice.care_directives"],
"instructions": [
{ "layer": "global.boundaries",
"text": "Visits are by video and are workers' compensation visits. Clinicians never examine hands-on." }
],
"references": [
{ "ref": "care:musculoskeletal/ankle", "layer": "practice.care_directives", "title": "Ankle",
"load": "always",
"body": "Assess weight bearing immediately after the injury and at the visit. Prompt the Ottawa Ankle Rules before any imaging discussion. Coach self-palpation of the posterior edge and tip of both malleoli. Escalate when the patient cannot bear weight for four steps." }
]
},
"transcript_window": [
{ "segment_id": "s1", "speaker": "provider", "text": "Tell me what happened.",
"started_at_ms": 0, "ended_at_ms": 2400 },
{ "segment_id": "s2", "speaker": "patient",
"text": "I stepped off the edge of the loading dock wrong and my right ankle rolled underneath me.",
"started_at_ms": 2400, "ended_at_ms": 9100 },
{ "segment_id": "s3", "speaker": "patient",
"text": "It swelled up pretty fast. I can hobble on it but it hurts a lot to put weight on that side.",
"started_at_ms": 9100, "ended_at_ms": 15600 }
]
}

Describing your reader​

audience is required and has no default: the prompt is written for this reader, and a default would silently write advice in one reader's scope for another. Describe them yourself:

  • label -- what they are called.
  • scope -- what they may act on and who they route to. It is placed in the prompt verbatim as the boundary of what a nudge may ask of them.
  • categories -- the categories they may receive. The safety categories are added for every audience whether listed or not, and a candidate outside the rest is dropped.

The engine assumes nothing else about your setting. If your visits are remote, or are workers' compensation visits, say so in your scope or your assist_context.instructions.

The brief and your directives​

When you send a brief, the model reads it, checks it against the transcript, and reports brief_check: consistent when the conversation still matches the brief's focus, shifted when it has moved on (it then works from the transcript and ignores directives about the old focus). Your instructions and loaded references are followed where they apply to what is being discussed.

Send the documents the brief named as references with load: "always" and their full body -- at most four travel, so a call stays well inside the payload limit. On-demand references are not accepted: the engine runs no read loop, and the helper has already chosen. The documents stay byte-identical between calls while the brief names the same refs, so they sit inside the cached part of the prompt.

Body fields​

FieldTypeRequiredConstraintsDescription
encounter_idstringNo1-64 charsOptional opaque correlation key. Send the same value on helper and check_resolution to keep the calls grouped in your own logs.
audienceobjectYes--Who reads the rail. See Describing your reader.
audience.labelstringYes1-80 charsWhat the reader is called.
audience.scopestringYes1-1,500 charsWhat they may act on and who they route to. Rendered verbatim.
audience.categoriesarrayYes1-64 entries, each a value from Nudge categoriesCategories they may receive. An unknown value is a 400. Safety categories are always eligible.
transcript_windowarrayYes1-256 segmentsFinalized transcript segments to inspect.
transcript_window[].segment_idstringYes1-256 charsConsumer-supplied id. Treated as opaque by the engine; a <session_id>:<seq_no> shape makes a returned triggered_by_segment_ids traceable.
transcript_window[].speakerstringYes1-32 charsAny speaker label your transcription source emits.
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.
active_nudgesarrayNo0-64 items, default []The cards currently on the clinician's screen. Effectively required for cadence callers -- see the caution above.
active_nudges[].card_idstringYes*1-32 charsThe card_id the engine issued. *Required when the enclosing object is present.
active_nudges[].categorystringYes*^[a-z0-9-]+$, max 64 charsThe card's category.
active_nudges[].bodystringYes*1-280 charsThe body the clinician saw.
intakeobjectNosame shape as on helperdescription (1-4,000), body_region (1-120), answers (0-40 pairs of question 1-200 / answer 1-1,000).
visit_contextobjectNo--Structural facts about the visit. Every field optional.
visit_context.visit_typeenumNointake | follow_upWhether this is the first visit for the complaint.
visit_context.encounter_purposeenumNocomprehensive | standardDepth of the encounter.
visit_context.days_since_injuryintegerNo0-25,000Days ELAPSED since the injury. A count, never a date.
visit_context.prior_encounter_countintegerNo0-1,000Prior encounters on this case, excluding the current one.
briefobjectNoa briefThe helper's latest brief, unchanged. At most 4 directive_refs and 3 notes.
assist_contextobjectNosee assist_contextYour persona, instructions, and references loaded always. On-demand references are rejected.

Outside the scrubbed free text (see below) the body must not contain identifier-shaped values -- names, dates of birth, SSN-shaped values, phone numbers, email addresses, MRNs.

Nudge categories​

category is a closed vocabulary. Every returned card carries exactly one of these values, which is what makes deduplication and cross-encounter analytics tractable.

GroupCategories
Safety and escalationemergency-activation, escalation-indicated, red-flag-screening, patient-location-unconfirmed
Subjective gapsmechanism-of-injury, work-relatedness, pain-characterization, onset-timeline, prior-history, functional-limitation, job-demands, secondary-complaint, psychosocial-context
Objective gapsvirtual-exam-opportunity, bilateral-comparison, neurovascular-screen, clinical-decision-rule, exam-source-unclear
Assessment and plandifferential-consideration, otc-contraindication-check, scope-boundary, patient-education-gap, follow-up-plan, return-to-work-expectation
Chartingdocumentation-gap
Non-clinicalcare-coordination, outside-care-history, outside-imaging-or-prescription, appointment-logistics, patient-rapport, language-or-access-barrier, transportation-or-attendance-barrier, consent-or-authorization-gap

The safety categories are the safety and escalation group plus otc-contraindication-check. They are eligible for every audience, and check_resolution never retires one -- only the clinician clears a safety card.

This vocabulary can gain entries in a future release. Treat an unrecognized category as renderable-but-uncategorized rather than an error.

Response​

Success -- 200 OK​

{
"candidates": [
{
"card_id": "01KSJTM03G3BQ299M77N8TQ017",
"category": "clinical-decision-rule",
"body": "Consider the Ottawa Ankle Rules -- ask whether the patient could bear weight immediately after and now, and coach self-palpation of both malleoli.",
"severity": "suggestion",
"triggered_by_segment_ids": ["s2", "s3"]
},
{
"card_id": "01KSJTM03P4XZ9K2W8RB5NC0AF",
"category": "pain-characterization",
"body": "Ask the patient to rate the ankle pain 0-10 now, at its best, and at its worst.",
"severity": "suggestion",
"triggered_by_segment_ids": ["s3"]
}
],
"brief_check": "consistent",
"request_id": "0926afd2-e6a7-4303-841b-667775135b87",
"timestamp": "2026-10-11T18:20:47.565951+00:00"
}

mechanism-of-injury was already open in active_nudges, so it was not proposed again even though s2 is nothing but mechanism detail. The Ottawa card follows the loaded ankle directive.

Generation is not deterministic. The same window will not always produce the same cards. Treat the categories as a sample of what the window supports, and never assert on an exact card set in your own tests.

Three candidates is the per-call ceiling, and at most two open cards may share a category. An empty candidates array is common and is not an error; it always comes with a silence_reason.

Response fields​

FieldTypeAlways presentDescription
candidatesarrayYesZero or more candidate nudges.
candidates[].card_idstringYesEngine-issued 26-char ULID, unique per candidate. Echo it back on check_resolution.
candidates[].categoryenumYesOne of Nudge categories, and always one your audience may receive.
candidates[].bodystringYesShort imperative copy, max 280 chars. Never markup.
candidates[].severityenumYesinfo | suggestion | reminder.
candidates[].triggered_by_segment_idsarrayYesThe request segment_id values the model cited. May be empty (rare).
silence_reasonenumOnly when candidates is emptynothing_to_flag (substantive conversation, no gap), all_covered_by_open_cards, or conversation_too_thin.
silence_notestringNoOne diagnostic sentence, only alongside silence_reason. Never advice.
brief_checkenumYesconsistent | shifted when you sent a brief; no_brief when you did not. A brief the model did not judge is reported as shifted, never as consistent.
request_idstringYesEngine-assigned request ID.
timestampstringYesISO 8601 UTC server timestamp at response emission.

Status codes​

CodeMeaningWhen
200OKCandidates generated (zero or more).
400ValidationErrorBody shape failure -- a missing audience, an audience.categories value outside the vocabulary, an on-demand reference, an unknown field, a malformed brief. field names the offender.
400DeIdentificationViolationAn identifier-shaped value outside the scrubbed fields.
401UnauthenticatedMissing or invalid API key.
402PaymentRequiredOut of credits, or a key or account budget is exhausted.
413PayloadTooLargeBody exceeds 512 KB.
429RateLimitedPer-consumer rate limit exceeded.
502UpstreamErrorUpstream model call failed after one retry. A candidate failing the outbound de-id pass does NOT produce a 502 -- it is dropped.
503ServiceUnavailableService temporarily unhealthy.

See errors.md for the full error code list and envelope.

Timeouts and cost​

One balanced-tier pass, held to 20 seconds per attempt with one retry, so at most about 40 seconds. A client timeout of about 65 seconds is right. The stable part of the prompt (rules, persona, instructions, the brief's documents) is cached between suggestions in the same visit, so consecutive calls are cheaper to serve than the first. See credits.md.

Examples​

curl​

curl -sS -X POST "https://engine.tripod-health.com/api/nudge/suggest" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"audience": {
"label": "Physician",
"scope": "Treating physician of record. May order imaging, prescribe and set work restrictions.",
"categories": ["pain-characterization", "clinical-decision-rule", "return-to-work-expectation"]
},
"transcript_window": [
{"segment_id": "s1", "speaker": "provider", "text": "Tell me what happened.", "started_at_ms": 0, "ended_at_ms": 2400},
{"segment_id": "s2", "speaker": "patient", "text": "My right ankle rolled underneath me on the dock.", "started_at_ms": 2400, "ended_at_ms": 9100}
]
}'

Python (requests)​

import os, requests

resp = requests.post(
"https://engine.tripod-health.com/api/nudge/suggest",
headers={"apikey": os.environ["ENGINE_API_KEY"]},
json={
"audience": {
"label": "Physician",
"scope": "Treating physician of record. May order imaging, prescribe and set work restrictions.",
"categories": ["pain-characterization", "clinical-decision-rule"],
},
"brief": current_brief, # from /api/nudge/helper, or omit
"active_nudges": open_cards,
"transcript_window": window,
},
timeout=65,
)
resp.raise_for_status()
body = resp.json()
for c in body["candidates"]:
print(f"[{c['severity']}] {c['card_id']} {c['category']}: {c['body']}")
print("brief_check:", body["brief_check"])

Intended use​

  • Driving a live in-call advisory rail, with the helper's brief deciding which of your directives travel.
  • One-shot passes over a recorded transcript window, with or without a brief.
  • Evaluation harnesses comparing prompts, audiences or directive sets.

Mistaken use​

  • Calling on a cadence without active_nudges. The engine cannot know what it already told you; every cycle re-proposes the same gaps under fresh card_ids.
  • Sending your whole directive library. Send the documents the brief named, loaded always. The helper exists so you do not have to send the rest.
  • Putting a reader's permissions anywhere but audience.scope. The scope is what bounds every card. Rules about the setting (remote visits, programme rules) belong in assist_context.instructions.
  • Treating a safety card as something the engine will retire on its own. It will not -- see check_resolution.
  • Sending an incident DATE in visit_context.days_since_injury. The field is an elapsed day count; a date is an identifier.
  • Persisting nudge bodies as clinical documentation. They are prompts to a clinician, not findings.

Debugging​

  1. Capture the request_id from the response body (or, if the response has no body from the engine, the X-Request-ID response header).
  2. Empty candidates -- read silence_reason. conversation_too_thin early in a visit is normal.
  3. 400 ValidationError on audience.categories[N] -- the value is not an engine category. Check it against Nudge categories.
  4. 400 ValidationError on assist_context.references[N].load -- on-demand references are not accepted here; send the document always with its body.
  5. brief_check is shifted every time -- the brief is older than the conversation. Refresh it from the helper more often, or check that you are sending the latest one.
  6. 502 UpstreamError -- retry on the next cadence tick rather than immediately.
  7. The same nudge keeps reappearing under new card_ids -- you are not sending active_nudges, or you are dropping dismissed cards from it while they are still on screen.

De-identification contract​

  • Inbound, free text: identifier spans inside transcript_window[].text, intake.description and intake.answers[].answer are SCRUBBED, not rejected. Everything downstream, including anything the engine persists, sees only the scrubbed text.
  • Inbound, everything else: the rest of the body is walked for identifier-shaped strings and a match returns 400 DeIdentificationViolation. Your instructions and references are scrubbed of identifier spans too; see assist_context.
  • Outbound: every generated body is scanned for identifier shapes. A matching candidate is DROPPED and logged; it does not fail the call.

See conventions.md.

Rate limits​

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

Change history​

DateChange
2026-10-11Breaking. audience is now an object you describe (label, scope, categories) instead of a fixed token; candidates outside its categories (safety excepted) are dropped. New optional intake and brief, and the response always carries brief_check. visit_context keeps only visit_type, encounter_purpose, days_since_injury and prior_encounter_count. One pass: on-demand references and the engine read loop are gone, and references must be loaded always. The engine no longer assumes a workers' compensation or telehealth setting.
2026-10-11Accepts assist_context, including on-demand references the engine reads for the model (at most two per call). Generation moved to the balanced capability tier; allow up to 45 seconds per call.
2026-08-09Replaced the worked request and response with a real production exchange. Documented that generation is not deterministic and that an empty candidates array is normal for a short window. Suggested cadence in the live-visit loop corrected from ~3 minutes to ~90 seconds. No wire change.
2026-08-08Added active_nudges and visit_context to the request. category became a closed vocabulary. The engine now refuses to re-propose a category present in active_nudges. Corrected the documented outbound de-id behaviour: a failing candidate is dropped, not a 502.
2026-05-26Initial publication. Replaces the retired GET /api/nudge/prompt registry lookup. Nudge bodies are now AI-generated within engine guardrails instead of resolved from a fixed prompt catalog.
2026-05-26Resource version bumped to 0.3.0. Response now carries an engine-issued card_id (26-char ULID) on every candidate; echo it back as the card_id on POST /api/nudge/check_resolution to evaluate resolution later. Request body gains an optional encounter_id (max 64 chars) to correlate suggest -> check_resolution calls in your own logs. Both additions are 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.