# Simulate, then confirm

> How dry runs, change reasons and expected versions make every OpenWAF write reviewable and safe against concurrent changes.

A configuration write in OpenWAF has three inputs besides its data: a **reason**, an optional **simulation** flag and the **expected version** of the zone. Every interface uses them the same way.

## The reason

Every configuration write needs a reason of 1 to 500 characters (after trimming spaces), without line breaks or other control characters. It is stored in the version and in the audit log. Without it the API answers `422 reason_required`.

The only exception is adding a zone: the API accepts it without a reason and records `alta de la zona`. The CLI still asks for `--reason`.

## The simulation

A simulation runs the whole write, including validation, the new snapshot and the configuration of every edge node that serves the zone, inside a transaction, and then rolls it back. It fails exactly where the real write would fail, and it touches neither the DNS nor the edge. The answer is the plan:

```json
{
  "dry_run": true,
  "from_version": 7,
  "to_version": 8,
  "diff": {
    "from": 7,
    "to": 8,
    "changes": [
      {"path": "settings.hsts.max_age", "op": "replace", "from": 0, "to": 300}
    ]
  },
  "warnings": []
}
```

Each change has a `path`, an `op` (`add`, `remove` or `replace`) and the values before and after. Lists are addressed by identity, not position: `rules[id=12]`, `records[id=…]`, `certificates[hostname=…]`. `warnings` lists anything the write would leave out, such as a site that could not be built. If `to_version` equals `from_version` and the diff is empty, the write would change nothing.

## The expected version

The confirmation carries the version you simulated against. If the zone is still at that version, the write is applied and creates the next one. If someone changed the zone in between, nothing is applied and the API answers:

```json
{"error": {"code": "version_conflict", "message": "the configuration changed: the current version is 9"}, "current_version": 9}
```

Read what changed (`zones diff`, **Changes → History**), simulate again and review the new plan before confirming. A version conflict never applies part of a change.

## Per interface

**Dashboard**

Every editor opens a dialog with a **Preview** of the change and a **Reason for the change** field; **Apply change** sends it with the version of the preview. On a conflict the dashboard reloads the zone, simulates your change again and asks you to review the new preview.
**CLI**

Every command that changes something requires `--reason`. The CLI simulates first with the zone's current version, prints the diff and asks `¿Aplicar? [s/N]`; it then applies with the **same** expected version. `--dry-run` stops after the simulation, `--yes` skips the question (required when there is no terminal, for example in CI). A conflict ends with exit code 3.

```sh
openwaf-cloud ctl zones set example.com hsts.max_age=300 --reason "Enable HSTS" --dry-run
openwaf-cloud ctl zones set example.com hsts.max_age=300 --reason "Enable HSTS" --yes --wait
```
**API**

| Header or parameter | Rule |
|---|---|
| `X-Change-Reason` | Required, 1–500 characters. |
| `If-Match: "<version>"` | The expected version. Optional on most writes, required on a revert (`428 if_match_required` without it). |
| `?dry_run=1` | Simulate: answer the plan with `200` and change nothing. |

A successful write answers the new version in `X-Config-Version` and `ETag`. Reads of a zone and of its edge settings answer the current version in `ETag`.

```sh
curl -sS -X PATCH "https://dash.openwaf.dev/v1/orgs/$ORG/zones/$ZONE/edge?dry_run=1" \
  -H "Authorization: Bearer $OPENWAF_TOKEN" \
  -H "X-Change-Reason: Enable HSTS" \
  -H 'If-Match: "7"' \
  -H "Content-Type: application/json" \
  -d '{"hsts": {"max_age": 300}}'
```

Repeat the request without `?dry_run=1` and with the same `If-Match` to apply it.
**MCP**

Every write tool takes `reason`, `confirm` and `expected_version`. With `confirm` false (the default) it only simulates and returns the plan and `current_version`; your assistant must show you the diff. With `confirm: true` it requires `expected_version` and sends it as `If-Match`.


## Changes that are not versioned

Some writes are audited but do not create a zone version:

- **Simulated, not versioned:** cache purges and origin certificates (issue and revoke). They take a reason and a dry run, but no expected version.
- **Not simulated:** clients, brand and white-label settings. The CLI's `--dry-run` only shows what it would send and sends nothing.
- **No reason:** checking a zone's delegation now (`zones check`) and scanning agent readiness change no configuration.
