Customizing Paperwork

Paperwork is a key component of every medical practice's intake process. The process varies widely from one practice to another but is a consistent challenge for patients, providers, and health tech developers. Ottehr utilizes features of the FHIR standard and the Oystehr platform to make this process easier, allowing you to achieve a custom intake flow tailored to the needs of your practice while hopefully simplifying the work of developers, front desk employees, and providers.

The Pieces

Overview

The Questionnaire Resource

This documentation goes into detail on FHIR resources. If you do not have any experience with FHIR, that's ok -- you don't need to have experience to customize the paperwork. If you would like to read about FHIR you can check out our documentation about FHIR.

Every intake flow begins with a FHIR Questionnaire (opens in a new tab) resource. It defines the form that is presented to your patients who will then fill out and submit a QuestionnaireResponse (opens in a new tab).

If you explore the Questionnaire spec, you might interpret it to be generic and minimalist by design. This is both good and bad; there's not much it can't be used for, but there's little chance the default properties are going to be sufficient to power those slick features your product manager is asking for. Ottehr fills in these gaps with some nifty extensions that we think will give you the tools to build most of those features -- and the ability to add additional features required.

The React Components

By convention, the Ottehr paperwork system divides your Questionnaire into pages with each top-level object on the "item" field (opens in a new tab) representing a page, and its child elements defining the contents of that page.

The heart of the UI is the PagedQuestionnaire component, which renders the form defined by the item the user is viewing. This component gets wrapped by the PaperworkPage in Ottehr, which passes in the Questionnaire items, initial values, and some handlers that are invoked to save progress at the end of each page and submit the completed QuestionnaireResponse at the end of the flow.

  <PagedQuestionnaire
    onSubmit={finishPaperworkPage}
    pageId={currentPage?.linkId ?? ''}
    options={{ controlButtons }}
    items={questionnaireItems}
    defaultValues={paperworkGroupDefaults}
    saveProgress={(data) => {
      const pageId = currentPage?.linkId;
      if (pageId) {
        saveProgress(pageId, data);
      }
    }} 
  />

The PagedQuestionnaire handles the task of translating the custom extensions on an Ottehr Questionnaire into dynamic behaviors, from the simple—like validating required fields, to the more complex—like conditionally rendering inputs, to the downright fancy—like asynchronously verifying a patient's insurance information with the Oystehr eligibility service.

The Harvest Module

As the user fills out the form, a patch-paperwork zambda is invoked as each page is completed, while a submit-paperwork zambda is invoked at the end, changing the paperwork status from 'in-progress', to 'complete'. During this whole process, Oystehr abstains from writing to any FHIR resources outside of the QuestionnaireResponse the patient is populating with answers. How then, do the users answers find their way into the more interesting resources, like Patient, RelatedPerson, Coverage, etc? The answer to that is something we've dubbed "The Harvest Module."

The Harvest Module is the locus of the complex bits of business logic where you translate the input from patients via your intake form(s) into the structured data that powers your practice. If we've done our jobs right, this is where you're likely to concentrate your own dev hours in adapting Ottehr to your use case. Outside of the Questionnaire resource itself, it's the layer you'll most likely want to customize.

But what is it? Under the hood, this module is implemented as an Oystehr subscription Zambda, and gets run each time a QuestionnaireResponse is updated with a status of 'completed' or 'amended'.

But why? Decoupling the generic work of saving user progress from the specialized work of translating any given completed form into other FHIR resources ensures that only the latter task requires new development work each time you change or add on to your paperwork flows. This allows you to go back to the well again and again, defining a Questionnaire resource, feeding it into the frontend React components, and mapping the output QuestionnaireResponse to its own specialized subscription zambda for processing. So you might split a complex intake processes into multiple paperwork flows fed by multiple Questionnaires, or you might find some clinical application for a Questionnaire, such as allowing a physician to fill out a medication order form that triggers a call to some erx integration when it's harvested. It is our ambition that the limitations are imposed by the needs of your business and/or your imagination, and not the Ottehr tooling.

Form Pre-Population

The most common patient complaint about the intake process is needing to re-fill out paperwork with answers that haven't changed since their last visit. To address this pain point, the Ottehr intake system includes a pre-population module that is invoked at the time of appointment creation. You can think of this chunk of code as the harvest module in reverse: rather than populating structured FHIR resources with answers from the patient's QuestionnaireResponse, you're seeding the patient's QR with data culled from existing FHIR resources comprising that patient's record. The default Ottehr implementation (opens in a new tab) is a simple function that takes a few resources and some input from the call to the create-appointment zambda and outputs a list of response items that get incorporated into the QuestionnaireResponse that the patient sees when they open up the paperwork for that visit. Keep in mind that this is another area of code you'll want to customize with your own logic, as it depends on both the Questionnaire resource that feeds your flow, as well as what you do with user input in your harvest module.

Getting Started

Step 1 - Author Your Questionnaire

Step 2 - Provision Your environment

Step 3 - Customize Pre-Population

Update the pre-population function (opens in a new tab) (or just replace with your own implementation) to pre-fill any form values you want to pre-fill from existing resources.

Step 4 - Customize the Harvest Module

Update the harvest subscription zambda (opens in a new tab) (or create a new subscription zambda if you prefer) to pull values from patients' QuestionnaireResponse answers and write them to whatever FHIR resources makes sense for your use case.

Author Your Questionnaire

To create a your own custom paperwork flow, start by creating a Questionnaire in the FHIR DB, then populate the item prop on that Questionnaire with your questions.

Create Your Questionnaire in the FHIR DB

You're a wizard, so of course you can always POST a new Questionnaire resource via the FHIR API (opens in a new tab), but the easiest way to stick a new resource in the DB is probably going to be the Oystehr console's GUI (opens in a new tab). The required inputs are pretty basic: The FHIR spec requires that the "status" (opens in a new tab) field be provided (go with 'active' here), while Ottehr requires that you populate the components of the "canonical url"—"url" (opens in a new tab) and "version" (opens in a new tab)—because these are the things that will be used to refer to your Questionnaire by other parts of the system, rather than the id.

Some tips here:

  1. You don't need to worry too much about the "url". It doesn't need to go anywhere; it's used primarily as a namespace. {some-domain-you-control}/{some-slug-you-make-up-for-your-form} should do just fine.
  2. Use semantic versioning (opens in a new tab) for "version"
  3. Each time you make changes to your Questionnaire that will impact validation, it is strongly recommended that you increment "version". The validation scheme Ottehr applies to a QuestionnaireResponse will be derived from the exact canonical url of its target Questionnaire, so dutifully bumping that version helps you avoid self-inflicted migration headaches when you make changes to your Questionnaire.

Populate the "item" Field on Your Questionnaire

Once your Questionnaire exists, you're ready to fill it up with some questions.

In the Ottehr system, every page of paperwork is represented by an item on the Questionnaire with type "group". Every question on a page of paperwork is represented by an item on that page. Thus, Questionnaire.item (opens in a new tab) represents a page of paperwork, and every Questionnaire.item.item (opens in a new tab) represents a question on that page. Therefore, to add a page to the paperwork, you add an item to the Questionnaire (opens in a new tab), and to add a question to that page, you add an item to its Questionnaire.item (opens in a new tab).

At the moment, this is all accomplished in JSON, so find a good JSON editor and refer to our copyable examples and the default resources in /packages/utils/lib/deployed-resources/questionnaires to guide you.

💡

A common workflow here is to take the JSON from your Questionnaire and drop it in the /packages/utils/lib/deployed-resources/questionnaires folder, allowing you to edit the JSON directly in your code editor and push the changes to Oystehr by restarting your local server. Change one of the PREVISIT_QUESTIONNAIRE env vars in your local.json file to point at your Questionnaire's canonical url and then test it on your local Ottehr app by navigating through the appropriate flow. As you fill out your form, pull up the corresponding QuestionnaireResponse in the Oystehr console (opens in a new tab) to see how the answers are getting stored.

Useful FHIR-native Props

There are a few properties on Questionnaire.item that bear highlighting:

"required" and "readOnly"

The first are the required (opens in a new tab) and readOnly (opens in a new tab) fields. These are intuitive and implemented by the Ottehr paperwork components: items that have required = true will have required validation enforced, while items that have readOnly = true will either be hidden or renderd as disabled inputs depending on the configuration in the disabledDisplay.

"enableWhen"

The next property to know about is enableWhen (opens in a new tab). This property allows you to conditionally enable/disable an item based on the value of some other item(s) in the QuestionnaireResponse and is important not only in its own right, but also because the pattern it establishes for expressing conditional behavior is borrowed by a number of custom Ottehr extensions.

The enableWhen field takes a list of objects with three parts:

  1. question - this the linkId of some other item in the Questionnaire/QuestionnaireResponse
  2. operator - = OR != in current implementation; this is the operator used to compare the current form-state value for the item identified by question to the value sepcified in...
  3. answer[x] - the value to check the current form value for question against using the specified operator.

So if we have an item with the following as the sole entry on its enableWhen field:

"enableWhen": [
  {
    "question": "current-medications-yes-no",
    "operator": "=",
    "answerString": "Patient takes medication currently"
  }
]

the input for this item will be enabled whenever the value in the QuestionnaireResponse for the item with linkId "current-medications-yes-no" contains a string answer equal to "Patient takes medication currently", otherwise it will be disabled.

You will see this same scheme for defining conditional behavior on the following custom extensions: requireWhen, textWhen, and filterWhen.

"enableBehavior"

Recall that enableWhen takes a list of condition-defining objects, not just a single one. enableWhen exists to define the logic that is applied to determine whether the overall condition is met when multiple entries are included. It is only relevant when an item has an enableWhen property containing mutliple conditions, and must be specified on such items. It has two options:

  1. any - the condition is met when any condition in the list is satified
  2. all - the condition is met when all conditions in the list are satisfied

Ottehr Extensions

Before your raw FHIR Questionnaire resource reaches the Ottehr intake app, it goes through a transformation pipeline that takes any recognized extensions inside your Questionnaire.item field and turns them into properties that drive custom behaviors when the intake app renders the form to the user. You can check out that code here (opens in a new tab), but it is probably most important at the outset to get familiar with the extensions available and the specific behaviors they support:

All QR answers are persisted as arrays, but the Ottehr validator will enforce a single-member rule for any answer submitted unless the corresponding Questionnaire item has this extension set to true


template:

{
  "url": "https://fhir.zapehr.com/r4/StructureDefinitions/accepts-multiple-answers",
  "valueBoolean": true
}

default value:

false

Let the Ottehr validator know this item accepts a list of answers

A powerful pattern is to include some pre-populated fields in the QuestionnaireResponse, and to use the values on those fields to drive dynamic behavior in the form. In such cases it is also commonly desired to remove those fields from the QR prior to persisting. Attaching this extension to an item ensures the answer items it stores will be filtered out of the completed form prior to persistence.


template:

{
  "url": "https://fhir.zapehr.com/r4/StructureDefinitions/always-filter",
  "valueBoolean": true
}

default value:

false

Any value found in this field will be stripped from the form prior to saving.

Whenever you define a choice, or free-choice type item, you have the ability to attach either a literal answer options list or reference to a ValueSet resource. By default these are packaged up in the get-paperwork zambda and returned as part of the package of data that powers the paperwork flow. If you have a particularly large answer set, you may want to hold off fetching it up until the user confronts the input where it's needed. answerLoadingOptions gives you the ability to implement that dynamic behavior. To do this, you'll specify dynamic on the strategy component (see template) and a FHIR query to fetch up the resources representing your answer options on the valueExpression.expression of the source component. It's also possible to omit the source when the item contains a ValueSet reference on the answerValueSet field, in which case that value set will be dynamically fetched up when the dynamic strategy is specified

ℹ️
When specifying a source, it is recommended to only use resources that contain a `name` property because that is what will be used to display the option to the user in the UI. In the template example, the user will see the InsurancePlan.name as the selection option. Selecting an option will populate the answer with a valueReference referencing the selected resource by its Oystehr id.

template:

{
  "url": "https://fhir.zapehr.com/r4/StructureDefinitions/answer-loading-options",
  "extension": [
    {
      "url": "https://fhir.zapehr.com/r4/StructureDefinitions/strategy",
      "valueString": "dynamic"
    },
    {
      "url": "https://fhir.zapehr.com/r4/StructureDefinitions/source",
      "valueExpression": {
        "language": "application/x-fhir-query",
        "expression": "InsurancePlan?status=active&_tag=insurance-payer-plan"
      }
    }
  ]
}

supported item types:

choice, open-choice

Some config that tells the Ottehr paperwork components how to dynamically fetch up the options the user selects from

This text will be displayed on the media upload element in the paperwork flow. It is only relevant for attahcment-type items.

💡
You can put markdown text in valueString and it will be presented with your styling.

template:

{
  "url": "https://fhir.zapehr.com/r4/StructureDefinitions/attachment-text",
  "valueString": "Take a picture of the **front side** of your card and upload it here"
}

supported item types:

attachment

Provide some context or instructions on components asking a user to upload media

Sometimes you'll find you want to disable a question because some other answer the user provided makes the data you would have collected already implicity known. For example, the user indicates the patient is the responsible party for billing purposese and the data you seek for the responsible party has already been collected via other fields in your form. This extension lets you specify that another field to copy the item's value froms when other business logic has renedered the it disabled.


template:

{
  "url": "https://fhir.zapehr.com/r4/StructureDefinitions/fill-from-when-disabled",
  "valueString": "patient-street-address"
}
ℹ️
The string in valueString is the linkId of some other item in your form. That item's value will be mapped to the item with this extension if and when the item with the extension becomes disabled.
Set the answer item to a value referenced elsewhere in the form when disabled
⚠️
Experimental - we're playing around with this, it's not guaranteed to remain as part of the API.

It's best to point to a concrete example to explain this one. In the Ottehr virtual intake flow, there is a page for collecting a list of the patient's allergies. The patient allergy parent item divides into two sub-items, medicine and other. As the user makes selections in the UI for rendering each child option, the combined selections are displayed on the parent and tagged according to which child they came from. The categoryTag just specifies the label to be used in that UI. We're not sure this is the right UI for this feature but are giving it a shot!


template:

{
  "url": "https://fhir.zapehr.com/r4/StructureDefinitions/category-tag",
  "valueString": "Other"
}

supported item types:

choice, open-choice

Tag an item as pertaining to a certain subcategory of its parent item

Often the same kind of item type can be used to collect very different kinds of data, for instance, a string type item may collect a zip code, or a phone number, or something else entirely. This extension allows your item to get more granular in terms of what it's collecting, which allows the Ottehr UI and validation layers to tailor the experience most suited for the specific data type.


template:

{
  "url": "https://fhir.zapehr.com/r4/StructureDefinitions/data-type",
  "valueString": "ZIP"
}

value options:

ZIP, Email, Phone Number, DOB, Signature, Image, PDF, Payment Validation

Identifies the type of data this item expects to collect

Sometimes a disabled item should just be hidden from the user, but other times you might want it to be "grayed-out" but still visible in order to provide some context to the user. This extension makes that behavior configurable.


template:

{
  "url": "https://fhir.zapehr.com/r4/StructureDefinitions/disabled-display",
  "valueString": "protected"
}

value options:

hidden, protected

default value:

hidden

Specify how an input element behaves when disabled

This extension is similar to alwaysFilter in its effect, but allows for conditional application based on the value of some other item in the form, following the same pattern as enableWhen.


template:

{
  "url": "https://fhir.zapehr.com/r4/StructureDefinitions/filter-when",
  "extension": [
    {
      "url": "https://fhir.zapehr.com/r4/StructureDefinitions/filter-when-question",
      "valueString": "payment-option"
    },
    {
      "url": "https://fhir.zapehr.com/r4/StructureDefinitions/filter-when-operator",
      "valueString": "!="
    },
    {
      "url": "https://fhir.zapehr.com/r4/StructureDefinitions/filter-when-answer",
      "valueString": "I will pay without insurance"
    }
  ]
}
💡
The pattern for defining the condition here is identical to "enableWhen".

supported item types:

boolean, string, text

Conditionally filter this item out of the QuestionnaireResponse prior to submitting

Similar to dataType, but specifically for group type items. Using one of the value options lets Ottehr know to handle the group defined by the item in a special way.


template:

{
  "url": "https://fhir.zapehr.com/r4/StructureDefinitions/group-type",
  "valueString": "credit-card-collection"
}

supported item types:

group

value options:

credit-card-collection, list-with-form

Identifies a special purpose a `group` type item is serving

Ottehr will add a tooltip with the provided valueString to assist a user. todo: picture worth many words here


template:

{
  "url": "https://fhir.zapehr.com/r4/StructureDefinitions/information-text",
  "valueString": "Secondary insurance or additional insurance details"
}
Helper text to be included on the item's input element

If this extension isn't used, or if max is assigned, every input element takes up the maximum available space on the form page.


template:

{
  "url": "https://fhir.zapehr.com/r4/StructureDefinitions/input-width",
  "valueString": "m"
}

value options:

s, m, l, max

default value:

max

Assign a width category for the input element for your item

Some item types can be reasonably represented with multiple different elements. The best example are string/text type items, with might be represented with an <h4>, <h3>, <h2>, etc., element.


template:

{
  "url": "https://fhir.zapehr.com/r4/StructureDefinitions/preferred-element",
  "valueString": "h3"
}

supported item types:

string, text, choice, boolean

Specify the input element you want Ottehr to use for this item

This works like other conditional-type extensions, which all model their structure and implmentation on enableWhen. If the condition is met, the Ottehr validator treats the item as required, otherwise it defaults to the value on the item's "required" field.

⚠️
Make sure the "required" prop on the item is not set to true if this extension is used

template:

{
  "url": "https://fhir.zapehr.com/r4/StructureDefinitions/require-when",
  "extension": [
    {
      "url": "https://fhir.zapehr.com/r4/StructureDefinitions/require-when-question",
      "valueString": "payment-option"
    },
    {
      "url": "https://fhir.zapehr.com/r4/StructureDefinitions/require-when-operator",
      "valueString": "="
    },
    {
      "url": "https://fhir.zapehr.com/r4/StructureDefinitions/require-when-answer",
      "valueString": "I have insurance"
    }
  ]
}
💡
The pattern for defining the condition here is identical to "enableWhen".
Let the Ottehr validator know this item is required, but only when the specified condition is met.

This is an alternative field for passing in helper text. Unlike the "infoText" field, the content passed in here is displayed via a different tool-tip. todo: picture worth many words here


template:

{
  "url": "https://fhir.zapehr.com/r4/StructureDefinitions/information-text-secondary",
  "valueString": "Our care team uses this to inform treatment recommendations and share helpful information regarding potential medication side effects, as necessary."
}
Some additional helper text

Sometimes the copy you want to display for an item changes depending on some answer that has been supplied on another item. This extension allows you to do that using the same condition-defining pattern used in enableWhen. Additionally, it includes a "text-when-substitute-text" component for specifying the text to use when the condition is satisfied.


template:

{
  "url": "https://fhir.zapehr.com/r4/StructureDefinitions/text-when",
  "extension": [
    {
      "url": "https://fhir.zapehr.com/r4/StructureDefinitions/text-when-question",
      "valueString": "display-secondary-insurance"
    },
    {
      "url": "https://fhir.zapehr.com/r4/StructureDefinitions/text-when-operator",
      "valueString": "="
    },
    {
      "url": "https://fhir.zapehr.com/r4/StructureDefinitions/text-when-answer",
      "valueBoolean": true
    },
    {
      "url": "https://fhir.zapehr.com/r4/StructureDefinitions/text-when-substitute-text",
      "valueString": "Remove Secondary Insurance"
    }
  ]
}
💡
The pattern for defining the condition here is identical to "enableWhen".
Change the text label on an item when a condition is met

Fairly: if the supplied date on the answer does not eclipse the minimum age (in years) specified, the Ottehr validator will throw an error


template:

{
  "url": "https://fhir.zapehr.com/r4/StructureDefinitions/validate-age-over",
  "valueInteger": 18
}

supported item types:

date

Let the Ottehr validator know that a minimum age restriction applies

Provision Your Environment

TL;DR

Replace the resource value on one or both of the json files in /packages/utils/lib/deployed-resources/questionnaires with the your custom Questionnaire, or else add a new file in that folder with its own unique url and version on the resource field, and give it its own envVarName.

Under the Hood

When a patient creates an appointment in Ottehr, a QuestionnaireResponse resource gets created that references the same canonical url (opens in a new tab)—that is, the same unique url and version pair—as some specific Questionnaire resource in the FHIR DB via its questionnaire field (opens in a new tab). Ottehr comes with some default Questionnaires and is wired up to use them, but they are easily overridable.

If you open up the Ottehr project and navigate to /packages/utils/lib/deployed-resources, you'll find a folder called questionnaires that's home to a couple of json files. Adding a custom intake flow begins with updating the content of one of these files or else adding a new one. Crack one of these files open and you'll see an object with two top-level fields, envVarName and resource:

{
  "envVarName": "IN_PERSON_PREVISIT_QUESTIONNAIRE",
  "resource": {
    "resourceType": "Questionnaire",
    "url": "https://ottehr.com/FHIR/Questionnaire/intake-paperwork-inperson",
    "version": "1.0.3"
    ...
  }
}

The intake zambdas package contains a script, setup-questionnaires, that is run each time you deploy or start up your local server, and acts to provision your project's library of Questionnaire resources using the contents of this folder. The script ensures that each Questionnaire is saved to the FHIR DB and then stores the canonical url in your project's secrets (and the .env/local.json file if you're running locally) so it will be available to reference throughout the codebase.

Have a look at this function that looks up the Questionnaire for a given intake flow:

export const getCanonicalUrlForPrevisitQuestionnaire = (
  serviceMode: ServiceMode,
  secrets: Secrets | null
): CanonicalUrl => {
  let secretKey = '';
  if (serviceMode === 'in-person') {
    secretKey = SecretsKeys.IN_PERSON_PREVISIT_QUESTIONNAIRE;
  } else if (appointmentType === 'virtual') {
    secretKey = SecretsKeys.VIRTUAL_PREVISIT_QUESTIONNAIRE;
  }
  const questionnaireCanonURL = getSecret(secretKey, secrets);
  const [questionnaireURL, questionnaireVersion] = questionnaireCanonURL.split('|');
  if (!questionnaireURL || !questionnaireVersion) {
    throw new Error('Questionnaire url secret missing or malformed');
  }
  return {
    canonical: questionnaireCanonURL,
    url: questionnaireURL,
    version: questionnaireVersion,
  };
};

The code finds the environment secret pointing to the canonical url for either of the two default Questionnaire resources, which are for the in-person and virtual service modes, respectively.

The values returned from that function are then used to form the canonical reference used on the questionnaire field of the QuestionnaireResponse when a patient books an appointment:

const questionnaireResponseResource: QuestionnaireResponse = {
    resourceType: 'QuestionnaireResponse',
    questionnaire: `${questionnaire.url}|${questionnaire.version}`,
    status: 'in-progress',
    subject: { reference: patientRef },
    encounter: { reference: encUrl },
    item, // contains the pre-populated answers for the Patient
  };

From there, the exact Questionnaire will be querried up from the reference on the QuestionnaireResponse and will be used to derive the validation logic and other special behaviors the patient sees when completing the form you've cooked up for them.

Questionnaire.item Examples

Simple, Two-page Questionnaire:

[
  {
     "linkId": "my-first-page",
     "type": "group",
     "item": [
      {
         "linkId": "some-label-on-my-first-page",
         "type": "display",
         "text": "This will be rendered as styled text, with no input to go with"
      }
      {
        "linkId": "my-first-question-on-my-first-page",
        "type": "string",
        "text": "This is the string that will be displayed to the user",
        "required": true
      },
      {
        "linkId": "my-second-question-on-my-first-page",
        "type": "string",
        "test": "And here we'll ask another question question, but this one is optional to answer",
        "required": false
      }
     ]
  },
  {
     "linkId": "my-second-page",
     "type": "group",
     "item": [
      {
         "linkId": "some-label-on-my-second-page",
         "type": "display",
         "text": "This page will show some choice-type questions"
      }
      {
        "linkId": "my-first-choice-type-question",
        "type": "choice",
        "text": "Which language are you most comforable speaking?",
        "required": true,
        "answerOption": [
          {
            "valueString": "English"
          },
          {
            "valueString": "Spanish"
          },
          {
            "valueString": "Esperanto"
          },
          {
            "valueString": "Klingon"
          }
        ]
      },
      {
        "linkId": "my-second-choice-type-question",
        "type": "choice",
        "test": "This is a canonical reference to a ValueSet resource that defines the options the user will choose from",
        "required": false,
        "answerValueSet": "http://www.somevalueseturl|1.1.0"
      }
     ]
  }
]

One-page Questionnaire with Conditionally Enabled Items:

[
  {
     "linkId": "my-page-with-conditional-items",
     "type": "group",
     "item": [
      {
         "linkId": "some-label",
         "type": "display",
         "text": "Here are some examples of conditional behavior"
      },
      {
        "linkId": "question-one",
        "type": "choice",
        "text": "Should I ask more questions?",
        "required": true,
        "answerOption": [
          {
            "valueString": "Yes"
          },
          {
            "valueString": "No"
          },
          {
            "valueString": "Maybe"
          }
        ]
      },
      {
        "linkId": "question-two",
        "type": "choice",
        "text": "Should we ask a third question?",
        "required": false,
        "enableWhen": [
          {
            "question": "question-one",
            "operator": "!=",
            "answerString": "No"
          }
        ],
         "answerOption": [
          {
            "valueString": "Yes"
          },
          {
            "valueString": "No"
          }
        ]
      },
      {
        "linkId": "question-three",
        "type": "string",
        "text": "This will be shown if either condition is true",
        "required": false,
        "enableWhen": [
          {
            "question": "question-one",
            "operator": "=",
            "answerString": "Yes"
          },
           {
            "question": "question-two",
            "operator": "=",
            "answerString": "Yes"
          }
        ],
        "enableBehavior": "any"
      }
     ]
  }
]

One-page Questionnaire with Conditionally Required Items:

[
  {
     "linkId": "my-page-with-conditional-items",
     "type": "group",
     "item": [
      {
         "linkId": "some-label",
         "type": "display",
         "text": "Here are some examples of conditional behavior"
      },
      {
        "linkId": "question-one",
        "type": "choice",
        "text": "Should I ask more questions?",
        "required": true,
        "answerOption": [
          {
            "valueString": "Yes"
          },
          {
            "valueString": "No"
          },
          {
            "valueString": "Maybe"
          }
        ]
      },
      {
        "linkId": "question-two",
        "type": "choice",
        "text": "This will start as optional but can become required depending on the answer to question-one",
        "required": false,
         "answerOption": [
          {
            "valueString": "Yes"
          },
          {
            "valueString": "No"
          }
        ],
        "extension": [
           {
                "url": "https://fhir.zapehr.com/r4/StructureDefinitions/require-when",
                "extension": [
                  {
                    "url": "https://fhir.zapehr.com/r4/StructureDefinitions/require-when-question",
                    "valueString": "question-one"
                  },
                  {
                    "url": "https://fhir.zapehr.com/r4/StructureDefinitions/require-when-operator",
                    "valueString": "="
                  },
                  {
                    "url": "https://fhir.zapehr.com/r4/StructureDefinitions/require-when-answer",
                    "valueString": "Yes"
                  }
                ]
              },
        ]
      }
     ]
  }
]

Conditional Group Item

Save yourself some boilerplate by wrapping multiple items that should be conditionally displayed in a "group":

[
  {
     "linkId": "my-page-with-conditional-items",
     "type": "group",
     "item": [
      {
         "linkId": "some-label",
         "type": "display",
         "text": "Here are some examples of conditional behavior"
      },
      {
        "linkId": "question-one",
        "type": "choice",
        "text": "Should I ask a bunch of additional quesitons?",
        "required": true,
        "answerOption": [
          {
            "valueString": "Yes"
          },
          {
            "valueString": "No"
          }
        ]
      },
      {
        "linkId": "conditional-question-group",
        "type": "group",
        "text": "We'll let the parent group own the condition logic here to avoid putting it on each child item",
         "enableWhen": [
          {
            "question": "question-one",
            "operator": "=",
            "answerString": "Yes"
          }
        ],
        "extension": [
           {
            "url": "https://fhir.zapehr.com/r4/StructureDefinitions/filter-when",
            "extension": [
              {
                "url": "https://fhir.zapehr.com/r4/StructureDefinitions/require-when-question",
                "valueString": "question-one"
              },
              {
                "url": "https://fhir.zapehr.com/r4/StructureDefinitions/require-when-operator",
                "valueString": "="
              },
              {
                "url": "https://fhir.zapehr.com/r4/StructureDefinitions/require-when-answer",
                "valueString": "No"
              }
            ]
          },
        ],
        "item": [
          {
            "linkId": "some-label",
            "type": "display",
            "text": "I only get displayed if my parent group is enabled"
          },
          {
            "linkId": "condition-question-one",
            "type": "text",
            "text": "I'm only here if my parent is enabled"
          },
          {
            "linkId": "condition-question-two",
            "type": "choice",
            "text": "What's your favorite color?",
            "answerOption": [
              {
                "valueString": "Pink"
              },
              {
                "valueString": "Brown"
              },
              {
                "valueString": "Green"
              }
            ]
          }
        ]
      }
     ]
  }
]