EHR access
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.liveruns it in the EHR itself.client.ehr.replicareads 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
- Your service app's manifest asks for the operation, in the mode you call it in, under
modules.ehrAccess. - 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
systemInstanceIdfromsystems(). - 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 startpollIntervalMsapart (default 2s) and back off 1.5× each time, up tomaxPollIntervalMs(default 15s). When Vim says how long to wait before the next poll, that wins, held to between 1 and 60 seconds. Passsignalto 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, a502/503/504or 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 withinvalid_inputwhen Vim holds no token for that patient — it never creates one. Avtk_you already hold, fromclient.api, passes as it is. Any other PHI param is refused withinvalid_inputbefore 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.
code | What 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_input | Nothing 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_failed | The 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_response | Upgrade @vimconnect/backend-sdk. With a taskId, Vim had already accepted the task: result() or discard() it as above. |
too_many_pending | Collect started tasks with result(taskId), or release them with discard(taskId), before starting more. |
task_timeout / aborted | Collect 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_failed | Vim could not run the task. Call again later. Vim's reason is on err.taskErrorCode. |
task_expired | Call the operation again. |
operation_failed | The EHR refused the operation. What it reported is on err.detail. |
result_unreadable | The task ran; the SDK cannot read its result. Upgrade if it repeats. |
result_key_missing | Call 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.