Parsing Questionnaire Responses | Medplum
On this page
A QuestionnaireResponse captures what a patient or clinician submitted. Downstream systems – analytics, search, CDS, exchange – usually need that turned into concrete FHIR resources: Observation, Condition, orders, and similar. This applies across charting, intake, registration, prior authorization, and other workflows.
Medplum supports two parsing approaches. You can use both in the same application – different Questionnaires can use different methods depending on their complexity.
Choosing an Approach
SDC annotations + $extract |
Subscription + Bot | |
|---|---|---|
| How it works | Extraction rules live in the Questionnaire itself as FHIR extensions; $extract reads them and returns a Bundle |
A Bot subscribes to QuestionnaireResponse creation; runs arbitrary TypeScript to write resources |
| Best for | Straightforward field-to-resource mappings; forms that change often | Scoring algorithms, conditional logic, multi-step workflows, external API calls |
| Change management | Edit the Questionnaire; no deployment needed | Redeploy Bot on logic changes; keep Bot and Questionnaire versions in sync |
| Limitations | Complex branching or external lookups get unwieldy | Requires Bot infrastructure; harder to inspect logic from the Questionnaire alone |
Pick $extract when the mapping is mostly one field → one resource field, and you want the logic to live beside the form definition.
Pick a Bot when you need to score a PHQ-9, write resources conditionally, call an external service, or otherwise do something that does not fit a declarative template.
You can also combine them: use $extract for simple demographic or intake data, and a Bot for the clinical scoring logic on the same submission.
For visit-level orchestration that launches forms and orders together, see Visit Templates and the SOAP Approach.
Approach 1: SDC Annotations + $extract
Annotate the Questionnaire with template resources and FHIRPath extraction rules. When a QuestionnaireResponse is submitted, call $extract; Medplum reads the annotations and returns a transaction Bundle of populated resources ready to upload.
Annotating the Questionnaire
Medplum implements template-based extraction, which requires the Questionnaire resource to contain template resources and the rules for populating the templates from a QuestionnaireResponse. The template resources (e.g. Observation or other resource types) are placed in Questionnaire.contained and given and internal reference id; a corresponding templateExtract extension on the Questionnaire or one of its descendant items initiates extraction into that resource.
{
"resourceType": "Questionnaire",
"status": "draft",
"contained": [
{
"resourceType": "Patient",
"id": "patientTemplate"
// ...
}
],
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtract",
"extension": [{ "url": "template", "valueReference": { "reference": "#patientTemplate" } }]
}
]
//...
}
The location of the templateExtract extension determines the initial context for the extraction: if placed at the root of the Questionnaire, the entire QuestionnaireResponse will be in scope for FHIRPath rules to extract data from. If placed on a specific item, only the corresponding item from the QuestionnaireResponse will be in scope, simplifying the extraction rules for that response item if it can be extracted in isolation.
Within the template resources, templateExtractValue and templateExtractContext extensions work together to define the rules for extracting data from the QuestionnaireResponse into the template.
Extraction Context
When the FHIRPath expression to extract a value is evaluated, the context on which it evaluates will initially be either the entire QuestionnaireResponse resource or a specific QuestionnaireResponse.item; the choice depends on whether the relevant templateExtract extension was placed at the top level of the Questionnaire or on a specific Questionnaire.item.
Combined with the value extraction logic described above for empty or multiple results, this can potentially produce resource JSON that is structurally invalid.
Example
Corrected Questionnaire using templateExtractContext
{
"resourceType": "Questionnaire",
"status": "draft",
// Extract extension at root of Questionnaire initiates extraction into specified template
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtract",
"extension": [{ "url": "template", "valueReference": { "reference": "#patientTemplate" } }]
}
],
"contained": [
{
"resourceType": "Patient",
"id": "patientTemplate",
"name": [
{
// Context extension placed at top of the object to be inserted for each answer
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtractContext",
// Context expression linked to response items
"valueString": "item.where(linkId = 'name')"
}
],
"_text": {
// Nested value extensions are relative to parent context,
// and are evaluated separately for each result item
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtractValue",
"valueString": "answer.value.first()"
}
]
}
}
],
"telecom": [
{
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtractContext",
"valueString": "item.where(linkId = 'phone')"
}
],
"system": "phone",
"_value": {
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtractValue",
"valueString": "item.where(linkId = 'number').answer.value.first()"
}
]
},
"use": "home", // Default value for field specified
"_use": {
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-templateExtractValue",
// Overrides the default if it returns a value
"valueString": "item.where(linkId = 'use').answer.value.first().code"
}
]
}
}
]
}
],
"item": [
{ "linkId": "name", "type": "string", "required": true },
{
"linkId": "phone",
"type": "group",
"repeats": true,
"required": true,
"item": [
{ "linkId": "number", "type": "string", "required": true },
{ "linkId": "use", "type": "choice", "answerValueSet": "http://hl7.org/fhir/ValueSet/contact-point-use" }
]
}
]
}
A corresponding QuestionnaireResponse shows how the data will be parsed:
{
"resourceType": "QuestionnaireResponse",
"status": "completed",
"item": [
{
"linkId": "name",
"answer": [{ "valueString": "John Jacob Jingleheimer-Schmidt" }]
}
]
}
Value Extraction
The template extraction extensions are expected to contain FHIRPath expressions that return the value(s) to be inserted into the template. If no values are returned, the field is removed from the template; more than one result is inserted as an array of values. Using functions like first() in the expression can help ensure the correct number of values are returned and ensure the resulting resource is well-formed.
Gathering Additional Data
If data from additional resources is required to populate the templates, search queries can be executed as part of the extraction process and their results stored in context to be operated on by later expressions. Queries are attached in context extensions using the application/x-fhir-query language to describe the search request, with the option to embed FHIRPath expressions as needed to construct the query string.
Linking Extracted Resources
When the resources being extracted from the questionnaire response are related, such as the above Patient and Observation, you may want to link the extracted resources together with a reference like Observation.subject.