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(...)  .


graphql
mutation createClient($input: createClientInput) {
  createClient(input: $input) {
    user {
      id
      metadata
    }
    messages {
      field
      message
    }
  }
}

json
{
  "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.


graphql
mutation updateAppointment($input: updateAppointmentInput) {
  updateAppointment(input: $input) {
    appointment {
      id
      metadata
    }
    messages {
      field
      message
    }
  }
}

json
{
  "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.


graphql
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.


graphql
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_id   or warehouse_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 messages   field on the mutation payload. Check messages   on 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

Related help articles

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.