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
scripts: object                 # Script execution 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).

scripts

Script execution definitions (see Script Definitions).

environment

Environment variable configuration (see Environment Configuration).


3. Package Definitions

Define software packages to install via Windows Package Manager (winget).

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"

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
    mode: "0644"                  # Optional: file permissions
    template: false               # Process as Handlebars template
    create_dirs: true             # Create parent directories

File Fields

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

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. Script Definitions

Define PowerShell or CMD scripts for installation steps and health checks.

Script Categories

scripts:
  pre_install:     # Run before package installation
    - path: scripts/pre-install.ps1
    
  post_install:    # Run after package installation
    - path: scripts/post-install.ps1
    
  health_check:    # Run during health checks
    - path: scripts/health-check.ps1

Full Script Options

scripts:
  post_install:
    - path: scripts/setup.ps1
      shell: powershell           # powershell, pwsh, cmd
      description: "Configure application settings"
      elevated: false             # Run as administrator
      timeout: 300                # Timeout in seconds
      continue_on_error: false    # Continue if script fails
      env:                        # Additional environment variables
        MY_VAR: "value"

Script Fields

FieldRequiredDefaultDescription
pathYes-Path relative to workload directory
shellNopowershellShell: powershell, pwsh, cmd
descriptionNo-Human-readable description
elevatedNofalseRun with administrator privileges
timeoutNo300Timeout in seconds
continue_on_errorNofalseContinue installation if script fails
envNo-Additional environment variables
nameNo-Display name (for health checks)

Health Check Scripts

Health check scripts verify system state:

scripts:
  health_check:
    - path: scripts/check-rust.ps1
      name: "Rust Toolchain"
      description: "Verify Rust is installed and configured"
      timeout: 30

Script Guidelines

Exit Codes

  • 0 - Success
  • Non-zero - Failure
# Good: explicit exit codes
if (Test-Path $requiredFile) {
    exit 0
} else {
    Write-Error "Required file not found"
    exit 1
}

Output Handling

  • Use Write-Host for informational output
  • Use Write-Error for error messages
  • Use Write-Warning for warnings
Write-Host "Installing components..."
Write-Warning "This may take a while"
Write-Error "Installation failed"

Idempotency

Scripts should be safe to run multiple times:

# Good: check before acting
if (-not (Test-Path "C:\Tools\mytool")) {
    Write-Host "Installing mytool..."
    # Install logic here
} else {
    Write-Host "mytool already installed"
}

Error Handling

Use try-catch for robust error handling:

try {
    # Risky operation
    Invoke-WebRequest -Uri $url -OutFile $path
    exit 0
}
catch {
    Write-Error "Failed to download: $_"
    exit 1
}

Sample Scripts

Pre-Install Check

# scripts/pre-install.ps1
# Check prerequisites before installation

$ErrorActionPreference = "Stop"

# Check Windows version
$version = [Environment]::OSVersion.Version
if ($version.Major -lt 10) {
    Write-Error "Windows 10 or later required"
    exit 1
}

# Check available disk space (need 1GB)
$drive = Get-PSDrive C
$freeGB = [math]::Round($drive.Free / 1GB, 2)
if ($freeGB -lt 1) {
    Write-Error "Insufficient disk space. Need 1GB, have ${freeGB}GB"
    exit 1
}

Write-Host "Prerequisites check passed"
exit 0

Post-Install Configuration

# scripts/post-install.ps1
# Configure application after installation

$ErrorActionPreference = "Stop"

try {
    # Install Rust components
    Write-Host "Installing Rust components..."
    rustup component add rustfmt
    rustup component add clippy
    
    # Install common cargo tools
    Write-Host "Installing cargo tools..."
    cargo install cargo-watch
    cargo install cargo-edit
    
    Write-Host "Post-install configuration complete"
    exit 0
}
catch {
    Write-Error "Post-install failed: $_"
    exit 1
}

Health Check

# scripts/health-check.ps1
# Verify Rust development environment

$ErrorActionPreference = "Stop"
$errors = @()

# Check rustc
try {
    $rustVersion = rustc --version
    Write-Host "✓ Rust compiler: $rustVersion"
}
catch {
    $errors += "rustc not found"
}

# Check cargo
try {
    $cargoVersion = cargo --version
    Write-Host "✓ Cargo: $cargoVersion"
}
catch {
    $errors += "cargo not found"
}

# Check rustfmt
try {
    $null = rustfmt --version
    Write-Host "✓ rustfmt installed"
}
catch {
    $errors += "rustfmt not installed"
}

# Report results
if ($errors.Count -gt 0) {
    Write-Error "Health check failed:"
    $errors | ForEach-Object { Write-Error "  - $_" }
    exit 1
}

Write-Host "All health checks passed"
exit 0

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
  script_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
scriptsMerged; child scripts run after parent scripts
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 Health Checks

    scripts:
      health_check:
        - path: scripts/verify.ps1
          name: "Installation Verification"
    

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

scripts:
  pre_install:
    - path: scripts/pre-check.ps1
      description: "Check prerequisites"
      
  post_install:
    - path: scripts/setup-rust.ps1
      description: "Configure Rust toolchain"
      elevated: false
      timeout: 600
      
  health_check:
    - path: scripts/health.ps1
      name: "Rust Environment"
      description: "Verify Rust development environment"

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

scripts:
  post_install:
    - path: scripts/setup-targets.ps1
      description: "Add Rust compilation targets"

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

With corresponding script:

# rust-advanced/scripts/setup-targets.ps1
# Add additional Rust compilation targets

$ErrorActionPreference = "Stop"

try {
    Write-Host "Adding WASM target..."
    rustup target add wasm32-unknown-unknown
    
    Write-Host "Adding embedded targets..."
    rustup target add thumbv7em-none-eabihf
    
    Write-Host "Installing cargo tools for embedded..."
    cargo install cargo-embed
    cargo install probe-run
    
    exit 0
}
catch {
    Write-Error "Setup failed: $_"
    exit 1
}

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.