Skip to content

Compare two plans with the same Rootform binary and Dialect selection. This guide uses the base and head plans in the commerce Playground: the head revision moves the catalog and cache behind private endpoints, adds an order notification pipeline and a payments namespace, and removes the public storage account and the legacy webhooks. Work from examples/playground/commerce-platform/ in a clone of the Rootform repository. Both sides contain plan.json and the saved plan.tfplan from the same Terraform run. To see the result before running anything, open the Playground on Comparison.

Plan JSON and saved plans can contain secrets in clear text. Keep them out of Git and public artifacts. Rootform reads both locally; its reports omit sensitive values but still reveal topology and resource names.

  1. Compare the proposed outcomes

    Pair each JSON export with its saved plan to recover direct identity traversals, then compare the two planned stages. The flags for the second input begin with --diff-. --no-serve writes the requested files and exits.

    rootform run base/plan.json --plan-file base/plan.tfplan \
    --diff head/plan.json --diff-plan-file head/plan.tfplan \
    --no-serve -o comparison.json -o comparison.md --color always
    Shell

    The command returns status 0 because the comparison completed, even though it found changes. The text summary sets the two inputs side by side, then reports uncertainty, then the differences (excerpt):

    Comparison summary, excerpt OUTPUT
    Inputs compared
    Before base/plan.json
    After head/plan.json
    Before After
    Origin Plan Plan
    Stage Planned Planned
    Instances 144 153
    Interpreted 144 153
    Relations 28 28
    Contexts 196 207
    Contributions 37 45
    Uncertainty
    Before After
    Indeterminate closures 3 3
    Unknown until apply 3 3
    Differences
    Instances 16 added, 7 removed
    Relations 5 added, 5 removed
    Contexts 28 added, 17 removed
    Contributions 9 added, 1 removed
    Indeterminate closures 3 before, 3 after
    Instances
    + azurerm_linux_function_app.order_notifications added
    + azurerm_private_endpoint.cosmos added
    + azurerm_private_endpoint.redis added
    + kubernetes_namespace_v1.payments added
    - azurerm_linux_function_app.legacy_webhooks removed
    - azurerm_storage_account.public removed

    Read the table first. Before and After name the two inputs and the stage compared on each side, here Planned on both. The instance counts cover observed resource instances; the Relation, Context, and Contribution counts cover facts that Rules established.

    Uncertainty counts indeterminate closures on each side and by cause: evidence that cannot decide a fact, and so cannot decide a change. Here three closures on each side wait on values unknown until apply. That does not mean the comparison failed, as Comparisons explains.

    Differences counts what differs between the two inputs. The report never calls these differences drift: they do not establish what drifted between the two exports. The head plan adds 16 resource instances and removes 7, and the text summary names each of the 23 under the totals. The excerpt keeps six: the new private endpoints for the catalog and the cache, the order notification function, the payments namespace, and two removals, the legacy webhooks function and the public storage account.

    If pairing is refused or the counts differ in your own project, inspect the warning and confirm each JSON was exported from its matching saved plan. Plan inputs explains pairing.

  2. Open the comparison in the browser

    To inspect the same result in the Explorer without reopening the plan JSON, make a self-contained HTML copy of the saved comparison:

    rootform run comparison.json --no-serve -o comparison.html
    Shell

    Open comparison.html locally. The selector reads Differences, Before to After, and the reading block at the bottom left switches between Before, Differences, and After. Each resource group carries the count of its changed entries, a removed relation is drawn dashed, and the Added, Removed, Changed, and Indeterminate filters narrow the canvas to one kind of change. The HTML makes no network requests. A local rootform run comparison.json --no-browser --port 0 instead serves the same result on loopback; stop that server with Ctrl+C when finished. See Explore a Form for navigation.

    The Explorer on the Differences view of the commerce comparison: four resource groups with their change counts, a removed Delivers to relation drawn dashed in red, and the filters counting 58 added, 30 removed, and 6 indeterminate entries The Explorer on the Differences view of the commerce comparison: four resource groups with their change counts, a removed Delivers to relation drawn dashed in red, and the filters counting 58 added, 30 removed, and 6 indeterminate entries

  3. Read every change

    The text summary lists every change under its exact totals, and on an interactive terminal it opens in a pager (Outputs). comparison.md is written for review instead: each list shows at most ten entries spread across added and removed, states how many it shows, and is folded when longer. Add --details to the command to list every entry there (Review with Markdown).

    comparison.json keeps the exact entries. It is itself a Form with kind: "comparison". Its top-level before and after embed complete state or plan Forms under form, each with its selected stage and selected_from. comparison.name is cross. Review comparable and problems before treating entries as comparable, then inspect Representation and fact changes alongside indeterminate.

  4. Compare other stage pairs

    A plan Form defaults to Planned, while a state Form has only a Recorded stage. For plans with a Recorded stage, --before-stage recorded --after-stage recorded compares Recorded architectures of separate inputs. Use refreshed for the state the plan starts from; the plan does not record whether or how far refresh ran. Rootform refuses a requested stage that the input does not contain; it does not treat it as empty.

    To compare your own state JSON with a later saved plan, export each with the same Terraform or OpenTofu binary. OpenTofu users replace terraform with tofu in these commands:

    terraform show -json > state.json
    terraform show -json later.tfplan > later-plan.json
    Shell

    The first file is state JSON; the second describes the later plan. Keep both private. Compare the state with the later proposed outcome:

    rootform run state.json --diff later-plan.json \
    --diff-plan-file later.tfplan --no-serve -o state-to-plan.json
    Shell

    This selects recorded before and planned after. The saved plan adds verified traversal evidence only to the second input. Cross-input Differences do not establish drift. To review drift reported in one plan, inspect its recorded to refreshed comparison and separate drift report, as in Compare both sides of one plan. Comparisons explains these boundaries.

  5. Use exit status deliberately

    Status 0 proves the comparison ran, not that its report is empty; run has no status that reports changes. Read comparison.md for review and keep comparison.json for the exact entries. run never evaluates Policies. To block a review on an architectural condition, check the saved comparison: rootform check comparison.json evaluates selected Policies on both Before and After by default. Use --side before or --side after to gate one side; a violation on either evaluated side exits 1. Outputs and exit status defines export formats and failures.

Continue with Review a pull request to plan both revisions in isolated worktrees, compare them, gate the head with the same Policies, and keep the review evidence. Check a Form with Policies selects a Pack and reads the verdict; Understand Policy outcomes explains what each verdict proves.