Getting started · tutorial
Your first playbook
In this tutorial you scaffold a playbook with config-weave init, read what it wrote, validate it, and then run it through the whole convergence loop: check, apply, apply again, check. By the end you have seen every status a step can report on a converging machine and proven that the scaffolded resource is idempotent.
Before you start
- config-weave is on your PATH. Install covers that.
- A Linux or Windows machine you are allowed to write to. The scaffolded play writes three small files under /tmp/my-playbook. Change the work_root variable if that path does not suit your machine.
- A terminal in a directory where you can create a folder.
Scaffold a playbook
Run the init command with the directory you want to create.
$ config-weave init ./my-playbook
scaffolded a playbook in ./my-playbook — next: edit, then `config-weave validate ./my-playbook`
The command refuses to run if the directory already holds a playbook.wcl. It writes these files.
| File | What it is |
|---|---|
| playbook.wcl | The playbook: one gatherer invocation, two variables, one composite and one play. |
| pkgs/example/package.wcl | The example package: a gatherer, a resource and a test. |
| pkgs/example/resources/file_present.ws | The resource script, exporting check and apply. |
| pkgs/example/gatherers/os_info.ws | The gatherer script, exporting gather. |
| pkgs/example/tests/greeting_verify.ws | A verify script the testlab runs inside a test instance. |
| lib/README.md, pkgs/example/lib/README.md | Where shared wscript helpers go, with a note on how imports resolve. |
| weave.wscripti, wscript.toml | The host API interface and a manifest, so an editor with the wscript language server type-checks your scripts. |
| .gitignore | Ignores .repo-cache/, where config-weave pkg clones package repositories. |
You now have a complete playbook folder that is ready to validate.
Read the playbook
Open playbook.wcl. The listing below is the generated file with its explanatory comments removed.
playbook "My Playbook" {
description = "Describe what this playbook converges"
version = "0.1.0"
gather "os" {
description = "Operating system facts"
from = "example.os_info"
}
vars {
work_root = "/tmp/my-playbook"
greeting_file = $"/hello.txt"
}
composite "greeting_pair" {
description = "Write a matching hello and goodbye into one directory"
arg "dir" {
description = "Directory to write both files into"
type = "string"
required = true
}
arg "who" {
description = "Who to address"
type = "string"
default = "world"
}
step "hello" {
description = "Write the greeting"
resource = "example.file_present"
properties {
path = $"/pair-hello.txt"
content = $"hello "
}
}
step "goodbye" {
description = "Write the farewell, after the greeting"
resource = "example.file_present"
requires = ["hello"]
properties {
path = $"/pair-goodbye.txt"
content = $"goodbye "
}
}
}
play "baseline" {
description = "A starter play with one step"
step "greeting" {
description = "Ensure the greeting file exists"
resource = "example.file_present"
condition = os.family != "plan9"
properties {
path = greeting_file
content = "hello from config-weave"
}
}
step "pair" {
description = "Write the greeting pair"
resource = "greeting_pair"
requires = ["greeting"]
properties {
dir = work_root
who = "config-weave"
}
}
}
}
Read it top to bottom.
- The gather block runs the os_info gatherer of the example package before any step and binds its result to os. Any expression can then read os.family.
- The vars block declares two variables. The second interpolates the first, and both are available to every step.
- The composite block is a named group of steps with arguments. A step invokes it like a resource, and the engine expands it into its inner steps. Its arguments are visible inside the body as args.dir and bare as who.
- The play block holds the steps that run. The first step conditions on a gathered fact, and the second requires the first, so they run in that order.
Now open pkgs/example/package.wcl. Comments are removed here as well.
package "example" {
description = "Example package scaffolded by config-weave init"
gatherer "os_info" {
description = "Report basic operating system facts"
script = "gatherers/os_info.ws"
returns "family" { description = "OS family (linux, windows, macos)" type = "string" }
returns "name" { description = "OS name" type = "string" }
returns "version" { description = "OS version" type = "string" }
returns "arch" { description = "CPU architecture" type = "string" }
returns "cpus" { description = "Logical CPU count" type = "int" }
}
resource "file_present" {
description = "Ensure a file exists with the given content"
script = "resources/file_present.ws"
concurrency = "parallel"
param "path" {
description = "Absolute path of the file"
type = "string"
required = true
}
param "content" {
description = "Desired file content"
type = "string"
default = ""
}
param "ensure" {
description = "Desired state of the file"
type = "symbol"
default = :present
symbol "present" { description = "Create the file and converge its content" }
symbol "absent" { description = "Remove the file if it exists" }
}
}
test "greeting_converges" {
description = "file_present creates the greeting file and is idempotent"
image = "debian:12"
verify = "tests/greeting_verify.ws"
step "greet" {
description = "Create the greeting file"
resource = "file_present"
properties {
path = "/tmp/my-playbook/hello.txt"
content = "hello from config-weave"
}
}
}
}
The package declares what it provides. The gatherer names its script and the fields it returns. The resource names its script, its concurrency class and a param for every input a step may pass. The engine checks each step's properties against those params during validation. The test block is for the testlab and does not run during check or apply. Test a package runs it.
The resource script is the part that touches the machine.
use value
use fs
use path
use log
if let Some = params.get if let Some = v.as_string return s
}
}
fallback
}
let p = param_str
if p == "" return Err
}
if param_str == "absent" if exists return Ok
}
return Ok
}
if !exists return Ok
}
if read? == param_str Ok
} else Ok
}
}
let p = param_str
if param_str == "absent" info
delete?
return Ok
}
info
mkdir?
write?
Ok
}
check reads the file and compares it with the requested content. It never writes. apply creates the parent directory and writes the file. After apply, the same check finds the content it wrote and reports AlreadyConfigured. That is the whole convergence contract, and every resource you write follows the same shape.
Validate
Run the validation pipeline. Nothing executes.
$ config-weave validate ./my-playbook
ok: playbook 'My Playbook' v0.1.0 — 2 package(s), 1 play(s), 3 step(s)
Validation parses the WCL, checks every block against the schema, resolves every resource and gatherer reference, builds the step graph and rejects cycles, and compiles every wscript script against the host API. A wrong parameter name or a misspelled host function is reported here, before any script runs. The summary counts two packages because the built-in weave package is always loaded beside yours, and three steps because the composite expands into two.
A validated playbook
The command exits 0. If it reports errors, each one carries the file, line and column, and the run stops with exit 2 until you fix them.
List the plays
Ask the playbook what you can run.
$ config-weave list ./my-playbook
My Playbook v0.1.0 — Describe what this playbook converges
baseline (3 steps) — A starter play with one step
composites:
greeting_pair (2 steps) — Write a matching hello and goodbye into one directory
There is one play, baseline. The next four commands all run it.
Check
Run the play in check mode. Every step's check function runs, and nothing on the machine changes.
$ config-weave check ./my-playbook baseline
check 'My Playbook' v0.1.0 — play 'baseline'
gathered:
os <- example.os_info
steps:
[ not configured] greeting (example.file_present)
[ not configured] pair/hello (example.file_present)
[ not configured] pair/goodbye (example.file_present)
summary: 0 already configured, 0 configured, 3 not configured, 0 reboot required, 0 skipped, 0 error, 0 not run (0.0s)
The report names the playbook and the play, lists the facts it gathered, and prints one line per step with its status and resource. Steps inside the composite are named pair/hello and pair/goodbye. All three files are missing, so every step is not configured. The summary line counts each status. On a terminal the report uses icons and colour instead of the bracketed status column, but the words are the same.
You now know what apply would change, without having changed anything.
Apply
Run the same play in apply mode.
$ config-weave apply ./my-playbook baseline
[greeting] info: writing /tmp/my-playbook/hello.txt
[hello] info: writing /tmp/my-playbook/pair-hello.txt
[goodbye] info: writing /tmp/my-playbook/pair-goodbye.txt
apply 'My Playbook' v0.1.0 — play 'baseline'
gathered:
os <- example.os_info
steps:
[ configured] greeting (example.file_present)
[ configured] pair/hello (example.file_present)
[ configured] pair/goodbye (example.file_present)
summary: 0 already configured, 3 configured, 0 not configured, 0 reboot required, 0 skipped, 0 error, 0 not run (0.0s)
For each step the engine ran check, found the step not configured, ran apply, and then ran check again. The three lines at the top are the log::info calls in the script. The status configured means apply changed the machine and the re-check confirmed the result. greeting ran before the pair because pair requires it, and pair/goodbye ran after pair/hello for the same reason.
The three files now exist under /tmp/my-playbook.
Apply again
Run apply a second time, with nothing changed in between.
$ config-weave apply ./my-playbook baseline
apply 'My Playbook' v0.1.0 — play 'baseline'
gathered:
os <- example.os_info
steps:
[ already configured] greeting (example.file_present)
[ already configured] pair/hello (example.file_present)
[ already configured] pair/goodbye (example.file_present)
summary: 3 already configured, 0 configured, 0 not configured, 0 reboot required, 0 skipped, 0 error, 0 not run (0.0s)
Every step reports already configured. The engine ran check for each, found the desired state in place, and did not call apply. No log lines appear because nothing was written. This is idempotence: a converged machine stays converged, and re-running the playbook is safe. This second run is in a fresh process, which is what proves the resource does not depend on state that lived only inside the first run.
A converged machine
A second apply reports every step already configured and exits 0. A step that reports configured again on the second run has a broken resource: its apply does not produce the state its check looks for.
Check again
Finish with a check, the way a scheduled drift check would run it.
$ config-weave check ./my-playbook baseline
check 'My Playbook' v0.1.0 — play 'baseline'
gathered:
os <- example.os_info
steps:
[ already configured] greeting (example.file_present)
[ already configured] pair/hello (example.file_present)
[ already configured] pair/goodbye (example.file_present)
summary: 3 already configured, 0 configured, 0 not configured, 0 reboot required, 0 skipped, 0 error, 0 not run (0.0s)
The report matches the second apply. Delete one of the files and run the check again: that step reports not configured and the summary changes, and the exit code stays 0, because check reports drift rather than failing on it. The exit code is 1 only when a step reports error, and 2 when validation fails.
Next steps
You have scaffolded, validated, checked and converged a playbook, and proven that its resource is idempotent. The convergence contract explains why the second apply matters and what the step lifecycle looks like inside the engine. Playbooks, plays and steps covers the blocks you read in playbook.wcl, and Packages, resources and gatherers covers package.wcl. To write your own resource, follow Add a resource to a package, and to prove it in a disposable instance, follow Test a package. The commands you ran are documented in config-weave init, config-weave validate, config-weave list, config-weave check and config-weave apply.