# Building Patient Timelines

> Assemble a chronological patient timeline from OpenMed-extracted clinical events, normalizing dates and resolving relative time expressions on-device. Use when the user wants to build a patient timeline, order events from clinical notes, reconstruct a longitudinal history, plot a course of illness, or turn analyze_text/deidentify output into a sorted sequence of dated encounters, diagnoses, medications, and procedures. Covers temporal normalization (absolute and relative), event modeling toward FHIR Encounter/Condition.onsetDateTime, anchoring to a document/admission date, and handling undated or ambiguous events. Consumes OpenMed analyze_text entities plus clinical temporality (resolving-clinical-context); produces a sorted event list ready for charting or FHIR export.

- Skill: `maziyarpanahi/building-patient-timelines` (Agent Skill)
- Install (CLI): `npx skillmds@latest add maziyarpanahi/building-patient-timelines`
- Raw SKILL.md: https://api.skillmd.com/api/skills/maziyarpanahi/building-patient-timelines/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: Apache-2.0
- Author: maziyarpanahi (https://skillmd.com/u/maziyarpanahi)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/maziyarpanahi/building-patient-timelines

---


# Building patient timelines

A patient timeline is a chronologically ordered list of clinical events —
diagnoses, medications, procedures, encounters — each carrying a normalized
date. OpenMed gives you the **events** (via `analyze_text`) and the **clinical
temporality** of each mention (current vs. historical, see
`resolving-clinical-context`); this skill turns those into a sorted timeline.
Everything runs **on-device** — de-identify first if the source notes contain
PHI, and keep raw identifiers out of logs.

## When to use this skill

After you have extracted entities from one or more notes and want them ordered
in time: a longitudinal history, a "course of illness" view, a feed for a
summary card, or a pre-step before FHIR export. If you only need to *extract*
entities, use `extracting-clinical-entities`. If you need negation/temporality
on a single mention, use `resolving-clinical-context`.

## Quick start

```python
import datetime as dt
import openmed

note = (
    "Discharge summary, 2024-03-12. Patient admitted 2024-03-08 with chest pain. "
    "History of type 2 diabetes diagnosed in 2019. Started on metformin two days "
    "after admission. Cardiac catheterization performed yesterday."
)

# 1) Extract clinical events (entities carry char offsets: start/end)
result = openmed.analyze_text(note, output_format="dict")
events = result["entities"]   # each: {text, label, confidence, start, end}

# 2) Normalize the temporal frame: an explicit document/anchor date drives
#    resolution of relative expressions ("two days after", "yesterday").
anchor = dt.date(2024, 3, 12)  # parsed from the note header or document metadata
```

`analyze_text` returns `{text, entities, model_name, timestamp, ...}`; each
entity is `{text, label, confidence, start, end}`. Use `start`/`end` to locate
each event in the source and to find the nearest date expression.

## Workflow

1. **De-identify if needed.** If notes carry PHI, run `openmed.deidentify(...)`
   first, or keep the timeline keyed by stable internal IDs — never log raw
   names/MRNs.
2. **Extract events.** `openmed.analyze_text(note)` for conditions, drugs,
   procedures; pick the model that matches your target entities
   (`choosing-openmed-models`).
3. **Resolve temporality.** For each event, use `resolving-clinical-context`
   to tag it `current` / `historical` / `hypothetical` and to drop negated or
   family-history mentions that should not appear on the patient's own line.
4. **Normalize dates.** Map each event to a date:
   - **Absolute** (`2024-03-08`, `March 2019`) → parse directly. Record the
     granularity (day / month / year) — a year-only event sorts to a coarse
     bucket, not a fake `Jan 1`.
   - **Relative** (`two days after admission`, `yesterday`, `on POD 2`) →
     resolve against an **anchor**: the document date, admission date, or a
     prior event's date. Without an anchor, relative expressions are
     unresolvable — flag them, don't guess.
5. **Build event records.** One record per event: `(date, granularity, label,
   surface_text, char_span, temporality, confidence, source_note_id)`.
6. **Sort and de-duplicate.** Sort by `(date, granularity)`; merge repeated
   mentions of the same event across notes (same label + overlapping date).
7. **Emit.** A sorted list for a UI, or FHIR resources (see hand-off).

## Worked example: events → sorted timeline

```python
def to_timeline(events, *, anchor, note_id):
    """events: list of {text,label,start,end,confidence}. anchor: date.
    Returns sorted [(date, granularity, label, text, confidence)]."""
    timeline = []
    for e in events:
        date, gran = resolve_event_date(e, note=note, anchor=anchor)  # your resolver
        if date is None:
            continue  # undated/unresolvable: route to an "undated" bucket, don't drop silently
        timeline.append((date, gran, e["label"], e["text"], e["confidence"]))
    # year-only ('Y') sorts before month ('M') before day ('D') on ties
    order = {"Y": 0, "M": 1, "D": 2}
    return sorted(timeline, key=lambda r: (r[0], order[r[1]]))

# resolve_event_date handles: ISO dates, "March 2019" (gran='M'),
# "yesterday"/"two days after admission" (relative to anchor/admission), POD-n, etc.
```

## Hand-off to / from OpenMed

- **From OpenMed:** `analyze_text` entities (`extracting-clinical-entities`) and
  `clinical` context tags (`resolving-clinical-context`) are the inputs. Run
  `deidentify` upstream when notes carry PHI.
- **To OpenMed / interop:** feed the sorted, dated events into
  `exporting-to-fhir` (`openmed.interop`). Map an admission/discharge event to a
  FHIR `Encounter`, a diagnosis date to `Condition.onsetDateTime`, a med-start
  to `MedicationStatement.effectiveDateTime`, a procedure to
  `Procedure.performedDateTime`.
- **Downstream:** the same timeline feeds `etl-to-omop-cdm` (start/end dates on
  `condition_occurrence` / `drug_exposure`) and clinical-summary cards.

## Edge cases & gotchas

- **No anchor → no relative dates.** "Two days later", "POD 2", "yesterday" are
  meaningless without a reference date. Parse the document date / admission date
  first; if absent, keep the event in an *undated* bucket rather than inventing
  a date.
- **Preserve granularity.** Don't coerce "2019" to `2019-01-01` and then sort it
  as if it were a precise day — it'll outrank real January events. Carry a
  granularity flag and sort coarse dates conservatively.
- **Drop the wrong people and tenses.** Negated ("no prior MI"), hypothetical
  ("would consider surgery if…"), and family-history mentions must not land on
  the patient's timeline. That's what the temporality pass is for.
- **Time zones and 2-digit years** are ambiguous — normalize to dates (not
  datetimes) for clinical timelines unless you genuinely have timestamps, and
  resolve `dd/mm` vs `mm/dd` from the document locale, not a guess.
- **Future/scheduled events** (follow-up appointments) are real but belong on a
  separate "planned" lane, not interleaved with what already happened.
- **No raw PHI in logs.** Log timeline events by label + offset + note id, never
  the patient's name or the raw note text.

## Standards & references

- FHIR R4 Encounter: https://www.hl7.org/fhir/encounter.html
- FHIR R4 Condition (`onsetDateTime`, `recordedDate`):
  https://www.hl7.org/fhir/condition.html
- ISO 8601 date/time representation:
  https://www.iso.org/iso-8601-date-and-time-format.html
- Background on clinical temporal expression normalization (TimeML / i2b2 2012
  temporal relations task): https://www.i2b2.org/NLP/TemporalRelations/
- OpenMed source: `openmed/processing/` (`analyze_text` output), `openmed.clinical`
  (temporality), `openmed.interop` (FHIR export).

