Workload Authoring

A comprehensive guide to creating custom workloads for Anvil.

1. Workload Structure

A workload is a directory containing configuration files, scripts, and assets that define a system configuration.

Directory Structure

workload-name/
├── workload.yaml       # Required: workload definition
├── files/              # Optional: files to deploy
│   ├── .config/
│   │   └── app.conf
│   └── settings.json
└── scripts/            # Optional: installation/health scripts
    ├── pre-install.ps1
    ├── post-install.ps1
    └── health-check.ps1

Required Files

  • workload.yaml: The main workload definition file (required)

Optional Directories

  • files/: Contains configuration files to be copied to the target system
  • scripts/: Contains PowerShell or CMD scripts for installation and validation

Naming Conventions

  • Workload directory names should be lowercase with hyphens: my-workload-name
  • Use descriptive names that indicate the workload's purpose
  • Avoid spaces and special characters

2. Schema Reference

The workload.yaml file defines all aspects of your workload configuration.

Complete Schema

# Required fields
name: string                    # Workload identifier
version: string                 # Semantic version (e.g., "1.0.0")

# Optional fields
description: string             # Human-readable description
extends: string[]               # Parent workloads to inherit from
packages: object                # Package definitions
files: array                    # File deployment definitions
commands: object                # Inline command definitions
environment: object             # Environment variable configuration
assertions: array               # Declarative health assertions
health: object                  # Health check configuration

Required Fields

name

Unique identifier for the workload. Must be alphanumeric with hyphens and underscores only.

name: my-workload           # Valid
name: my_workload_v2        # Valid
name: "my workload"         # Invalid (spaces)
name: my.workload           # Invalid (dots)

version

Semantic version string. Recommended to follow SemVer.

version: "1.0.0"
version: "2.1.0-beta"
version: "0.1.0"

Optional Fields

description

Human-readable description displayed in listings.

description: "Development environment for Rust projects with debugging tools"

extends

List of parent workloads to inherit from.

extends:
  - essentials
  - rust-developer

packages

Package installation definitions (see Package Definitions).

files

File deployment definitions (see File Definitions).

commands

Inline command definitions (see Commands (Inline)).

environment

Environment variable configuration (see Environment Configuration).


3. Package Definitions

Define software packages to install. Anvil supports multiple package managers: winget (Windows), brew (macOS/Linux), and apt (Debian/Ubuntu).

Basic Structure

packages:
  winget:
    - id: Publisher.PackageName
    - id: Another.Package

Full Package Options

packages:
  winget:
    - id: Git.Git
      version: "2.43.0"           # Optional: pin to specific version
      source: winget              # Optional: winget, msstore
      override:                    # Optional: additional winget arguments
        - "--scope"
        - "machine"

Package Fields

FieldRequiredDescription
idYesWinget package identifier
versionNoSpecific version to install
sourceNoPackage source: winget or msstore
overrideNoAdditional arguments passed to winget

Finding Package IDs

Use winget to search for packages:

# Search for packages
winget search vscode

# Get exact ID
winget search --exact "Visual Studio Code"

# Show package details
winget show Microsoft.VisualStudioCode

Common package IDs:

PackageID
Visual Studio CodeMicrosoft.VisualStudioCode
GitGit.Git
Windows TerminalMicrosoft.WindowsTerminal
Node.jsOpenJS.NodeJS
PythonPython.Python.3.12
RustRustlang.Rustup
PowerShellMicrosoft.PowerShell

Version Pinning

Pin specific versions when compatibility matters:

packages:
  winget:
    # Pin exact version
    - id: Python.Python.3.12
      version: "3.12.0"
    
    # Use latest (default)
    - id: Git.Git

Check available versions:

winget show Python.Python.3.12 --versions

Override Arguments

Pass custom arguments to winget:

packages:
  winget:
    - id: Microsoft.VisualStudioCode
      override:
        - "--scope"
        - "machine"           # Install for all users
        - "--override"
        - "/SILENT"

Multi-Manager Packages

Define packages for multiple package managers in a single workload. Anvil selects the appropriate manager for the current platform.

packages:
  winget:
    - id: Git.Git
    - id: Microsoft.VisualStudioCode
  brew:
    - name: git
    - name: visual-studio-code
      cask: true
  apt:
    - name: git
    - name: build-essential

Homebrew Packages (macOS/Linux)

packages:
  brew:
    - name: git                        # CLI formula
    - name: visual-studio-code         # GUI cask
      cask: true
    - name: font-cascadia-code         # Cask from a tap
      cask: true
      tap: "homebrew/cask-fonts"
FieldRequiredDefaultDescription
nameYes-Homebrew formula or cask name
caskNofalseWhether this is a cask (GUI app) vs formula (CLI tool)
tapNo-Tap source (e.g., "homebrew/cask-fonts")

APT Packages (Debian/Ubuntu)

packages:
  apt:
    - name: git
    - name: build-essential
      version: "12.9"
FieldRequiredDefaultDescription
nameYes-APT package name
versionNo-Specific version constraint

Note: Homebrew and APT support is schema-complete but not yet fully implemented. Winget is the primary supported manager today.


4. File Definitions

Define configuration files to copy to the target system.

Basic Structure

files:
  - source: config.json
    destination: "~/.config/app/config.json"

Full File Options

files:
  - source: relative/path/in/workload/file.conf
    destination: "~/target/path/file.conf"
    backup: true                  # Backup existing file
    permissions: "0644"           # Optional: file permissions
    template: false               # Process as Handlebars template

File Fields

FieldRequiredDefaultDescription
sourceYes-Path relative to workload directory
destinationYes-Target path on system
backupNotrueBackup existing files before overwriting
permissionsNo-File permissions (Unix-style, informational on Windows)
templateNofalseProcess file as Handlebars template

Path Variables

Use these variables in destination paths:

VariableDescriptionExample
~User home directoryC:\Users\username
${HOME}User home directoryC:\Users\username
${USERPROFILE}User profile directoryC:\Users\username
${APPDATA}Application dataC:\Users\username\AppData\Roaming
${LOCALAPPDATA}Local app dataC:\Users\username\AppData\Local
${WORKLOAD_DIR}Workload directoryPath to current workload

Examples:

files:
  # Home directory
  - source: .gitconfig
    destination: "~/.gitconfig"
  
  # AppData
  - source: settings.json
    destination: "${APPDATA}/MyApp/settings.json"
  
  # Local AppData
  - source: cache.db
    destination: "${LOCALAPPDATA}/MyApp/cache.db"

Templating

Enable Handlebars templating for dynamic content:

files:
  - source: config.toml.hbs
    destination: "~/.config/app/config.toml"
    template: true

Template file (config.toml.hbs):

# Configuration for {{username}}
# Generated on {{date}}

[user]
name = "{{username}}"
home = "{{home}}"

[paths]
workload = "{{workload_dir}}"

Available template variables:

VariableDescription
{{username}}Current username
{{home}}Home directory path
{{computername}}Computer name
{{workload_dir}}Workload directory
{{workload_name}}Workload name
{{date}}Current date
{{env.VAR_NAME}}Environment variable

Directory Copying

Copy entire directories:

files:
  - source: config/
    destination: "~/.config/myapp/"

5. Commands (Inline)

Commands let you run arbitrary shell commands directly from workload.yaml. They support conditional execution via the same predicate engine used by Assertions.

Note: scripts.pre_install and scripts.post_install were removed in v1.0. Use the commands block instead.

Basic Structure

commands:
  pre_install:
    - run: "echo Preparing environment"
  post_install:
    - run: "cargo install ripgrep"
      description: "Install ripgrep via cargo"

Full Command Options

commands:
  post_install:
    - run: "cargo install cargo-watch"
      description: "Install cargo-watch"
      timeout: 600                    # Timeout in seconds (default: 300)
      elevated: false                 # Require admin privileges (default: false)
      continue_on_error: false        # Continue if this command fails (default: false)
      when:                           # Condition — skip if not met
        type: command_exists
        command: cargo

Command Fields

FieldRequiredDefaultDescription
runYes-Shell command string to execute
descriptionNo-Human-readable description
timeoutNo300Timeout in seconds
elevatedNofalseRequire administrator privileges
continue_on_errorNofalseContinue to next command if this one fails
whenNo-Condition predicate; command is skipped when not met

Conditional Execution

The when field accepts any condition type from the Assertions predicate engine:

commands:
  post_install:
    # Only runs if cargo is on PATH
    - run: "cargo install sccache"
      description: "Install sccache"
      when:
        type: command_exists
        command: cargo

    # Only runs if the config file doesn't already exist
    - run: "echo '{}' > ~/.config/app/config.json"
      description: "Create default config"
      when:
        type: file_exists
        path: "~/.config/app/config.json"

Command Phases

PhaseWhen it runs
pre_installBefore package installation
post_installAfter package installation

Error Handling

By default, if a command fails (non-zero exit code), execution stops and subsequent commands are skipped. Set continue_on_error: true to keep going:

commands:
  post_install:
    - run: "cargo install cargo-watch"
      continue_on_error: true        # Failure won't stop the next command
    - run: "cargo install cargo-edit"

6. Environment Configuration

Configure environment variables and PATH additions.

Basic Structure

environment:
  variables:
    - name: MY_VARIABLE
      value: "my-value"
      scope: user
      
  path_additions:
    - "C:\\Tools\\bin"
    - "~\\.local\\bin"

Environment Variables

environment:
  variables:
    - name: RUST_BACKTRACE
      value: "1"
      scope: user              # user or machine
      
    - name: EDITOR
      value: "code"
      scope: user
FieldRequiredDefaultDescription
nameYes-Variable name
valueYes-Variable value
scopeNouserScope: user or machine

PATH Additions

Add directories to the system PATH:

environment:
  path_additions:
    - "C:\\Tools\\bin"
    - "~\\.cargo\\bin"
    - "${LOCALAPPDATA}\\Programs\\bin"

PATH additions are appended to the existing PATH for the specified scope.

Scope

ScopeDescriptionRequires Admin
userCurrent user onlyNo
machineAll users on systemYes

7. Assertions

Assertions are declarative health checks defined directly in workload.yaml. They let you validate system state — such as installed commands, existing files, environment variables, and PATH entries — without writing PowerShell scripts.

Use assertions when your checks are simple conditions (command exists, file exists, env var set). Use health check scripts for complex validation that requires multi-step logic or custom output.

Assertion Structure

Each assertion has a name and a check that specifies a condition:

assertions:
  - name: Git is installed
    check:
      type: command_exists
      command: git

Condition Types

command_exists

Checks whether a command is available on PATH.

- name: cargo is available
  check:
    type: command_exists
    command: cargo

file_exists

Checks whether a file exists at the given path. Supports ~ expansion.

- name: Git config exists
  check:
    type: file_exists
    path: "~/.gitconfig"

dir_exists

Checks whether a directory exists at the given path. Supports ~ expansion.

- name: Cargo directory exists
  check:
    type: dir_exists
    path: "~/.cargo"

env_var

Checks whether an environment variable is set, optionally matching a specific value.

# Check existence only
- name: RUST_BACKTRACE is set
  check:
    type: env_var
    name: RUST_BACKTRACE

# Check existence and value
- name: RUST_BACKTRACE is 1
  check:
    type: env_var
    name: RUST_BACKTRACE
    value: "1"

path_contains

Checks whether the system PATH contains a given substring.

- name: Cargo bin on PATH
  check:
    type: path_contains
    substring: ".cargo/bin"

registry_value

Queries a Windows registry value under HKCU or HKLM. If expected is omitted, the check only asserts the value exists.

- name: Developer mode enabled
  check:
    type: registry_value
    hive: HKLM
    key: "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock"
    name: AllowDevelopmentWithoutDevLicense
    expected: "1"

shell

Runs an arbitrary shell command; the condition passes when the exit code is 0.

- name: Rust compiler responds
  check:
    type: shell
    command: "rustc --version"
    description: "Rust compiler version check"

Composition with all_of and any_of

Combine conditions with logical operators for complex checks.

all_of — all conditions must pass (AND)

- name: Full Rust toolchain
  check:
    type: all_of
    conditions:
      - type: command_exists
        command: rustc
      - type: command_exists
        command: cargo
      - type: dir_exists
        path: "~/.cargo"

any_of — at least one must pass (OR)

- name: Python is available
  check:
    type: any_of
    conditions:
      - type: command_exists
        command: python
      - type: command_exists
        command: python3

Enabling Assertion Checks

Assertions are evaluated during anvil health when assertion_check is enabled in the health config (it defaults to true):

health:
  package_check: true
  file_check: true
  assertion_check: true   # Evaluate declarative assertions

Complete Example

name: rust-developer
version: "1.0.0"
description: "Rust development environment"

packages:
  winget:
    - id: Rustlang.Rustup

assertions:
  - name: cargo command exists
    check:
      type: command_exists
      command: cargo
  - name: rustc command exists
    check:
      type: command_exists
      command: rustc
  - name: Cargo directory exists
    check:
      type: dir_exists
      path: "~/.cargo"
  - name: Cargo bin on PATH
    check:
      type: path_contains
      substring: ".cargo/bin"
  - name: RUST_BACKTRACE is set
    check:
      type: env_var
      name: RUST_BACKTRACE
      value: "1"

health:
  package_check: true
  assertion_check: true

8. Inheritance

Workloads can extend other workloads to inherit their configuration.

Basic Inheritance

name: my-rust-dev
version: "1.0.0"

extends:
  - essentials      # Inherit base development tools

Multiple Inheritance

name: full-stack-dev
version: "1.0.0"

extends:
  - essentials
  - rust-developer
  - python-developer

Merge Behavior

When a workload extends parents, configuration is merged:

SectionMerge Behavior
packagesMerged; child packages added to parent packages
filesMerged; child files override parent files with same destination
commandsMerged; child commands run after parent commands
environmentMerged; child variables override parent variables

Override Example

Parent (base/workload.yaml):

name: base
version: "1.0.0"

packages:
  winget:
    - id: Git.Git
    - id: Microsoft.VisualStudioCode

files:
  - source: .gitconfig
    destination: "~/.gitconfig"

Child (extended/workload.yaml):

name: extended
version: "1.0.0"

extends:
  - base

packages:
  winget:
    # Adds to parent packages
    - id: Rustlang.Rustup

files:
  # Overrides parent's .gitconfig
  - source: .gitconfig
    destination: "~/.gitconfig"

Inheritance Chains

Anvil resolves inheritance chains automatically:

my-workload
└── rust-developer
    └── essentials

Circular dependencies are detected and rejected:

# workload-a extends workload-b
# workload-b extends workload-a
# ERROR: Circular dependency detected

View Inheritance

# Show inheritance tree
anvil show my-workload --inheritance-tree

# Show fully resolved workload
anvil show my-workload --resolved

9. Variable Expansion

Use variables in paths and values for dynamic configuration.

Supported Variables

VariableDescriptionExample Value
~User home directoryC:\Users\username
${HOME}User home directoryC:\Users\username
${USERNAME}Current usernameusername
${COMPUTERNAME}Machine nameWORKSTATION-01
${WORKLOAD_DIR}Workload directoryC:\Workloads\my-workload
${USERPROFILE}User profile pathC:\Users\username
${APPDATA}Roaming AppDataC:\Users\username\AppData\Roaming
${LOCALAPPDATA}Local AppDataC:\Users\username\AppData\Local
${env:VAR_NAME}Any environment variable(varies)

Usage Examples

files:
  # Home directory shorthand
  - source: .bashrc
    destination: "~/.bashrc"
  
  # Explicit home variable
  - source: config.json
    destination: "${HOME}/.config/myapp/config.json"
  
  # AppData paths
  - source: settings.json
    destination: "${APPDATA}/MyApp/settings.json"
  
  # Environment variable
  - source: custom.conf
    destination: "${env:MY_CUSTOM_PATH}/config.conf"

environment:
  variables:
    - name: MY_APP_HOME
      value: "${HOME}/.myapp"
  
  path_additions:
    - "${HOME}/.local/bin"
    - "${LOCALAPPDATA}/Programs/bin"

10. Best Practices

Workload Design

  1. Use Descriptive Names

    name: rust-developer           # Good
    name: workload1                # Bad
    
  2. Include Version Information

    version: "1.2.0"               # Good: semantic versioning
    version: "latest"              # Bad: not meaningful
    
  3. Write Helpful Descriptions

    description: "Complete Rust development environment with debugging tools and VS Code extensions"
    
  4. Use Inheritance for Common Bases

    # Create a base workload for team-wide tools
    # Then extend it for role-specific setups
    extends:
      - team-base
    

Package Management

  1. Pin Versions When Necessary

    packages:
      winget:
        # Pin when compatibility matters
        - id: Python.Python.3.12
          version: "3.12.0"
        
        # Use latest for frequently updated tools
        - id: Git.Git
    
  2. Use Machine Scope for Shared Tools

    packages:
      winget:
        - id: Microsoft.VisualStudioCode
          override:
            - "--scope"
            - "machine"
    

File Management

  1. Always Enable Backups for Important Files

    files:
      - source: .gitconfig
        destination: "~/.gitconfig"
        backup: true
    
  2. Use Templates for Dynamic Content

    files:
      - source: config.toml.hbs
        destination: "~/.config/app/config.toml"
        template: true
    

Script Safety

  1. Make Scripts Idempotent

    # Check before acting
    if (-not (Test-Path $target)) {
        # Create/install
    }
    
  2. Handle Errors Gracefully

    try {
        # Risky operation
    }
    catch {
        Write-Error "Operation failed: $_"
        exit 1
    }
    
  3. Include Assertions

    assertions:
      - name: "Installation verified"
        check:
          type: command_exists
          command: my-tool
    

Testing

  1. Validate Before Committing

    anvil validate my-workload --strict
    
  2. Test with Dry Run

    anvil install my-workload --dry-run
    
  3. Test on Clean System

    • Use a VM or container
    • Document dependencies

11. Example Workloads

Minimal Workload

The simplest valid workload:

# minimal/workload.yaml
name: minimal
version: "1.0.0"
description: "A minimal workload example"

Package-Only Workload

Install software without files or scripts:

# dev-essentials/workload.yaml
name: dev-essentials
version: "1.0.0"
description: "Essential development tools"

packages:
  winget:
    - id: Git.Git
    - id: Microsoft.VisualStudioCode
    - id: Microsoft.WindowsTerminal
    - id: JanDeDobbeleer.OhMyPosh

Complete example with all features:

# full-example/workload.yaml
name: full-example
version: "1.0.0"
description: "Complete workload demonstrating all features"

extends:
  - essentials

packages:
  winget:
    - id: Rustlang.Rustup
    - id: LLVM.LLVM
      version: "17.0.6"
    - id: Microsoft.VisualStudio.2022.BuildTools
      override:
        - "--add"
        - "Microsoft.VisualStudio.Workload.VCTools"

files:
  - source: files/.cargo/config.toml
    destination: "~/.cargo/config.toml"
    backup: true
    
  - source: files/vscode/settings.json.hbs
    destination: "${APPDATA}/Code/User/settings.json"
    template: true

commands:
  pre_install:
    - run: "echo Checking prerequisites..."
      description: "Check prerequisites"
  post_install:
    - run: "rustup default stable && rustup update stable"
      description: "Configure Rust toolchain"
      timeout: 600
    - run: "rustup component add rustfmt clippy"
      description: "Add Rust components"
      when:
        type: command_exists
        command: rustup

assertions:
  - name: cargo is available
    check:
      type: command_exists
      command: cargo
  - name: Cargo bin on PATH
    check:
      type: path_contains
      substring: ".cargo/bin"

environment:
  variables:
    - name: RUST_BACKTRACE
      value: "1"
      scope: user
      
  path_additions:
    - "~/.cargo/bin"

Inherited Workload

Workload that builds on others:

# rust-advanced/workload.yaml
name: rust-advanced
version: "1.0.0"
description: "Advanced Rust development with WASM and embedded support"

extends:
  - rust-developer

packages:
  winget:
    - id: Docker.DockerDesktop
    - id: WasmEdge.WasmEdge

commands:
  post_install:
    - run: "rustup target add wasm32-unknown-unknown"
      description: "Add WASM target"
    - run: "rustup target add thumbv7em-none-eabihf"
      description: "Add embedded target"
    - run: "cargo install cargo-embed probe-run"
      description: "Install embedded cargo tools"
      continue_on_error: true

environment:
  path_additions:
    - "~/.wasmedge/bin"

12. Private Workload Repositories

You can maintain your own workloads in a separate Git repository and configure Anvil to discover them.

my-workloads/
├── my-dev-env/
│   ├── workload.yaml
│   ├── files/
│   │   └── .gitconfig
│   └── scripts/
│       └── setup.ps1
├── team-tools/
│   ├── workload.yaml
│   └── scripts/
│       └── post-install.ps1
└── README.md

Each subdirectory containing a workload.yaml is treated as a separate workload, following the same directory structure as bundled workloads.

Setup

  1. Clone your workloads repository:

    git clone https://github.com/your-org/workloads ~/my-workloads
    
  2. Configure Anvil to search this path:

    anvil config set workloads.paths '["~/my-workloads"]'
    

    Or edit ~/.anvil/config.yaml directly:

    workloads:
      paths:
        - "~/my-workloads"
    
  3. Verify discovery:

    anvil list
    

Tips

  • Inheritance: Private workloads can extends: built-in workloads (e.g., extends: [essentials])
  • Version control: Keep workloads in Git for team sharing and history
  • Multiple repos: Add multiple paths for team-shared and personal workloads
  • Precedence: If your workload has the same name as a built-in one, yours takes priority
  • Validation: Run anvil validate <name> --strict to check your workloads before committing

Complete Config Example

# Anvil global configuration
# Location: ~/.anvil/config.yaml

workloads:
  paths:
    - "~/my-workloads"           # Personal workloads
    - "~/work/team-workloads"    # Team-shared workloads

logging:
  level: info

Resources


This guide is for Anvil v0.3.1. For other versions, check the corresponding documentation.