Skip to content
kix /docs
Install the CLI

Reference CLI

plan

Preview a deploy as a server-side dry run: the field changes the apiserver would make, the writes it would refuse, prune fates, migrations, and drift.

kix plan runs the deploy pipeline with every write sent as dryRun=All, so the apiserver answers for each object. It builds the cluster, reads the running activation, computes the same plan kix deploy computes, and sends each apply and prune delete as a dry run. Nothing is written, and no Activation record is created or changed.

kix deploy makes the same plan before it asks for confirmation and prints it, with field changes shown under -v. kix deploy --dry-run runs kix plan with the same flags.

This page is hand-maintained. Check cli/kix-cli/src/cli.rs, cli/kix-cli/src/commands/plan.rs, and cli/kix-cli/schema/plan.schema.json when in doubt.

kix plan <CLUSTER> [--prune | --prune-mode default|no] [--reconcile]
[--force-conflicts] [--accept-migrations] [--current-activation]
[-o text|json|yaml|markdown] [--detail plain|fields|manifests]
[--flake <FLAKE>] [--context <CONTEXT>]
Argument or flagMeaning
<CLUSTER>Cluster name to build from the flake.
--prunePlan the prune a kix deploy --prune would run: each orphan’s delete is sent as a dry run. Without it, orphans are listed as kept.
--prune-mode <MODE>default prunes according to --prune. no plans no prune, even with --prune.
--reconcilePlan a re-apply of every resource, including those whose identity hash matches the running activation.
--force-conflictsPlan the apply with forced field ownership. Fields another manager owns show as drift the deploy reverts instead of as conflicts.
--accept-migrationsAccept the stateful migrations the plan lists, so they do not count as a reason the deploy would stop.
--current-activationTake the previous activation from the local cache and connect to nothing. The plan compares the cached build with the new one, and every write-path check reads not_checked. See Offline plans.
-o, --outputtext (default), json, yaml, or markdown. With anything but text, progress lines go to stderr so stdout holds the document alone.
--detail <LEVEL>How much json and yaml say about each change, as for kix diff. At manifests, the two manifests of an object the cluster checked are the live object and the dry-run result, with server-maintained metadata and status left out.
--flake, --context, --override-inputSee Global flags.

The planning flags are the ones kix deploy takes, so a plan made with the same flags is the plan the deploy runs.

A dry run is authorised like the write it simulates, so kix plan needs the permissions of the deploy it previews. A user who may only get and list gets forbidden refusals. Use kix diff for a read-only comparison.

SectionContent
Field changesFor each object the dry run answered, the live object compared with the object the apiserver says the apply would produce. Server-maintained metadata and status are left out, and Secret values are not shown. A package the build comparison called unchanged becomes changed when the apiserver says it would change.
Writes the deploy would stop onField ownership conflicts, with the manager that owns each field; admission webhook denials; ValidatingAdmissionPolicy denials; validation errors; changes to immutable fields; RBAC refusals; missing namespaces and unknown kinds the build does not create; SOPS Secrets that do not decrypt; and refused prune deletes.
PruneEach orphan and what the deploy does with it. See Prune fates.
Stateful migrationsStateful resources the change removes and adds in the same slot. A deploy stops on them unless run with --accept-migrations.
DriftFields where the live object differs from the deployed build, the managers that own them live, and whether the deploy puts the deployed value back.
Not checkedObjects the dry run could not check. See Objects not checked.

Every refusal shows, not only the first. The dry run carries on past a refused write and still tries the objects that depend on it, since nothing a dry run writes persists.

No preview can say whether Pods start, pull their images, and pass their probes, so readiness is always not_checked.

The package-grouped plan kix deploy prints, with every field change shown. A change that puts back a drifted field is tagged (reverts drift). After the packages come these sections, each only when it has something to say:

  • The deploy would stop on N writes:, one line per conflict, refusal, or refused prune delete, with the owning manager and fields or the apiserver’s message.
  • Drift:, one line per drifted field with its live and deployed values, the managers that set it, and whether the deploy reverts it.
  • Prune:, orphans that stay because another Kix cluster deploys them or they changed since they were read.
  • Not checked by the dry run (N), grouped by reason, five names per group.
  • A closing note that no preview checks whether Pods start.

The Plan document. kix diff prints the same document, so a consumer can read either. Its JSON Schema is cli/kix-cli/schema/plan.schema.json.

KeyContent
schema_versionA date string, YYYY-MM-DD, currently "2026-10-03". It changes only when the document changes in a way that is not backward compatible.
made_bydry_run from kix plan; builds from kix plan --current-activation or kix diff --from <build>; live_read from kix diff against the cluster or a snapshot.
kix{ version, store_path } of the Kix that made the plan. store_path is the Nix store path of the binary, null for a binary built outside the store.
clusterThe name the build gives the Kix cluster.
buildThe build the plan is for: { store_path, identity_hash }. store_path is the build’s result directory, the path a deploy records as kix.run/built-via; it locates the build. identity_hash is the Activation record’s kix.run/identity-hash, a hash of every rendered manifest and what is upstream of it; two builds with the same identity hash deploy the same objects. Present whether or not anything changes.
deployedThe build the plan compares against: { activation, identity_hash, store_path } from the cluster’s current Activation record (the cached record with --current-activation). null on a first deploy.
flagsThe planning flags: { prune, prune_mode, reconcile, force_conflicts, accept_migrations }. prune_mode is default, no, or null when not given. null from kix diff.
packagesThe package-grouped changes, described under kix diff.
migrationsChecked section of { slot, source, target, reason }.
conflictsChecked section of { resource, owners, message }, each owner { manager, fields }.
admissionChecked section of { resource, reason, webhook, message }. See Refusal reasons.
driftChecked section of { resource, path, live, deployed, managers, reverted }. A plan lists the drift on the objects the deploy applies, so reverted is always true; kix diff also lists drift the deploy leaves alone, with reverted false.
pruneChecked section of { resource, fate, detail }. See Prune fates.
readinessAlways { "status": "not_checked", "reason": "not_previewable" }.
not_checkedArray of { resource, reason, detail }. Empty unless made_by is dry_run.

Each checked section is one of:

{ "status": "checked", "items": [ ... ] }
{ "status": "not_checked", "reason": "read_only" }
reasonMeaning
builds_onlyTwo builds were compared and no cluster was read. kix plan against the cluster checks it.
read_onlyA read-only comparison cannot see it, because it shows only on the write path. kix plan checks it.
deployed_build_unavailableThe deployed build’s manifests are not on this machine, so a change the commit makes cannot be told apart from drift.
not_previewableNo preview can tell.

Resource identifiers have the form Kind/name@namespace.

GitHub-flavoured Markdown for pull request comments: the package plan, then the writes the deploy would stop on, and the objects the dry run did not check in a <details> block.

Each field change under packages.changed[].modified_resources[].changes[] is { path, change, old, new, labels }. change is added, removed, or changed; old or new is absent when that side has no value. When the deployed build’s manifests are on this machine, labels says what made the change:

LabelMeaning
intendedThe deployed build and the new build differ here.
reverts_driftThe live object differs from the deployed build here, and the deploy applies the object, so it puts the deployed value back.
drift_keptFrom kix diff only. The live object differs from the deployed build here, and the deploy leaves the object alone because its identity hash did not change. kix deploy --reconcile applies it.

A change can carry both intended and a drift label when the commit changes a field that was also changed on the cluster. Without the deployed build, labels is absent and drift is not_checked with reason deployed_build_unavailable.

A field another manager changed is usually also owned by that manager, so a plan reports it as an ownership conflict naming the manager. With --force-conflicts the plan shows it as reverts_drift instead.

reasonMeaning
webhook_deniedAn admission webhook denied the write. webhook names it when the apiserver did.
policy_deniedA ValidatingAdmissionPolicy denied the write. webhook names the policy.
invalidThe object fails validation against the schema or a CRD’s rules.
immutable_fieldA changed field cannot be modified; the object must be deleted and created again.
forbiddenRBAC or an admission plugin forbids the write.
namespace_missingThe namespace does not exist and the build does not create it.
unknown_kindThe apiserver does not serve the kind and the build has no CRD for it.
decryptionA SOPS Secret could not be decrypted.
otherThe deploy refuses the object before sending it, or the apiserver gave another error.
fateMeaning
delete--prune deletes it, and the dry-run delete was accepted.
delete_refused--prune would delete it, but the dry-run delete was refused. The deploy would stop on it.
kept_warn_onlyKept by a deploy without --prune, and recorded on the new Activation for a later --prune or kix gc.
kept_skippedKept by --prune-mode no.
kept_never_prunedA Namespace or CustomResourceDefinition, which Kix never deletes automatically.
kept_sharedAnother Kix cluster’s current build contains it.
kept_changedIt changed since it was read; the next prune checks it again.

A dry run persists nothing, so an object that needs something the same deploy creates cannot be checked. These objects are listed under not_checked, not as refusals, and their entries in packages come from comparing builds. Most of a cluster’s first deploy is in this list.

reasonMeaning
crd_created_by_this_deployIts kind comes from a CRD this deploy creates.
crd_changed_by_this_deployIts kind’s CRD is changed by this deploy, and the cluster refused the object when checking it against the CRD it has now. The deploy applies the CRD first, so the refusal may not happen.
namespace_created_by_this_deployIts namespace is created by this deploy.
webhook_no_dry_runAn admission webhook that would be called does not support dry run.
dependency_not_checkedThe engine did not try the object, for example because the dry run was cancelled. detail names the object it waited on when there is one.

With --current-activation, kix plan connects to nothing. It compares the cached build with the new one, made_by is builds, and conflicts, admission, drift, and prune are not_checked with reason builds_only. The cache is keyed by cluster name only; see kix deploy for what an offline plan cannot see.

CodeMeaning
0The deploy would change nothing and delete nothing
2The deploy would change something, or the prune would delete something
3The deploy would stop: a refused write, a refused prune delete, or a stateful migration without --accept-migrations
1Evaluation, build, or cluster error

The status is the same for every output format. A command line that cannot be parsed also exits 2; see Exit codes.

In CI, treat 0 and 2 as success and fail the job on anything else:

Terminal window
status=0
kix plan prod --output json > plan.json || status=$?
case "$status" in
0|2) ;;
3) echo "the deploy would stop; see plan.json" >&2; exit 1 ;;
*) exit "$status" ;;
esac

A build with no resources prints no document and exits 0.

To deploy exactly what a reviewer approved, save the JSON and pass it to kix deploy --plan, which refuses when the plan made at deploy time differs.

A dry run relies on every server it reaches to honour dryRun=All. An aggregated API server that ignores the parameter, or an admission webhook that declares sideEffects: None but has side effects, can still change something.

kix plankix diff
PermissionsThe deployer’sget and list; none with --from <build>
Field changesLive object against the dry-run resultBuild against the Kix-owned view of the live object
Ownership conflicts, webhooks, admission policies, validation, RBACCheckednot_checked (read_only)
Prune fatesCheckednot_checked
DriftFields the deploy touchesEvery drifted field, with whether the deploy reverts it
made_bydry_run (builds with --current-activation)live_read, or builds with --from <build>

Use kix plan to preview a deploy you are about to run. Use kix diff when the job has read-only credentials or no cluster at all, such as a pull request check against a snapshot.

kix deploy makes the same plan before it asks for confirmation. Under --on-error stop, the default, it stops before applying anything when the plan shows a refused write. See kix deploy.

❱ kix plan demo
❱ kix plan demo --prune --reconcile
❱ kix plan demo -o json > plan.json
❱ kix plan demo -o markdown > plan.md