φ(ai) PHI AI
On this page — 8 sections

Orchestration & identity

Moving a record between two EMRs is not a data transfer; it is a chain of compliance decisions, and every link in that chain is a place the platform can refuse. This section is the model behind the Orchestration screens: the registry of vendor seams, the patient link set, the practitioner crosswalk, the permission lattice with its narrowest-wins rule, the categories that never leave, and the replay that proves a run from the audit trail alone. Nothing on those screens is inferred - every list is derived from its source of truth and every decision names the bound that made it.

1. Why orchestration is a compliance object

The platform reads from many sources and delivers to many targets, and a run is the unit in which those two halves meet. The demo treats the run as a governed object rather than a job: it has a selection (which systems, which patients, under which purpose of use), a preflight that says what will refuse before anything moves, a ledger that records each step with the bound that decided it, and a replay that rebuilds the ledger from the audit trail. Sixbounds are consulted for every step, in a fixed order, and the first refusal ends the evaluation:

  1. identity — is there an operator-verified identifier for this patient on this system? Matching is not offered.
  2. practitioner — is the person delivering explicitly mapped to a Practitioner on the target, with a standard role code?
  3. source — does the source advertise the read (Patient, or Bulk Data Export) and is it not administratively paused?
  4. phi — does this role hold the permission, may it assert this purpose, and does the purpose permit heightened categories?
  5. necessity — minimum necessary (45 CFR 164.502(b)(1)): does the asserted purpose need this resource type at all? Treatment and patient request are exempt by 164.502(b)(2)(i) and (ii) and are not bounded here; payment, operations and research are bounded to the types they need; legal may read a named chart only, because 164.512(e)(1)(i) discloses only what an order expressly authorises and no order can be attached here.
  6. target — does the target advertise create for this resource type, and does its profile record conditional create (If-None-Exist) so an unattended write is duplicate-safe? Without it writer.py refuses an unattended run and stops at dry run.

Three harmonisations sit underneath: identity (which chart on each system is this person), practitioner (who on each system is this user), and permission (what may cross, and in which direction). Each is explicit. None is inferred from a name, a date of birth, an encounter author or a persona label, because each inference has a failure mode that writes one person's history into another person's chart.

2. The registry of seams

Each vendor's own documentation is the source of truth for its seams; the demo's mirror was built from it and is pinned to the platform's profile registry (core/fhir/emr_profiles.py) by tests/test_orchestration_consistency.py. Where a capability is not advertised the run refuses and says so - it never assumes. The table is the registry, rendered; there is no hand-kept copy.

VendorAuth flowAssertionScopes Bulk exportConditional createReadsCreatable (target)
Epic smart_backend_services PHI AI signs RS384 no scope in the token request yes
Group-level Bulk Data Export only; requires a Group FHIR ID from Epic; rate-limited to once per 24 hours per group and client; every kickoff is a full re-extract
no — unattended run refused; dry run or --allow-duplicates 15 DocumentReference
Oracle Health (Cerner) smart_backend_services PHI AI signs RS384 explicit scopes required yes
Bulk Data Access documented and requires the JWT auth mode; Group-level availability is tenant configuration; polling cadence, Retry-After values, group sizes and kickoff throttles are not documented on the pages cited
yes — confirm per tenant 14 DocumentReference, Condition, Observation
athenahealth oauth2_client_credentials — no scope in the token request yes
Bulk export supported with vendor-specific constraints — confirm per practice; poll cadence, group procedure and throttles are not documented in the source cited, and rate limits are tighter than the other targets
no — unattended run refused; dry run or --allow-duplicates 14 DocumentReference
eClinicalWorks smart_backend_services PHI AI signs RS384 no scope in the token request yes
The portal documents backend (single-patient) and bulk (multiple-patient) FHIR APIs; availability for a specific practice may still require contracting; Group procedure, poll cadence and throttles are not documented on the portal page cited
no — unattended run refused; dry run or --allow-duplicates 14 none — read-only
MEDITECH Expanse smart_backend_services PHI AI signs RS384 no scope in the token request yes
"Bulk Data" is a documented topic in the Greenfield explorer and MEDITECH appears in the SMART team's registry of bulk-data implementations; export level, Group procedure, poll cadence and throttles are not documented publicly — the client defaults stand until MEDITECH says
no — unattended run refused; dry run or --allow-duplicates 10 none — read-only
NextGen Healthcare smart_backend_services PHI AI signs RS384 no scope in the token request no
NO Bulk Data Export in the vendor profile — the platform's bulk scheduler refuses this vendor at startup rather than silently falling back to paged search
no — unattended run refused; dry run or --allow-duplicates 14 DocumentReference
ModMed smart_backend_services PHI AI signs ES384 explicit scopes required yes
System-, Patient- and Group-level $export documented (ndjson only; _since and _type supported); async with a fixed Retry-After of 120s; enabled per practice by an admin adding the app ClientID under "Manage Bulk FHIR"; Group IDs are created by ModMed on request
no — unattended run refused; dry run or --allow-duplicates 25 none — read-only
Altera Digital Health smart_backend_services PHI AI signs RS384 explicit scopes required yes
Group-level Bulk Data Export only ([FHIR path]/Group/{id}/$export), System apps with JWKS auth only; asynchronous with Retry-After polling and HTTP 429 if polled early; 28 resource types; Group IDs discoverable via GET Group
no — unattended run refused; dry run or --allow-duplicates 12 none — read-only
Greenway Health smart_backend_services PHI AI signs ES384 explicit scopes required yes
Group-level Bulk Data Export only, tenant (site)-wide by default; Groups are discoverable via GET /Group; a kickoff without _since exports only the last 24 hours
no — unattended run refused; dry run or --allow-duplicates 18 none — read-only
Veradigm smart_backend_services PHI AI signs RS384 no scope in the token request yes
Group-level Bulk Data Export only, on a Group the organization builds from a Reporting segment; async with Retry-After — polling early is a 429, and export files expire
no — unattended run refused; dry run or --allow-duplicates 15 none — read-only
Practice Fusion smart_backend_services PHI AI signs RS384 explicit scopes required yes
Bulk Data Access v1.0.1 at practice scope: Patient/$export (every patient of the practice) or Group/{id}/$export (a practice-defined group, max 1,000 patients); per-practice base URL and token URL
no — unattended run refused; dry run or --allow-duplicates 22 none — read-only
TruBridge smart_backend_services PHI AI signs RS384 explicit scopes required yes
Bulk Data Access v2.0.0 documented at system, Group and Patient level; how a Group id is issued, kickoff throttles and Retry-After are not documented — confirm per facility (one base URL per facility, from TruBridge's endpoint directory)
no — unattended run refused; dry run or --allow-duplicates 11 none — read-only
MEDHOST smart_backend_services PHI AI signs RS384 no scope in the token request yes
Group-level Bulk Data Export only — System and Patient export not supported; the Group is created by MEDHOST Support from a facility-submitted MRN list, max 5,000 patients per group; kickoff documents 429
no — unattended run refused; dry run or --allow-duplicates 12 none — read-only
Netsmart smart_backend_services PHI AI signs RS384 explicit scopes required yes
Group-level Bulk Data Export (Bulk Data 2.0.0) with _type and _since documented; Group IDs discoverable via GET /Group; polling guided by Retry-After; 429 on concurrent exports
no — unattended run refused; dry run or --allow-duplicates 18 DocumentReference, DiagnosticReport
Nextech smart_backend_services PHI AI signs RS384 explicit scopes required yes
Bulk Data Export documented at all-patient, Group (practice "letter writing" group) and system level on Select/NexCloud 16.9+ and IntelleChartPRO; _since supported; poll Export/{id} with Retry-After and X-Progress; export files may be served from separate storage with requiresAccessToken false
no — unattended run refused; dry run or --allow-duplicates 11 DocumentReference

"Reads" counts the resource types the profile records for ingestion - the vendor's published surface where one is published (Epic's lists what this codebase ingests; MEDITECH's is deliberately narrow, a recorded uncertainty); confirm on the instance's CapabilityStatement. "Creatable" is what the emulator's CapabilityStatement advertises create for, which is what the delivery writer honours; the vendor's own sentence sits in each cell's title. A profile is a planning aid; the live CapabilityStatement is the authority, and the writer re-reads it at delivery time.

A patient has one identity in PHI AI and, on each connected system, an identifier the system itself minted. The link set is the list of those identifiers for one patient, and every row in it is one of three states: candidate (typed by an operator, not yet verified), verified (a named person asserted these are the same patient, with a note), or rejected (kept as evidence, never deleted). Delivery consults exactly one reader - the single verified row for that patient on that system - and an ingest or delivery step with no verified row refuses at the identity bound.

The rule that follows the platform's identity.py: no matching, ever. The screen does not offer "find this patient on Epic by name and date of birth", because a false positive writes one person's history into another person's chart, and this corpus makes the failure concrete - thousands of patients share a surname and a screen of identical names is an ordinary search result here. The identifier is the EMR's own (^[A-Za-z0-9.-]{1,64}$), not an MRN and not a name; verified_by is required, not optional - a named person, with a note that says how, and not necessarily a second person (the platform requires a name, not a distinct verifier). The same identifier already held for another PHI AI patient on the same system is refused at entry, as a duplicate, on every system: the platform's IdentityMap raises for a source id mapped twice and only warns for a target id mapped twice (two source charts merging into one destination chart), but the demo refuses both at the door, so the platform's merge warning cannot arise on these screens and no panel pretends it can. A candidate typed from a stale tab against a different chart from the one in context is refused as a context mismatch. Every add, verify, reject and refusal is an identity.* event on the trail, and a refusal is also shown on the screen it returns to.

4. The practitioner crosswalk

A delivery is authored by someone, and the target needs to know who. The crosswalk maps each person who works here to a Practitioner identifier on each system, typed by an administrator and never inferred from the persona's display name or from the free-text author on an encounter. Alongside the identifier the platform records a standard role code from the HL7 Practitioner role CodeSystem (http://terminology.hl7.org/CodeSystem/practitioner-role, v1.0.1, read from its JSON on 2026-09-02), which defines 6 concepts: doctor (Doctor), nurse (Nurse), pharmacist (Pharmacist), researcher (Researcher), teacher (Teacher/educator), ict (ICT professional). The vendor's own local role is deliberately not recorded, because it is that system's vocabulary and not this one's. The CodeSystem has no records-management, audit or behavioural-health concept; the assignments below are PHI AI's nearest choice among its six codes, fixed in ORCH_ROLE_CODES, and a target that needs its own vocabulary assigns its local role itself. A role with no fitting standard code is unmappable and says so. The mapping is derived from the role, never posted - the code column below is the table the module enforces:

PHI AI roleStandard practitioner-role codeCan author a delivery once mapped
Clinician viewer doctor · Doctor A qualified/registered medical practitioner no — does not hold integration:export
Health Information Management him ict · ICT professional Someone who is qualified in Information and Communication Technologies yes, holding integration:export
Auditor auditor ict · ICT professional Someone who is qualified in Information and Communication Technologies no — does not hold integration:export
Analyst analyst researcher · Researcher A practitioner that may perform research no — does not hold integration:export
Researcher researcher researcher · Researcher A practitioner that may perform research no — does not hold integration:export
Psychotherapy psychotherapy doctor · Doctor A qualified/registered medical practitioner no — does not hold integration:export
Administrator admin ict · ICT professional Someone who is qualified in Information and Communication Technologies yes, holding integration:export
Disposition disposition ict · ICT professional Someone who is qualified in Information and Communication Technologies no — does not hold integration:export
System Administrator sysadmin ict · ICT professional Someone who is qualified in Information and Communication Technologies yes, holding integration:export

An unmapped person cannot author a delivery: the practitioner bound refuses before the target is even consulted, which is why on the Orchestration screen a read-only target can still show "unmapped" as its deciding bound. Mapping and unmapping are admin:config actions and land as identity.practitioner_mapped / identity.practitioner_unmapped.

5. The lattice and narrowest-wins

The lattice is the full grid of role x purpose x system x action, each cell holding the five bound states and one decision. The decision is the narrowest of the consulted bounds - refuse below dry run below allow - never a union: a permission somewhere else never widens a refusal here. The actions the demo orchestrates, from ORCH_ACTIONS:

ActionDirectionPermissionFHIR resourceLevel
Read chart read_chart ingestpatient:read Patientpatient
Export population export_population ingestintegration:view Grouppopulation
Deliver note deliver_note deliverintegration:export DocumentReferencepatient
Deliver problem deliver_problem deliverintegration:export Conditionpatient
Deliver result deliver_result deliverintegration:export Observationpatient
Deliver report deliver_report deliverintegration:export DiagnosticReportpatient
Deliver order deliver_order deliverintegration:export ServiceRequestpatient

deliver_order is on the list on purpose: no profiled vendor advertises create for ServiceRequest, so it refuses on every target - the row that proves the target gate is real rather than decorative.

5.1 Worked example, computed live

For Ryan Gomez (System Administrator) under purpose Health care operations, source Epic, target Epic, with no patient in context (identity cells are decided per patient at run time)— the first selected pair on the Orchestration screen, or the configured pair when nothing is selected:

ActionSystem identitypractitionersourcephinecessitytarget Narrowest winsReason
Read chart Epic na na allow allow allow na allow allowed on every consulted bound
Export population Epic na na allow allow allow na allow allowed on every consulted bound
Deliver note Epic na allow na allow refuse na refuse at necessity DocumentReference is more than purpose operations needs — minimum necessary applies (45 CFR 164.502(b)(1)). Permitted for this purpose: Patient, Group, Condition, Observation, DiagnosticReport, ServiceRequest
Deliver problem Epic na allow na allow allow allow allow allowed on every consulted bound
Deliver result Epic na allow na allow allow allow allow allowed on every consulted bound
Deliver report Epic na allow na allow allow allow allow allowed on every consulted bound
Deliver order Epic na allow na allow allow allow allow allowed on every consulted bound

Reading a row: the cells after the deciding bound read na because they were not consulted - the module short-circuits at the first refusal, so a refused step never reveals what a later bound would have said. A dry_run at the target means the record was composed and audited but not written: the vendor documents no conditional create, so the platform's writer refuses an unattended run without --allow-duplicates and stops at its default, a dry run. The PHI bound is the narrowest of the role's permissions: a patient-level step needs patient:read as well as the action's own permission, so a role that may not open a chart cannot deliver one either, whatever else it holds.

6. Withheld categories

Heightened categories are counted at delivery and withheld in full: no target in this demo has earned any of them, because no disclosure-consent record exists here - release of information is the only path that carries one, and it is judged per request. Independently of the target, only the purposes Treatment and Patient request may carry them at all, so a run under any other purpose names the purpose as a second gate. A patient imported this session is classified by the same classifier over the chart that arrived (its conditions, medications and any document written for them here); an import whose chart cannot be classified refuses every delivery rather than reporting nothing to withhold. The categories and their counts are shown only to a role holding sensitivity:view (the psychotherapy-notes count only with psychotherapy:read); every other role sees the number of records withheld and the permission it would take. The categories the classifier knows, from SENSITIVITY_LABELS, plus psychotherapy notes, which never travel on a deliver path:

  • Domestic & intimate partner violence domestic_violence
  • Abuse & neglect abuse_neglect
  • SUD — 42 CFR Part 2 sud_part2
  • Mental health mental_health
  • Reproductive health reproductive
  • HIV hiv
  • Genetic genetic
  • Sexually transmitted infection sti
  • Gender identity & affirming care gender_identity
  • Minor-consented confidential service minor_confidential
  • Psychotherapy notes psychotherapy_notes — a separate table, a separate role, and never on a deliver path.

The run ledger carries the withheld counts per step, so "what did not leave" is on the record beside "what did", never silently absent; the run's headline and the matrix count each chart's records once, because every deliver step for a patient withholds the same records and a sum over steps would multiply one chart by the number of steps. Record classes without a sensitivity column (encounters, observations, imaging, claims) are not gated by category, and the screens say so rather than promising more than the classifier can see.

7. Replay from the audit trail

Every step writes its audit event before the simulated write - the same order as the platform's record.deliver - with a resource reference of the form orch/<run>/<system>/<patient>/<action>:<decision>, and stores the resulting event hash on the step. Replay rebuilds the step list from those events alone and compares it with the stored ledger; the hash chain is verified at the same time. A ledger that cannot be rebuilt from the trail is reported, not hidden - and because the per-session trail is capped, a run that has rolled off the tail reports how many steps are missing rather than claiming a match.

8. What the demo does not do

  • It does not write to any EMR. Nothing is posted to a real system. Delivery is real inside the demonstration, though: a run moves rows between the emulators' own stores and records every movement in a ledger keyed to your session, which is what the reconciliation and QA screens read. That ledger, and everything your visit moved, is cleared when your visit starts and swept two hours after it goes quiet - a run belongs to one visitor and outlives no one. The emulators and tests/test_delivery.py are the proof that the platform's writer honours the same gates against a live CapabilityStatement.
  • It does not match patients. There is no probabilistic or demographic matcher, and none is planned for the screen: a link is typed and verified by a named person.
  • It does not read a vendor's local roles. The crosswalk records the standard code; the vendor-local role is "not recorded".
  • It does not release heightened categories to a target. There is no disclosure-consent record in the demo, so nothing has earned them; release of information is the path that judges consent per request.
  • It does not keep purpose as a mode. The purpose is asserted on the selection and applied per request; a role may only assert the purposes its role allows.

8.1 Running the walkthrough

  1. As the system administrator, open Integration > Orchestration, tick sources and targets, keep the purpose, and save: the access widget now counts them.
  2. Open a chart so a patient is in context; the preflight turns red on every patient-level row, grouped by problem. Each group names its deciding bound and links to its fix (link set, crosswalk, lattice, configuration) - or says the refusal is the vendor's own documentation and nothing here can change it. Because the bounds short-circuit, only the identity reason shows until a link is verified; the practitioner reason appears once identity passes.
  3. As HIM, type the vendors' own identifiers on the Patient link set - one for every target you mean to reach, the read-only one included, so its delivery reaches the practitioner bound instead of stopping at identity - and verify them with a note; try the same identifier on a second patient and read the refusal, on the screen and on the trail.
  4. As the administrator, map HIM on the Practitioner crosswalk without holding a patient (the administrator cannot: holding a chart needs patient:read, and the link set says so instead of offering a selector); leave one target unmapped.
  5. Back as HIM, execute; open the run ledger, the matrix and the replay; then the Permission lattice with the same patient held.
  6. As the psychotherapist, open Orchestration and read the refusal page. She holds no audit:read, so switch to an auditor or the administrator to see access.denied on the trail and no orchestration.run_started after it.

The scripted version of this walkthrough is www-demo/scripts/smoke_orchestration.sh, which drives the same screens over HTTP and asserts every refusal above. Companions: EMR connections for each vendor's documented seams and Compliance & responsibility for the rules the bounds enforce.