Skip to main content

Value resolution

Resolution is the rule that turns "here is a library set and some tags" into "here is one value per variable".

The rule

Given a set of tags:

  1. Start from the default. The value carrying no tags is the answer unless something beats it.
  2. Prefer an exact match. A value whose tags are exactly the set you asked for — same tags, same count — wins outright.
  3. Otherwise take the best partial match. Among values whose tags are all present in what you asked for, the one carrying the most of them wins. Where several tie, the one with the fewest tags overall is preferred.
  4. A value carrying a tag you did not ask for cannot win. Every one of a value's tags must be in your set for it to be considered at all.

Point 4 is the one that catches people out, and it is the point of the design. A value tagged Environment/Production, Region/EU is not a candidate when you ask only for Environment/Production — it is more specific than where you are, not less.

Worked example

One variable, four values:

ValueTags
localhost(none)
prod.internalEnvironment/Production
eu-prod.internalEnvironment/Production, Region/EU
eu-stage.internalEnvironment/Staging, Region/EU

What comes back:

Tags asked forResultWhy
(none)localhostOnly the untagged value is a candidate.
Environment/Productionprod.internalExact match. The EU value carries a tag not asked for.
Environment/Production, Region/EUeu-prod.internalExact match on both.
Environment/Production, Region/USprod.internalBest partial: one tag of two.
Environment/StaginglocalhostThe staging value also needs Region/EU, so it is not a candidate. Falls back to the default.
Region/EUlocalhostBoth EU values also carry an Environment tag.
Environment/Staging, Region/EUeu-stage.internalExact match.

Nothing about this depends on the order of tag groups, or on any inheritance chain. There is only "how much of what you asked for does this value know about".

Several library sets at once

?set=1&set=2&set=3 fetches all three. Sets are walked in name order — not the order of the query string — and where two sets both hold a variable of the same name, the set that sorts later wins.

If you rely on one set overriding another, name them so the ordering is obvious and stable. Renaming a set can change which one wins.

Asking for a resolution

GET /variablevalues?set=1&tag=Environment/Production&tag=Region/EU
  • set — repeated once per library set. At least one is required; none produces 400 Bad Request.
  • tag — repeated once per tag, in Group/Tag form. Optional; none means every variable resolves to its default.

A set naming a library set that does not exist, or belongs to another organization, produces 404 Not Found — the whole request, not just that set.

Requires VariableRead.

200 OK
{
"total": 3,
"items": {
"DatabaseConnection": "Server=eu-prod.internal;Database=orders",
"LogLevel": "Warning",
"FeatureFlags": "checkout-v2"
}
}

The response is a flat name-to-string map. That is deliberate: it is meant to be poured straight into environment variables or an app settings file without anything having to walk a structure first.

Previewing a resolution

When you want to know not just what you would get but which value it was, ask GET /variablevalues/preview with the same parameters. It runs identical resolution and returns the whole winning value:

200 OK
{
"total": 1,
"items": {
"DatabaseConnection": {
"id": 3,
"value": "Server=eu-prod.internal;Database=orders",
"sensitive": false,
"hint": null,
"hash": null,
"tags": [
{ "id": 1, "name": "Production", "tagGroupId": 1 },
{ "id": 3, "name": "EU", "tagGroupId": 2 }
]
}
}
}

This is what the management UI's preview pane uses, and it is the quickest way to diagnose a value you did not expect — the tags on the winner tell you immediately whether the problem is a mistagged value or a tag you forgot to ask for.

Secrets during resolution

The two endpoints withhold secrets at different moments, and the difference shows in the response.

/variablevalues blanks sensitive values before resolving, replacing each with its hint. Whichever value wins is therefore already the hint, and a consumer without VariableReadSecrets gets a map whose sensitive entries hold hints rather than secrets.

/variablevalues/preview resolves first and maps on the way out, so a withheld secret comes back as an empty value with its hint beside it — a value that is plainly absent rather than one pretending to be real.

Both require VariableRead; seeing the actual secret additionally requires VariableReadSecrets. See Sensitive values.