The guide · how-to

Add a resource to a package

4 min read · 2026-09-02 · config-weave 0.1.0

Goal

Declare a new resource in an existing package and implement its check and apply functions in wscript against the host API, so a playbook step can invoke it. You need a playbook that already has a pkgs/<name>/package.wcl. The example adds a file_present resource; substitute your own name, params and script body.

1. Declare the resource and its params

Add a resource block to package.wcl. script is a path relative to the package directory. Declare one param block per input the resource accepts. Every param carries a description and a type, and is either required or has a default. The engine validates each step's properties against these params, so a step that passes an unknown or mistyped property fails validation before anything runs.

pkgs/core/package.wclwcl
resource "file_present" {
  description = "Ensure a file exists with the given content"
  script = "resources/file_present.ws"
  concurrency = "parallel"
  param "path"    { description = "Absolute path"  type = "string"  required = true }
  param "content" { description = "File content"   type = "string"  default = "" }
}

concurrency names the class the scheduler uses for this resource. The classes and what they allow to run alongside each other are in package.wcl, and the reasoning is in Scheduling and concurrency.

2. Implement check and apply

Write the script at the path you declared. It exports check(params) and apply(params). Import each host module you use with use. The params value is a map of the step's properties, with defaults filled in.

pkgs/core/resources/file_present.wsrust
use value
use fs
use path

// A missing or non-string property is an error the engine reports against the step.
fn param_str(params: Value, key: string) -> Result[string, string] {
    match params.get(key) {
        Some(v) => match v.as_string() {
            Some(s) => Ok(s),
            None => Err(fmt("property '{}' is not a string", key)),
        },
        None => Err(fmt("missing property '{}'", key)),
    }
}

fn check(params: Value) -> Result[CheckResult, string] {
    let p = param_str(params, "path")?
    if !fs::exists(p) { return Ok(CheckResult::NotConfigured) }
    if fs::read(p)? == param_str(params, "content")? {
        Ok(CheckResult::AlreadyConfigured)
    } else { Ok(CheckResult::NotConfigured) }
}

fn apply(params: Value) -> Result[ApplyResult, string] {
    let p = param_str(params, "path")?
    fs::mkdir(path::parent(p))?
    fs::write(p, param_str(params, "content")?)?
    Ok(ApplyResult::Success)
}

The contract

check must never mutate the machine. apply must converge, so that a re-check returns AlreadyConfigured even in a fresh process. A resource that breaks either rule passes validation and fails at the second apply, or in the testlab.

The exact signatures, and the other variants of CheckResult and ApplyResult, are in Script entry points. Every fallible host function returns a Result, so propagate errors with ? and let the engine report them against the step.

To have your editor type-check the script against the real host surface, run config-weave wscripti in the playbook directory. It writes weave.wscripti and a wscript.toml that the wscript language server reads, so a misspelled host function is flagged as you type. config-weave wscripti describes the output.

3. Validate that the script compiles

Run validation. Its final stage compiles every script against the host API, so a wrong signature, an unknown host function or a type error is a hard error here.

console
$ config-weave validate ./my-playbook
ok: playbook 'My Playbook' v0.1.0 — 2 package(s), 1 play(s), 3 step(s)

Done

config-weave validate exits 0 with the new resource's script compiled. A step can now name it as core.file_present, or as file_present from inside the same package's tests.

Next steps