This document describes the format of the two export files produced by EMR-Bear to satisfy ONC/ASTP Health IT criterion 45 CFR § 170.315(b)(10):
- Single-patient EHI export — § 170.315(b)(10)(i)
- Patient-population EHI export — § 170.315(b)(10)(ii)
Both use the same package format. The population export is the general case; the
single-patient export is the same format narrowed to one patient. Same filenames, same columns, same
CSV conventions, same asset layout, same dictionary. Software that can read one can read the other,
and the single key that tells them apart is export.spec in
dictionary.json.
Under § 170.315(b)(10)(i)(E) and § 170.315(b)(10)(ii), the hyperlink to this document is included inside every export of either kind. Under § 170.315(b)(10)(iii), this document is kept up to date as the format changes.
01The two exports
| Export | Criterion | Contains |
|---|---|---|
| Single patient | (b)(10)(i) | All of the electronic health information EMR-Bear stores for one patient |
| Patient population | (b)(10)(ii) | All of the electronic health information EMR-Bear stores, for every patient |
The population export is larger than the sum of every single-patient export. A single-patient export contains what can be attributed to that patient. The population export contains everything that is electronic health information, including records that hold EHI but cannot be tied to one individual. See § 7.
Where the data comes from
Both exports draw from the same three stores:
| Store | Contents |
|---|---|
| Relational (MySQL) | Demographics, enrollment, scheduling, encounters, orders, billing, claims, consents, assessments, audit and workflow records |
| Document (MongoDB) | Clinical document content — allergies, medications, problems, clinical notes, registries, referrals |
| Object storage | Scanned documents, images and other file attachments |
How the exported set is decided
In both exports the set of tables is discovered at run time by reflecting over the live database schema. It is not a hand-maintained list, so a schema change that adds new storage is picked up automatically rather than silently omitted. What differs is the test for inclusion:
- Single A table is included when its rows can be linked to the patient, by the mechanisms in § 6.
- Population A table is included when it holds electronic health information, whether or not any given row can be attributed to an individual patient.
The single-patient rule is a patient-scoping algorithm; the population rule is not. This is why the population export reaches storage the single-patient export cannot, and why the two have different exclusion lists.
Which people are patients
EMR-Bear stores patients in a shared people table alongside non-patient persons such as
staff. A person record is exported as a patient when it is marked as a client. The population export
writes every client record to people.csv; the single-patient export writes one —
or, where duplicate records have been merged or hidden, the small linked set described in
§ 8.
Where a patient’s record refers to another person, what that record holds about them is exported with the patient’s data. That person’s own record is not, being outside this patient’s designated record set.
Deleted and hidden records are included in both exports. The export reads tables without the application’s default scopes, so soft-deleted rows, superseded rows and hidden duplicate records are exported. They remain part of the health information.
02How an export is produced
Single-patient export Single
Export is limited to a specific set of identified users, satisfying § 170.315(b)(10)(i)(C)(1). A user must hold one of the following roles:
sysadminhuman resourcesbiller managerreport
Users without one of these roles are not shown the export controls and cannot invoke the export endpoint.
A permitted user starts an export from either of two places in the application, with no assistance from EMR-Bear staff or developers, satisfying § 170.315(b)(10)(i)(B):
- Patient chart → Admin tab → “Export Client Data” — exports the patient whose chart is open.
- Reports → “Client EHI Export (ONC)” — select a single patient by name.
The export runs as a background job. When it completes, the ZIP file is available for download from the Reports page. Exactly one patient must be selected: the export refuses to run with no patient selected, and refuses to run with more than one.
Population export Population
A population export is the entire organisation’s health information in one package. It is not a self-service function within the application. § 170.315(b)(10)(ii) carries neither the “without subsequent developer assistance” requirement of § 170.315(b)(10)(i)(B) nor the user-limiting requirement of § 170.315(b)(10)(i)(C), and EMR-Bear produces a population export as a coordinated process rather than an in-application report.
An organisation requests a population export through its usual EMR-Bear support channel. The request is verified as originating from the organisation whose data is being exported, and the export is then scheduled and produced offline. For a large organisation, production takes several hours.
The resulting package is encrypted at rest and delivered through a separate, expiring download. Requests and deliveries are recorded.
The dictionary.json inside the package describes the run that actually happened,
including the window over which it read — see started_at and
completed_at in § 5.
03Package structure
Each export is a single ZIP archive.
Single-patient package Single
ehi-export-client-{patient_id}-{YYYY-MM-DD}.zipPopulation package Population
ehi-export-population-{org_id}-{YYYY-MM-DD}.zipRules that apply to both
dictionary.jsonis the authoritative index. Read it first. It names every data file, where it came from, how it was linked to a patient, every column and its type, and how many rows were written. It also records what was excluded and why, and anything that failed.- A CSV containing only a header row is normal and is not an error. It means EMR-Bear stores nothing in that table for the scope of this export. A file that is missing entirely while listed in
dictionary.jsonis a defect. - The
mongo_prefix distinguishes document-store collections from relational tables and prevents a collection name from colliding with a table name. The_index/directory is prefixed for the same reason: it can never collide with a table namedindex. assets/is absent when there are no stored documents.dictionary.jsonalways reports the document count, so an absent directory is never ambiguous.- Files are linked to their records by the
idcolumn of the corresponding CSV: a file atassets/documents/45231/scan.pdfbelongs to the row withid45231indocuments.csv. Becauseidis a primary key, this layout is unchanged between the two exports — the path is already unambiguous across the whole population.
Additional rules for the population package Population
-
patients.csvis the roster. One row per exported patient, with columnsclient_id,person_id,mrn,row_countandasset_count. It deliberately carries no demographics — those are inpeople.csv— so it serves as a completeness checklist without duplicating patient information. -
Large files are split into parts. A table is written as
<table>.part-0001.csv,<table>.part-0002.csvand so on once it would exceed 1 GB uncompressed or 5,000,000 rows, whichever comes first. Every part carries its own header row. Parts of one table are contiguous and ordered by primary key; concatenating them requires dropping the repeated headers. A table small enough not to be split has nopart-suffix at all. -
_index/attribution/<table>.csvmaps rows to patients for files where the link is not a column in the row itself. Columns:row_id,client_id. A row shared between patients appears once in the data file and several times in the map. Only files whoseattributionisindexhave one — see § 6. - The archive uses ZIP64. A population package routinely exceeds the 4 GB and 65,535-entry limits of the original ZIP format. Use a ZIP64-capable extractor; most modern tools are, but some older built-in utilities are not.
04CSV conventions
All CSV files in both exports follow RFC 4180 and the conventions below. No columns are added or removed between the two exports. A given table has the same columns, in the same order, whichever export it appears in — which is why patient attribution is carried in a separate index rather than as an extra column.
| Property | Value |
|---|---|
| Character encoding | UTF-8 |
| Delimiter | Comma (,) |
| Quoting | Fields containing a comma, quote or newline are double-quoted; embedded quotes are doubled |
| Line terminator | \n |
| Header row | Always present, always first, even when there are no data rows — and present in every part of a split file |
| Empty value | An empty, unquoted field represents NULL / absent |
Value representation
| Stored type | Representation in CSV |
|---|---|
| NULL | Empty field |
| Boolean | true / false |
| Timestamp | ISO 8601 in UTC — 2026-03-14T09:22:31Z |
| Date | YYYY-MM-DD — 2026-03-14 |
| Object or array | JSON — {"key":"value"} |
| Binary / BLOB | Base64, prefixed with the literal base64: — base64:iVBORw0KGgo… |
| Everything else | The stored value as text |
Text handling
Clinical free text in a long-lived record can contain byte sequences that are not valid UTF-8, and NUL bytes. Because a corrupted CSV is worse than an absent one — it looks complete — the export normalises text rather than failing:
- Invalid UTF-8 byte sequences are removed.
- NUL bytes (
0x00) are removed. - Newlines inside a value are preserved and the field is quoted.
Parse these files with a real CSV parser. Do not count lines to count records — clinical notes contain newlines.
The [unrepresentable: …] sentinel
If a single stored value cannot be rendered at all, the cell is written as the literal marker:
[unrepresentable: Encoding::UndefinedConversionError]This is a marker, not patient content. It exists so that one bad value cannot cost the rest of the table. It is rare; where you encounter one, the underlying value is still held in EMR-Bear and can be retrieved on request.
Document-collection (mongo_*.csv) specifics
A document store has no fixed schema, so:
- The header row is the union of the field names actually present across the documents in scope for that collection, in first-seen order. Fields no document uses do not appear.
- A document lacking a field that appears in the header has an empty cell for it.
- Nested objects and arrays are written as JSON.
- Column types are inferred from the data — the type of the first non-
NULLvalue seen for that field — and are reported indictionary.jsonas lower-cased runtime type names such asstring,integer,time,bson::objectid. They are not declared SQL types.
Because the header is the union of fields actually seen, a collection’s header in a population export is generally wider than in any one patient’s export, and the columns may appear in a different order. Read columns by name, never by position.
05dictionary.json
The machine-readable description of the package. It is generated while the export is being written, so it describes what actually happened rather than reconstructing it afterwards. Both exports produce one, with the same top-level shape.
Top-level structure
{
"export": { … }, // object — summary of this export
"notes": [ … ], // strings — conventions and stated limits, in prose
"files": [ … ], // objects — one per data file written
"assets": [ … ], // objects — one per attachment type copied
"excluded": [ … ], // objects — what was deliberately not exported, and why
"failures": [ … ] // objects — what could not be read (empty in a clean export)
}
There is no patient roster in dictionary.json. In a population export the roster is
patients.csv, so that a consumer does not have to parse a JSON array of every patient in
the organisation to learn anything about the package.
export
| Key | Type | Scope | Meaning |
|---|---|---|---|
| spec | string | Both | The discriminator. ONC 170.315(b)(10)(i) for a single-patient export, ONC 170.315(b)(10)(ii) for a population export. Branch on this key. |
| client_id | integer | Single | EMR-Bear’s internal identifier for the exported patient. Absent in a population export. |
| client_count | integer | Population | Number of patients in the export; matches the row count of patients.csv. Absent in a single-patient export. |
| generated_at | string | Both | ISO 8601 UTC timestamp of the export |
| started_at | string | Population | ISO 8601 UTC timestamp at which reading began |
| completed_at | string | Population | ISO 8601 UTC timestamp at which the last file was written |
| consistency | string | Population | not-point-in-time. See § 8. |
| file_count | integer | Both | Number of data files described in files, counting each part of a split file separately |
| row_count | integer | Both | Total data rows across all files |
| asset_count | integer | Both | Documents and images copied into assets/ |
| assets_missing | integer | Both | Files a record refers to that could not be retrieved from storage |
files[] — one entry per CSV written, sorted by filename
| Key | Type | Scope | Meaning |
|---|---|---|---|
| file | string | Both | Filename within the package, e.g. encounters.csv |
| source | string | Both | mysql or mongodb — see vocabularies below |
| table | string | Both | Source table or collection name. Every part of a split file carries the same value. |
| tier | string | Both | How the rows were linked to a patient — see vocabularies below |
| links | string[] | Both | Human-readable description of the link(s) used to select rows. Not a machine-parseable column list — values include plain column names ("client_id"), polymorphic pairs ("contactable_id + contactable_type"), indirect hops ("through crew_teams") and, for people.csv, "id (type = 'Client')". Treat it as provenance for a reader, not as a filter to re-execute. |
| attribution | string | Population | How a row in this file is tied to a patient — see vocabularies below |
| description | string | Both | Plain-language description of what the table holds, or Not yet described. |
| row_count | integer | Both | Data rows in this file, excluding the header. For a split file, rows in this part only. |
| part | integer | Population | 1-based index of this part. Present only on split files. |
| part_count | integer | Population | Total parts for this table. Present only on split files. |
| columns | object[] | Both | {"name", "type"}, in file column order |
assets[] — one entry per table and attachment kind
| Key | Type | Meaning |
|---|---|---|
| table | string | The table whose rows own these files |
| attachment | string | The attachment field on that record, e.g. document |
| files | integer | Files successfully copied into assets/ |
| bytes | integer | Total size in bytes of those files |
| missing | integer | Files the record refers to that were not retrievable |
excluded[] and failures[]
| Array | Keys | Meaning |
|---|---|---|
| excluded | table, reason | A table or collection deliberately not exported, with the reason. The list differs between the two exports — see § 7. |
| failures | table, error, message | A table that could not be read and was not exported at all; its absence is declared rather than silent. message is truncated to 500 characters. Present and empty in a clean export. |
Controlled vocabularies
source takes exactly one of:
| Value | Meaning |
|---|---|
| mysql | Relational database table |
| mongodb | Document database collection |
tier takes exactly one of:
| Value | Scope | Meaning |
|---|---|---|
| 0-root | Both | A patient’s own record (people.csv), matched on the primary key |
| 1-person_scoped | Both | Linked by a person_id or patient_id column |
| 1-person_polymorphic | Both | Linked by a polymorphic owner pair declared on the person record |
| 2-client_id | Both | Linked by a client_id column, or a custom foreign key holding a patient’s id |
| 3-client_polymorphic | Both | Linked by a polymorphic owner pair, e.g. contactable_id + contactable_type |
| 4-second_hop | Both | Reached through an intermediate record, for tables with no direct patient column |
| 5-mongodb | Both | Document collection, linked by the client_id field |
| 6-unscoped | Population | Included because the table holds electronic health information, though its rows cannot be attributed to an individual patient. Never appears in a single-patient export. |
attribution Population takes exactly one of:
| Value | Meaning |
|---|---|
| row | The patient link is a column in the row itself. links names it. Filter the CSV directly. |
| index | The link is not readable from the row. _index/attribution/<table>.csv maps row_id to client_id, one-to-many where a record is shared. |
| none | The rows are electronic health information but are not attributable to an individual patient. Always paired with tier 6-unscoped. |
Worked example — single-patient Single
Abbreviated. All values are synthetic.
{
"export": {
"spec": "ONC 170.315(b)(10)(i)",
"client_id": 100042,
"generated_at": "2026-08-28T14:03:11Z",
"file_count": 312,
"row_count": 4871,
"asset_count": 26,
"assets_missing": 0
},
"notes": [
"One CSV per table. A file with only a header row means nothing is stored for this
client in that table; a table listed under \"failures\" was not exported at all."
],
"files": [
{
"file": "encounters.csv",
"source": "mysql",
"table": "encounters",
"tier": "2-client_id",
"links": ["client_id"],
"description": "Clinical encounters — a visit or contact that clinical
documentation hangs from.",
"row_count": 143,
"columns": [
{ "name": "id", "type": "integer" },
{ "name": "client_id", "type": "integer" },
{ "name": "start_time", "type": "datetime" },
{ "name": "note", "type": "text" }
]
}
],
"assets": [
{ "table": "documents", "attachment": "document",
"files": 26, "bytes": 8134902, "missing": 0 }
],
"excluded": [
{ "table": "hl7_endpoints",
"reason": "Holds TLS certificates and private keys — must never be exported" }
],
"failures": []
}Worked example — population Population
The same package, widened. All values are synthetic.
{
"export": {
"spec": "ONC 170.315(b)(10)(ii)",
"client_count": 24718,
"generated_at": "2026-09-11T02:14:56Z",
"started_at": "2026-09-10T22:40:03Z",
"completed_at": "2026-09-11T02:14:56Z",
"consistency": "not-point-in-time",
"file_count": 488,
"row_count": 91344027,
"asset_count": 412903,
"assets_missing": 17
},
"notes": [
"Rows span all patients. Use patients.csv as the roster and the file's
\"attribution\" value to determine how to tie a row to a patient.",
"This export is not a point-in-time snapshot. See the published format
documentation, section 8."
],
"files": [
{
"file": "encounters.part-0001.csv",
"source": "mysql",
"table": "encounters",
"tier": "2-client_id",
"links": ["client_id"],
"attribution": "row",
"description": "Clinical encounters — a visit or contact that clinical
documentation hangs from.",
"row_count": 5000000,
"part": 1,
"part_count": 3,
"columns": [
{ "name": "id", "type": "integer" },
{ "name": "client_id", "type": "integer" },
{ "name": "start_time", "type": "datetime" },
{ "name": "note", "type": "text" }
]
},
{
"file": "crew_team_members.csv",
"source": "mysql",
"table": "crew_team_members",
"tier": "4-second_hop",
"links": ["through crew_teams"],
"attribution": "index",
"description": "Membership of a care team.",
"row_count": 88214,
"columns": [
{ "name": "id", "type": "integer" },
{ "name": "crew_team_id", "type": "integer" },
{ "name": "person_id", "type": "integer" }
]
}
],
"assets": [
{ "table": "documents", "attachment": "document",
"files": 412903, "bytes": 119338204471, "missing": 17 }
],
"excluded": [
{ "table": "hl7_endpoints",
"reason": "Holds TLS certificates and private keys — must never be exported" }
],
"failures": []
}06Linkage and attribution
How rows are selected
In a single-patient export, every row is tied to the patient by one of the mechanisms below, and the
mechanism used for each file is reported in that file’s tier and links
values. The same mechanisms describe how population rows relate to patients.
- The patient’s own record. Matched by primary key in the shared
peopletable and written topeople.csv. - A direct identifier column. The table carries
client_id,person_idorpatient_idholding the patient’s identifier. - A polymorphic owner. The table carries an
<owner>_id/<owner>_typepair. - A second hop. For tables with no direct patient column, rows are reached through an intermediate record.
A polymorphic type column holds the value Person, never Client. Patients
are stored as a subtype of person and the stored discriminator is the base type. Filtering on
Client returns no rows.
Union of links Single
Where a table can be reached in more than one way — for example a table carrying both a
client_id column and a polymorphic owner — the single-patient export takes the
union of the direct links, so rows set by either mechanism are included. Indirect
second-hop links are not unioned with direct ones, because some intermediate records are shared
between patients and unioning them would pull in another patient’s rows.
This restriction does not exist in the population export, where there is no other patient to leak into. A population export takes every row of an included table.
Attribution Population
A single-patient package carries attribution implicitly: everything in it belongs to the one patient,
including second-hop rows that have no patient column of their own. A population package cannot rely
on that, so every file declares an attribution value in dictionary.json:
row— filter the CSV on the column named inlinks. This covers the large majority of files.index— read_index/attribution/<table>.csv, a two-column map ofrow_idtoclient_id.none— the rows are health information not attributable to an individual.
A record shared between patients — a care team, say — is written once in the data file and appears once per patient in the attribution map. This is a deliberate difference from the single-patient exports of those same patients, where the shared record is duplicated into each package because each package stands alone.
Attachments are copied only for rows that were actually written, so a document can never ship without the record it belongs to.
07What is not included, and why
Everything else is exported. Each exclusion is listed with its reason in the excluded
array of every export. The lists differ: some exclusions exist only because a
single-patient export must be scoped to one patient, and those do not survive into the population
export.
| Excluded | Scope | Reason |
|---|---|---|
| hl7_endpoints | Both | Holds TLS certificates and private keys — must never be exported, and is not health information |
| client_imports authorization_imports superbill_imports otp_order_imports otp_verification_imports |
Both | Import staging areas, not records in the designated record set. The resulting patient records are exported. |
| billing_configurations terms_conditions fax_cover_sheets education_materials |
Both | System configuration, not health information |
| Person records not marked as clients | Both | Not patients. A patient’s record of another person is exported with the patient’s data; that person’s own record is outside the patient’s designated record set. See § 1. |
Collections with no client_id |
Single only | Cannot be scoped to a single patient. These are included in the population export, with tier 6-unscoped and attribution none. |
| Records with no patient link | Single only | Same reason, for relational tables. Included in the population export on the same terms. |
Nothing else is withheld from either export. In particular, soft-deleted rows, superseded rows and hidden duplicate patient records are included in both.
08Stated limits of the export
These are properties a recipient could not infer from the files themselves. They are repeated in the
notes array of dictionary.json.
Both exports
people.csvmay contain more rows than you expect. Where duplicate records for the same person have been merged or hidden, the related rows are linked byhidden_active_client_idand all of them are exported. A single-patient export can therefore contain severalpeople.csvrows for one patient.- A document a record references may be unretrievable. Where a record names a stored file that could not be retrieved from object storage, the record still ships and the file is counted in
assets[].missingandexport.assets_missing. A non-zero value there means the package is short by that many files — declared, rather than silent. - A table that could not be read is declared. It appears in
failures[]with the error and is never silently omitted.failures[]is empty in a clean export. - Row counts in
dictionary.jsonare authoritative. If a file’s row count disagrees with the dictionary, treat the package as defective and request a fresh export.
Single-patient export only Single
versions.csvis not a complete audit trail. Only rows whoseitem_typeisPersonare patient-scoped and exported. Change history for related records carries otheritem_typevalues and is not included.- Records with no patient link are absent. Health information that cannot be attributed to the patient is out of scope for this export by construction. It is present in the population export.
Population export only Population
-
This is not a point-in-time snapshot. A population export runs for hours and reads
two independent stores. Rows written near the start and near the end reflect different states of
the database, and there is no cross-store transactional boundary between MySQL and MongoDB. The
window is declared in
export.started_atandexport.completed_at, andexport.consistencyisnot-point-in-time. Where exact consistency matters, reconcile against the source system rather than inferring it from the package. -
versions.csvis complete — the single-patient restriction toitem_typePersondoes not apply here. The full change history ships. -
Rows with
attributionnonecannot be tied to a patient. They are included because they are health information; no map exists to attribute them, and none can be constructed from the package. -
Split files repeat their header. Concatenating
<table>.part-*.csvnaively produces a header row in the middle of the data. Drop the first line of every part after the first. -
Document-collection headers are wider. A
mongo_*.csvheader in a population export is the union of fields across all patients, so it is generally wider than the same collection in a single-patient export and the column order may differ. Read by column name.
09Reading an export
Both packages are designed to be readable with ordinary tooling and no EMR-Bear software.
- Unzip the archive. Use a ZIP64-capable extractor for a population package.
- Open
dictionary.json. Readexport.specto learn which export you have. - Use
files[]to find the tables relevant to your purpose — each carries a plain-languagedescription. - Load the CSVs with any RFC 4180 parser — spreadsheet software,
pandas.read_csv, R’sreadr, Ruby’sCSV. Parse them; do not count lines. - Where a file has
part_count, load every part and drop the repeated header rows. - Join across files on the identifier columns, which retain EMR-Bear’s internal values consistently across the whole package.
- Find a record’s documents at
assets/<table>/<id>/, where<id>is the record’sidcolumn value. - Decode any value beginning
base64:by stripping that prefix and Base64-decoding the remainder.
Isolating one patient from a population export Population
- Find the patient in
patients.csvand take theirclient_id. - For each file in
files[], branch onattribution:row— filter the CSV on the column named inlinks.index— read_index/attribution/<table>.csv, collect therow_idvalues for thatclient_id, and select those rows byid.none— skip; these rows belong to no individual.
- Collect the corresponding
assets/<table>/<id>/directories for the rows you selected.
The result is equivalent in content to that patient’s single-patient export, with two known
differences: the population export also contains their versions history beyond
item_type Person, and shared records appear once rather than duplicated.
10Format versioning and currency
This document describes format version 2.0.
A consumer written against version 1.0 reads a version 2.0 single-patient export without modification. No structure, key, convention or vocabulary value used by the single-patient export changed in 2.0. Everything added in 2.0 is either new to the population export or additive within it.
Changes in 2.0
- Added the patient-population export, § 170.315(b)(10)(ii).
- Added
patients.csv,_index/attribution/, and file splitting — population export only. - Added
exportkeysclient_count,started_at,completed_atandconsistency— population export only. - Added
files[]keysattribution,partandpart_count— population export only. - Added
tiervalue6-unscopedand theattributionvocabulary — population export only. - Documented that non-client person records are not exported, in both exports. This describes existing behaviour; it is not a change to the export.
Currency
The format is generated directly by the export code rather than transcribed from it: the column
lists, types, row counts and table descriptions in dictionary.json are produced from the
live schema at export time. As a result, the dictionary.json inside any given package is
always an accurate description of that package, even if this document has not yet caught up with a
very recent schema change.
Per § 170.315(b)(10)(iii), this document is kept up to date for both export formats. It is
maintained in the same repository as the export implementation and is reviewed whenever either
export’s structure, conventions or exclusions change. Changes that would affect a consumer
— a new or removed top-level dictionary.json key, a change to value encoding, a
change to package layout, or a new source, tier or attribution
value — increment the version above.
11Contact
Questions about this format, or a request for data that appears to be missing from an export, should be directed to EMR-Bear support through your organisation’s usual support channel.