Skip to main content
Version: v5.7.0

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 updatable on /metadata<node path> — which node write access never implies. Grant it as an admin:

cat > metadata-writers.yaml <<'YAML'
name: LimeBank metadata writers
description: May classify the LimeBank subtree
authorisation_rules:
- path: /metadata/projects/limebank
readable: true
updatable: true
YAML
opschain admin authz policies create -f metadata-writers.yaml
opschain admin authz policies assign "LimeBank metadata writers" --user thomas

Writes that touch a protective field (core.criticality, core.data_sensitivity, core.external_exposure) additionally require an elevated session — see opschain 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
  • show prints the node's own values; --effective prints the inheritance-resolved view, with provenance — the SOURCE column 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 unclassified selector is the adoption workhorse: annotate the unclassified remainder without touching deliberate classifications.
  • Date fields bring their own selectors: before/after compare against a fixed YYYY-MM-DD, and is elapsed matches 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_predicates governs 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 --effective print beside an inherited value that metadata show does not?

What you learned​

  • The generic core.* vocabulary ships seeded and subtree-inherits; --effective shows the resolved view with provenance.
  • Writes need the delegable metadata-writer determination on /metadata<node path>; protective fields add the step-up floor.
  • annotate with the unclassified selector 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).