SDC form packages: PlanDefinition to completion
A complete API walkthrough: reusable forms, FHIRPath branching, package prefills, draft saves, atomic completion, and extraction.
38 cells · updated Sep 8, 2026
SDC form packages: from PlanDefinition to completion
This walkthrough creates three reusable Questionnaires and a PlanDefinition, starts a CarePlan for a fictional patient, saves drafts, follows a FHIRPath branch, and completes the applicable responses together. First and last names flow from the intake response into later forms through rules on the PlanDefinition.
Run the REST cells in order. Use an Aidbox with this branch's SDC package operations and FHIR R4 or R4B. Requests use the current Aidbox session. The first run creates resources with the package-demo prefix.
Notebook cells do not substitute earlier responses automatically. Before running a cell, replace each {{PLACEHOLDER}} in that cell with the value described in the preceding instructions. In a Parameters response, locate resources by parameter name, such as carePlan or response.
The default path includes all three forms. For another package run, change the start request's requestId; repeating the same value resumes the existing package. Published definitions use business version 1.0.0; keep that version unchanged and publish another version when changing their meaning.
1. Create the reusable Questionnaires
The intake form uses ordinary SDC initial expressions to populate names from the Patient. The assessment and review forms declare their own name fields; the package will supply their values later. The pulse item also declares observation extraction, which runs when the package is completed.
Each request should return 200 or 201.
PUT /fhir/Questionnaire/package-demo-intake
Accept: application/fhir+json
Content-Type: application/fhir+json
{
"resourceType": "Questionnaire",
"id": "package-demo-intake",
"url": "https://example.org/fhir/Questionnaire/package-demo-intake",
"version": "1.0.0",
"name": "PackageDemoIntake",
"title": "Package demo: intake",
"status": "active",
"subjectType": [
"Patient"
],
"item": [
{
"linkId": "first-name",
"type": "string",
"text": "First name",
"required": true,
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "%patient.name.first().given.first()"
}
}
]
},
{
"linkId": "last-name",
"type": "string",
"text": "Last name",
"required": true,
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "%patient.name.first().family"
}
}
]
},
{
"linkId": "needs-assessment",
"type": "boolean",
"text": "Include the pulse assessment?",
"required": true
}
]
}PUT /fhir/Questionnaire/package-demo-assessment
Accept: application/fhir+json
Content-Type: application/fhir+json
{
"resourceType": "Questionnaire",
"id": "package-demo-assessment",
"url": "https://example.org/fhir/Questionnaire/package-demo-assessment",
"version": "1.0.0",
"name": "PackageDemoAssessment",
"title": "Package demo: pulse assessment",
"status": "active",
"subjectType": [
"Patient"
],
"item": [
{
"linkId": "first-name",
"type": "string",
"text": "First name",
"required": true
},
{
"linkId": "last-name",
"type": "string",
"text": "Last name",
"required": true
},
{
"linkId": "pulse",
"type": "integer",
"text": "Pulse rate (beats per minute)",
"required": true,
"code": [
{
"system": "http://loinc.org",
"code": "8867-4",
"display": "Heart rate"
}
],
"extension": [
{
"url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-observationExtract",
"valueBoolean": true
}
]
}
]
}PUT /fhir/Questionnaire/package-demo-review
Accept: application/fhir+json
Content-Type: application/fhir+json
{
"resourceType": "Questionnaire",
"id": "package-demo-review",
"url": "https://example.org/fhir/Questionnaire/package-demo-review",
"version": "1.0.0",
"name": "PackageDemoReview",
"title": "Package demo: review",
"status": "active",
"subjectType": [
"Patient"
],
"item": [
{
"linkId": "first-name",
"type": "string",
"text": "First name",
"required": true
},
{
"linkId": "last-name",
"type": "string",
"text": "Last name",
"required": true
},
{
"linkId": "note",
"type": "text",
"text": "Review note"
}
]
}2. Define order, branching, and prefills
The PlanDefinition below has three ordered actions:
intake: always available.assessment: available when the intake answerneeds-assessmentis true.review: always available.
The condition uses FHIRPath and refers to the earlier response as %intake. An unanswered condition is pending; false excludes the assessment.
Both later actions have two sdc-package-prefill extensions. Each names an earlier sourceAction, a destination targetLinkId, and a FHIRPath expression evaluated against that source response. These mappings belong to this package, so the same Questionnaires can participate in other packages with different mappings.
PUT /fhir/PlanDefinition/package-demo
Accept: application/fhir+json
Content-Type: application/fhir+json
{
"resourceType": "PlanDefinition",
"id": "package-demo",
"url": "https://example.org/fhir/PlanDefinition/package-demo",
"version": "1.0.0",
"name": "PackageDemo",
"title": "Intake, pulse assessment, and review",
"status": "active",
"action": [
{
"id": "intake",
"title": "Package demo: intake",
"definitionCanonical": "https://example.org/fhir/Questionnaire/package-demo-intake|1.0.0"
},
{
"id": "assessment",
"title": "Package demo: pulse assessment",
"definitionCanonical": "https://example.org/fhir/Questionnaire/package-demo-assessment|1.0.0",
"condition": [
{
"kind": "applicability",
"expression": {
"language": "text/fhirpath",
"expression": "%intake.item.where(linkId = 'needs-assessment').answer.value = true"
}
}
],
"extension": [
{
"url": "http://health-samurai.io/fhir/sdc/StructureDefinition/sdc-package-prefill",
"extension": [
{
"url": "sourceAction",
"valueId": "intake"
},
{
"url": "targetLinkId",
"valueString": "first-name"
},
{
"url": "expression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "item.where(linkId = 'first-name').answer.value"
}
}
]
},
{
"url": "http://health-samurai.io/fhir/sdc/StructureDefinition/sdc-package-prefill",
"extension": [
{
"url": "sourceAction",
"valueId": "intake"
},
{
"url": "targetLinkId",
"valueString": "last-name"
},
{
"url": "expression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "item.where(linkId = 'last-name').answer.value"
}
}
]
}
]
},
{
"id": "review",
"title": "Package demo: review",
"definitionCanonical": "https://example.org/fhir/Questionnaire/package-demo-review|1.0.0",
"extension": [
{
"url": "http://health-samurai.io/fhir/sdc/StructureDefinition/sdc-package-prefill",
"extension": [
{
"url": "sourceAction",
"valueId": "intake"
},
{
"url": "targetLinkId",
"valueString": "first-name"
},
{
"url": "expression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "item.where(linkId = 'first-name').answer.value"
}
}
]
},
{
"url": "http://health-samurai.io/fhir/sdc/StructureDefinition/sdc-package-prefill",
"extension": [
{
"url": "sourceAction",
"valueId": "intake"
},
{
"url": "targetLinkId",
"valueString": "last-name"
},
{
"url": "expression",
"valueExpression": {
"language": "text/fhirpath",
"expression": "item.where(linkId = 'last-name').answer.value"
}
}
]
}
]
}
]
}3. Create the fictional patient
The intake form will start with Alex Rivera, populated from this Patient.
PUT /fhir/Patient/package-demo-patient
Accept: application/fhir+json
Content-Type: application/fhir+json
{
"resourceType": "Patient",
"id": "package-demo-patient",
"active": true,
"name": [
{
"use": "official",
"family": "Rivera",
"given": [
"Alex"
]
}
]
}4. Start a package instance
Start creates an active CarePlan, resolves the versioned definitions, and opens the first QuestionnaireResponse using the existing population mechanism. Responses are created when a form is first opened.
Expected: result = form, actionId = intake, populated names, and response status = in-progress.
Record these values from the returned Parameters:
CARE_PLAN_ID: thecarePlanresource'sid.INTAKE_RESPONSE_ID: theresponseresource'sid.INTAKE_VERSION: that response'smeta.versionId.
These resource versions are concurrency tokens; they are separate from a Questionnaire's business version.
POST /fhir/PlanDefinition/package-demo/$sdc-package-start
Accept: application/fhir+json
Content-Type: application/fhir+json
{
"resourceType": "Parameters",
"parameter": [
{
"name": "subject",
"valueReference": {
"reference": "Patient/package-demo-patient"
}
},
{
"name": "requestId",
"valueString": "package-demo-run-1"
}
]
}5. Observe the unanswered branch
Before saving the intake answer, try to advance. Expected: result = pending-input and assessment availability pending. The operation stops at that unresolved branch.
POST /fhir/CarePlan/{{CARE_PLAN_ID}}/$sdc-package-next
Accept: application/fhir+json
Content-Type: application/fhir+json
{
"resourceType": "Parameters",
"parameter": [
{
"name": "currentActionId",
"valueId": "intake"
}
]
}6. Save the intake draft
Correct the last name once to Rivera Chen and choose to include the pulse assessment. JSON Patch changes the answers while preserving the response's references and in-progress status.
Replace INTAKE_VERSION with the version returned by start. Expected: 200, with a new meta.versionId. Record that new value as INTAKE_VERSION for completion.
PATCH /fhir/QuestionnaireResponse/{{INTAKE_RESPONSE_ID}}
Accept: application/fhir+json
Content-Type: application/json-patch+json
If-Match: W/"{{INTAKE_VERSION}}"
[
{
"op": "add",
"path": "/item/0/answer",
"value": [
{
"valueString": "Alex"
}
]
},
{
"op": "add",
"path": "/item/1/answer",
"value": [
{
"valueString": "Rivera Chen"
}
]
},
{
"op": "add",
"path": "/item/2/answer",
"value": [
{
"valueBoolean": true
}
]
}
]7. Open the assessment
The saved Boolean now makes the assessment available. Expected: actionId = assessment, names Alex / Rivera Chen, and a blank pulse answer.
The corrected name comes from the intake draft. Opening this form leaves intake in-progress.
Record the returned response's id as ASSESSMENT_RESPONSE_ID and meta.versionId as ASSESSMENT_VERSION.
POST /fhir/CarePlan/{{CARE_PLAN_ID}}/$sdc-package-next
Accept: application/fhir+json
Content-Type: application/fhir+json
{
"resourceType": "Parameters",
"parameter": [
{
"name": "currentActionId",
"valueId": "intake"
}
]
}8. Save only the pulse answer
The third item in this Questionnaire is pulse. Patch only its answer; the prefilled first and last names stay in place.
Expected: 200, still in-progress. Record the returned meta.versionId as the new ASSESSMENT_VERSION. Saving the draft does not extract the Observation.
PATCH /fhir/QuestionnaireResponse/{{ASSESSMENT_RESPONSE_ID}}
Accept: application/fhir+json
Content-Type: application/json-patch+json
If-Match: W/"{{ASSESSMENT_VERSION}}"
[
{
"op": "add",
"path": "/item/2/answer",
"value": [
{
"valueInteger": 70
}
]
}
]9. Open the review form
Expected: actionId = review and names Alex / Rivera Chen, copied through this action's own prefill rules.
Record the returned response's id as REVIEW_RESPONSE_ID and meta.versionId as REVIEW_VERSION.
POST /fhir/CarePlan/{{CARE_PLAN_ID}}/$sdc-package-next
Accept: application/fhir+json
Content-Type: application/fhir+json
{
"resourceType": "Parameters",
"parameter": [
{
"name": "currentActionId",
"valueId": "assessment"
}
]
}10. Save the review note
The third item is note. Again, only the new answer is sent. Record the returned version as the new REVIEW_VERSION; the response remains in-progress.
PATCH /fhir/QuestionnaireResponse/{{REVIEW_RESPONSE_ID}}
Accept: application/fhir+json
Content-Type: application/json-patch+json
If-Match: W/"{{REVIEW_VERSION}}"
[
{
"op": "add",
"path": "/item/2/answer",
"value": [
{
"valueString": "Forms reviewed with the patient."
}
]
}
]11. Revisit the assessment
Use actionId to select a form directly. Expected: the same assessment response, its saved pulse 70, and the existing names. Revisiting does not populate a second response or overwrite saved answers.
POST /fhir/CarePlan/{{CARE_PLAN_ID}}/$sdc-package-next
Accept: application/fhir+json
Content-Type: application/fhir+json
{
"resourceType": "Parameters",
"parameter": [
{
"name": "actionId",
"valueId": "assessment"
}
]
}12. Finish navigation
Advance after the final action. Expected: result = ready-for-review. This finishes navigation; required-answer validation happens during completion.
POST /fhir/CarePlan/{{CARE_PLAN_ID}}/$sdc-package-next
Accept: application/fhir+json
Content-Type: application/fhir+json
{
"resourceType": "Parameters",
"parameter": [
{
"name": "currentActionId",
"valueId": "review"
}
]
}13. Read the package before completion
The batch below reads the CarePlan and all three responses. Expected: CarePlan active; every response in-progress.
Before completing, finish pending saves and copy the current meta.versionId from each entry.resource:
- Entry 1, CarePlan →
CARE_PLAN_VERSION. - Entry 2, intake response →
INTAKE_VERSION. - Entry 3, assessment response →
ASSESSMENT_VERSION. - Entry 4, review response →
REVIEW_VERSION.
Use the CarePlan's latest version, which changes as responses are linked to it.
POST /fhir
Accept: application/fhir+json
Content-Type: application/fhir+json
{
"resourceType": "Bundle",
"type": "batch",
"entry": [
{
"request": {
"method": "GET",
"url": "CarePlan/{{CARE_PLAN_ID}}"
}
},
{
"request": {
"method": "GET",
"url": "QuestionnaireResponse/{{INTAKE_RESPONSE_ID}}"
}
},
{
"request": {
"method": "GET",
"url": "QuestionnaireResponse/{{ASSESSMENT_RESPONSE_ID}}"
}
},
{
"request": {
"method": "GET",
"url": "QuestionnaireResponse/{{REVIEW_RESPONSE_ID}}"
}
}
]
}14. Complete the package
Fill in the versions just read. The operation reevaluates applicability, validates all included answers, and atomically completes the CarePlan and its applicable responses together with extraction outputs.
Expected: 200, result = completed, and CarePlan status = completed. If a version is stale, read the resources again before retrying. If answers are incomplete, save the corrected draft first.
POST /fhir/CarePlan/{{CARE_PLAN_ID}}/$sdc-package-complete
Accept: application/fhir+json
Content-Type: application/fhir+json
{
"resourceType": "Parameters",
"parameter": [
{
"name": "carePlanVersion",
"valueId": "{{CARE_PLAN_VERSION}}"
},
{
"name": "responseVersion",
"part": [
{
"name": "actionId",
"valueId": "intake"
},
{
"name": "versionId",
"valueId": "{{INTAKE_VERSION}}"
}
]
},
{
"name": "responseVersion",
"part": [
{
"name": "actionId",
"valueId": "assessment"
},
{
"name": "versionId",
"valueId": "{{ASSESSMENT_VERSION}}"
}
]
},
{
"name": "responseVersion",
"part": [
{
"name": "actionId",
"valueId": "review"
},
{
"name": "versionId",
"valueId": "{{REVIEW_VERSION}}"
}
]
},
{
"name": "requestId",
"valueString": "package-demo-complete-1"
}
]
}15. Verify the final state
Expected: the CarePlan and all three QuestionnaireResponses are now completed. Each response still points to its versioned Questionnaire and to this CarePlan through basedOn.
POST /fhir
Accept: application/fhir+json
Content-Type: application/fhir+json
{
"resourceType": "Bundle",
"type": "batch",
"entry": [
{
"request": {
"method": "GET",
"url": "CarePlan/{{CARE_PLAN_ID}}"
}
},
{
"request": {
"method": "GET",
"url": "QuestionnaireResponse/{{INTAKE_RESPONSE_ID}}"
}
},
{
"request": {
"method": "GET",
"url": "QuestionnaireResponse/{{ASSESSMENT_RESPONSE_ID}}"
}
},
{
"request": {
"method": "GET",
"url": "QuestionnaireResponse/{{REVIEW_RESPONSE_ID}}"
}
}
]
}16. Inspect the extracted pulse Observation
Completion extracts the pulse 70 with LOINC code 8867-4. The query returns pulse Observations for this demo patient, including any from earlier completed runs.
GET /fhir/Observation?subject=Patient/package-demo-patient&code=http%3A%2F%2Floinc.org%7C8867-4
Accept: application/fhir+json17. Replay the same completion request
Use the same requestId and the same completion body as step 14. Expected: 200, result = completed. This replay does not extract another Observation. Run the preceding Observation query again to inspect the result.
POST /fhir/CarePlan/{{CARE_PLAN_ID}}/$sdc-package-complete
Accept: application/fhir+json
Content-Type: application/fhir+json
{
"resourceType": "Parameters",
"parameter": [
{
"name": "carePlanVersion",
"valueId": "{{CARE_PLAN_VERSION}}"
},
{
"name": "responseVersion",
"part": [
{
"name": "actionId",
"valueId": "intake"
},
{
"name": "versionId",
"valueId": "{{INTAKE_VERSION}}"
}
]
},
{
"name": "responseVersion",
"part": [
{
"name": "actionId",
"valueId": "assessment"
},
{
"name": "versionId",
"valueId": "{{ASSESSMENT_VERSION}}"
}
]
},
{
"name": "responseVersion",
"part": [
{
"name": "actionId",
"valueId": "review"
},
{
"name": "versionId",
"valueId": "{{REVIEW_VERSION}}"
}
]
},
{
"name": "requestId",
"valueString": "package-demo-complete-1"
}
]
}Try the excluded branch in another run
Start a fresh package with a different requestId, record its new IDs, and save needs-assessment = false in step 6.
The advance-after-intake request in step 7 now returns actionId = review. Record that response as REVIEW_RESPONSE_ID and its version as REVIEW_VERSION. Skip steps 8 and 9, save the review note in step 10, skip the assessment revisit in step 11, and continue from step 12.
Remove the assessment GET entry from both resource-read batches in steps 13 and 15. Complete with only the intake and review responseVersion entries in steps 14 and 17. No assessment response is created, and this package extracts no pulse Observation.
If an assessment was already opened before an intake answer changed to false, its response stays in-progress, retaining its answers. Include its current version in completion too: versions cover every opened response, including excluded ones. Completion marks the excluded activity cancelled while completing only applicable responses.
The package lifecycle ends at completion. Ordinary resource CRUD remains available, with callers responsible for consistency when editing package resources directly.
Reference: Aidbox Notebook resource.