Nudge Helper
Status:
stableCategory:trinityassistAdded:2026-10-11Last reviewed:2026-10-11
Summary
Read the patient's intake, the recent conversation and your index of care directives, and return a brief: what the visit is about right now, how the conversation compares with what the patient reported, whether the focus has shifted, and up to four of your directive documents that apply.
The brief is the input to suggest. The helper never
produces a nudge card itself. It is one member of a three-call family:
| Route | Job | Tier | Typical cadence |
|---|---|---|---|
POST /api/nudge/helper | Work out the visit's focus and pick the directives that apply. | fast | every 90 seconds |
POST /api/nudge/suggest | Write suggestions from the latest brief and the documents it named. | balanced | every 60 seconds |
POST /api/nudge/check_resolution | Decide which open cards the conversation has dealt with. | fast | every 30 seconds |
The helper runs less often than suggest on purpose: what a visit is about changes far more slowly than what is worth suggesting next, and suggest checks the brief against the transcript on every call, so a brief up to 90 seconds old cannot mislead it.
:::info Everything about your world travels in the request
The engine knows nothing about your clinicians, visit types or programme
rules. Your intake, your directive index and your instructions are the whole
picture the model gets. A rule such as "visits are by video" belongs in
assist_context.instructions.
:::
Endpoint
| Field | Value |
|---|---|
| Method | POST |
| Path | /api/nudge/helper |
| Full URL | https://engine.tripod-health.com/api/nudge/helper |
| Authentication | API key (required) |
| Request body | application/json |
| Response body | application/json |
| Idempotent | No (each call asks the model afresh) |
| Rate-limited | Yes |
Purpose
A live visit drifts. The patient wrote "hip" on the intake and is now describing a thigh injury; a second complaint comes up halfway through. Your care directives may run to over a hundred documents, and sending them all to every suggestion is slow, expensive and over the payload limit. The helper reads the conversation against the intake and your index, says what the visit is about, and names the few documents that matter, so suggest can carry just those.
A working loop:
- Once real conversation has started, call the helper. Its brief becomes the current brief.
- Call
suggestwith that brief, and with the bodies of the documents inbrief.directive_refsasassist_contextreferences loadedalways. - Call the helper again every ~90 seconds, sending the previous brief as
previous_brief. Each new brief replaces the last.
If a helper call fails or times out, keep suggesting without a brief.
Suggest works from the transcript alone and reports brief_check: "no_brief". A failed helper never needs to block the rail.
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. |
Body
{
"encounter_id": "visit-ankle-01",
"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",
"answers": [
{ "question": "What were you doing when it happened?", "answer": "Unloading boxes from a truck" },
{ "question": "When did it start?", "answer": "Two days ago, right away" }
]
},
"care_directives": [
{ "ref": "care:general-care-philosophy", "path": ["Practice-wide"], "title": "General care philosophy" },
{ "ref": "care:musculoskeletal/overview", "path": ["Musculoskeletal"], "title": "Musculoskeletal overview",
"summary": "How the practice approaches acute musculoskeletal injuries" },
{ "ref": "care:musculoskeletal/ankle", "path": ["Musculoskeletal", "Ankle"], "title": "Ankle",
"summary": "Sprain pathway, decision rules and escalation" },
{ "ref": "care:musculoskeletal/knee", "path": ["Musculoskeletal", "Knee"], "title": "Knee" },
{ "ref": "governing:escalation-protocol", "path": ["Governing documents"], "title": "Escalation protocol" }
],
"assist_context": {
"instructions": [
{ "layer": "global.boundaries", "text": "Visits are by video. Clinicians never examine hands-on." }
]
},
"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 }
]
}
How the evidence is weighed
- The patient's description is weighted heavily. It is treated as the strongest evidence of why they came, above all early in the visit.
- The picked body region is weighted moderately. Patients pick a near-enough region from a list (elbow for a hand injury), so a region that disagrees with the description or the conversation loses.
- The transcript is what is being discussed now. When the conversation is clearly about something, that is the focus.
Body fields
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
encounter_id | string | No | 1-64 chars | Optional opaque correlation key. Send the same value on suggest and check_resolution to group the calls in your own logs. |
transcript_window | array | Yes | 1-256 segments | Finalized transcript segments, the same shape as on suggest. |
intake | object | No | -- | What the patient reported before the visit. Every field is optional. |
intake.description | string | No | 1-4,000 chars | The patient's own words about what happened. Clinical free text; identifier spans are scrubbed, not rejected (see below). |
intake.body_region | string | No | 1-120 chars | The region the patient picked, as a free label (for example "hip / pelvis"). Not validated against any vocabulary. |
intake.answers | array | No | 0-40 entries | Other intake answers as question/answer pairs: mechanism, activity, equipment, onset. |
intake.answers[].question | string | Yes* | 1-200 chars | The question as asked. *Required when the enclosing object is present. |
intake.answers[].answer | string | Yes* | 1-1,000 chars | The patient's answer. Scrubbed like description. |
care_directives | array | No | 0-400 entries, refs unique | Your directive index. Never bodies. |
care_directives[].ref | string | Yes* | 1-120 chars, ^[A-Za-z0-9_.:\-/]+$ | Your id for the document. A ref the brief returns is always one of these. |
care_directives[].path | array of string | Yes* | 1-4 entries, each 1-120 chars | The folder trail, for example ["Musculoskeletal", "Ankle"]. |
care_directives[].title | string | Yes* | 1-200 chars | The document's title. |
care_directives[].summary | string | No | up to 300 chars | One line on what it covers. Helps the model choose. |
previous_brief | object | No | a brief | The last brief this call returned for the visit, sent back unchanged, so shift can say whether the focus moved. |
assist_context | object | No | see assist_context | Your persona and instructions. References are not accepted -- the helper picks documents from the index and never reads one. |
The rest of the body must not contain identifier-shaped values. See De-identification contract.
Response
Success -- 200 OK
{
"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 and the ankle region they picked."
},
"shift": { "shifted": false, "note": null },
"directive_refs": [
"care:musculoskeletal/ankle",
"care:musculoskeletal/overview",
"governing:escalation-protocol"
],
"notes": [
"Weight bearing immediately after the injury and now has not been established.",
"No pain score yet."
]
},
"request_id": "5b0f4c2e-7d1a-4f7e-9a51-2c6f0d8e3b94",
"timestamp": "2026-10-11T18:19:47.565951+00:00"
}
The brief
| Field | Type | Always present | Description |
|---|---|---|---|
brief.focus.summary | string | Yes | Up to 400 chars. What the visit is about right now. |
brief.focus.body_region | string | null | Yes | Up to 120 chars. The region actually involved, or null when none is (a mood or sleep complaint). |
brief.focus.complaint_kind | string | null | Yes | Up to 120 chars. The kind of complaint, or null when not yet clear. Never a diagnosis. |
brief.intake_alignment.status | enum | Yes | consistent | differs | unclear -- how the conversation compares with what the patient wrote and picked. unclear when there is no intake or too little conversation. |
brief.intake_alignment.note | string | Yes | Up to 300 chars. One line on the comparison. |
brief.shift.shifted | boolean | Yes | Whether the focus moved since previous_brief (or since the window began, with none). |
brief.shift.note | string | null | Yes | Up to 300 chars. What it moved from and to, or null. |
brief.directive_refs | array of string | Yes | 0-4 refs from your care_directives, most relevant first. Validated: a ref the model returns that is not in your index is dropped, never passed on. Empty when you sent no index. |
brief.notes | array of string | Yes | 0-3 pointers of up to 200 chars each for the suggestion pass: what to verify, what is missing. |
request_id | string | Yes | Engine-assigned request ID. |
timestamp | string | Yes | ISO 8601 UTC server timestamp at response emission. |
Pass brief to suggest exactly as you received it.
Status codes
| Code | Meaning | When |
|---|---|---|
200 | OK | Brief generated. |
400 | ValidationError | Body shape failure, a repeated care_directives[].ref, assist_context.references present, or an unknown field. field names the offender. |
400 | DeIdentificationViolation | An identifier-shaped value outside the scrubbed fields. |
401 | Unauthenticated | Missing or invalid API key. |
402 | PaymentRequired | Out of credits, or a key or account budget is exhausted. |
413 | PayloadTooLarge | Body exceeds 512 KB. |
429 | RateLimited | Per-consumer rate limit exceeded. |
502 | UpstreamError | The model call failed after one retry, or the brief's focus.summary or intake_alignment.note failed the outbound identifier scan. Carry on without a brief. |
503 | ServiceUnavailable | Service temporarily unhealthy. |
See errors.md for the full error code list and envelope.
Timeouts and cost
One fast-tier call, held to 10 seconds per attempt with one retry. A client timeout of 30 seconds is right. Priced as one call; see credits.md. Your index is cached with the engine's rules between calls in the same visit, so consecutive calls are cheaper to serve than the first.
Examples
curl
curl -sS -X POST "https://engine.tripod-health.com/api/nudge/helper" \
-H "apikey: $ENGINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"intake": {"description": "Rolled my right ankle stepping off the loading dock.", "body_region": "Lower leg / ankle"},
"care_directives": [
{"ref": "care:musculoskeletal/ankle", "path": ["Musculoskeletal", "Ankle"], "title": "Ankle"},
{"ref": "care:musculoskeletal/knee", "path": ["Musculoskeletal", "Knee"], "title": "Knee"}
],
"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/helper",
headers={"apikey": os.environ["ENGINE_API_KEY"]},
json={
"intake": {"description": "Rolled my right ankle stepping off the loading dock."},
"care_directives": [
{"ref": "care:musculoskeletal/ankle", "path": ["Musculoskeletal", "Ankle"], "title": "Ankle"},
],
"transcript_window": [
{"segment_id": "s1", "speaker": "patient",
"text": "My right ankle rolled underneath me on the dock.",
"started_at_ms": 0, "ended_at_ms": 4000},
],
},
timeout=30,
)
resp.raise_for_status()
brief = resp.json()["brief"]
print(brief["focus"]["summary"], brief["directive_refs"])
Intended use
- The slow half of a live-visit nudge loop: refresh the brief every ~90 seconds and hand it, with the named documents, to suggest.
- Choosing which of a large directive library is relevant to a conversation, without sending the library.
Mistaken use
- Waiting on the helper before suggesting. A failed or slow helper should never stop the rail; suggest runs without a brief.
- Sending document bodies in the index. The index carries refs, paths, titles and summaries only. Bodies go to suggest, and only for the refs the brief named.
- Treating
focusas a diagnosis. It describes what is being discussed; it concludes nothing clinically. - Editing the brief before passing it on. Suggest validates it again and checks it against the transcript; an edited brief only removes that signal.
De-identification contract
- Inbound, free text: identifier spans in
transcript_window[].text,intake.descriptionandintake.answers[].answerare SCRUBBED, not rejected -- patients describing an injury in their own words can quote a phone number. Everything downstream sees only the scrubbed text. - Inbound, everything else: the rest of the body, including the intake
questions, the body region and your index, is walked for identifier-shaped
strings and a match returns
400 DeIdentificationViolation. - Outbound: every prose field in the brief is scanned. A failing
body_region,complaint_kindorshift.notecomes backnull, a failing note is dropped, and a failingfocus.summaryorintake_alignment.notefails the call with502.
See conventions.md.
Related
POST /api/nudge/suggest-- writes suggestions from the brief.POST /api/nudge/check_resolution-- retires cards the conversation has dealt with.assist_context-- the persona and instructions object.
Change history
| Date | Change |
|---|---|
| 2026-10-11 | Initial publication. |
| 2026-10-14 | The 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. |