VO4

CDS Hooks Medication Safety Testing — Integration Guide

This guide describes how to connect your medication safety system (System Under Test, SUT) to the VO4 self-test platform. The platform sends standardized CDS Hooks requests to your system and evaluates the response cards against expected outcomes.

Two APIs, two roles:

  • CDS Hooks API — this is what you implement. VO4 calls your endpoints with test scenarios. See the CDS Hooks Swagger UI for the full specification.
  • VO4 Test Runner API — this is what you call. Use it to start test runs, poll status, and retrieve results (e.g. from your CI/CD pipeline). See the Test Runner Swagger UI for the full specification.

Overview

VO4 Self-Test Platform                 Your System (SUT)
┌──────────────┐                       ┌──────────────────┐
│              │  POST /cds-services/  │                  │
│  Test Runner │  ───────────────────> │  CDS Hooks       │
│              │  (FHIR patient data   │  Server           │
│  Scenarios   │   + draft medication) │                  │
│              │                       │  Medication       │
│  Evaluator   │  <─────────────────── │  Safety Engine    │
│              │  (cards with signals) │                  │
└──────────────┘                       └──────────────────┘

The platform:

  1. Loads a test scenario (patient, medications, clinical data)
  2. Transforms it into a CDS Hooks request with FHIR resources
  3. Sends the request to your CDS Hooks endpoint
  4. Evaluates the returned cards against expected signals
  5. Reports PASS / FAIL / ERROR per scenario

What You Need to Implement

Your system must expose CDS Hooks 3.0 compliant HTTP endpoints:

Discovery Endpoint

GET {your-base-url}/cds-services

Returns the list of CDS services your system supports. The VO4 platform calls this to discover available hooks.

{
  "services": [
    {
      "id": "order-sign",
      "hook": "order-sign",
      "title": "Medicatiebewaking",
      "description": "Controleert medicatievoorschriften op veiligheid",
      "prefetch": {
        "patient": "Patient/{{context.patientId}}",
        "medicationData": "MedicationRequest?patient={{context.patientId}}&status=active"
      }
    }
  ]
}

Note: The prefetch field in the discovery response advertises which FHIR queries your system needs. The VO4 platform does not use these templates — it always sends the full set of prefetch resources defined in the test scenario. However, implementing the discovery endpoint is required by the CDS Hooks specification.

Hook Endpoints

POST {your-base-url}/cds-services/{serviceId}

Where serviceId is one of:

  • order-sign — Medication check at prescription time (primary hook)
  • medication-review — Scheduled medication review (no draft orders)
  • run-protocol — Execute a scheduled MFB protocol

Authentication

Bearer token authentication is optional. When configured, the platform sends a Bearer token in the Authorization header:

Authorization: Bearer {token}

The token is configured when setting up the test run. Your system may validate this token, or accept unauthenticated requests.

Two tokens, two purposes: There are two separate authentication tokens involved when using VO4:

Token Purpose Required?
SUT Bearer token Sent by VO4 to your system with each CDS Hooks request. Configured as bearer_token when creating a test run. Optional — only if your system requires authentication
VO4 API token Sent by you to VO4 when calling the Test Runner API (e.g. starting test runs from CI/CD). A Sanctum token created in your VO4 account settings. Required for all Test Runner API calls

Request Format

Headers

POST /cds-services/order-sign HTTP/1.1
Content-Type: application/json
Accept: application/json
Authorization: Bearer {token}

Note: The Authorization header is only present when a bearer token is configured for the test run.

Body

{
  "hook": "order-sign",
  "hookInstance": "d1577c69-dfbe-44ad-938d-e3d3953cfc04",
  "context": {
    "patientId": "patient-uuid",
    "draftOrders": {
      "resourceType": "Bundle",
      "type": "collection",
      "entry": [
        {
          "fullUrl": "urn:uuid:draft-med-uuid",
          "resource": {
            "resourceType": "MedicationRequest",
            "id": "draft-med-uuid",
            "status": "draft",
            "intent": "order",
            "medicationReference": {
              "reference": "urn:uuid:med-product-uuid",
              "type": "Medication",
              "display": "Paracetamol tablet 500mg"
            },
            "subject": {
              "reference": "Patient/patient-uuid"
            },
            "dosageInstruction": [
              {
                "text": "3 maal per dag 2 tabletten",
                "timing": {
                  "repeat": {
                    "frequency": 3,
                    "period": 1,
                    "periodUnit": "d"
                  }
                },
                "route": {
                  "coding": [
                    {
                      "system": "urn:oid:2.16.840.1.113883.2.4.4.9",
                      "code": "9",
                      "display": "Oraal"
                    }
                  ]
                },
                "doseAndRate": [
                  {
                    "doseQuantity": {
                      "value": 2,
                      "unit": "stuk",
                      "system": "http://unitsofmeasure.org",
                      "code": "1",
                      "extension": [
                        {
                          "url": "http://hl7.org/fhir/StructureDefinition/iso21090-PQ-translation",
                          "valueQuantity": {
                            "value": 2,
                            "unit": "stuk",
                            "system": "urn:oid:2.16.840.1.113883.2.4.4.1.900.2",
                            "code": "245"
                          }
                        }
                      ]
                    }
                  }
                ]
              }
            ]
          }
        },
        {
          "fullUrl": "urn:uuid:med-product-uuid",
          "resource": {
            "resourceType": "Medication",
            "id": "med-product-uuid",
            "code": {
              "coding": [
                {
                  "system": "urn:oid:2.16.840.1.113883.2.4.4.1",
                  "code": "3891",
                  "display": "Paracetamol tablet 500mg"
                }
              ],
              "text": "Paracetamol tablet 500mg"
            }
          }
        }
      ]
    },
    "extensions": {
      "nl.medicatiebewaking.scenarioContext": {
        "hospitalized": false,
        "practitionerInPharmacy": false
      }
    }
  },
  "prefetch": {
    "patient": {
      "resourceType": "Patient",
      "id": "patient-uuid",
      "gender": "female",
      "birthDate": "1991-04-07",
      "name": [
        {
          "given": ["Anna"],
          "family": "Testpatient"
        }
      ]
    },
    "medicationData": {
      "resourceType": "Bundle",
      "type": "searchset",
      "entry": []
    },
    "conditions": {
      "resourceType": "Bundle",
      "type": "searchset",
      "entry": []
    },
    "observations": {
      "resourceType": "Bundle",
      "type": "searchset",
      "entry": []
    }
  }
}

Context Fields

Field Type Description
patientId string FHIR Patient resource ID
draftOrders Bundle New/changed medications to check (for order-sign)
protocolNumber integer MFB protocol number (for run-protocol only)
triggerMedicationId string ID of trigger medication in prefetch (for run-protocol, optional)
extensions object Scenario context extensions (see below)

draftOrders Bundle Structure

The draftOrders bundle is a FHIR collection bundle containing the medication(s) being prescribed and their supporting resources. Per CDS Hooks 3.0, this is not a searchset — there are no entry.search or entry.request elements.

Entry ordering: Primary resources (MedicationRequest) appear first, followed by supporting resources (Medication, Condition).

Resource Role Description
MedicationRequest Primary (draft) The medication being prescribed. Uses medicationReference to point to a Medication resource in the same bundle. May include reasonReference pointing to a Condition.
Medication Supporting The pharmaceutical product (GPK/PRK/HPK code). Referenced by MedicationRequest.medicationReference.
Condition Supporting (optional) Prescription reason (reden van voorschrijven). Referenced by MedicationRequest.reasonReference. Only present when a reason is specified.

Important: A Condition in draftOrders that is not referenced by any MedicationRequest's reasonReference is treated as a standalone draft — for example, a new contra-indication being added. This triggers contra-indication-change evaluation instead of medication-start.

Medication coding: The Medication resource uses medicationReference (not medicationCodeableConcept), following the Nictiz MP9 profile. The referenced Medication resource contains the product code in code.coding with the G-Standaard system OID. Dose quantities use UCUM codes with an iso21090-PQ-translation extension for the G-Standaard unit code.

Context Extensions

The context.extensions object may contain a nl.medicatiebewaking.scenarioContext object with the following fields:

Field Type Default Description
hospitalized boolean false Indicates whether the patient is currently hospitalized. When true, your system should apply inpatient medication safety rules (e.g., different dosage limits for intensive care).
practitionerInPharmacy boolean false Indicates whether the prescriber is operating within a pharmacy context. This may affect which medication safety checks are applicable (e.g., OTC vs. prescription-only rules).

Prefetch Resources

Key FHIR Resource Description
patient Patient Demographics: gender, birthDate, name
medicationData Bundle (searchset) of MP9 medication resources Match entries: MedicationRequest (MA, VV), MedicationDispense (TA, MVE), MedicationStatement (MGB), MedicationAdministration. Include entries: Medication (pharmaceutical product), Condition (prescription reason). Match entries appear first, includes after.
conditions Bundle of Condition Active conditions and contra-indications
observations Bundle of Observation Lab values, vital signs (weight, height, eGFR, etc.)
allergies Bundle of AllergyIntolerance Known allergies and intolerances

All FHIR resources follow the Dutch Medication Process 9 (MP9) profiles. See the Nictiz MP9 FHIR IG for detailed profile specifications.

Key Coding Systems

System OID Description Example
urn:oid:2.16.840.1.113883.2.4.4.1 G-Standaard GPK (Generiek Product) 3891 (Paracetamol)
urn:oid:2.16.840.1.113883.2.4.4.10 G-Standaard PRK (Prescriptie Product) 46027
urn:oid:2.16.840.1.113883.2.4.4.7 G-Standaard HPK (Handels Product) 1034537
http://www.whocc.no/atc ATC code N02BE01
urn:oid:2.16.840.1.113883.2.4.4.9 Toedieningsweg (Route) 9 (Oraal)
urn:oid:2.16.840.1.113883.2.4.4.1.900.2 Eenheden (Units) 245 (Stuk)
http://hl7.org/fhir/sid/icpc-1-nl ICPC-1 NL L88
http://snomed.info/sct SNOMED CT 38341003

Response Format

Your system must return a JSON response with CDS Hooks cards:

{
  "cards": [
    {
      "uuid": "550e8400-e29b-41d4-a716-446655440000",
      "summary": "Dosering is hoger dan gebruikelijk.\nPas zo nodig de dosering aan.",
      "indicator": "warning",
      "detail": "**Ernst:** Hoog",
      "source": {
        "label": "zindex.dosage-check"
      }
    }
  ]
}

Card Fields

Field Type Required Constraints Description
uuid string Recommended UUID v4 format Unique card identifier. If omitted, the platform generates one. Providing a stable UUID helps with tracking and debugging.
summary string Required Non-empty Brief signal description. For MFB protocol signals, include the action text from the G-Standaard — the platform matches cards to expected signals based on this text.
indicator string Required Enum: info, warning, critical Severity level (see below)
detail string Optional Markdown supported Extended description
source object Required Source attribution
source.label string Optional Rule identifier for your own reference. See Signal Types for recommended values.
source.protocolNumber integer Optional Integer > 0 MFB protocol number, for your own reference.
extensions object Optional Custom metadata. You may include your own metadata under extensions['nl.medicatiebewaking.metadata'] for debugging and traceability.

How card matching works: The VO4 platform matches returned cards to scenario assertions primarily via the signal text in summary and detail. For MFB protocol signals, the platform looks up the expected action text from the G-Standaard and matches it against your card text. You do not need to populate source.label or source.protocolNumber for the matching to work — these fields are optional and serve as useful metadata for your own system's logging and debugging.

Signal Types (source.label)

The source.label field is optional. When provided, it helps identify the signal type and can be used for filtering and debugging. Recommended values:

Label Description
zindex.dosage-check Dosage limit exceeded
zindex.mfb-protocol MFB protocol signal
zindex.allergy Allergy or intolerance detected
zindex.duplicate-medication Duplicate medication detected
zindex.derived-contra-indication Derived contra-indication
zindex.special-characteristic Special patient characteristic

CDS Hooks Indicator

The indicator field is required by the CDS Hooks specification and must be one of:

Indicator Meaning
info Informational — no immediate action required
warning Warning — attention or review required
critical Critical — urgent, potentially dangerous situation

Note: The VO4 self-test platform does not currently validate the indicator value in its test assertions. However, your system must include this field with a valid value as it is required by the CDS Hooks standard. Downstream consumers of your cards may rely on this field for visual presentation and clinical decision support workflows.

Test Evaluation

The VO4 self-test platform evaluates your response cards using three assertion types:

Required Signals

Cards that must be present. The test fails if any required signal is missing.

Optional Signals

Cards that may be present. These are not counted as unexpected.

Forbidden Signals

Cards that must not be present. The test fails if any forbidden signal appears.

Unexpected Cards

Any card not matched to a required or optional signal is considered unexpected, causing the test to fail.

Hook Types

order-sign (Primary)

Used when a new medication is being prescribed. The draft medication(s) are in context.draftOrders, existing medications in prefetch.medicationData.

Per the CDS Hooks 3.0 specification, draftOrders is a FHIR Bundle that may contain multiple MedicationRequests. This occurs for example when prescribing a tapering schedule (afbouwschema) with separate sequential medication agreements:

{
  "context": {
    "draftOrders": {
      "resourceType": "Bundle",
      "type": "collection",
      "entry": [
        { "resource": { "resourceType": "MedicationRequest", "status": "draft", "...": "MA1 - day 1: 4x daily" } },
        { "resource": { "resourceType": "MedicationRequest", "status": "draft", "...": "MA2 - day 2: 3x daily" } },
        { "resource": { "resourceType": "MedicationRequest", "status": "draft", "...": "MA3 - day 3: 2x daily" } },
        { "resource": { "resourceType": "MedicationRequest", "status": "draft", "...": "MA4 - day 4: 1x daily" } }
      ]
    }
  }
}

When your system receives multiple draft MedicationRequests, the recommended evaluation strategy is sequential accumulation:

  1. Evaluate draft-1 as trigger, with only prefetch medications as context
  2. Evaluate draft-2 as trigger, with prefetch medications plus draft-1 as context
  3. Evaluate draft-3 as trigger, with prefetch medications plus draft-1 and draft-2 as context
  4. Continue for all remaining drafts

This ensures each medication is checked with awareness of the preceding drafts in the sequence. All results are merged into a single CDS Cards response.

medication-review

Used for periodic medication review. No draft orders — all medications are in prefetch.medicationData.

run-protocol

Used to execute a specific MFB protocol. Includes context.protocolNumber (integer) and optionally context.triggerMedicationId.

Error Handling

The VO4 platform enforces a 30-second timeout per request. Your system must respond within this window.

Scenario Expected behavior
Request timeout (30s) Test marked as ERROR
Non-2xx HTTP response Test marked as ERROR
Invalid JSON response Test marked as ERROR
Empty cards array Valid response (no signals)

When your system has no signals to return, respond with an empty cards array:

{
  "cards": []
}

Note: The VO4 platform sends test requests sequentially (one at a time). There are no concurrent requests to your system during a test run.

Example Scenarios

Note: The source.label and source.protocolNumber fields shown in these examples are optional. They are included for illustration but are not required for the platform to match your cards to scenario assertions. The platform matches based on the signal text in summary and detail.

Scenario 1: Overdose Detection

Input: Metoprolol 100mg, 1x daily 10 tablets (1000mg/day)

Expected card:

{
  "summary": "Dosering is hoger dan gebruikelijk.",
  "indicator": "warning",
  "detail": "<p>Pas zo nodig de dosering aan.</p>",
  "source": {
    "label": "zindex.dosage-check"
  }
}

Scenario 2: Allergy Check

Input: Doxycycline for patient with tetracycline allergy

Expected card:

{
  "summary": "Allergie: Doxycycline behoort tot ongewenste groep(en): TETRACYCLINES",
  "indicator": "critical",
  "source": {
    "label": "zindex.allergy"
  }
}

Scenario 3: MFB Protocol Signal

Input: RAAS-remmer + kaliumsparend diureticum

Expected card:

{
  "summary": "MFB: RAAS-remmers + Kaliumsparende diuretica",
  "indicator": "warning",
  "source": {
    "label": "zindex.mfb-protocol",
    "protocolNumber": 363
  }
}

Getting Access

Requesting an Account

Access to the VO4 self-test platform is by invitation. To request an account, contact the development team (see Support). Provide your name and organisation; you will receive an email with a registration link.

Once registered and approved, you can log in at vo4.mp9.nl and access all test suites, reports, and the Test Runner API.

Creating API Tokens

To call the Test Runner API from a script or CI/CD pipeline, you need a personal API token.

  1. Log in and go to Settings → API Tokens (/api-tokens)
  2. Enter a descriptive name for the token (e.g. ci-pipeline or my-guard-staging)
  3. Click Create — the token is displayed once; copy it immediately
  4. Use the token as a Bearer token in the Authorization header of every API request:
Authorization: Bearer {your-token}

Tokens expire after one year. You can revoke a token at any time from the same page. Create a separate token per environment or pipeline so you can revoke individual access without affecting others.

Getting Started

  1. Implement the CDS Hooks endpoint: POST /cds-services/order-sign
  2. Parse the incoming FHIR resources from context and prefetch
  3. Run your medication safety checks
  4. Return cards in the CDS Hooks response format
  5. Contact us to configure a test run with your endpoint URL and optional bearer token

Test Suites

The available test suites and their scenarios can be browsed on the Scenario's page.

Pipeline Integration (CI/CD)

The VO4 self-test platform exposes a REST API that you can call from your CI/CD pipeline to automatically run tests against your system after each deployment. See the VO4 Test Runner API Swagger UI for the full specification.

Making Your SUT Externally Reachable

The VO4 platform needs to send HTTP requests to your CDS Hooks endpoint. If your SUT runs in a local or private environment (e.g., behind a firewall or on localhost), the platform cannot reach it directly. Options to expose your SUT:

  • Tunnel service — Use a tool like ngrok, Cloudflare Tunnel, or localhost.run to create a temporary public URL for your local server:

    # Example with ngrok
    ngrok http 8080
    # Outputs: https://abc123.ngrok-free.app -> http://localhost:8080
    

    Use the generated URL as sutUrl in your test run. These URLs are temporary and change on restart.

  • Staging environment — Deploy your SUT to a publicly reachable staging server with a stable URL. This is recommended for CI/CD pipelines where a tunnel is impractical.

  • VPN / network peering — If both the VO4 platform and your SUT are within the same private network or VPN, direct internal URLs can be used.

Base URL

{vo4-platform-url}/api/cds-tests

Endpoints

Method Path Description
GET /health Health check (no auth)
GET /suites List available test suites
GET /test-cases List test cases (filterable)
GET /test-cases/{id} Get single test case details
POST /run Run tests against your system

Authentication

API requests require a Bearer token. Create one in Settings → API Tokens (/api-tokens) — see Creating API Tokens.

Authorization: Bearer {api-token}

The /health endpoint does not require authentication.

Running Tests

curl -X POST {vo4-platform-url}/api/cds-tests/runs \
  -H "Authorization: Bearer {api-token}" \
  -H "Content-Type: application/json" \
  -d '{
    "sut_base_url": "https://abc123.ngrok-free.app/api/cds-services",
    "name": "My test run",
    "directories": ["knmp-test-set"],
    "bearer_token": "optional-sut-auth-token"
  }'

Request Parameters

Parameter Type Required Description
sut_base_url string (URL) Yes Your CDS Hooks base URL (see URL requirements below)
sut_query_params string No Query parameters appended to every request to the SUT, without leading ? (e.g. "api_key=abc&mode=test")
name string No Human-readable run name
directories string[] No Test suites to run (e.g. ["knmp-test-set"]). Omit to run all.
filter string No Filter test cases by ID pattern
bearer_token string No Bearer token sent with each request to the SUT
rate_limit integer No Maximum requests per minute to the SUT
response_format string No Format for prefetch resources: fhir-json (default) or hl7v3-cda. See HL7v3 CDA format below.

SUT URL Requirements

The sut_base_url must meet the following requirements:

  • Must use http:// or https:// scheme
  • Must be publicly reachable from the VO4 platform (no private/internal IP addresses such as 10.x.x.x, 172.16.x.x, 192.168.x.x, or 127.0.0.1)
  • Must not point to localhost or loopback addresses
  • Use a tunnel service (e.g. ngrok) or a staging server if your system is not publicly accessible (see Making Your SUT Externally Reachable)

Response

{
  "id": 42,
  "name": "My test run",
  "status": "completed",
  "total_tests": 20,
  "passed_tests": 18,
  "failed_tests": 1,
  "error_tests": 1,
  "skipped_tests": 0,
  "results": [
    {
      "id": 100,
      "testcase_id": "knmp-casus-01",
      "testcase_name": "Interactie RAAS-remmer + NSAID",
      "status": "pass",
      "duration": 0.45,
      "error": null
    }
  ]
}

CI/CD Example (GitHub Actions)

The recommended approach is to start your Guard/SUT as a Docker container in the CI runner, expose it via a tunnel service like ngrok, and let the VO4 platform send test requests to it.

ngrok rate limit: The free ngrok tier allows 100 connections per minute. Use the rate_limit parameter to throttle VO4's request rate (e.g., 90 requests per minute to stay safely within the limit).

Prerequisites

Create the following GitHub Actions secrets:

Secret Description
VO4_PLATFORM_URL VO4 platform URL (e.g., https://vo4.mp9.nl)
VO4_API_TOKEN Your Sanctum API token for the VO4 platform
NGROK_AUTH_TOKEN Free ngrok auth token from dashboard.ngrok.com
SUT_BEARER_TOKEN (Optional) Bearer token if your SUT requires authentication

Workflow

name: Medication Safety Tests
on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  cds-tests:
    runs-on: ubuntu-latest
    steps:
      # Step 1: Start your SUT (replace with your own Docker image / startup command)
      - name: Start SUT
        run: |
          docker run -d --name sut -p 8080:80 your-org/your-guard:latest
          # Wait for SUT to be ready
          for i in $(seq 1 30); do
            curl -sf http://localhost:8080/api/cds-services/health > /dev/null 2>&1 && break
            echo "Waiting for SUT... ($i/30)"
            sleep 5
          done

      # Step 2: Expose SUT via ngrok
      - name: Start ngrok tunnel
        id: ngrok
        run: |
          curl -sf https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-linux-amd64.tgz | tar xz
          ./ngrok config add-authtoken ${{ secrets.NGROK_AUTH_TOKEN }}
          ./ngrok http 8080 --log=stdout > /tmp/ngrok.log 2>&1 &

          for i in $(seq 1 15); do
            NGROK_URL=$(curl -sf http://localhost:4040/api/tunnels 2>/dev/null \
              | jq -r '.tunnels[0].public_url // empty')
            if [ -n "$NGROK_URL" ]; then
              echo "Tunnel: $NGROK_URL"
              echo "url=$NGROK_URL" >> "$GITHUB_OUTPUT"
              break
            fi
            sleep 2
          done

      # Step 3: Start test run on VO4
      - name: Run tests
        id: run
        run: |
          RESPONSE=$(curl -sf -X POST "${{ secrets.VO4_PLATFORM_URL }}/api/cds-tests/runs" \
            -H "Authorization: Bearer ${{ secrets.VO4_API_TOKEN }}" \
            -H "Content-Type: application/json" \
            -d "{
              \"sut_base_url\": \"${{ steps.ngrok.outputs.url }}/api\",
              \"name\": \"CI #${{ github.run_number }}\",
              \"bearer_token\": \"${{ secrets.SUT_BEARER_TOKEN }}\",
              \"rate_limit\": 90
            }")
          RUN_ID=$(echo "$RESPONSE" | jq -r '.id')
          echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"

      # Step 4: Poll for completion
      - name: Wait for results
        id: results
        run: |
          for i in $(seq 1 120); do
            RESPONSE=$(curl -sf \
              "${{ secrets.VO4_PLATFORM_URL }}/api/cds-tests/runs/${{ steps.run.outputs.run_id }}" \
              -H "Authorization: Bearer ${{ secrets.VO4_API_TOKEN }}")
            STATUS=$(echo "$RESPONSE" | jq -r '.status')
            PASSED=$(echo "$RESPONSE" | jq -r '.passed_tests')
            TOTAL=$(echo "$RESPONSE" | jq -r '.total_tests')
            FAILED=$(echo "$RESPONSE" | jq -r '.failed_tests')
            ERRORS=$(echo "$RESPONSE" | jq -r '.error_tests')

            echo "[$i] $STATUS — $PASSED/$TOTAL passed (failed: $FAILED, errors: $ERRORS)"

            if [ "$STATUS" = "completed" ]; then
              echo "passed=$PASSED" >> "$GITHUB_OUTPUT"
              echo "failed=$FAILED" >> "$GITHUB_OUTPUT"
              echo "errors=$ERRORS" >> "$GITHUB_OUTPUT"
              break
            fi
            sleep 5
          done

      # Step 5: Report
      - name: Summary
        if: always() && steps.run.outputs.run_id
        run: |
          echo "## Medication Safety Test Results" >> "$GITHUB_STEP_SUMMARY"
          echo "| Passed | Failed | Errors |" >> "$GITHUB_STEP_SUMMARY"
          echo "|--------|--------|--------|" >> "$GITHUB_STEP_SUMMARY"
          echo "| ${{ steps.results.outputs.passed }} | ${{ steps.results.outputs.failed }} | ${{ steps.results.outputs.errors }} |" >> "$GITHUB_STEP_SUMMARY"
          echo "[View details](${{ secrets.VO4_PLATFORM_URL }}/cds-reports/${{ steps.run.outputs.run_id }})" >> "$GITHUB_STEP_SUMMARY"

Alternative: Fixed SUT URL (no ngrok)

If your SUT is deployed to a publicly reachable staging server, you can skip the ngrok step and pass the URL directly:

      - name: Run tests
        run: |
          curl -sf -X POST "${{ secrets.VO4_PLATFORM_URL }}/api/cds-tests/runs" \
            -H "Authorization: Bearer ${{ secrets.VO4_API_TOKEN }}" \
            -H "Content-Type: application/json" \
            -d '{
              "sut_base_url": "https://staging.your-system.com/api",
              "bearer_token": "${{ secrets.SUT_BEARER_TOKEN }}"
            }'

Listing Available Suites

curl {vo4-platform-url}/api/cds-tests/suites \
  -H "Authorization: Bearer {api-token}"

Health Check

Use the health endpoint for monitoring or pipeline readiness checks:

curl {vo4-platform-url}/api/cds-tests/health
{
  "status": "ok",
  "version": "1.0.0",
  "suites": 3
}

HL7v3 CDA Format

By default, VO4 sends FHIR R4 JSON requests as specified by the CDS Hooks standard. For systems that work with HL7v3 CDA, VO4 can send the entire request as XML with CDA resources instead.

How It Works

When response_format is set to hl7v3-cda, the entire CDS interaction switches to XML:

  1. Request: VO4 converts the CDS Hooks JSON request to an XML envelope. FHIR bundles in prefetch and draftOrders are converted to HL7v3 CDA via the Nictiz YATC XSLT pipeline (FHIR JSON → FHIR XML → ADA → CDA).
  2. Content-Type: The request is sent as application/xml instead of application/json.
  3. Response: The SUT responds with XML cards in the same structure as JSON cards.

XML Request Format

VO4 sends an XML POST to {sut_base_url}/cds-services/{service_id}:

<?xml version="1.0" encoding="UTF-8"?>
<CdsRequest xmlns="http://vo4.mp9.nl/cds">
  <hook value="order-sign"/>
  <hookInstance value="550e8400-e29b-41d4-a716-446655440000"/>
  <context>
    <patientId value="patient-123"/>
    <draftOrders>
      <!-- HL7v3 CDA organizer with medication data -->
      <organizer xmlns="urn:hl7-org:v3" classCode="CLUSTER" moodCode="EVN">
        <templateId root="2.16.840.1.113883.2.4.3.11.60.20.77.10.9411"/>
        <!-- ... CDA content ... -->
      </organizer>
    </draftOrders>
  </context>
  <prefetch>
    <medicationData>
      <!-- HL7v3 CDA organizer with patient medication history -->
      <organizer xmlns="urn:hl7-org:v3" classCode="CLUSTER" moodCode="EVN">
        <!-- ... CDA content ... -->
      </organizer>
    </medicationData>
  </prefetch>
</CdsRequest>

XML Response Format

The SUT responds with Content-Type: application/xml:

<?xml version="1.0" encoding="UTF-8"?>
<CdsResponse xmlns="http://vo4.mp9.nl/cds">
  <cards>
    <card>
      <summary value="MFB: Interactie diclofenac - acenocoumarol"/>
      <indicator value="warning"/>
      <detail>Bij gelijktijdig gebruik van diclofenac en acenocoumarol is er een verhoogd risico op bloedingen.</detail>
      <source>
        <label value="zindex.mfb-protocol"/>
      </source>
    </card>
  </cards>
</CdsResponse>

Card Fields (XML)

Element Attribute Required Description
summary value Yes One-sentence summary, max 140 characters
indicator value Yes info, warning, or critical
detail text content or value No Detailed description
source/label value Yes Signal type identifier (see Signal Types)
source/url value No URL to additional information

Usage

Via the web interface, select "HL7v3 CDA" in the Response formaat dropdown when creating a new test run.

Via the API:

curl -X POST {vo4-platform-url}/api/cds-tests/runs \
  -H "Authorization: Bearer {api-token}" \
  -H "Content-Type: application/json" \
  -d '{
    "sut_base_url": "https://your-cda-system.example.com/api",
    "response_format": "hl7v3-cda",
    "directories": ["knmp-test-set"]
  }'

Requirements

  • The SUT must accept Content-Type: application/xml on its /cds-services/{id} endpoint
  • The SUT must respond with Content-Type: application/xml using the CdsResponse format above
  • The Saxon XSLT sidecar service must be running (for the FHIR → CDA conversion)
  • CDA conversion uses the Nictiz Medicatieproces 9.3.0 XSLT mappings

Support

For questions about the VO4 self-test platform or scenario format, contact the development team.