# CDS Hooks on Epic and Cerner: patient-view, order-select, order-sign

> A playbook for CDS Hooks against Epic and Oracle Health (Cerner) — patient-view versus order-select versus order-sign, cards versus suggestions, prefetch, and de-identified fixtures.

Author: Ala Ben Aicha

Canonical page: https://alabenaicha.me/insights/cds-hooks-epic-cerner

Updated: 2026-09-17

## Direct answer

patient-view, order-select and order-sign are different workflow points. A card is displayed guidance; a suggestion is an actionable FHIR change. Prefetch is data the EHR may send so the service stays fast.

## Hook and card mapping

CDS Hooks is a JSON-over-HTTPS pattern: the EHR (CDS Client) POSTs to `{baseUrl}/cds-services/{id}` when a named workflow hook fires. Pin the [CDS Hooks specification](https://cds-hooks.org/specification/current/) and the hook pages for [patient-view](https://cds-hooks.org/hooks/patient-view), [order-select](https://cds-hooks.org/hooks/order-select) and [order-sign](https://cds-hooks.org/hooks/order-sign). Discovery is `GET {baseUrl}/cds-services`.

This is not the same article as [AI in clinical workflows](https://alabenaicha.me/insights/healthcare-ai-clinical-workflows) (model behaviour inside the EHR) or [Epic vs Cerner](https://alabenaicha.me/insights/epic-vs-cerner-comparison) (platform flavour). Both vendors expose CDS Hooks; the hook names and the card schema are the portable contract.

| Hook             | When it fires                                                     | Required context                                            | Typical response                                                                 |
| ---------------- | ----------------------------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------- |
| **patient-view** | User opens a patient's chart                                      | `userId`, `patientId`; `encounterId` often present          | Info / warning card, or a SMART `link`                                           |
| **order-select** | Clinician selects one or more orders, still draft                 | `userId`, `patientId`, `selections[]`, `draftOrders` Bundle | Alternatives, coverage, interaction warnings while details are incomplete        |
| **order-sign**   | Clinician is about to sign; last chance before draft leaves draft | `userId`, `patientId`, full `draftOrders`                   | Harder guidance; suggestions that `create` / `update` / `delete` draft resources |

Card versus suggestion versus link:

| Return         | What it is                                                                                                    | What it is not                                                                                                                                   |
| -------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Card**       | `summary` (<140 chars), `indicator` (`info` \| `warning` \| `critical`), `source`, optional `detail` markdown | Not a write. An empty `cards: []` is a valid 200 — “no guidance”                                                                                 |
| **Suggestion** | Labelled action set the user may accept; `actions[]` of FHIR `create` / `update` / `delete`                   | Not auto-applied unless you also send `systemActions` and the EHR opts in. If `suggestions` exist, `selectionBehavior` is `at-most-one` or `any` |
| **Link**       | URL, optionally `type: smart` to launch a SMART app                                                           | Not a substitute for a suggestion when you need a MedicationRequest change in-place                                                              |
| **Prefetch**   | Discovery-time FHIR read/search templates, filled by the EHR and POSTed as `prefetch`                         | Not a guarantee. The client MAY honour zero, some, or all keys                                                                                   |

Prefetch tokens are `{{context.patientId}}` (and other first-level primitive context fields). Example discovery fragment:

```json
{
  "hook": "order-sign",
  "id": "example-pgx",
  "description": "Example pharmacogenomics check on sign (de-identified).",
  "prefetch": {
    "patient": "Patient/{{context.patientId}}",
    "medications": "MedicationRequest?patient={{context.patientId}}"
  }
}
```

If required data is in neither prefetch nor an authorized `fhirServer`, the service responds **412 Precondition Failed**. Do not stall the EHR for seconds while you paginate the chart.

## Identifier traps

| Trap                                           | Symptom                                                                     | Fix                                                                                                                  |
| ---------------------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Treating `context.patientId` as a Patient URL  | Prefetch `Patient/{{context.patientId}}` vs `Patient?identifier=` confusion | Hook context is a FHIR **id**, not an MRN. Map MRN in prefetch or via FHIR search                                    |
| Using nested context as a prefetch token       | `{{context.draftOrders.entry.0.resource.id}}` is invalid                    | Only first-level primitive context fields; hooks that need an order id expose it as a top-level field                |
| Suggestions without `selectionBehavior`        | Client must treat the card as an error                                      | Always set `at-most-one` or `any`                                                                                    |
| Returning clinical advice as `detail` only     | Clinician never expands “view more”                                         | Put the decision in `summary`; keep `detail` for evidence                                                            |
| Prefetch from another service on the same hook | Over-sharing                                                                | Clients MUST only send keys **this** service registered                                                              |
| Oracle Health vs Epic draft resource shapes    | Suggestion `update` fails validation                                        | Fixture per EHR: same hook name, different MedicationRequest constraints. Do not assume one card JSON writes to both |

CDS Hooks is decision support plumbing. It is not a licence to give treatment, dosing, or diagnosis. Model output that changes an order still needs a clinician and, where the software is a medical device, a regulatory owner — see the [MDR software guide](https://alabenaicha.me/insights/eu-mdr-software-medical-device-guide) if that question is open.

## Test fixtures

De-identified. The Patient id `example-p` is not a person.

Discovery (truncated):

```json
{
  "services": [
    {
      "hook": "patient-view",
      "id": "static-patient-greeter",
      "title": "Static example",
      "description": "Returns a static info card.",
      "prefetch": {
        "patientToGreet": "Patient/{{context.patientId}}"
      }
    }
  ]
}
```

patient-view request (truncated):

```json
{
  "hookInstance": "d1577c69-dfbe-44ad-ba6d-3e05e953b2ea",
  "hook": "patient-view",
  "fhirServer": "https://ehr.example.org/fhir",
  "context": {
    "userId": "Practitioner/example-pract",
    "patientId": "example-p",
    "encounterId": "example-enc"
  },
  "prefetch": {
    "patientToGreet": {
      "resourceType": "Patient",
      "id": "example-p",
      "gender": "female",
      "birthDate": "1977-04-12",
      "active": true
    }
  }
}
```

Info card (no suggestion):

```json
{
  "cards": [
    {
      "summary": "Example info card — not clinical advice.",
      "indicator": "info",
      "source": { "label": "Example CDS Service" },
      "links": [
        {
          "label": "Open example SMART app",
          "url": "https://app.example.org/launch",
          "type": "smart"
        }
      ]
    }
  ]
}
```

Suggestion shape on order-sign (resource truncated, not a real medication order):

```json
{
  "cards": [
    {
      "summary": "Example: replace the draft order (fixture).",
      "indicator": "warning",
      "source": { "label": "Example CDS Service" },
      "selectionBehavior": "at-most-one",
      "suggestions": [
        {
          "label": "Apply example update",
          "actions": [
            {
              "type": "update",
              "description": "Update the draft ServiceRequest identifier only.",
              "resource": {
                "resourceType": "ServiceRequest",
                "id": "draft-order-1",
                "status": "draft",
                "intent": "order",
                "subject": { "reference": "Patient/example-p" }
              }
            }
          ]
        }
      ]
    }
  ]
}
```

Minimum tests:

1. patient-view with prefetch present — 200, ≤1 info card, latency budget the EHR actually enforces.
2. patient-view with prefetch key omitted — either 200 with a narrower card or 412; never hang.
3. order-select with two `selections` — card talks about the selected drafts only.
4. order-sign suggestion accepted vs dismissed (feedback API if the EHR sends it).
5. Same service registered on Epic sandbox and an Oracle Health sandbox — compare draft MedicationRequest required fields.

## Failure modes and rollback

| Failure                               | Detection                                | Rollback                                                                                                  |
| ------------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| Slow service                          | Chart open blocked                       | Time out; return `cards: []`; never block sign on a dead callback                                         |
| Stale cards                           | order-select cards still showing at sign | Clients should drop previous cards from the same `id` when a new hook fires                               |
| Suggestion writes the wrong resource  | Identifier / id mismatch on `update`     | Suggestions MUST send the full updated resource; disable auto-apply (`systemActions`) until fixtures pass |
| Token leakage via `fhirAuthorization` | Long-lived bearer in logs                | Treat the token as transient; EHR should revoke after the hook; do not persist                            |
| Dual-EHR JSON assumed portable        | Production 400 from one vendor           | Per-EHR schema tests; shared hook names, not shared resource payloads                                     |

Turn a dangerous service off at discovery (remove it from `{baseUrl}/cds-services`) rather than returning critical cards you cannot support. Rollback is: unregister the hook, keep the EHR workflow native, replay nothing — CDS Hooks is advisory unless you opted into `systemActions`.

## Related service

If you need CDS Hooks wired into Oracle Health / Cerner (and the same cards tested against Epic), that is [Cerner integration](https://alabenaicha.me/services/cerner-integration). For a scoped hook spike, use [contact with project intent](https://alabenaicha.me/contact?intent=project).

This page is not clinical advice, not a CDS Hooks certification, and not an EHR vendor partnership. Empty cards are a successful response.
