Library sets and variables
A library set is a named grouping of variables that a consumer fetches as a unit. A variable is a named thing inside one, and it holds one or more values.
The API and the database call it a variable set; the management UI and the import file call it a library set.
They are the same record. This documentation says "library set" in prose and variableset where a path or field
name requires it.
Choosing library sets
A library set is the unit of consumption, so the question to ask is what gets fetched together. Common shapes:
- One per service —
orders-service,billing-service. Each deployment fetches exactly its own. - One shared plus one per service — a
commonset holding logging levels and shared endpoints, fetched alongside the service's own. - One per team — where a team's services share most of their configuration.
Fetching several at once is supported and normal: ?set=1&set=2. Where two sets both hold a variable of the same
name, they are walked in name order and the later one wins.
A group's VariableList/Read/ReadSecrets/Create/Update/Delete may each be restricted to specific
library sets instead of every one in the organization — see
Library-set scoping. Resolving a value can pull one in from an
ancestor set (see resolution), so reading via /variablevalues needs VariableRead
for every set actually reached, not only the ones named in set.
Variables and values
A variable has a name, a type, whether it is sensitive, and a list of values. Both type and sensitivity are properties of the variable, not of any one of its values — every value of a variable is the same type, and either all of a variable's values are sensitive or none of them are.
Type
| Type | Stored in | Notes |
|---|---|---|
String | value | The default when a variable does not say otherwise. |
Multiline | value | Text spanning more than one line. Presented as a multi-line editor in the management UI. |
Number | numberValue | |
Boolean | booleanValue | Can never be sensitive, and can never be switched to or from another type once the variable has been saved. |
Json | value | Presented with a JSON-aware editor (Monaco) in the management UI. |
Certificate | value |
Every type other than Boolean can be switched to any other non-Boolean type after the variable has been
saved — String to Multiline, Json to Certificate, and so on. Boolean is fixed at creation: a variable
cannot be created or later changed into Boolean, and a Boolean variable cannot be changed into anything
else.
{
"name": "DatabaseConnection",
"variableSetId": 1,
"type": "String",
"sensitive": false,
"values": [
{ "value": "Server=localhost;Database=orders", "tags": [] },
{ "value": "Server=prod.internal;Database=orders", "tags": ["Environment/Production"] },
{ "value": "Server=eu-prod.internal;Database=orders", "tags": ["Environment/Production", "Region/EU"] }
]
}
{
"name": "MaxConnections",
"variableSetId": 1,
"type": "Number",
"values": [
{ "numberValue": 100, "tags": [] }
]
}
Tags may be given either as "Group/Tag" strings or as numeric tag IDs; both forms work in the same list. A tag
that names nothing is silently skipped rather than refused, so a typo produces a value that matches less
specifically than you expected rather than an error — worth checking with the
preview endpoint after a bulk change.
Name uniqueness
Two variables in the same set may not share a name without regard to case — ApiKey and apikey are the
same name as far as a set is concerned. Attempting to create or rename one into a collision is refused with
409 Conflict.
This follows from variable substitution: a @[name] marker
matches a variable's name case-insensitively too, so two variables differing only by case would be
indistinguishable to anything that referenced either of them.
Value uniqueness
No two values of the same variable may carry the same tag combination. Attempting it is refused with
409 Conflict. Two values with no tags is the same violation: there can only be one default.
Versioning and authorship
Library sets, tag groups and variables all carry:
| Field | Meaning |
|---|---|
version | Incremented on update. Used for optimistic concurrency. |
createdAt / createdById | When and by whom the record was first written |
updatedAt / updatedById | When and by whom it was last changed |
The user IDs are stamped from the JWT. The library set API does not store users, so when it returns a record it
asks the account API to put names to those IDs — via POST /users/names/lookup, which answers only about IDs it
is given. That is why createdBy and updatedBy come back as { id, name } objects alongside the raw
createdById and updatedById.
Every create, update and delete also writes an audit entry.
Reading a library set
curl "$LIBRARY_API/variablesets/1" -H "x-tagshape-auth: $JWT"
{
"id": 1,
"version": 3,
"name": "orders-service",
"description": "Config for the orders service",
"variables": [
{
"id": 1,
"name": "DatabaseConnection",
"variableSetId": 1,
"type": "String",
"sensitive": false,
"values": [
{ "id": 1, "value": "Server=localhost;Database=orders", "numberValue": null, "booleanValue": null,
"sensitive": false, "hint": null, "hash": null, "tags": [] },
{ "id": 2, "value": "Server=prod.internal;Database=orders", "numberValue": null, "booleanValue": null,
"sensitive": false, "hint": null, "hash": null,
"tags": [{ "id": 1, "name": "Production", "tagGroupId": 1 }] }
]
}
],
"createdBy": { "id": "…", "name": "Neil Docherty" },
"createdAt": "2026-08-01T10:00:00Z",
"updatedBy": { "id": "…", "name": "Neil Docherty" },
"updatedAt": "2026-08-12T14:22:00Z"
}
Values are returned ordered by ID, so the same variable does not list its values differently between reads.
Sensitive values come back blank unless the caller holds VariableReadSecrets — see
Sensitive values.
Deleting
A library set cascades the whole way down: its variables, and their values. A tag group cascades to its tags. The join rows between a value and its tags cascade from either side, so nothing is left orphaned.