Lab Results

A lab panel with each value placed on the lab's reference range, plus trends from earlier tests.

Experience · Shares patterns with Data Table, Chart

≈ 1,594 tokens per use (tool 925 · guidance 365 · typical payload 304)How token costs are estimated

Example: Lab Results

Show my blood test results

Metabolic panel

3 within range · 1 above · 1 below

Northgate DiagnosticsOct 2, 2026
Filter markers

Electrolytes

  • SodiumNa

    139mmol/LWithin range
    Lab's range: 135–145 mmol/L.

Kidney

  • Creatinine

    84µmol/LWithin rangeincreaseUp 3 since Mar 12, 2026
    Lab's range: 60–110 µmol/L.
  • eGFR

    92mL/min/1.73m²Within range
    Lab's range: > 90 mL/min/1.73m².

Liver

  • Alanine aminotransferaseALT

    58U/LAbove rangeincreaseUp 14 since Mar 12, 2026
    Lab's range: 7–40 U/L.

Vitamins

  • Vitamin D, 25-hydroxy25-OH D

    42nmol/LBelow rangedecreaseDown 9 since Mar 12, 2026
    Lab's range: 75–250 nmol/L.

ClinicianWe'll go through these on Oct 14.

Source: Northgate patient portalNot medical advice.

Metabolic panel results, all markers
MarkerValueUnitLab's rangeStatus
Sodium (Na)139mmol/L135–145Within range
Creatinine84µmol/L60–110Within range
eGFR92mL/min/1.73m²> 90Within range
Alanine aminotransferase (ALT)58U/L7–40Above range
Vitamin D, 25-hydroxy (25-OH D)42nmol/L75–250Below range

When to use

  • The user asks to see lab results you've read from a report or health record: "Show my blood test results", "Is my vitamin D low?", "How has my cholesterol changed?"
  • The report gives a reference range for each marker, so values can be placed on it.

When not to use

  • You have no lab report or record. Never invent values or ranges.
  • The user asks what a result means for their health or what to do about it. Say you can't interpret results and suggest they discuss them with their clinician.
  • It's a reading the user tracks themselves, like weight or a blood pressure log. Use a Chart or a KPI Card.

How the agent should use it

  • Copy values, units, ranges, flags, history and clinicianNote from the report unchanged. They're all data-bound. Reference ranges come only from the lab report, never from general knowledge, because they differ between labs.
  • Set flag only when the lab printed one. Without it, the component works out the position from the range. A printed flag always wins.
  • Never diagnose, suggest causes or recommend treatment, in the component or in the text around it. Describe positions only: within, below or above the lab's range.
  • Set explain on the markers the user is likely to ask about. On explain, say in general terms what the marker measures (the Term Explainer fits), without interpreting the user's value.
  • Keep group names as printed on the report. Up to 30 markers; split a larger report by panel.
  • Lifecycle: none needed. A lab report is historical, so it doesn't go stale or expire. When a newer report replaces it in the conversation, mark the old call superseded.

Reading the results

  • Header. The panel, one quiet line with the date the sample was taken and the lab, and a count in words: "3 within range · 1 above · 1 below".
  • Spectrum bar. The track covers the lab's range plus 25% of its span on each side (never below 0 for non-negative ranges), so values just outside stay visible. The within zone is a soft fill and the lab's optimal band, when printed, a stronger one; the zones outside the range are the plain track. The low and high bounds are labeled under the track, so the zones never rely on shade alone. A dot with a vertical tick marks the value; beyond the padded track, an arrow at the edge shows which way it lies.
  • One-sided ranges. "< 5.2" runs from 0 to the bound; "> 90" shows the bound and as much again above it. A range with no numbers (say "Negative") is shown as text ("Range Negative"), and the status reads "See lab's range". Without any range the row says "No range given".
  • Status. An icon plus text ("Above range"), and the value is bold when it's outside. A lab's critical flag adds a "Critical" badge.
  • Optimal band. When the lab printed one, it's the stronger fill on the bar; whether the result is inside it is read to screen readers.
  • Trend. With history, the change since the last result ("↑ 14 since Mar") sits beside the value. The trend button (a chevron, named "Show trend") opens a line chart of every result with the range as a band.
  • Bottom. The clinician's note (verbatim, labeled "Clinician"), then one quiet line with the source and "Not medical advice." (always shown). Results are general information to discuss with a clinician; the card keeps that to the one short line.

Interaction

  • Filter. When some results are outside the range, All and Out of range chips filter the list. Group headings stay.
  • What is this? Each marker with explain gets a help icon button, named "What is this?", that sends { action: "explain", markerId } and leaves a short note under the results. The card never locks: it's a handoff, and the user may ask about several markers.

Accessibility

The bars and trend charts are hidden from screen readers. Each row states the value, unit and status in text, with the lab's range in hidden text; change arrows have hidden "Up 14 since Mar 12, 2026" text. A visually hidden table lists every marker with its value, unit, range and status, and each opened trend chart has its own hidden table of dates and values. Filter chips use aria-pressed, and trend toggles aria-expanded.

Installation

npx shadcn@latest add https://your-site.com/r/lab-results.json

Usage

app/api/chat/route.ts
import { tool } from "ai"
import { labResultsTool } from "@/components/genui/lab-results.schema"

const tools = {
  [labResultsTool.name]: tool({
    description: labResultsTool.description,
    inputSchema: labResultsTool.inputSchema,
  }),
}

Without any explain markers the card is display-only, so give the tool an execute that returns immediately. With them, the agent receives:

{ action: "explain", markerId: "ferritin" }

Render it through the Renderer under show_lab_results, or directly:

<LabResults
  {...part.input}
  status={part.state === "input-streaming" ? "streaming" : undefined}
  result={part.state === "output-available" ? part.output : undefined}
  onResult={(result) => {
    if (part.state !== "output-available") {
      addToolResult({ tool: "show_lab_results", toolCallId: part.toolCallId, output: result })
    } else {
      sendMessage({ text: `Lab results action: ${JSON.stringify(result)}` })
    }
  }}
/>

While streaming, the header shows first and the markers appear one by one. The marker still being written is held back until it's complete, so values never jump. The counts, filter, clinician's note and source wait for the finished payload.

With LangGraph

Push it from a graph node with ui.push({ name: "show_lab_results", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.

Restoring from history

Pass the stored result back as result, or an array when the user asked about several markers. The notes show under the results and onResult isn't called. The buttons stay available, since asking is a handoff.

<LabResults {...part.input} result={part.state === "output-available" ? part.output : undefined} />

Schema

PropTypeNotes
panelstringData-bound. e.g. "Lipid panel"
takenAtstringData-bound. When the sample was taken (ISO date)
lab{ name }?Data-bound.
markersMarker[]1–30, see below
clinicianNotestring?Data-bound. Shown verbatim, up to 600 characters
source{ label, url? }?Data-bound. Linked when the URL is http(s)

Marker

FieldTypeNotes
idstringReturned in the result
namestringe.g. "Potassium"
abbreviationstring?e.g. "K"
valuenumberData-bound.
unitstringData-bound. e.g. "mmol/L"
range{ low?, high?, optimalLow?, optimalHigh?, text? }Data-bound. As printed by the lab; text for ranges like "< 5.2" or "Negative"
flag"low" | "high" | "critical-low" | "critical-high"?Data-bound. As printed; computed from the range when absent
history{ date, value }[]?Data-bound. Up to 12 earlier results
groupstring?Section heading, e.g. "Electrolytes"
explainboolean?Adds "What is this?"

Host props: status (streaming or ready), onResult, result and className.

Source

On this page