§ Reference · Data and formats
The plan document
The JSON document that pravo plan --json prints, the form of a saved plan file, and the events that apply and destroy stream.
On this page
pravo plan --json prints one JSON document on standard output. A plan policy is judged against the same document. Progress goes to standard error.
| Field | Present | Meaning |
|---|---|---|
format |
always | The layout version of the document. It is 1. |
created |
always | When the document was made: RFC 3339, UTC. |
actor, reason |
when set | The labels from --actor and --reason, or PRAVO_ACTOR and PRAVO_REASON. The caller chooses them. |
git_revision |
in a git repository | The short revision of HEAD, 12 digits, with -dirty appended when the working tree has changes. |
trace_id |
when tracing is on | The OpenTelemetry trace of the run. |
destroy |
for a destroy plan | true, for plan --destroy and destroy. |
summary |
always | Counts of actions and the blast radius. |
stats |
always | How much of the graph the plan walked. |
changes |
always | One entry for each node the plan ran: a resource or data source it changes, reads, deletes, moves or imports, an ephemeral resource it opens and a function it calls. --include-skipped adds an entry for each node it left alone. An empty list, never null. A consumer that needs the whole estate reads the counts in stats, passes --include-skipped, or queries the state with pravo state query. |
outputs |
always | The outputs by name. A value that is unknown until apply is {"$unknown": true}. |
hints |
when there are any | The text of each Hint: line the plan prints. |
providers |
when the root has providers | The resolved version of each provider, by local name. |
reservation |
in pravo show --json only |
The nodes a --reserve plan reserved and until when. |
Work in an empty directory. This root has a managed resource that depends on another, a data source, an ephemeral resource, a function call and an output.
readme
package estate
import (
"pravo.dev/pravo"
rnd "pravo.dev/providers/hashicorp/random"
loc "pravo.dev/providers/hashicorp/local"
k8s "pravo.dev/providers/hashicorp/kubernetes"
)
$random: rnd.#Provider
$local: loc.#Provider
$k8s: k8s.#Provider
pet: $random.#Resource.random_pet & {
length: 2
keepers: owner: "platform"
}
ticket: $random.#Resource.random_integer & {
min: 1
max: 99
keepers: source: pet.id
}
readme: $local.#Data.local_file & {filename: "README.md"}
secret: $random.#Ephemeral.random_password & {length: 16}
kind: $k8s.#Function.manifest_decode & {args: ["apiVersion: v1\nkind: ConfigMap\nmetadata: {name: demo}"]}
name: pravo.#Output & {value: pet.id}
{
"format": 1,
"created":"2026-01-01T00:00:00Z",
"summary": {
"add": 2,
"change": 0,
"destroy": 0,
"read": 1,
"blast_radius": 2
},
"stats": {
"total": 5,
"reached": 3,
"skipped": 0,
"refreshed": 1,
"fresh": 0,
"not_read": 0,
"drifted": 0,
"opened": 1,
"called": 1,
"imported": 0,
"moved": 0
},
"outputs": {
"name": {
"$unknown": true
}
},
"providers": {
"kubernetes": "3.4.0",
"local": "2.9.1",
"random": "3.9.1"
}
}
summary and stats
summary counts actions.
| Field | Counts |
|---|---|
add |
Creates and replacements. |
change |
Updates. |
destroy |
Deletes and replacements. |
read |
Data source reads. |
blast_radius |
The distinct nodes that have a pending change (a create, update, replacement, delete or deferred read), together with every node that depends on one of them, transitively, as the state records the dependencies at that moment. |
stats says how much of the graph the plan walked.
| Field | Counts |
|---|---|
total |
The declarations in the specification. |
reached |
Declarations planned with the provider. |
skipped |
Declarations reused from the state without planning. |
removed |
Records of resources that the specification does not declare and that the plan deletes. The field is absent when there are none. The Graph: line calls them removed from the specification. refreshed, fresh and not_read count them, and total does not. |
refreshed |
Records read from the provider during the plan. |
fresh |
Records that were not read because they were within their refresh age. The Graph: line calls them within refresh age. |
not_read |
Records that were not read and were older than their refresh age, because the plan did not reach them. The Graph: line calls them not read. |
drifted |
Refreshed records that had changed. |
opened |
Ephemeral resources opened. |
called |
Function calls made. |
imported, moved |
Imports and moves carried out by the plan. |
Under -refresh=false no record is compared with its age, so none is counted in fresh. Under -refresh=all every record is read and counted in refreshed.
changes
Every entry has addr, mode, type, name and action.
| Field | Present | Meaning |
|---|---|---|
addr |
always | The address: the CUE path of the declaration, such as pet or servers[0]. |
mode |
always | managed, data, ephemeral or call. |
type |
always | The provider's type of the resource, or the name of the function. |
name |
always | The last element of the path. |
action |
always | What the plan does. See actions. |
provider |
when a provider runs it | The provider instance key, such as random or local.$primary. |
reasons |
when there are any | Why the plan chose the action, in words. The wording is for people. |
requires_replace |
for a replacement | The attributes that force it. |
before |
by action | The recorded state of the object. |
after |
by action | The planned state of the object. |
deferred |
when true | A data source whose read waits for the apply. |
skipped |
when true | Unchanged, and not planned with the provider. |
refreshed |
when true | The record was read from the provider during this plan. |
drifted |
when true | The refresh found the object changed. |
applied |
when true | Already applied by a staged apply of the plan file. |
dependencies |
when there are any | The addresses this change depends on. |
dependents |
for a pending change, when there are any | The addresses that depend on it in the state. |
warnings |
when there are any | The summaries of the provider's warnings. |
A boolean that is false is left out. A field marked "when there are any" is left out when empty.
A change that creates a resource, and that depends on another:
{
"addr": "ticket",
"mode": "managed",
"type": "random_integer",
"name": "ticket",
"action": "create",
"provider": "random",
"reasons": [
"not in the state: a new resource"
],
"after": {
"id": {
"$unknown": true
},
"keepers": {
"source": {
"$unknown": true
}
},
"max": 99,
"min": 1,
"result": {
"$unknown": true
},
"seed": null
},
"dependencies": [
"pet"
]
}
The values of after that the provider computes are {"$unknown": true}: here id and result, and the key source of keepers, which reads pet.id. An attribute that is not set is null: here seed.
The entries that are not managed resources carry neither before nor after, except a data source that was read:
{"addr":"readme","mode":"data","type":"local_file","action":"read"}
{"addr":"secret","mode":"ephemeral","type":"random_password","action":"open"}
{"addr":"kind","mode":"call","type":"manifest_decode","action":"call"}
Actions
| Action | before |
after |
Meaning |
|---|---|---|---|
create |
absent | present | The object does not exist yet. |
update |
present | present | The object is changed in place. |
replace |
present | present | The object is destroyed and created again. after may hold unknown values. |
delete |
present | absent | The object is destroyed. |
read |
absent | present | A data source is read. after is absent when the read is deferred to the apply. |
no-op |
absent | absent | Nothing changes. |
open |
absent | absent | An ephemeral resource is opened for the run. |
call |
absent | absent | A provider function is called. |
move |
absent | absent | A record is renamed by $from. addr is the new address, and the ordinary entry for that address follows. |
A replace is its own action: it is not a delete followed by a create.
open and call entries are not written to a saved plan, because their results are never recorded. Values that pravo reads itself, pravo.#Env, pravo.#SecretEnv and pravo.#RemoteOutputs, produce no entry.
After an apply
Apply complete! Resources: 2 added, 0 changed, 0 destroyed. [txn <id>]
["readme","secret","kind"]
{"addr":"pet","mode":"managed","type":"random_pet","name":"pet","action":"no-op","skipped":true}
{"addr":"ticket","mode":"managed","type":"random_integer","name":"ticket","action":"no-op","skipped":true}
The plan ran the data source, which it reads each time, the ephemeral resource, which it opens each time, and the function call. It left pet and ticket alone: nothing the provider saw has changed, so the plan did not ask the provider, and neither has an entry. --include-skipped lists them as entries with the action no-op and skipped, and with nothing else that the plan decided. pravo why prints the reasons of a node that the plan left alone. A policy is judged against a document that lists every node of the specification this way, so a rule over the type or the address of a node reaches it whether or not the plan ran it.
A replacement
{
"addr": "pet",
"mode": "managed",
"type": "random_pet",
"name": "pet",
"action": "replace",
"provider": "random",
"reasons": [
"the specification changed since the last apply (hash 8b5b7a59ded5d995 -> cd8f659037768681)",
"refreshed because the record is older than its refresh age; no drift",
"the provider cannot change length in place; replacement required"
],
"requires_replace": [
"length"
],
"before": {
"id": "<pet>",
"keepers": {
"owner": "platform"
},
"length": 2,
"prefix": null,
"separator": "-"
},
"after": {
"id": {
"$unknown": true
},
"keepers": {
"owner": "platform"
},
"length": 3,
"prefix": null,
"separator": "-"
},
"refreshed": true,
"dependents": [
"ticket"
]
}
requires_replace names the attribute that forces the replacement. dependents lists ticket, which reads pet.id. The entry for ticket is a replacement too, because its keepers change.
{"summary":{"add":2,"change":0,"destroy":2,"read":1,"blast_radius":2},"stats":{"total":5,"reached":3,"skipped":0,"refreshed":3,"fresh":0,"not_read":0,"drifted":0,"opened":1,"called":1,"imported":0,"moved":0}}
A destroy plan
pravo plan --destroy sets destroy. Every recorded node is a delete, a data source included:
{"destroy":true,"summary":{"add":0,"change":0,"destroy":3,"read":0,"blast_radius":3}}
{"addr":"readme","mode":"data","action":"delete"}
{"addr":"ticket","mode":"managed","action":"delete"}
{"addr":"pet","mode":"managed","action":"delete"}
A hint and a move
Change the length back, and rename the pet. When a plan creates one resource and deletes another of the same type with identical configuration, it adds a hint and changes nothing:
["owl looks like pet under a new address; to keep the existing object, add $from: \"pet\" to owl"]
{"add":2,"change":0,"destroy":2,"read":1,"blast_radius":3}
Name the old address in $from, and the plan renames the record. The entry has the action move, followed by the ordinary entry for the new address:
{"addr":"owl","action":"move"}
{"addr":"owl","action":"no-op"}
Labels and the revision
--actor and --reason are recorded on the document, on every transaction, lock and reservation, and on saved plans. pravo does not verify them.
{"actor":"ci","reason":"nightly","git_revision":null}
git_revision is null here because the directory is not in a git repository.
Not masked
The text of a plan hides sensitive attributes. The document does not. Sensitive attributes and the outputs marked sensitive appear in it as they are. Do not write rules over secrets, and do not commit a plan document as a fixture without scrubbing it.
pravo show --json
pravo show <planfile> --json wraps the document of a saved plan in an object.
| Field | Meaning |
|---|---|
planfile |
The path as given. |
stale |
null when the plan can still be applied. Otherwise {"reasons": [...]}. |
plan |
The document. created, actor, reason and git_revision are those of the plan file. It carries reservation when the plan reserved its nodes. |
{"planfile":"change.plan","stale":null,"reservation":{"until":"2026-01-01T00:00:00Z","addrs":["owl","ticket"]}}
After the specification changes, stale says why, and pravo apply change.plan is refused with exit code 3:
{"reasons":["the configuration changed since the plan was made"]}
Error: saved plan is stale; run "pravo plan" again:
- the configuration changed since the plan was made
The saved plan file
pravo plan --out FILE writes the plan as JSON, with mode 0600. The values of the objects are msgpack inside the JSON, so that unknown values survive. A --policy does not enter the file. The file is not signed.
| Key | Contents |
|---|---|
format |
The layout version of the file. It is 1. A file with a higher number is refused: was written by a newer pravo. |
created, command |
When it was made and the command line that made it. |
lineage |
The lineage of the state the plan was made against. |
config_fingerprint |
A digest of every CUE file of the root. |
tags, sys_tags |
The -t and -T values. pravo apply <planfile> uses them. |
providers |
The provider versions the plan used. |
destroy, stats |
As in the document. |
changes |
The changes of the nodes the plan ran, with the configuration, prior and planned values, private provider data and the revision (prior_revision) and the generation (prior_generation) of each node when the plan was made. A node that the plan left alone has no entry. A data source whose read returned its record has an entry with kept set: the apply does not write it. |
inputs, input_generations |
The records that the changes read, by address, with the revision and the generation each had when the plan was made. A record that has no generation is not in input_generations. |
eval_outputs, drop_outputs |
The outputs that the apply evaluates, and the recorded outputs that it removes. The others keep the value that is recorded. |
outputs, unknown_outputs, sensitive_outputs |
The outputs known after planning, the ones known only after apply, and the sensitive ones. |
actor, reason, git_revision, reservation |
As in the document. |
apply_txn |
The transaction of the last apply made from the file. A file whose only changes are records and outputs to write has them left to apply until it has one. |
pravo apply <planfile> refuses a plan that is stale: the configuration changed (config_fingerprint), the state has another lineage, a provider has another version, or a node that the plan writes, or a record listed in inputs, was written since the plan was made (its revision differs), or was removed and created again (its generation differs, at whatever revision). A node that the plan neither writes nor reads does not count. The exit code is 3. The apply compares the revisions and generations again, with the state itself, once it holds the locks of the nodes it writes, so a write that lands between loading the file and taking the locks is refused the same way.
Apply events
pravo apply --json and pravo destroy --json stream one JSON object per line instead of a document. Each event has time, kind and phase, and addr, message, id, elapsed_ms, txn and trace_id when they apply.
| Field | Values |
|---|---|
kind |
refresh, read, open, call, create, update, replace, destroy, lock, note, plan, apply. |
phase |
start, done, skip, error. |
The last line is a summary with the counts, the outputs and the transaction. Remove the extra line, and apply the saved plan as the actor that reserved its nodes:
{"kind":"replace","addr":"owl","phase":"done"}
{"kind":"replace","addr":"ticket","phase":"done"}
{"kind":"apply","addr":null,"phase":"done"}
A plain apply --json that has changes to make and no --auto-approve is an error with exit code 1: there is no prompt to answer.
See also
- Plans explains what a plan contains.
pravo planprints and saves a plan.- Writing policies judges a plan document.