Skip to main content
Version: v0.25.0 (Latest)

The Attribute Set (L3)

An attribute set is a role-scoped traversal subselection of exactly one published profile. Where the catalog (L1) defines every attribute the organization manages and the profile (L2) assembles those into use-case roles, the set (L3) answers a sharper question: which attributes from which roles does a specific output (a form, a portal page, a PDF, an API payload, or a credential) need, and exactly where does each one live in the entity graph?

The set is the consumable contract that L4 channels bind to. It is versioned and reuses the same DRAFT | PUBLISHED lifecycle as every other layer.

VDX presents a guided authoring UI for the attribute set layer. The concepts and the resulting model are identical whether you work over the REST API documented here or through the UI.

Four-layer semantic model: L1 catalog declares entities and relationships; L2 profile binds them to use-case roles; L3 set is a role-scoped traversal subselection of one profile; L4 channels are the set-bound artifacts that issuer designs and verifier DCQL both derive from.

Core Concepts

profileRef, pinning one profile version

A set references exactly one published profile via profileRef: { profileId, profileVersion }. Pinning the version keeps the set's view of the world stable: if the profile is revised and republished the set continues to resolve against the version it was authored against.

selected, role-scoped traversal paths

selected is an array of { roleRef, path } entries. Each entry selects one attribute reachable from the named role. path is always an array of string segments, and it may cross a relationship boundary.

This is the pivotal capability of the set layer. Consider the employee role, which the profile binds to the Person entity (narrowed by the Employee specialization). Person has a residentialAddress relationship to the Address entity, and Address carries a country attribute (an ISO 3166-1 picklist). To select that country value the path simply steps across the relationship:

{ "roleRef": "employee", "path": ["residentialAddress", "country"] }

The first segment, residentialAddress, is the relationship name declared in the catalog. The second segment, country, is the attribute on the Address entity at the other end. The set traverses from the employee role, across the relationship, and arrives at the attribute on the related entity.

A direct attribute on the role's entity uses a single-segment path:

{ "roleRef": "employee", "path": ["given_name"] }

There is no special syntax to distinguish direct from traversal paths; the platform resolves both against the profile's entity graph.

overrides, narrow-only tightening

overrides is an array of { roleRef, path, ... } entries that tighten constraints on a selected (role, path) pair. Overrides are narrow-only: they cannot widen anything the catalog or profile already declared.

The fields accepted in a set override are:

FieldEffect
conformanceTighten from OPTIONAL to MANDATORY.
entryCodesSubsetRestrict a picklist to a subset of the codes declared in overlays.entryCodes at the catalog level.
cardinalityNarrow the occurrence count within the set's scope.
sensitiveMark the attribute as needing extra handling (masked in UIs).

The walkthrough set uses one override: employment_status is narrowed to entryCodesSubset: ["active", "suspended"], removing the terminated code. The catalog declares all three codes; the set never admits a terminated employment status, whatever output consumes it.

Governance flow from L1 catalog through L2 profile to L3 set and L4 channel: jurisdiction, classification, and sdPolicy values set at the catalog entity flow through role bindings and role-scoped traversal selections, narrowing at each layer but never widening, reaching the wire form with full provenance.

The through-line from set traversal to channel output

The traversal paths in selected are not discarded after the set is published; they are what each channel consumes. A form channel binds a path to an input field, a portal page to a display row, a PDF channel to a placement, an API channel to a payload key, and a VC channel to a claimMappings entry. Whatever the channel, the path crosses the relationship boundary once (here, in the set) and the channel decides where the value lands in its output. Nothing about the resolution is repeated downstream; the set is the single place where cross-entity selection is declared.

As one example, the Employee SD-JWT channel resolves the traversal entries like this:

Set traversal pathWire claim path
["residentialAddress", "country"]SD-JWT: ["address", "country"]
["residentialAddress", "postal_code"]mdoc: ["postal_code"] (placed under the channel's namespace)

The set knows how to reach the value across the entity graph; each channel decides where to place it in its own output. See Channels (L4) for the full per-channel shapes.


REST: Create, Publish, and Resolve an Attribute Set

Every call requires the operator's OIDC bearer token. The tenant is resolved from the token's tenant_id claim. These are license-gated enterprise writes.

Authorization: Bearer <operator access token>
Content-Type: application/json

Create the set

POST /api/attributes/v1/sets
{
"name": "Acme Employee Badge Set (layered)",
"description": "L3 set selecting employee identity + residential country traversal + employer legal name.",
"profileRef": { "profileId": "a1f0...profileId", "profileVersion": 1 },
"selected": [
{ "roleRef": "employee", "path": ["given_name"] },
{ "roleRef": "employee", "path": ["family_name"] },
{ "roleRef": "employee", "path": ["employee_id"] },
{ "roleRef": "employee", "path": ["job_title"] },
{ "roleRef": "employee", "path": ["employment_status"] },
{ "roleRef": "employee", "path": ["email"] },
{ "roleRef": "employee", "path": ["residentialAddress", "postal_code"] },
{ "roleRef": "employee", "path": ["residentialAddress", "country"] },
{ "roleRef": "employer", "path": ["legal_name"] }
],
"overrides": [
{
"roleRef": "employee",
"path": ["employment_status"],
"entryCodesSubset": ["active", "suspended"]
}
]
}

The set selects nine paths in total:

  • Six direct employee attributes: given_name, family_name, employee_id, job_title, employment_status, email. These are attributes declared on the Person or Employee entity and reached without crossing a relationship.
  • Two relationship-traversal paths: ["residentialAddress", "postal_code"] and ["residentialAddress", "country"]. Both cross the residentialAddress relationship from the employee role's Person entity to the Address entity.
  • One employer attribute: legal_name, a direct attribute on the Employer entity reached via the employer role.

The two relationship-traversal entries are worth examining explicitly:

{ "roleRef": "employee", "path": ["residentialAddress", "postal_code"] }

This crosses the residentialAddress relationship (declared in the catalog as has-address, from Person to Address) and arrives at postal_code on the Address entity.

{ "roleRef": "employee", "path": ["residentialAddress", "country"] }

This crosses the same relationship and arrives at country, the ISO 3166-1 picklist attribute on Address. This is the path that ultimately maps to ["address", "country"] in the SD-JWT credential wire form.

The overrides entry narrows employment_status down to two codes. The catalog declared ["active", "suspended", "terminated"]; the override removes terminated from the set's effective code list. The entryCodesSubset must be a subset of the catalog's entryCodes.codes; the API rejects any code not present at the catalog level.

Publish the set

Once the set is reviewed, publish it with a status transition. Publishing locks the version so that downstream channels pin a stable view.

PUT /api/attributes/v1/sets/{setId}/status
{ "status": "PUBLISHED" }

Resolve the set

GET /api/attributes/v1/sets/{setId}/resolved returns the fully resolved set: all selected attributes expanded against the profile snapshot (which is itself resolved against the pinned catalog version), with overrides applied.

GET /api/attributes/v1/sets/{setId}/resolved

Notice that the resolved employment_status entry carries only the two active codes (active, suspended) and their localized display strings, the terminated code is absent because the set-level entryCodesSubset override removed it. The resolved ["residentialAddress", "country"] entry carries the full ISO 3166-1 picklist shape, including all four locale blocks, resolved from the Address entity in the catalog.


Version Pinning and Narrowing Precedence

Once published, the set's version is frozen. Any channel that references this set via setRef: { setId, setVersion: 1 } always resolves the v1 shape regardless of whether the set is later revised and republished at v2.

The override precedence from catalog to set follows the same narrow-only rule as every other layer:

  1. Catalog (L1): valueType, overlays.entryCodes, governance overlays, canonical.
  2. Profile override (L2): may tighten conformance, sdPolicy, entryCodesSubset, sensitive. The walkthrough profile tightens email to MANDATORY for the employee role.
  3. Set override (L3): may tighten further. The walkthrough set tightens employment_status to a two-code subset.
  4. Channel (L4): selection and output mapping only, no governance override.

No layer widens what the layer above declares. An attempt to specify an entryCodesSubset that adds a code not present in the catalog's entryCodes.codes is rejected at write time.


What the Published Set Delivers

With the set published you have a stable, versioned, cross-entity selection that:

  • Names exactly which attributes are in scope for the use case, by role and traversal path.
  • Carries the full resolved attribute shape (value type, governance overlays, localized labels) derived from the catalog through the profile.
  • Applies the final narrow-only tightening (the employment_status subset in this walkthrough).
  • Is ready to be consumed by one or more channels: a form, a portal page, a PDF document, an API payload, or a verifiable credential, each of which maps these traversal paths to its own output.

The two relationship-traversal paths, ["residentialAddress", "postal_code"] and ["residentialAddress", "country"], make the cross-entity selection concrete. A downstream SD-JWT channel maps ["residentialAddress", "country"] to the wire claim path ["address", "country"]; an mdoc channel maps ["residentialAddress", "postal_code"] to the element ["postal_code"] and places it under its namespace; a form or PDF channel would bind the same paths to a field or a placement. The set is where those traversals are declared once; each channel decides where they land in its output.

Next Steps

  • Channels (L4): bind this set to two credential formats (SD-JWT and mdoc), declare claim mappings including the residentialAddress traversal paths, and create the OID4VCI issuance channel
  • Attribute Profile (L2): review the profile this set is derived from
  • Modeling Your World: the catalog entities, relationships, and governance that underpin every layer
  • Provenance & Operations: usage-lineage rows written when a VC channel or DCQL query consumes a set traversal path