Skip to main content

Questionnaire actions

Header, row, and batch buttons on ResourceListPage can open questionnaire modals instead of custom React forms. You define two FHIR resources:

  1. Questionnaire — the form UI (items, validation, launch context, prefill expressions).
  2. Mapping — an extraction template that turns the submitted QuestionnaireResponse into a FHIR Bundle transaction (create, update, or delete resources).

Beda EMR loads the questionnaire by id, renders it in a modal, runs the linked mapping on submit, and applies the resulting bundle to the FHIR server. No custom save logic is required in the list page container.

Mapping templates use the FHIRPath Mapping Language (JSON/YAML data DSL with {{ }} FHIRPath expressions). Older Beda EMR mappers may use JUTE; new mappers should prefer FHIRPath.

End-to-end flow

At the code level, a questionnaireAction opens QuestionnaireResponseForm via questionnaireIdLoader:

questionnaireAction(<Trans>Add patient</Trans>, 'patient-create')

The second argument (patient-create) must match the Questionnaire.id stored on the FHIR server.

Step 1 — Define the Questionnaire

Create a Questionnaire resource with:

FieldPurpose
idStable identifier referenced by questionnaireAction
statusactive for production forms
itemForm fields (linkId, type, text, validation, widgets)
mappingLink to the extract mapping (see step 2)
launchContextExpected context parameters (for edit/confirm flows)
initialExpressionFHIRPath prefill on items from launch context
meta.profilehttps://emr-core.beda.software/StructureDefinition/fhir-emr-questionnaire

Header action example — create patient (patient-create.yaml):

id: patient-create
resourceType: Questionnaire
name: patient-create
title: Patient create
status: active
mapping:
- reference: urn:uuid:Mapping:patient-create
item:
- linkId: last-name
type: string
text: Last name
required: true
- linkId: first-name
type: string
text: First name
required: true
- linkId: birth-date
type: date
text: Birth date
# ...
meta:
profile:
- https://emr-core.beda.software/StructureDefinition/fhir-emr-questionnaire

Create flows typically need only item definitions — no launchContext unless the form must read session context (for example, the current Author).

Row action example — confirm payment (pay-invoice.yaml):

Edit and confirm flows declare launchContext and use initialExpression to copy resource ids into hidden fields:

id: pay-invoice
resourceType: Questionnaire
status: active
title: Pay invoice
mapping:
- reference: urn:uuid:Mapping:pay-invoice-extract
launchContext:
- name:
code: Invoice
type:
- Invoice
item:
- linkId: current-invoice-id
type: string
hidden: true
initialExpression:
language: text/fhirpath
expression: "%Invoice.id"
- linkId: are-you-sure
type: display
text: Are you sure to pay this invoice?

The list page passes the row resource as launch context automatically for row actions (see Launch context by action type).

Step 2 — Define the Mapping

Create a Mapping resource with:

FieldPurpose
idReferenced from Questionnaire.mapping
typeFHIRPath for FHIRPath Mapping Language
bodyExtraction template — usually a Bundle with type: transaction

Create example (patient-create.yaml mapping):

id: patient-create
resourceType: Mapping
type: FHIRPath
body:
"{% assign %}":
- lastName: "{{ %QuestionnaireResponse.answers('last-name') }}"
- firstName: "{{ %QuestionnaireResponse.answers('first-name') }}"
- birthDate: "{{ %QuestionnaireResponse.answers('birth-date') }}"

resourceType: Bundle
type: transaction
entry:
- request:
method: POST
url: /Patient
resource:
resourceType: Patient
active: true
name:
- family: "{{ %lastName }}"
given:
- "{{ %firstName }}"
birthDate: "{{ %birthDate }}"

QuestionnaireResponse.answers('linkId') is an Aidbox helper that reads answers by linkId. You can also use standard FHIRPath over %QuestionnaireResponse.repeat(item)....

Update example — conditional POST vs PATCH (patient-create mapping):

entry:
- request:
"{% if %patientId.exists() %}":
url: "/Patient/{{ %patientId }}"
method: PATCH
"{% else %}":
url: /Patient
method: POST
resource:
resourceType: Patient
# ...

Multi-resource transaction (healthcare-service-create-extract.yaml) — one form creates HealthcareService and ChargeItemDefinition with internal urn:uuid: references.

FHIRPath Mapping Language essentials

Full specification: FHIRPathMappingLanguage README.

SyntaxMeaning
"field": "literal"Constant value
"field": "{{ expression }}"First result of FHIRPath expression
"field": "{[ expression ]}"Array result
{% assign %}Scoped variables (%varName in later expressions)
{% if expr %} / {% else %}Conditional object branches
{% for item in expr %}Iterate and build array entries
{% merge %}Merge multiple objects

Context available to the mapper includes:

  • %QuestionnaireResponse — the submitted response
  • Launch context resources — for example %Patient, %HealthcareService, %Invoice
  • %Author and other session parameters from Clinical context

Use SDC IDE or the TypeScript reference implementation to test templates against sample context.

In the Questionnaire, reference the Mapping by id:

mapping:
- reference: urn:uuid:Mapping:patient-create

The Mapping.id must match the suffix (patient-create in this example).

Step 4 — Deploy to the FHIR server

Beda EMR ships questionnaires and mappings as seed files:

In a custom EMR build, add your YAML files to contrib/fhir-emr/resources/init-seeds/ (or the equivalent path in your fork). Aidbox loads them on startup when the init-seeds volume is mounted (see compose.yaml).

You can also create or edit questionnaires at runtime via the FHIR API or the in-app Form Builder / SDC IDE.

Step 5 — Wire the action in a list page

Import questionnaireAction and register it in the appropriate callback on ResourceListPage:

import { ResourceListPage, questionnaireAction } from 'src/uberComponents/ResourceListPage';

<ResourceListPage<Patient>
headerTitle={t`Patients`}
resourceType="Patient"
getHeaderActions={() => [
questionnaireAction(<Trans>Add patient</Trans>, 'patient-create', { icon: <PlusOutlined /> }),
]}
getRecordActions={(record) => [
questionnaireAction(<Trans>Edit</Trans>, 'patient-edit'),
]}
getBatchActions={() => [
questionnaireAction(<Trans>Archive selected</Trans>, 'patient-archive-batch'),
]}
// ...
/>
CallbackButton locationWhen to use
getHeaderActionsPage header (primary button)Create, import, bulk setup
getRecordActionsPer-row Actions columnEdit, confirm, cancel, status change
getBatchActionsBatch bar above table (requires row selection)Operations on multiple selected records

getBatchActions receives a Bundle of selected resources and enables row checkboxes automatically.

Launch context by action type

Questionnaire actions merge launch context from Clinical context (Author, session resources), defaultLaunchContext on the list page, getClinicalContext(record), and action-specific parameters.

Action typeWhat the form receivesQuestionnaire design
HeadergetClinicalContext(undefined) merged with defaultLaunchContextCreate forms — usually no launchContext required; use for empty resource populate
RowRow resource as { name: resourceType, resource } plus merged clinical contextDeclare launchContext for expected resource types; prefill with initialExpression
BatchSelected resources as individual parameters and a Bundle parameter containing all selected rowsDeclare Bundle in launchContext; iterate %Bundle.entry in the mapping

Header action — create medication

MedicationManagement:

getHeaderActions={() => [
questionnaireAction(<Trans>Add Medication</Trans>, 'medication-knowledge-create', { icon: <PlusOutlined /> }),
]}

The questionnaire (medication-knowledge-create.yaml) only defines item fields. The mapping (medication-knowledge-create-extract.yaml) creates a MedicationKnowledge with ingredients, packaging, and cost.

Row action — edit healthcare service

HealthcareServiceList:

getRecordActions={(record) => [
questionnaireAction(<Trans>Edit</Trans>, 'healthcare-service-edit'),
]}

The edit questionnaire (healthcare-service-edit.yaml) declares launchContext: [HealthcareService], runs a sourceQueries bundle to load related ChargeItemDefinition, and prefills fields with initialExpression like %HealthcareService.name.

Row action — pay invoice

InvoiceList uses pay-invoice and cancel-invoice questionnaires. The mapping (pay-invoice-extract.yaml) PATCHes the invoice status:

entry:
- request:
method: PATCH
url: "/Invoice/{{ %currentInvoiceId }}"
resource:
status: balanced

Batch action — operate on selected rows

When users select rows, BatchQuestionnaireAction passes a Bundle of selected resources into launch context alongside per-row parameters. Design the questionnaire to accept Bundle and iterate in the mapping:

# Questionnaire excerpt
launchContext:
- name:
code: Bundle
type:
- Bundle
# Mapping excerpt — patch status for each selected Patient
body:
resourceType: Bundle
type: transaction
entry:
- "{% for entry in %Bundle.entry %}":
request:
method: PATCH
url: "/Patient/{{ %entry.resource.id }}"
resource:
active: false

Register on the list page:

getBatchActions={() => [
questionnaireAction(<Trans>Deactivate selected</Trans>, 'patient-deactivate-batch'),
]}

Batch launch context is built from defaultLaunchContext, per-row getClinicalContext results, and the Bundle parameter. See Clinical context — Resource list pages for merge rules.

Advanced questionnaire features

sourceQueries and contained bundles

Edit forms often need related data beyond the primary resource. The healthcare service edit questionnaire loads ChargeItemDefinition via a contained search bundle:

contained:
- resourceType: Bundle
id: ChargeItemDefinitionBundle
type: transaction
entry:
- request:
method: GET
url: /ChargeItemDefinition?healthcare-service={{%HealthcareService.id}}
sourceQueries:
- reference: "#ChargeItemDefinitionBundle"

Prefill price fields from the query result:

initialExpression:
language: text/fhirpath
expression: "%ChargeItemDefinitionBundle.entry[0].resource.entry.resource.propertyGroup.priceComponent.where(type='base').amount.value"

Repeating groups

medication-knowledge-create uses a repeating ingredients group. The mapping iterates with {% for ingredient in %ingredientsItems %}.

Multiple resources from one form

medication-batch-create asks for items-number, batch-number, and expiration-date, then the mapping creates N Medication resources in one transaction. This is a single-form bulk create (not a list batch action), useful for header actions on detail tabs.

Testing with SDC IDE

Use the in-app SDC IDE (Edit in SDC IDE on the Questionnaires list) to:

  1. Set launch context parameters and verify initialExpression prefill.
  2. Edit the Questionnaire YAML and preview the rendered form.
  3. Edit the Mapping and inspect the extracted Bundle in real time.
  4. Debug FHIRPath expressions with the built-in evaluator.

See the Form Builder user guide for SDC IDE workflow details.

Built-in examples in Beda EMR

Questionnaire idAction typeList container
patient-createHeaderPatientList
healthcare-service-createHeaderHealthcareServiceList
healthcare-service-editRowHealthcareServiceList
medication-knowledge-createHeaderMedicationManagement
medication-batch-createHeader (detail tab)MedicationManagementDetail
pay-invoice / cancel-invoiceRowInvoiceList
medication-request-confirmRowPrescriptions
practitioner-createHeaderPractitionerList

Browse all seeds: resources/init-seeds/.