11.1 Classification and the register
Included from Basic tier
After this you can: classify nodes with the generic vocabulary, inherit with provenance, annotate in bulk, add your own profile, and export the register.
Before you start — tick each one:
- Lesson 1.2 — Build and change your own (
opschain learn 1.2) - Lesson 7.2 — Users, groups, rules, policies (
opschain learn 7.2)
In the GUI: Governance → Classification — the metadata profiles and the classification register, with staleness and coverage per field.
Every governance mechanism in OpsChain keys off one question: what is
this node, and how much does it matter? The answer is node metadata: typed, schema'd classification values annotated onto your
projects, environments and assets. Approval policies, notification
mandates and ${{ metadata.* }} conditions all select on it — but the
values themselves are structurally absent from the execution plane, so
classifying a node can never change what a step receives as input.
This lesson classifies the Lime Group demo estate (see the
opschain-governance-demo kit; any estate of your own works the same).
The full technical reference is opschain docs metadata; the API and
CLI surfaces are on opschain docs api-v2 and
opschain docs cli/metadata.
Who can do this. Reading a node's metadata rides your ordinary node read access. Writing requires the delegable metadata-writer determination — a path-RBAC rule granting
updatableon/metadata<node path>— which node write access never implies. Grant it as an admin:cat > metadata-writers.yaml <<'YAML'name: LimeBank metadata writersdescription: May classify the LimeBank subtreeauthorisation_rules:- path: /metadata/projects/limebankreadable: trueupdatable: trueYAMLopschain admin authz policies create -f metadata-writers.yamlopschain admin authz policies assign "LimeBank metadata writers" --user thomasWrites that touch a protective field (
core.criticality,core.data_sensitivity,core.external_exposure) additionally require an elevated session — seeopschain learn 7.4.
1. The core vocabulary
The core profile ships with the platform, seeded at boot: the
generic base vocabulary every enterprise can classify with. OpsChain
holds no regulatory opinion in core — regulatory bindings (CPS 230/234,
SOCI, DORA) ship their own profiles and map these fields into their
determinations. List what your server knows:
opschain metadata profiles list
opschain metadata profiles show core
key: core
title: Core classification themes
description: The generic base vocabulary every regulatory binding maps onto — OpsChain holds no regulatory opinion; bindings (CPS 230/234, SOCI, DORA) ship their own profiles and map core fields into their determinations.
publisher: core
version: 1
adopted: yes
FIELD TYPE ENUM VALUES APPLIES TO INHERIT PROTECTIVE REMOVED
core.criticality enum critical,high,medium,low any subtree yes -
core.data_sensitivity enum restricted,confidential,internal,public any subtree yes -
…
The core fields are all subtree-inherited: core.criticality
(critical|high|medium|low — how important the node is to the
business), core.data_sensitivity
(restricted|confidential|internal|public),
core.rto_class/core.rpo_class (class_0..class_4, tightest
first — recovery time/point objective classes), core.cost_centre
(free-form string — your chart-of-accounts code), core.lifecycle
(pilot|active|deprecated|retired), core.external_exposure
(internal|partner|public_internet), core.managed_by
(internal|vendor|managed_service) and its companion
core.managed_by_name (free-form string naming the team, vendor or
MSP — the enum is what policies select on, the name is for registers
and escalation), and core.end_of_support (a YYYY-MM-DD date — when
the node's platform or software leaves vendor or organisational
support, the end-of-life anchor an unsupported-platform posture selects
on).
2. Classify a node
Classify the LimeBank production environment, then confirm what the estate below it now resolves. As thomas (elevated — three of these fields are protective):
opschain metadata set /projects/limebank/environments/prod \
core.criticality=critical core.data_sensitivity=restricted \
core.rto_class=class_0 core.external_exposure=public_internet \
core.cost_centre=CC-4411
metadata set on /projects/limebank/environments/prod (5 field write(s))
opschain metadata show /projects/limebank/environments/prod
opschain metadata show /projects/limebank/environments/prod/assets/core_banking --effective
FIELD VALUE SOURCE INHERITED
core.cost_centre CC-4411 /projects/limebank/environments/prod yes
core.criticality critical /projects/limebank/environments/prod yes
core.data_sensitivity restricted /projects/limebank/environments/prod yes
core.external_exposure public_internet /projects/limebank/environments/prod yes
core.rto_class class_0 /projects/limebank/environments/prod yes
showprints the node's own values;--effectiveprints the inheritance-resolved view, with provenance — theSOURCEcolumn names the governing ancestor for every inherited field. Classify at the environment and the assets follow — the resolved value, not a copy.- Clear a field with
opschain metadata clear <node-path> <field>; the subtree falls back to the nearest classified ancestor. - Every write is audited with per-field before/after:
opschain audit --type-prefix api:node_metadata:.
3. Bulk adoption
Classifying an existing estate node by node does not scale. annotate
applies one write across a selector — a path glob AND-composed with
node kinds and metadata predicates:
opschain metadata annotate --scope '/projects/limebank/**' --kind asset \
--where 'core.criticality is unclassified' core.criticality=high
annotated 7 node(s); skipped 0 node(s) no written field applies to
- Covered nodes you cannot read are not covered; nodes a field does not
apply to are skipped and counted. Each node's write is individually
audited (
api:node_metadata:bulk_annotate). - The
unclassifiedselector is the adoption workhorse: annotate the unclassified remainder without touching deliberate classifications. - Date fields bring their own selectors:
before/aftercompare against a fixedYYYY-MM-DD, andis elapsedmatches a date already in the past — "everything now out of support" needs no new value as dates age:
opschain metadata annotate --scope '/projects/limebank/**' --kind asset \
--where 'core.end_of_support is elapsed' core.lifecycle=deprecated
4. Your own vocabulary
Customer fields live under your organisation's x-<org>.* namespace
(non-x- families are reserved for the base core.* themes and
pack-shipped bindings):
cat > x-lime-ops.yaml <<'YAML'
key: x-lime-ops
title: Lime Group operations
description: Lime Group operational ownership vocabulary
fields:
- key: x-lime.support_tier
type: enum
enum_values: [platinum, gold, silver]
applies_to: asset
inherit: none
YAML
opschain metadata profiles create -f x-lime-ops.yaml
created metadata profile x-lime-ops (version 1, 1 field(s))
opschain metadata set /projects/limebank/environments/prod/assets/core_banking x-lime.support_tier=platinum
metadata set on /projects/limebank/environments/prod/assets/core_banking (1 field write(s))
Revising a profile (opschain metadata profiles revise) bumps its
version. Additive revisions are free; narrowing ones (a removed field, a
shrunk enum) stale-flag existing values — nothing is deleted and
nothing auto-migrates. A revision can declare per-field old-to-new
value_mappings, and opschain metadata profiles migrate <key>
applies them to the stale values as per-node audited writes. Unmapped
stale values stay counted and reportable.
5. The register
The register is the estate-wide node × field view — the asset-register evidence lane your auditor asks for:
opschain metadata register --profile core
opschain metadata register --field core.criticality --value critical
opschain metadata register --stale-only
opschain metadata register --export csv --out register.csv
Each listing ends with its coverage line — how much of the estate the selection has actually reached:
coverage: 9 of 12 node(s) classified
Rows are read-authz filtered per row. The OWNER column is joined live
from the /governance/owners fabric (nearest covering declaration
wins, lesson 10.2): ownership lives on the governance fabric; the
register joins it, never stores it. The CSV export carries the same
rows, inherited values included, each with its provenance.
6. Where classification bites
- Approval policies match on metadata predicates: a policy with
metadata_predicatesgoverns exactly the classified subset — and fails closed, matching nothing without a classification (lesson 10.1). - Notification subscriptions and mandates select on the same predicates, and the metadata event family supports "notify on any reclassification" as a single subscription (lesson 11.4).
- Workflow
${{ metadata.* }}conditions read the effective view at entry — but metadata never reaches action inputs, rendered templates or the runner's step context.
Check yourself
- Which column of the register answers "who owns this", and where does it come from? Then: which two columns does
metadata show --effectiveprint beside an inherited value thatmetadata showdoes not?
What you learned
- The generic
core.*vocabulary ships seeded and subtree-inherits;--effectiveshows the resolved view with provenance. - Writes need the delegable metadata-writer determination on
/metadata<node path>; protective fields add the step-up floor. annotatewith theunclassifiedselector is the bulk adoption path; profile revisions stale-flag and migrate explicitly, never silently.metadata register(and its CSV export) is the audit evidence lane.
Next: lesson 11.2 — Controls and the evidence ledger (opschain learn 11.2), or back to your track (opschain learn).