l4 tags apply
l4 tags apply makes a virtual key in LevelFour match a YAML file. When no virtual key has the file's name, it creates one. When one does, the file replaces that key's whole definition. The command prints the difference before it sends anything.
l4 tags apply -f <file> [flags]What it changes
The command reads GET /api/v1/tags/keys to find a virtual key named like the file, then:
- with no such key, posts the file to
/api/v1/tags/virtualand creates one - with one whose rules the file changes, sends
PUT /api/v1/tags/virtual/{key_id}with the whole file, and the key'srules_versionasexpected_version - with one whose rules the file leaves alone, sends
PATCH /api/v1/tags/virtual/{key_id}with the settings that moved, so editing a description does not reprocess a year of spend
The file replaces the definition, so a config you removed from the file is gone from the key once the command finishes. A change to the configs, the collapsed keys, can_override or effective_from starts a job that reprocesses the key from effective_from to the current month, and the key reads reprocessing until it ends.
apply finds the key by name. Change name in the file and the next run creates a second key, leaving the first in place. To rename a key, send its new name to PATCH /api/v1/tags/virtual/{key_id}, then change the file to match.File format
A tag file describes one virtual key. l4 tags preview reads the same format.
name: Teams
description: Owning team across AWS and GCP.
effective_from: "2026-06" # optional, YYYY-MM; omitted = current month
can_override: false # optional
collapsed_keys: # optional, in priority order
- source: {origin: provider, key: owner}
providers: [aws] # optional scope
prefix: "" # optional value prefix
configs: # in priority order, first match wins
- value: data
rules:
- providers: [aws, gcp]
where:
- {dimension: service, operator: is, values: [Amazon Redshift, BigQuery]}
- value: team-a
rules:
- providers: [aws, gcp]
where:
- {dimension: tag, key: team, operator: flexible_match, values: [Team A]}
- split:
- {value: infra, pct: 60}
- {value: mobile, pct: 40}
rules:
- providers: [aws]
where:
- {dimension: account, operator: is, values: ["999900001111"]}
- cost_based:
source: {origin: provider, key: team}
input:
- providers: [aws]
where:
- {dimension: charge_type, operator: is, values: [Usage]}
rules:
- providers: [aws]
where:
- {dimension: service, operator: starts_with, values: [AWS Support]}
time_frame: {start: "2026-07-01"} # optional| Field | Meaning |
|---|---|
name | Required. The key's name, and how apply finds the saved key |
description | Free text |
effective_from | First billing month the key covers, as a quoted YYYY-MM. Omitted means the current month |
can_override | On a key named after a provider key, whether configs replace the provider value. Defaults to false |
collapsed_keys[].source | The key to copy from: origin (provider or virtual) and key |
collapsed_keys[].providers | Limits the source to these providers |
collapsed_keys[].prefix | Text put in front of the copied value |
configs[] | Required. Each config carries one of value, split or cost_based, and only one, plus rules and an optional time_frame |
configs[].value | One value for every line the config matches |
configs[].split[] | value and pct pairs that add up to 100 |
configs[].cost_based | source, the key whose values share the cost, and input, rules that select the cost to measure the shares on |
configs[].rules[] | providers (aws, gcp) and where. One matching rule is enough |
configs[].rules[].where[] | Conditions, all of which must hold: dimension, key, operator (default is) and values |
configs[].time_frame | start and end dates as YYYY-MM-DD. Either one can be left out |
l4 rejects a field it does not recognise, so a typo such as condition: for where: fails the command instead of dropping the rule. Virtual tags has the dimensions, the operators, and a worked example for each kind of config.
Flags
| Flag | Description |
|---|---|
-f <file> | Tag file to apply. Required |
--dry-run | Print the difference and send nothing. Exits 2 when the file differs from the saved key |
--yes | Skip the confirmation prompt |
Global flags (--json, --jq, --template, --quiet, --token, --api) apply. See CLI overview.
Dry run
--dry-run reads the saved key, prints the difference and stops. It only reads, so a read-only key can run it. The exit code says what it found:
| Exit | Meaning |
|---|---|
0 | The saved key matches the file |
2 | Applying would change the key, or no key has that name yet |
1 or 4 | An error, as in Errors |
Run it on each pull request that touches a tag file, and the pipeline fails whenever the saved key and the file disagree:
l4 tags apply -f tags/teams.yaml --dry-run --quiet2 is the check doing its job: it found a difference to apply. Exit 1 is the one that means something broke.Confirmation
By default, the command prints the difference, then asks for confirmation with a [y/N] prompt. Anything other than y or yes aborts, prints Aborted., and exits 0 without sending the change. Pass --yes to skip the prompt in an interactive shell. When the file matches the saved key, there is nothing to confirm and nothing is sent.
apply without --dry-run rewrites the key and reprocesses its costs from effective_from with no one asked to confirm.Requires a read-write key
Writes need the org:tags:write permission, which a read-write key carries, and l4 auth login mints a read-only key. A read-only key fails with exit 1 and permission denied: this API key lacks permission for this operation. See CLI authentication for how to create a read-write key.
Re-running it
A second run with the same file finds no difference and sends nothing. The file wins over the dashboard: a run after someone edited the key there replaces their edit, and the printed difference shows what it undoes.
If someone changes the key between the command's read and its write, the API answers 409 version_conflict and saves nothing. Run apply again to diff against the new version.
Examples
l4 tags apply -f teams.yaml
l4 tags apply -f teams.yaml --dry-run
l4 tags apply -f teams.yaml --yes --json
LEVELFOUR_TOKEN=$L4_RW_KEY l4 tags apply -f teams.yaml --yes --quietApply several files in order, with the key that another one reads going first:
for f in tags/applications.yaml tags/business-units.yaml; do
l4 tags apply -f "$f" --yes || exit 1
doneOutput
Default output is the difference, then a success line naming the key and the reprocessing job. With --json the whole API envelope is printed. The fields it carries:
.success
.data.key # the key as l4 tags show --json prints it
.data.key.id
.data.key.rules_version
.data.key.status # "reprocessing" while the job runs
.data.job # null when nothing needs reprocessing
.data.job.id
.data.job.status # "queued" or "running" right after a change
.data.job.from_period
.data.job.to_period
.timestamp.data.job.status at queued or running is the expected result after a rules change: the definition is saved and LevelFour is reprocessing costs. Follow the job with l4 tags show. A run that only changed settings sends PATCH and answers with .data.job at null, which is also correct.--dry-run --json prints the plan instead of an API envelope: action (create, update or none), key_id on an existing key, and definition, the body the real run would send.
apply asks for confirmation the same way delete does. Outside a terminal, without --yes, it exits 1 with applying <key> outside a terminal needs --yes and sends nothing. A pipeline that means to apply passes --yes; one that means to check passes --dry-run.Validation problems
A file that breaks a rule fails with 422 validation_failed, and the error lists every problem at once. Each problem carries a code and, where one applies, the position of the config, rule, condition, split or collapsed key at fault. The API counts those from 0 and the CLI prints them from 1, matching how l4 tags show numbers configs. The codes a file author meets most:
| Code | Cause |
|---|---|
rule_no_provider | A rule lists no provider |
rule_no_condition_value | A condition has no values |
key_required | A tag or virtual_tag condition has no key |
dimension_unsupported_for_provider | The dimension does not exist on one of the rule's providers, such as category on GCP |
operator_unsupported | The operator does not apply to the dimension |
split_not_100 | A split's percentages do not add up to 100 |
split_value_duplicate | A value appears twice in one split |
time_frame_invalid | The time frame ends before it starts |
effective_from_invalid | effective_from is not a YYYY-MM month |
effective_from_unavailable | effective_from is further back than the backfill limit |
collapsed_source_unknown | A collapsed key names a key that does not exist |
cost_source_unknown | A cost-based source names a key that does not exist |
nested_key_unknown | A virtual_tag condition names a key that does not exist |
nested_cycle | A chain of keys that read each other loops back to its start |
nesting_too_deep | A chain of nested keys goes past the depth limit |
nested_allocation_unsupported | A virtual_tag condition reads a key that holds split or cost-based configs |
limit_exceeded | The key goes past one of the limits |
The Tags API reference lists every code.
Errors
| Exit | Symptom | Cause |
|---|---|---|
4 | Authentication required | No token resolvable; run l4 auth login or set LEVELFOUR_TOKEN |
2 | --dry-run found a difference | The file and the saved key differ. Nothing was sent |
1 | The file does not parse, or has an unknown field | Fix the file against File format |
1 | 401 authentication failed | Token revoked, expired, or from the wrong environment |
1 | 403 permission denied | The key is read-only, or your role lacks org:tags:write |
1 | 409 version_conflict | The key changed between the read and the write. Run apply again |
1 | 422 validation_failed | The file breaks a rule. See Validation problems |
Related
l4 tags preview: try the file before you apply itl4 tags show: the saved key and its reprocessing jobl4 tags delete: remove a key- Virtual tags: worked examples for every kind of config
- CLI authentication: read versus read-write keys
l4 tags preview
Dry-run a virtual tag file against your recent costs. Shows the spend each value would take and the spend left unallocated, and stores nothing.
l4 tags delete
Delete a virtual tag key and every value it assigned. Provider tags stay untouched, and a key another key reads cannot be deleted.