# The full arc

Draft a real plan from a description, review it, dry run it against two hosts, put an approval in front of it, and read the record afterwards.

Source: https://opafra.com/docs/tutorial

---

The [quickstart](/docs/quickstart) ran one step against one host. This runs a real change
across two hosts, through every control Opafra has: drafted, reviewed, previewed,
approved, executed, recorded.

About thirty minutes. Nothing here is a toy: at the end you have a plan worth keeping.

## Before you begin

- Two Linux hosts registered and passing their connection check
- The connecting user able to `sudo -n`
- nginx installed on both, or willing to be

> **Note**
>
> Two hosts rather than one is the point. Most of what a dry run tells you is where your
> machines disagree, and you cannot see that with a sample size of one.

## 1. Put the hosts in an environment

Create an environment called `tutorial`, set its tier to **Production**, and add both
hosts.

Creating it as Production turns on **requires approval** by default. Leave it on. That is
the gate you will meet in step 5.

You should see the environment listed with both hosts and the gate on:

```text title="environments"
tutorial    Production    requires approval    2 servers
```

## 2. Draft a plan

Open the composer and describe the outcome rather than the commands:

```text
Deploy a new nginx config to the tutorial environment.
Back up the existing config first, validate the new one
before reloading, and stop if validation fails.
```

What comes back should be roughly four steps: a backup, a config write, a validation, and
a reload. It is a draft. Nothing has run and it is not yet in your plan list.

## 3. Review it before anything else

Read it in the order that catches the most, from
[reviewing a draft](/docs/plans/reviewing-a-draft):

**Targets first.** Every step should target `tutorial`. A step that picked up a single
hostname is the mistake worth catching now.

**Then order.** The backup must come before the write, and the validation before the
reload. Both read fine in the wrong order and are useless there.

**Then anything raw.** If the validation is an `ssh.cmd` running `nginx -t`, consider
whether `nginx.reload` already does it. It validates first and refuses to reload a broken
config, which is the behaviour the plan is trying to construct by hand.

Fix what you find, then **Keep** the draft. It is a real plan now.

## 4. Dry run it

Run it in dry-run mode against both hosts. You should see:

```text title="dry run"
Step 1  Back up the config        file.backup
  host-a   would change   /etc/nginx/nginx.conf.bak exists, would overwrite
  host-b   would create   /etc/nginx/nginx.conf.bak

Step 2  Write the config          template.deploy
  host-a   no change
  host-b   would change   /etc/nginx/nginx.conf   +6 -2
```

Read what that says. The hosts are **not** in the same state: `host-a` already has the
config and `host-b` does not. That is the finding, and in a real estate it is usually the
first sign that someone changed a machine by hand.

Check the bottom of the report for unresolved variables. Any `{{ vars.x }}` listed there
will be empty at run time.

## 5. Run it, and meet the gate

Start a normal run. It does not execute. It pauses immediately with status **Paused** and
appears in the approvals inbox, because the environment requires approval.

You should see:

```text title="run"
Status        Paused
Waiting for   approval
Steps run     0
```

Notice what did *not* happen: no step ran, and the gate applied without the plan
mentioning approval anywhere. The rule belongs to the environment.

> **Important**
>
> Dry runs are never gated, which is why step 4 did not stop. Preview freely; the gate
> stands where changes happen.

## 6. Approve it

Open the approvals inbox and approve the run. You will see the plan, the environments it
touches, the mode it will proceed in, and the plan revision being approved.

Try this first, though: **edit the plan while the run is waiting**, then approve. The
record will show the requested revision and the approved revision differing. Opafra
reports the change rather than blocking it, and the discrepancy is in the audit trail
permanently.

Once approved, the run resumes from the beginning and executes. You should see the
revision mismatch recorded rather than hidden:

```text title="run record, after editing then approving"
Requested revision   3
Approved revision    4
Revision changed     yes
```

## 7. Read the record

Open the completed run. It holds what was asked for, who started it, who approved it and
against which revision, and what each step did on each host, including the output.

You should see:

```text title="audit log, filtered to this run"
execution.start      you@example.com    2026-09-08 14:20
approval.continue    you@example.com    2026-09-08 14:22    requested 3, approved 4
```

Both revision numbers are on the record permanently, which is what makes "what exactly
was approved" answerable months later.

## Clean up

Delete the plan, then remove both hosts from the `tutorial` environment and delete it. The
config you deployed stays on the hosts: Opafra does not assume every step has an inverse,
so undoing a change is a plan you write, not a button.

Run records and audit entries remain by design.

## What you built

A plan that is written down, reviewable, previewable per host, gated by a policy attached
to the environment rather than to the plan, and permanently recorded.

That is the whole product. Everything else is more tools, more hosts, and running it on a
schedule.

## Next steps

- [Scheduling](/docs/running/scheduling) to run this nightly, including as a drift check
- [Modules](/docs/plans/modules) to extract the sequence once it appears in a third plan
- [Safety model](/docs/security/safety-model) for the controls you just used, in full
