· 9 min read
Semantic Metadata: Making a SysML v2 Model Queryable Like an Ontology
A keyword that implies a type relationship turns your architecture model into something you can also reason over — cross-cutting classifications that inherit, and queries you could not otherwise write.
Every model eventually grows a set of labels. Mission critical. Safety critical. Externally exposed. Somebody adds them as a stereotype, or a tagged value, or a naming convention, and for a while it works — you can find things.
Then someone asks a question the labels cannot answer. Every component that is security-relevant, including the ones that are security-relevant because they are externally exposed, that has not completed cyber evidence. The usual answers — a hand-written query, a spreadsheet, somebody’s memory — all degrade the same way.
SysML v2 has a construct built for this, and it is easy to walk past because it looks like tagging. It reads like a tag, and what it produces is a specialization.
Three ways to say “this is safety critical”
The useful question is not which one is best, but what each one produces in the model.
Plain metadata attaches values to an element:
metadata def ReleaseStatus {
:> annotatedElement : SysML::PartDefinition;
attribute safetyEvidenceComplete : Boolean;
attribute releaseApproved : Boolean;
}
This is the right construct for mutable project state. It has no type relationship, nothing inherits it, and nothing about the model’s structure changes — which is what you want for a status flag that flips next week.
Specialization is a real type relationship:
part def FlightControlSystem :> SafetyCriticalComponent;
The classification is now structural. It inherits, and queries can traverse it.
The cost is that you get one taxonomy, and if SafetyCriticalComponent carries
features, every specialization drags them along. Stack a few of these and the
features start to collide.
Semantic metadata is a keyword that implies the specialization:
part def SafetyCriticalPartSemantic;
part safetyCriticalParts : SafetyCriticalPartSemantic[*] nonunique;
metadata def <safetyCritical> SafetyCriticalMetadata :> SemanticMetadata {
:> annotatedElement : SysML::PartDefinition;
:> annotatedElement : SysML::PartUsage;
:>> baseType = safetyCriticalParts meta SysML::PartUsage;
}
Declare that once and the model gains a keyword:
#safetyCritical part def FlightControlSystem :> SoftwareComponent;
#safetyCritical does not decorate that definition. Per the specification, each
annotated element of a SemanticMetadata usage implicitly specializes the
definition determined from its baseType. So the part now genuinely is a
SafetyCriticalPartSemantic, with all the structural consequences — while the
model text still reads like a tag.
Tagging ergonomics, typing semantics.
The design rule that makes it safe: keep the bases thin
Look again at what the semantic base actually is:
part def SafetyCriticalPartSemantic;
No features. No attributes. Nothing.
This is the decision the whole approach rests on. Because the semantic bases carry nothing, an element can specialize several of them at once and there is nothing to conflict. Classification becomes free to stack.
Engineering properties travel a separate, ordinary hierarchy:
abstract part def SoftwareSecurityProperties {
attribute riskLevel : String[0..1];
attribute attackSurfaceDescription : String[0..1];
attribute protectedData : String[0..1];
attribute boundaryDescription : String[0..1];
}
abstract part def SoftwareComponent :> SoftwareSecurityProperties;
So there are three jobs and one construct for each: semantic metadata classifies; normal specialization carries features; plain metadata holds mutable state. Run together they produce the feature collisions that give multiple inheritance its reputation. Kept apart, cross-cutting classification works.
Cross-cutting is the thing single inheritance cannot do
Real components are several things at once, along axes that do not nest:
#missionCritical
#runtimeObservable
#externallyExposed
part def SatelliteCommandReceiver :> Gateway { … }
Mission-critical is a program concern. Runtime-observable is an instrumentation concern. Externally-exposed is a security concern. There is no sensible single taxonomy that contains all three, and a stereotype gives you none of the structure.
And the classifications compose. The security vocabulary is itself a small taxonomy:
part def SecurityRelevantSoftwareComponent;
part def ExternallyExposedSoftwareComponent :> SecurityRelevantSoftwareComponent;
part def SensitiveDataSoftwareComponent :> SecurityRelevantSoftwareComponent;
part def TrustBoundaryEnforcementComponent :> SecurityRelevantSoftwareComponent;
So #externallyExposed does not give you one classification. It gives you two —
the element is externally exposed, and therefore security-relevant — without
anyone writing the second one down.
#externallyExposed alone yields two classifications, because its semantic base specializes SecurityRelevantSoftwareComponent. The hardened child declares one ordinary specialization and gets all four.And it keeps going down. A child that specializes the receiver inherits every classification through ordinary transitive subclassification:
part def HardenedSatelliteCommandReceiver :> SatelliteCommandReceiver {
attribute riskLevel redefines SatelliteCommandReceiver::riskLevel = "Medium";
}
No keywords reapplied. It is mission-critical, runtime-observable, externally exposed, and security-relevant, because it is a kind of something that was. That is ontology behavior, in an architecture model, with no second tool.
Two questions, and the difference between them
Once classification is structural, you get two genuinely different queries — and knowing which one you are asking is most of the value.
“Was the keyword applied here?” — a metadata filter:
view 'Explicit Safety Critical Classifications' : 'Part Semantic Query Table' {
expose AURORA::Architecture::*;
filter @AuroraPartSemantics::SafetyCriticalMetadata;
}
“Is this element in this category, directly or by inheritance?” — a specialization test:
DS_Functions::IsSpecific(
rowElement as SysML::PartDefinition,
AuroraPartSemantics::SafetyCriticalPartSemantic meta SysML::PartDefinition
)
The first is an authoring question: who declared this, and where. The second is an engineering question: what is true of this element. The hardened receiver answers no to the first and yes to the second, and both answers are correct.
A tag can answer the first question. It cannot answer the second, and the second is usually the one being asked.
What it unlocks
Once the classification graph is real and the mutable status is kept separate, a few things that are normally assembled by hand become queries.
A release gate is the intersection of a semantic class and a status flag:
Is this element safety-critical (inherited or direct) and is its
safetyEvidenceCompleteflag false?
IsSpecific(rowElement, SafetyCriticalPartSemantic) and
(GetAppliedMetadata(rowElement) as ReleaseStatus).safetyEvidenceComplete == false
That column maintains itself. A new component that specializes a safety-critical part appears in the gate the moment it is created, without anyone remembering to add it.
Compose a few of those with or and you have a release blocker column
covering safety, cyber, runtime monitoring, and mission verification at once.
The same structure serves several other things that are usually built by hand:
| Use | The query |
|---|---|
| Change-impact reasoning | everything transitively in a classification, not just what was tagged |
| AI agent routing | send safety-critical elements to the safety reviewer, security-relevant ones to the cyber reviewer |
| RAG context selection | retrieve the classified subset instead of the whole model |
| Digital-thread integration | a stable, queryable vocabulary other tools can join on |
The third one repays attention. When you point a language model at a model this size, the hard part is choosing what goes in the context window. “Everything security-relevant, including inherited” is a precise and cheap answer to that. A hand-maintained tag list is neither.
The vocabulary is a deliverable
One consequence worth planning for: the semantic libraries become a program asset, separate from any one model.
AuroraPartSemantics missionCritical, safetyCritical, runtimeObservable,
singlePointOfFailure, aiControlled, fieldReplaceable,
securityRelevant, externallyExposed, sensitiveData,
trustBoundaryEnforcer
AuroraRequirementSemantics contractual, safetyRequirement, cyberRequirement,
kpp, derivedRequirement
AuroraActionSemantics nominal, exception, recovery, failsafe,
humanApprovalRequired
AuroraPortSemantics externalInterface, trustBoundary, latencyCritical,
safetyInterface
AuroraDataSemantics authoritativeData, sensitiveDataItem, realTimeData,
derivedData
AuroraStateSemantics safeState, degradedState, failureState, missionState
AuroraVerificationSemantics acceptanceTest, qualificationTest, regressionTest,
runtimeVerification, safetyVerification, cyberVerification
AuroraParameterSemantics moe, mop, tpm, safetyMargin
AuroraLifecycleMetadata ReleaseStatus, EvidenceStatus ← plain, mutable
Note the last line. Lifecycle status stays ordinary metadata, deliberately. A classification is a claim about what something is and should inherit; a status is a claim about where it currently stands and must not. Putting release approval into the type system would mean re-typing the model every time a review closes.
Note also that every layer of the model gets a vocabulary — not just parts. Requirements, actions, ports, items, states, verification cases, and attributes all carry semantic classification, which is what makes cross-layer queries possible at all. Every safety requirement whose verifying case is not a safety verification is answerable only if both ends are classified.
A note on tool support
The specification permits multiple extension keywords on a single definition,
and says each contributes its implied specialization. In Magic Systems of Systems
Architect 2026x Refresh1, all of the metadata applications are created but the
implied specialization is materialized for only the first keyword. The second
and third classifications never appear in ownedSpecialization.general, so
inherited semantic queries return false for them. That is one build; others have
not been checked.
The workaround costs one line per element: let the first keyword do its normal work, and add the remaining semantic bases explicitly.
#missionCritical
#runtimeObservable
#externallyExposed
part def SatelliteCommandReceiver
:> Gateway,
RuntimeObservablePartSemantic,
ExternallyExposedSoftwareComponent { … }
That is what the model above does: four keywords on FlightControlSystem, and
three companion specializations — every one except the first.
Because the semantic bases are thin, the extra subclassifications are safe. That
design rule pays for itself here as well as in the ordinary case. Every capability above
survives: the keyword vocabulary, the inherited classification, IsSpecific
queries, release gates, agent routing, all of it. What is temporarily lost is
elegance: for an element with more than one classification there is a redundant statement in
the text saying something the keyword already said.
When tool support catches up with the specification, those companion lines are deleted and nothing else changes. The libraries, keywords, queries, dashboards, and model structure are all built against the standard semantics rather than against the workaround, which is the reason this is worth doing now rather than waiting.
When not to reach for it
Not every keyword-shaped fact belongs in semantic metadata, and the boundary is worth knowing before you build a vocabulary.
Take marking derived states and transitions with which route through a workflow they lie on — nominal, alternate, exception. It looks like the job: a cross-cutting classification, applied as a keyword, read by a style rule and a query. It is not, for three reasons.
The claim is not a classification. baseType asserts the annotated element
is that type. That is right for #safetyCritical — the part genuinely is a
safety-critical part, and it stays true tomorrow. “On the nominal route” is a
relation between an element and a traversal somebody computed, and it changes
when the flow changes. It is a fact about the element’s position, not about what
the element is.
There is no base type to point at. Every semantic keyword names a type that
exists for its own sake and can be extended: action def PaymentDeclined :> ExceptionBehavior is a real thing a project would want to write. A route stamp
would need a base type invented purely to be the target of the baseType, which
is circular and extends to nothing.
baseType pins one metaclass. meta SysML::StateUsage names one. A route
stamp goes on states and transitions, so either choice is false of half the
elements carrying it.
Those three make a usable test. Before writing :> SemanticMetadata, ask: does
this assert the element is something; is there a type that would exist even
if this keyword did not; and does it apply to exactly one metaclass? Three noes
means a plain metadata def. Style rules read those, queries filter on them, and
nothing about the model’s type structure changes — which is correct, because
nothing about what those elements are has changed either.
Four things that are easy to get wrong
None of these announce themselves. Each one presents as a different problem than it is.
A short name is not a segment of a qualified name. The lowercase name in
angle brackets — <safetyCritical> — is an alternative identifier for the
declaration, not a member you can path to. #MyLibrary::safetyCritical parses
and resolves to nothing, and @MyLibrary::safetyCritical in a filter returns
Could not evaluate function ”@” with the table empty. Write the keyword
unqualified and import the library; use the declared name in a filter or a
cast.
Red on a Cameo symbol is a validation marker before it is a color. An element outlined in red usually means an annotation that resolved to nothing — “a metadata feature must be typed by a non-abstract metaclass” — most often because the attached library is an older version that does not declare the keyword yet. Hover the element and read the message before reasoning about style rules.
A view applies one style sheet. Adding a second to the same diagram does not combine them; the second is never reached, its colors never appear, and nothing reports it. A rule that has to reach two diagrams belongs in both sheets.
A Boolean column needs an untyped row parameter and a null negative branch.
Declare it in rowElement; — typing it : Element yields a value per row and
a second one, which renders as a tick and a cross in the same cell. Return null
rather than false when the metadata is absent, because absent is empty, not
false.
Why it is worth the setup
The cost is a day building the semantic libraries and deciding the vocabulary. The second part is the real work, and it is work worth doing anyway — arguing about whether “mission critical” and “safety critical” mean the same thing is a good argument to have early.
What you get is a model that answers questions rather than storing answers. The classification graph is maintained by the act of modeling rather than by somebody remembering to update a list. A component added next year, three specializations deep, shows up in the right gate and the right review packet without anyone touching either.
That is the difference between a model you read and a model you can ask.
More of this, on video
Walkthroughs in SysML v2 and MBSE tooling, with the model on screen.
