Storing Metadata in Healthie
Storing Metadata in Healthie
Metadata is a flexible field available on many Healthie resources that lets you attach your own structured data to a record without changing your data model or standing up a separate mapping table. It is the standard way to carry external identifiers, integration state, and system-of-origin context alongside the Healthie records they belong to.
This article covers where metadata is supported, how to write and read it, what the limits are, and when to use something else instead.
When to use metadata
Metadata is a good fit when you need to:
- Store the ID of a matching record in your own system, a CRM, a billing platform, or a data warehouse
- Record where a record came from, such as a specific intake funnel, campaign, partner, or migration batch
- Carry integration state that your application needs but Healthie does not model, such as a sync timestamp or a processing status
- Tag records for downstream reporting in your warehouse
Metadata is not a good fit for clinical data, anything a provider needs to read in the Healthie UI, or anything you need to search or filter on. See When to use something else below.
Where metadata is supported
Metadata is available across scheduling, charting, billing, and patient records. The field is typed as either String or JSON depending on the resource, and character limits vary.
Patients and providers
| Resource | Write with | Type | Limit |
|---|---|---|---|
| User (patient) | createClient , updateClient |
String |
128,000 |
| Organization member | updateOrganizationMember |
String |
10,000 |
Scheduling
| Resource | Write with | Type | Limit |
|---|---|---|---|
| Appointment | createAppointment , updateAppointment |
JSON |
No documented limit |
| Appointment type | createAppointmentType , updateAppointmentType |
String |
128,000, staff and providers only |
Charting and forms
| Resource | Write with | Type | Limit |
|---|---|---|---|
| Custom module form (form template) | createCustomModuleForm , updateCustomModuleForm |
String |
10,000 |
| Custom module (form field) | createCustomModule , updateCustomModule |
String |
128,000 |
| Form answer group (completed form) | createFormAnswerGroup , updateFormAnswerGroup |
String |
128,000, staff and providers only |
| Form answer | FormAnswerInput |
String |
128,000 |
| Requested form completion | createRequestedFormCompletion |
String |
128,000, staff and providers only |
| Smart phrase | createSmartPhrase , updateSmartPhrase |
JSON |
50 keys, 2,000 characters total |
Clinical records
| Resource | Write with | Type | Limit |
|---|---|---|---|
| Allergy or sensitivity | createAllergySensitivity , updateAllergySensitivity |
JSON |
50 keys, 2,000 characters total |
| Medication | createMedication , updateMedication |
JSON |
50 keys, 2,000 characters total |
| Diagnosis | DiagnosesInput |
JSON |
50 keys, 2,000 characters total |
Billing and insurance
| Resource | Write with | Type | Limit |
|---|---|---|---|
| Billing item | createBillingItem , updateBillingItem |
String |
128,000 |
| Requested payment | createRequestedPayment , updateRequestedPayment |
JSON |
2,000 |
| Policy | updatePolicy , UserPolicyInput , ClientPolicyInput |
JSON |
2,000 |
| Insurance plan | createInsurancePlan , updateInsurancePlan |
JSON |
2,000 |
| Benefit | BenefitInput |
JSON |
2,000 |
Everything else
| Resource | Write with | Type | Limit |
|---|---|---|---|
| Task | createTask , updateTask |
JSON |
2,000 |
| Conversation | createConversation , updateConversation |
JSON |
2,000 |
| Document | createDocument , updateDocument |
String |
128,000, staff and providers only |
| Referral | createReferral , updateReferral |
String |
128,000 |
| Referring physician | createReferringPhysician , updateReferringPhysician |
String |
128,000 |
A small number of resources document a different limit on the read field than on the write input. Before you build against a limit, confirm it on the reference page for the exact input type you are using.
Writing metadata
String fields
For resources where metadata is typed as String , serialize your JSON object before sending it. In JavaScript, that means JSON.stringify(...) .
mutation createClient($input: createClientInput) {
createClient(input: $input) {
user {
id
metadata
}
messages {
field
message
}
}
}
{
"input": {
"first_name": "Jordan",
"last_name": "Reyes",
"email": "jordan.reyes@example.com",
"dietitian_id": "12345",
"metadata": "{\"crm_id\":\"cus_8823\",\"source\":\"partner_referral\",\"synced_at\":\"2026-08-06T14:22:00Z\"}"
}
}
JSON fields
For resources where metadata is typed as JSON , send the object directly. No string escaping is needed.
mutation updateAppointment($input: updateAppointmentInput) {
updateAppointment(input: $input) {
appointment {
id
metadata
}
messages {
field
message
}
}
}
{
"input": {
"id": "98765",
"metadata": {
"scheduling_source": "member_app",
"campaign_id": "q3_reengagement",
"external_visit_id": "visit_44219"
}
}
}
Updates replace, they do not merge
Writing metadata overwrites the entire stored value. There is no partial update. If you need to change one key, read the current metadata, merge your change in your own code, and write the full object back. If two systems write metadata on the same record, coordinate ownership of keys or you will lose data.
Reading metadata
Request metadata like any other field. It is returned in the same form it is stored, so String fields come back as a serialized string that your client needs to parse.
query user($id: ID) {
user(id: $id) {
id
first_name
last_name
metadata
}
}
Metadata is also available on list queries, so you can pull it in bulk alongside the records themselves.
query users($page_size: Int, $offset: Int) {
users(page_size: $page_size, offset: $offset) {
id
metadata
}
}
Limitations to plan around
Metadata is not searchable or filterable. No query accepts metadata as a filter argument. You cannot ask Healthie for "the patient whose crm_id is cus_8823 ." If you need lookup by an external identifier, use one of the alternatives below or maintain the mapping on your side.
Metadata is not visible in the Healthie UI. Providers and staff will not see it in the patient chart or anywhere else in the product. It exists for your integration, not for clinical users.
Webhooks do not carry metadata. Healthie sends thin payloads containing the resource ID, resource type, and event type. Fetch the record through the API after receiving the webhook if you need its metadata.
Some fields are permissioned. Metadata on appointment types, documents, form answer groups, and requested form completions is only accessible to staff and providers. Client-scoped API keys will not return it.
Sandbox does not allow PHI. Do not put protected health information in metadata in the Sandbox environment. In Production, treat metadata as you would any other field holding patient data.
When to use something else
| If you need to | Use |
|---|---|
| Look up a patient by an ID from your system | record_identifier or additional_record_identifier on User. Both are searchable through the keywords argument on the users query. |
| Relate a form template, form field, completed form, or journal entry to a third-party object | external_id and external_id_type , available on CustomModule , CustomModuleForm , FormAnswerGroup , and Entry . |
| Capture clinical or intake information | A custom module on a charting form, so the data is visible to providers, chartable, and reportable. |
| Segment or filter patients in the UI | Tags or user groups. |
Best practices
- Namespace your keys. Prefix keys with the owning system, such as
crm_idorwarehouse_synced_at, so it stays obvious where a value came from when a second integration is added. - Keep values flat and small. Short key-value pairs are easier to reason about and stay well inside the limits. Several resources cap metadata at 2,000 characters, which is smaller than teams expect.
- Validate length before you write. Exceeding the limit returns a validation error in the
messagesfield on the mutation payload. Checkmessageson every write. - Do not use metadata as a source of truth. It is a place to carry references and integration state, not a substitute for storing the record properly in Healthie or in your own database.
- Document your schema internally. Metadata is untyped by design. Without a documented key convention, it becomes unreadable within a few quarters.
Guides
Schema reference
JSONscalar lists every type that uses aJSONmetadata fieldUserandcreateClientInputAppointmentandupdateAppointmentInputFormAnswerGroupandcreateFormAnswerGroupInputBillingItemandcreateBillingItemInputTaskandcreateTaskInput
Related help articles