Semantic Rules

Write scoped, deterministic business and interoperability validation rules using the Groovy DSL.

Semantic rules verify business, clinical, and interoperability correctness after core HL7 → FHIR conversion and postConvert(), but before FHIR delivery. Rules inspect the generated Bundle and report deterministic validation findings without modifying the Bundle.

Rule lifecycle

Semantic validation postConvert() → scope match → validate → issues → deliver or reject
FHIR BundleGenerated and enriched
→
Scope matchHL7 message type + trigger event
→
ValidateRun matching rule closures
→
Collect issuesINFO · WARNING · ERROR
→
DecisionDeliver when no ERROR; otherwise reject

Semantic validation runs after transformation and enrichment, immediately before the platform delivery decision.

Rule file structure

A semantic rule file extends SemanticRulesDsl through @BaseScript. One Groovy file may declare one rule or a group of related rules.

package core

import static io.firesonic.script.api.validation.RuleSeverity.*

import org.hl7.fhir.r4.model.*

import groovy.transform.BaseScript
import io.firesonic.script.api.validation.dsl.*

@BaseScript SemanticRulesDsl dsl

rule("SEMANTIC-CORE-ENC-001") { SemanticRuleBuilder r ->

    r.severity ERROR

    // Global rule: appliesTo(...) is intentionally omitted.

    r.category "Core platform"
    r.title "Encounter identifier is required"
    r.description "Every Encounter resource must contain at least one identifier."
    r.failureMessage "Encounter does not contain an identifier"

    r.validate { SemanticRuleContext ctx, SemanticIssueCollector issues ->
        ctx.each(Encounter) { Encounter encounter ->
            if (!encounter.hasIdentifier()) {
                issues.add(r, encounter)
            }
        }
    }
}

Rule anatomy

ElementRequiredPurpose
rule("...")YesStable rule identifier used in diagnostics, reporting, and the Administration Console.
severityYesControls whether the finding is informational, warning-only, or blocking.
categoryNoFunctional grouping such as Laboratory, Scheduling, Encounter, or Insurance. Defaults to General.
titleYesShort human-readable rule name.
descriptionYesExplains the business or interoperability condition being validated.
failureMessageYesDefault issue message when the rule reports a validation finding.
appliesTo(...)NoRestricts the rule to selected HL7 workflows. Omit it for a global rule.
validate { ctx, issues -> ... }YesValidation logic executed when the rule scope matches.

Severity

SeverityBehaviorTypical use
INFOProcessing continues and the Bundle may be delivered.Audit, visibility, or low-priority data-quality findings.
WARNINGProcessing continues and the Bundle may be delivered.Suspicious or incomplete data that does not make delivery unsafe.
ERRORThe message is marked REJECTED and the Bundle is not delivered.Business or interoperability violations that must block delivery.

Scopes and global rules

Use appliesTo(...) when a rule belongs to specific HL7 workflows. Scope helpers combine the HL7 message family with one or more trigger events.

rule("SEMANTIC-PAT-ADT-001") { r ->
    r.severity ERROR

    r.appliesTo(
        ADT(A01, A04, A08, A31)
    )

    ...
}

Multiple message families may be combined when the same rule applies across workflows:

r.appliesTo(
    SIU(S12, S13, S14),
    ADT(A01, A04)
)
Global rules

If appliesTo(...) is omitted, the rule is global and is evaluated for every converted message. Use global rules only for requirements that genuinely apply to every workflow.

Validation context

SemanticRuleContext provides indexed, read-only access to the generated Bundle, originating message, and FHIR resources. Prefer these helpers instead of manually traversing bundle.entry.

MethodPurpose
bundle()Generated FHIR Bundle.
envelope()Current MessageEnvelope.
messageCode() / triggerEvent()Originating HL7 workflow.
resources(Class)All generated resources of the requested FHIR Java type.
each(Class, ...)Iterate over matching resources.
exists(Class, ...)Check whether any matching resource satisfies a predicate.
findAll(Class, ...)Return all matching resources satisfying a predicate.
count(Class)Count generated resources of a type.
resourcesByFhirType(String)Lookup by FHIR resource type name without importing the Java class.
Do not assume a single resource

A Bundle may contain multiple resources of the same type. Merge, scheduling, and order workflows can legitimately contain multiple Patients, Encounters, Observations, Appointments, or other related resources.

Real-world example: active appointment for a deceased patient

This scheduling rule demonstrates why semantic validation goes beyond structural FHIR validity. A Patient and an Appointment may each be technically valid resources, while the combination is operationally inconsistent if the patient is marked as deceased and the appointment remains active.

The rule applies only to SIU scheduling workflows that create or update appointment state. It first checks whether the Bundle contains a deceased Patient, then inspects Appointment resources and reports an ERROR for active states.

package scheduling

import groovy.transform.BaseScript

import io.firesonic.script.api.validation.dsl.*

import org.hl7.fhir.r4.model.*

import static io.firesonic.script.api.validation.RuleSeverity.*
import static io.firesonic.script.api.validation.dsl.Hl7Scopes.*
import static io.firesonic.script.api.validation.dsl.SIUEvent.*

@BaseScript SemanticRulesDsl dsl

rule("SEMANTIC-APT-001") { SemanticRuleBuilder r ->

    r.severity ERROR

    r.appliesTo(SIU(S12, S13, S14))

    r.category "Scheduling"
    r.title "Active appointment for deceased patient"
    r.description "A patient marked as deceased must not have an active appointment."
    r.failureMessage "Active appointment exists for deceased patient"

    r.validate { SemanticRuleContext ctx, SemanticIssueCollector issues ->

        boolean hasDeceasedPatient = ctx.exists(Patient) { Patient patient ->

            patient.hasDeceasedBooleanType() &&
                    patient.getDeceasedBooleanType().booleanValue()
        }

        if (!hasDeceasedPatient) {
            return
        }

        ctx.each(Appointment) { Appointment appointment ->

            if (appointment.getStatus() in [
                    Appointment.AppointmentStatus.BOOKED,
                    Appointment.AppointmentStatus.PENDING,
                    Appointment.AppointmentStatus.PROPOSED
            ]) {
                issues.add(r, appointment)
            }
        }
    }
}
What this rule demonstratesHow
Workflow scopingSIU(S12, S13, S14) limits execution to scheduling workflows relevant to appointment creation and updates.
Cross-resource validationThe rule correlates Patient state with Appointment state inside the same generated Bundle.
Efficient context queriesctx.exists(...) short-circuits the rule when no deceased Patient is present.
Resource iterationctx.each(Appointment) evaluates every generated Appointment without manually scanning Bundle entries.
Blocking issueERROR causes the message to be marked REJECTED and prevents FHIR delivery.
Resource-linked diagnosticsissues.add(r, appointment) associates the validation finding with the offending Appointment resource.

Reporting validation issues

Rules report findings through SemanticIssueCollector. The rule identifier and configured severity are inherited automatically from the current rule definition.

MethodBehavior
issues.add(r)Report a rule issue not associated with a specific FHIR resource.
issues.add(r, resource)Report an issue and associate it with the affected FHIR resource.
issues.add(r, resource, message)Report an issue with a custom message instead of the rule's default failure message.

When a resource is supplied, FireSonic records its FHIR resource type and resource ID with the semantic issue when available.

r.validate { SemanticRuleContext ctx, SemanticIssueCollector issues ->

    ctx.each(Patient) { Patient patient ->

        if (!patient.hasIdentifier()) {
            issues.add(r, patient)
        }
    }
}

Validation result and postValidation()

The platform builds a SemanticValidationResult containing all collected INFO, WARNING, and ERROR issues. It is passed to Lifecycle Hooks through postValidation(...) after semantic validation and before the final delivery/rejection decision.

MethodPurpose
issues()All collected semantic issues.
hasErrors() / hasWarnings() / hasInfo()Check which severities are present.
errors() / warnings() / info()Return issues for one severity.
byRuleId()Group findings by rule identifier.
summary()Compact summary such as errors=1, warnings=2, info=0.
diagnostics()Human-readable diagnostics suitable for logs, notifications, and administrative views.
@Override
void postValidation(
        MessageEnvelope envelope,
        Bundle bundle,
        SemanticValidationResult result) {

    if (result.isEmpty()) {
        return
    }

    LOG.info("Semantic validation completed: ${result.summary()}")

    result.errors().each {
        LOG.error("[${it.ruleId}] ${it.message}")
    }

    result.warnings().each {
        LOG.warn("[${it.ruleId}] ${it.message}")
    }

    result.info().each {
        LOG.info("[${it.ruleId}] ${it.message}")
    }
}
Keep validation read-only

Use semantic rules to report validation findings. Keep FHIR transformation and Bundle modification in integration scripts such as postConvert(); do not mutate FHIR resources during semantic validation.

Rule hot reload Commercial

Automatic semantic-rule hot reload lets FireSonic activate validated rule changes without restarting the FireSonic runtime.

When a rule file is created, changed, or removed, FireSonic compiles and validates a new rule generation before activation. If the new generation fails compilation or validation, the current valid generation remains active and message processing continues. Successfully prepared generations are used for newly processed messages, while in-flight messages finish with the generation they started with.

Best practices

  • Keep each rule focused on one business or interoperability requirement.
  • Prefer workflow-specific scopes with appliesTo(...); use global rules only for truly universal requirements.
  • Use stable, descriptive rule identifiers, titles, descriptions, and failure messages.
  • Keep validation deterministic: the same input Bundle should produce the same validation result.
  • Prefer each(), exists(), findAll(), and count() over manual Bundle traversal.
  • Report findings through issues; do not construct SemanticValidationResult manually.
  • Reserve ERROR for conditions that must block delivery; use WARNING or INFO for non-blocking observations.
  • Keep transformation logic in integration scripts and validation logic in semantic rule files.