EHR access

Open in Claude

Run operations against an account's connected EHR with client.ehr, live or from Vim's replica

client.ehr runs an operation against one of the account's connected EHR systems. Two modes:

  • client.ehr.live runs it in the EHR itself.
  • client.ehr.replica reads from a copy Vim keeps of the account's EHR data.

Which operations each mode offers depends on your SDK version; Ehr.LiveNamespaceMap and Ehr.ReplicaNamespaceMap list them. Not every operation is available in both modes, and those types are the source of truth. The examples below use patient.getPatient to show the call shape.

Before a call can succeed

  1. Your service app's manifest asks for the operation, in the mode you call it in, under modules.ehrAccess.
  2. The account approved that ask, per connected system.

client.ehr.systems() lists the systems you can call right now, with the operations and modes granted on each. A system that is not listed cannot take a call at the moment.

const { systems: [system] } = await client.ehr.systems();
// system: { systemInstanceId, systemType, displayName,
//           operations: [{ operation: 'patient.getPatient', modes: ['live'] }] }
if (!system) throw new Error('this account has no connected EHR system');

const { data: patient } = await client.ehr.live.patient.getPatient(
  { patientId: '29878147' },          // the patient's id in the EHR
  { systemInstanceId: system.systemInstanceId },
);

This differs from client.api in four ways:

  • It names a system. Every call takes a systemInstanceId from systems().
  • It can take minutes. A live call opens an EHR session. The promise polls until the task finishes, for up to timeoutMs (default 5 minutes). Polls start pollIntervalMs apart (default 2s) and back off 1.5× each time, up to maxPollIntervalMs (default 15s). When Vim says how long to wait before the next poll, that wins, held to between 1 and 60 seconds. Pass signal to stop waiting; the task keeps running either way. A signal already aborted when the call starts sends nothing. Aborting while a read is being sent cancels it; a write, once sent, is never cancelled, because Vim may already have accepted it.
  • A lost response is resent safely. Each call carries its own Idempotency-Key, so the client resends it after a network error, a 502/503/504 or an expired token (retries), and Vim answers the resend with the task the first one created, never a second task or a second write.
  • PHI travels as vault references. The record's PHI identifier is sent as a vtk_…@<vaultKey> reference: the client looks the identity up in the account's vault, keyed on the system, and refuses the call with invalid_input when Vim holds no token for that patient — it never creates one. A vtk_ you already hold, from client.api, passes as it is. Any other PHI param is refused with invalid_input before anything is sent. Vim's API stores only the reference; the runtime that drives the EHR resolves it for that one task. The result comes back encrypted to a key minted for the call. The encryption keeps the result unreadable to Vim's API; it does not prove the result came from the EHR — its authenticity rests on Vim's API and runtime.

Collecting a result later

When a request handler cannot wait, start the operation, return the task id, and collect it from a later request with the same operation's result:

const { taskId } = await client.ehr.live.patient.getPatient.start(
  { patientId }, { systemInstanceId },
);

// a later request, in the same process, on the same client instance
const { data: patient } = await client.ehr.live.patient.getPatient.result(taskId);

This is per process. The key that decrypts the result lives in the memory of the client instance that started the task, until the task is collected. A restarted process or another instance cannot collect it and gets result_key_missing; call the operation again. A task you will not collect, say after a task_timeout you gave up on, should be released with discard(taskId): a client holds at most 1,000 uncollected tasks, and refuses another start with too_many_pending past that.

A read called while an identical one (same system, operation and input) is still in flight on the same client shares that call and its result, under the first call's wait options. A call passing a signal, start, and writes are never shared.

EHR errors

Every client.ehr failure is a VimConnectEhrError — a VimConnectError with a code, and a taskId once Vim accepted the task. statusCode is the HTTP status when the API answered, and 0 when the failure was reported by the task or found by the SDK.

codeWhat to do
not_granted (403)Ask the account to approve this operation, in this mode, on this system.
system_not_linked (404)Take a systemInstanceId from client.ehr.systems().
system_unavailable (409)This system can't run EHR operations right now. Try again later; if it persists, ask the account to check the system in the Vim Console.
not_servable (400)Use another mode or system; this one cannot run the operation.
undeclared_param (400)Send only the params the operation declares. The rejected path is on err.detail.
quota_exceeded (429)Too many operations are in progress on this system, and nothing was started. Retry after the number of seconds in the response's Retry-After header (err.detail.response.headers.get('Retry-After')); a write is safe to resend.
invalid_inputNothing was sent. Pass a systemInstanceId from systems(), identify the patient by the record's identifier or its vtk_ reference (Vim must already hold a token for that patient), and send no other PHI as a plain value.
request_failedThe API refused the request or did not answer; statusCode says which (0: no answer). With a taskId, Vim had already accepted the task: collect it with result(err.taskId) or free it with discard(err.taskId) — don't retry. Without one, retry a read; for a write, check the EHR first, since it may already have been applied.
unexpected_responseUpgrade @vimconnect/backend-sdk. With a taskId, Vim had already accepted the task: result() or discard() it as above.
too_many_pendingCollect started tasks with result(taskId), or release them with discard(taskId), before starting more.
task_timeout / abortedCollect it later with result(err.taskId), or free it with discard(err.taskId). Do not reissue a write. An aborted with no taskId sent nothing.
task_failedVim could not run the task. Call again later. Vim's reason is on err.taskErrorCode.
task_expiredCall the operation again.
operation_failedThe EHR refused the operation. What it reported is on err.detail.
result_unreadableThe task ran; the SDK cannot read its result. Upgrade if it repeats.
result_key_missingCall a read again. For a write, check the EHR first.

No error message carries text the server sent. What Vim's API, its runtime or the EHR said is on err.detail, and may hold PHI: it is non-enumerable, so neither JSON.stringify nor a default console.error / util.inspect prints it. err.cause only ever holds a failure on this side — network, abort or decryption.

The model types behind client.ehr are exported under the Ehr namespace (Ehr.Patient), with their schemas beside them (Ehr.PatientSchema). They describe the EHR's shape, which differs from the entity-store types of the same name.

On this page