Reference · reference

Lab file: vm and its children

18 min read · 2026-09-02 · vmlab 0.9

This chapter is the field-by-field reference for the vm {} block and every block it can carry. The blocks a vm shares with a container (login, nic, provision, playbook) are documented here once and referenced from container and its children. How a VM's hardware resolves is explained in the lab file guide; what the schema does not say is enforced by vmlab validate, and each entry below lists those rules.

vm {}

One virtual machine. The only required field is template; every hardware value not set here is inherited from the template's recorded hardware, then from the guest OS profile.

wcl
vm "<name>" {
  template    = "<arch>/<name>[@<version>][#<variant>]"   // or "scratch", or a registry ref
  arch        = "x86_64"
  profile     = "linux-modern"
  cpus        = 2
  memory      = 4GiB
  disk        = 64GiB          // scratch VMs only
  cdrom       = "./isos/boot.iso"
  floppy      = "./boot.img"
  depends_on  = ["dc01"]
  nested      = false
  gui         = false
  agent_update = true
  prevent_sleep = false
  clock       = "host"         // or "snapshot", or "2020-01-01T00:00:00Z"
  display     = "virtio-vga"
  cpu_model   = "pentium3"
  accel       = "tcg"
  firmware    = "ovmf"
  tpm         = false
  secure_boot = false
  qemu_args   = []
  gpu       { … }
  nic       { … }
  disk "…"  { … }
  share     { … }
  media     { … }
  login "…" { … }
  provision "…" { … }
  playbook "…" { … }
}
FieldTypeDefaultMeaning
nameutf8 (label)requiredVM name, a DNS label, unique per lab; the inline block label.
templateutf8required<arch>/<name>[@<version>][#<variant>] from the store, scratch, or an OCI registry reference with an optional #<variant>.
archutf8from templateArchitecture. Required for scratch and for registry references.
profileutf8from templateGuest OS profile supplying hardware defaults. Required for scratch.
cpusi64inheritedvCPU count, greater than 0. Inherited from template, then profile, if omitted.
memoryByteSizeinheritedRAM as a byte size, for example 8GiB or 512MiB. Inherited if omitted.
diskByteSizenonePrimary disk size, for example 64GiB. Scratch VMs only; rejected on cloned VMs.
cdromutf8nonePath to an ISO to attach as a CD-ROM, relative to the lab root.
floppyutf8nonePath to a floppy image to attach, relative to the lab root.
depends_onlist<utf8>noneVM or container names to wait for before this one starts. No cycles.
nestedboolfalseExpose hardware virtualisation (VMX/SVM) to the guest. Without it, an x86 guest under KVM has both masked. up warns when the host's KVM module has nested virtualisation off. No effect under TCG or on other architectures.
guiboollab guiOpen a VNC viewer on up. The VM always runs headless.
agent_updateboollab agent_update, else trueRefresh the guest agent on up and vm start when its version stamp differs from the agent this vmlab ships, and mark the VM diverged. false leaves the agent the template sealed. See vmlab up.
prevent_sleepboolfalseKeep the guest awake: the moment it suspends to RAM (ACPI S3), the lab daemon wakes it with QMP system_wakeup and records vm.woken with cause = prevent_sleep. Declared here only, never inherited: it is the daemon's policy, not hardware. The template build VM always has it on. Not accepted on a container {}. See vmlab status.
clockutf8hostGuest clock policy. host: the RTC runs from the host's time, and after an online snapshot restore the lab daemon sets the guest's clock to the host's current time through the agent. snapshot: a restored guest keeps the time it was captured at. An RFC 3339 date-time such as 2020-01-01T00:00:00Z (any offset, converted to UTC): the RTC starts at that instant on every boot and runs forward, and vmlab never sets it, after a restore included. Declared here only, never inherited. The RTC holds UTC unless the profile sets rtc = "localtime", which starts it at the host's local time instead. See Snapshots.
displayutf8inheritedQEMU display device string. Inherited from template, then profile.
cpu_modelutf8inheritedQEMU -cpu model, for example pentium3, pentium2, qemu32 or Westmere, replacing the default (host under KVM, max under TCG) under either accelerator. For guests the host CPU breaks, such as Windows 9x Setup. Inherited from template, then profile. Any non-empty name is accepted; QEMU refuses an unknown one at start. Cannot be combined with nested = true.
accelutf8inheritedAccelerator: kvm or tcg. Unset, KVM when usable, else TCG with a warning. tcg always emulates, even where KVM works, for guests KVM cannot run reliably, such as Windows 95 on an AMD host; -cpu stays max unless cpu_model names one. kvm refuses to start the VM when KVM is unusable rather than falling back. Inherited from template, then profile. tcg cannot be combined with nested = true.
firmwareutf8inheritedFirmware: ovmf or seabios. Inherited from template, then profile.
tpmboolinheritedEnable a TPM 2.0 device. Inherited from template, then profile.
usbboolinheritedBoot with a hot-plug xHCI controller so vmlab usb can pass host USB devices in. Inherited from the profile only — a template has no say; off when neither sets it. Changes the VM's hardware, so online snapshots taken without it no longer restore.
secure_bootboolinheritedEnable secure boot; OVMF only. Inherited from template, then profile.
secure_boot_keysutf8inherited, else microsoftThe key store secure boot starts from. microsoft: the bundled store with Microsoft's keys enrolled. setup: the blank store, so the firmware starts in setup mode and the guest enrolls its own keys on first boot; the VM's OVMF_VARS.fd keeps them. Inherited from template, then profile. Needs secure_boot = true. See Host and firmware.
secure_boot_varsutf8noneA pre-enrolled UEFI VARS file, relative to the lab root, copied once at the VM's first start in place of the bundled store, so the guest enforces your keys from its first boot. Must be the size of the secure-boot firmware's VARS template. Needs secure_boot = true; exclusive with secure_boot_keys. Overrides a template's stored VARS.
qemu_argslist<utf8>noneRaw QEMU flags appended last. The escape hatch; they win over everything vmlab generates.
gpu {}childnoneGPU acceleration: passthrough, virgl or vulkan.
nic {}childrennoneNetwork interfaces. No NICs means air-gapped. Shares need at least one.
disk {}childrennoneAdditional disks beyond the primary disk.
share {}childrennoneShared folders over virtiofs or SMB. SMB shares require at least one NIC.
media {}childrennoneISO or floppy images built from a folder.
login {}childrennoneIdentities a person's commands run as on this VM. Without one every verb keeps the agent identity.
provision {}childrennonewscript scripts run on vmlab up once this VM is ready, interleaved with its playbooks in declaration order.
playbook {}childrennoneconfig-weave playbooks applied on vmlab up, interleaved with its provisions in declaration order.

Template references

The template value takes one of three forms, and the form decides what else the block must say.

Known architectures are x86_64, x86, aarch64, riscv64, loongarch64, s390x and ppc64.

The #<variant> suffix selects one disk of a template version that has variants. Without it the template's default variant is used, or its only disk when it has none. A variant name is 1 to 32 lowercase letters, digits and hyphens, starting with a letter or digit. #base names the unsealed base disk and is refused on a template that has variants. The rules are in Templates.

Validation

examples/mixed-lab/vmlab.wclwcl
vm "winsrv" {
  template = "x86_64/windows-server-2025"
  cpus = 4
  memory = 8GiB
  nic {
    segment = "lan"
    ip = "10.70.0.10"
  }  # DHCP reservation
  share {
    host = "./shared"
    guest = "S:"
  }  # auto-mounted when ready

  # Runs once winsrv is ready; nix01 depends on it, so it waits.
  provision "scripts/setup.ws" { }
}

The @dev decorator

@dev is written on the line before a vm or container block and marks it as a dev machine: when a workspace is named, vmlab syncs that directory onto it and keeps the two copies in step. It is a decorator rather than a child block because it states something about the machine; nothing it carries is a setting the guest sees. A bare @dev is complete. Any number of machines may carry it, and zero is normal.

wcl
@dev(default = true, workspace = "./workspace", workspace_guest = "C:\\src")
vm "dev01" { … }
ArgumentTypeDefaultMeaning
defaultboolfalseMake this the lab's default dev machine. At most one per lab. The only @dev machine in a lab is the default implicitly.
workspaceutf8noneHost directory whose contents sync into the workspace, relative to the lab root. Without it the machine carries @dev but has nothing to sync.
workspace_guestutf8profile, else /srcGuest path the workspace lands at. Inherited from the profile (C:\src on Windows profiles, /src on Linux ones) if omitted.

Unset arguments resolve in the order @dev argument, then the machine's effective profile, then the vmlab floor of /src. A profile that sets no workspace_guest still hosts a dev machine. The schema rejects an unknown argument, a wrong type, a repeated @dev, or @dev on a block that is not a vm or container. Validation adds one rule: two machines with default = true is an error naming both. With more than one @dev machine and none declaring default = true, the lab has no default; the selection ladder in vmlab dev then needs an argument. The dev-container example carries it on a container:

examples/dev-container/vmlab.wclwcl
@dev(default = true, workspace = "./workspace")
container "dev01" {
  image   = "alpine:3.22"
  profile = "container"
  // A dev machine builds things, and a container names its own size when
  // the profile's floor is not the right one.
  cpus    = 2
  memory  = 1GiB
  // `:idle` keeps the micro-VM up without running the image's entrypoint
  // — a dev container has no service to be.
  mode    = :idle
  nic { segment = "lan" }

  // The container identity floor (§19.2): the agent is root and root needs
  // no credential to become an account, so a Linux `login {}` may declare
  // the account alone. `elevated` is a validation error on this side.
  login "dev" { user = "dev" default = true }

  provision "scripts/dev-user.ws" { }
  provision "scripts/home-bits.ws" { }
}

login {}

A labelled identity on a machine: the guest account exec, shell and a script's as_login run as. See Logins. Repeatable, so one account may be declared twice at different elevation, and --user selects between labels.

wcl
login "<label>" {
  user     = "PROBE\\dev"
  password = "…"
  elevated = true
  default  = false
}
FieldTypeDefaultMeaning
labelutf8 (label)requiredIdentity label, what --user and as_login select it by, for example dev. Unique per machine; the inline block label.
userutf8requiredGuest account to log on as, for example PROBE\dev.
passwordutf8noneThe account's password, written plainly. Required on a Windows-family profile.
elevatedbooltrueRun the session elevated. Windows only; declaring it on a Linux-family profile is an error.
defaultboolimpliedMake this the machine's default identity. Implied when the machine declares exactly one login.

The family the rules are judged against is the machine's resolved profile: a name starting with windows is Windows, one starting with linux is Linux, and anything else, including custom and a registry template not yet pulled, is unknown and gets neither family rule. A container is always Linux. Validation enforces:

The secret is in the lab file

The password is written plainly because the lab's own provision script created the account with the same string. There is no credential store, no login verb and no wscript credential API.

nic {}

One network interface, attached to a declared segment or to the lab's built-in NAT segment. A machine with no nic {} blocks has no network hardware at all. See Networking.

wcl
nic {
  segment  = "corp"      // or: nat = true
  ip       = "10.50.0.10"
  gateway  = false
  mac      = "52:54:00:ab:cd:ef"
  isolated = false
}
FieldTypeDefaultMeaning
segmentutf8noneSegment name to attach to. Required unless nat = true.
natboolfalseShorthand: attach to the per-lab built-in NAT segment.
iputf8dynamicStatic IPv4, which becomes a DHCP reservation. Must be in the subnet and unique.
gatewayboolfalseMake this NIC the segment gateway. It must own the subnet's first usable address.
macutf8generatedFixed MAC, for example 52:54:00:ab:cd:ef. Generated and persisted otherwise.
isolatedboolfalsePort isolation: reach the gateway and forwards but not segment neighbours.

Validation enforces these rules:

disk {}

An additional disk beyond the primary one: blank at a given size, or a fresh FAT filesystem with a folder copied onto it, or both.

wcl
disk "data" { size = 10GiB }
disk "payload" { from = "./payload/" }
FieldTypeDefaultMeaning
nameutf8 (label)requiredDisk identifier; the inline block label.
sizeByteSizenoneBlank disk size, for example 10GiB. One of size or from is required.
fromutf8noneFolder copied onto a fresh FAT filesystem. One of size or from is required.

At least one of size and from must be set; both together is allowed. A from folder must exist under the lab root.

share {}

A host directory mounted into the guest, over virtiofs when host and guest support it, otherwise over SMB served at the segment gateway. See Shared folders.

wcl
share {
  host      = "./shared"
  guest     = "/mnt/src"
  readonly  = false
  smb1      = false
  name      = "src"
  transport = "auto"
}
FieldTypeDefaultMeaning
hostutf8requiredHost directory to share. Must exist.
guestutf8requiredGuest mount path, for example /mnt/src or D:\data.
readonlyboolfalseMount read-only.
smb1boolfalseEnable the SMB1 dialect and the auth relaxation XP and 2003-era guests need.
nameutf8derivedShare name. Derived from the guest path if omitted.
transportutf8autoauto picks virtiofs when host and guest support it, else SMB; virtiofs or smb force one.

The derived name joins the alphanumeric runs of the guest path with underscores, so /mnt/src becomes mnt_src and D:\data becomes d_data. Validation requires host to be a directory, a derivable or explicit name, and rejects smb1 = true with transport = "virtiofs". A VM whose shares are not all virtiofs needs a NIC.

media {}

An ISO or floppy image built from a folder and attached to the machine. Built images are content-addressed under the lab's .vmlab/ directory, so an unchanged folder is not rebuilt.

wcl
media { kind = "iso" from = "./unattend/" label = "UNATTEND" }
FieldTypeDefaultMeaning
kindutf8requiredImage kind: iso or floppy.
fromutf8requiredSource folder built into the image. Must exist.
labelutf8noneVolume label for the image.

Validation requires from to be a directory under the lab root.

gpu {}

GPU acceleration for the VM. At most one per VM. Passthrough hands a host device to the VM exclusively; virgl and vulkan render on the host GPU, which stays shared. The GPU itself is a host GPU, declared by name in the host configuration file: a PCI address is a fact about one machine, and the lab file travels between machines.

wcl
gpu { mode = "vulkan" }                    // the host's default GPU
gpu { mode = "vulkan" host_gpu = "AMD1" }  // a named one
FieldTypeDefaultMeaning
modeutf8requiredpassthrough, virgl or vulkan.
host_gpuutf8the host's default GPUName of a host GPU declared in the host config, matched case-insensitively. Under passthrough the device handed to the VM; under virgl and vulkan the GPU that renders.

The lab daemon picks the host GPU when the VM starts. A host_gpu this host declares is used. A host_gpu it does not declare makes virgl and vulkan warn and take the host's default; passthrough refuses, naming the host's GPUs, because passing through a different card takes it away from the host. No host_gpu takes the default. When nothing resolves — no GPU declared, or the name missing and no default — virgl and vulkan render where QEMU puts them, and passthrough refuses. vmlab status -v shows the picked GPU as host_gpu=<name>.

A paravirtual GPU is the guest's only display adapter: virtio-vga-gl on x86 machines, virtio-gpu-gl with -vga none on virt. vulkan adds venus=true,blob=true,hostmem=4G.

vmlab reads the picked GPU's render node and kernel driver from /sys/bus/pci/devices/<address>/. The render node goes on the egl-headless display. Under vulkan the driver sets VK_LOADER_DRIVERS_SELECT in QEMU's environment: *radeon* for amdgpu, *intel* for i915 and xe, *nvidia* for nvidia; a host GPU's vulkan_icd replaces that with VK_DRIVER_FILES. vmlab also loads its own implicit layer, VK_LAYER_VMLAB_pci_filter, through VK_ADD_IMPLICIT_LAYER_PATH, with VMLAB_VK_PCI_ADDRESS set to the GPU's address. The layer hides every other physical device, so the guest sees one Venus device even when two identical cards both match the driver filter. The other Vulkan loader variables are cleared, and MESA_VK_DEVICE_SELECT is never set, because it breaks Venus. vmlab up warns when a virgl or vulkan VM gets no host GPU and the host has more than one render node. Each run's qemu.log starts with the QEMU command line and the variables vmlab set. Under passthrough the address becomes -device vfio-pci,host=<address>.

Validation runs the same pick: it rejects an empty host_gpu; a passthrough name this host lacks, or passthrough with nothing to pick; a picked GPU whose address names no PCI device, or no render node under virgl or vulkan; and a vulkan_icd that does not exist. A virgl or vulkan name this host lacks is a warning. The v0.24 fields address and vulkan_icd moved to the host config, and a lab still carrying either fails with the message in Troubleshooting.

provision {}

A wscript provision script run on vmlab up once this machine is ready. It runs once, at its position among the machine's provision and playbook blocks. See Guest automation with wscript.

wcl
provision "scripts/setup.ws" { }
FieldTypeDefaultMeaning
scriptutf8 (label)requiredPath to the .ws file, relative to the lab root; the inline label. Must exist and compile.

Validation checks the file exists and compiles it, reporting compile errors against the block. Across machines, steps follow the order the machine blocks appear, with depends_on gating when each becomes eligible.

playbook {}

A config-weave playbook applied on vmlab up, interleaved with the machine's provisions in declaration order, and runnable on demand with vmlab playbook check and apply. See Playbooks.

wcl
playbook "playbooks/baseline" {
  play = "baseline"
  var "tz" { value = "UTC" }
}
FieldTypeDefaultMeaning
pathutf8 (label)requiredPlaybook folder containing playbook.wcl, relative to the lab root; the inline label.
playutf8requiredPlay name inside the playbook to run.
timeoutduration1hCeiling on each config-weave run, such as 2h or 90min. A run after a reboot gets the full value again.
guest_dirutf8profile's playbook_dir, else /var/lib/vmlab/weave (Linux) or C:\weave (Windows)Absolute guest directory the config-weave binary and the playbook folders land in. Created if missing.
guest_binutf8unsetAbsolute path of a config-weave binary already in the guest. When set, vmlab pushes no binary and probes this one with version before each run; the playbook folder is still pushed to guest_dir.
var {}childrennoneVariable overrides passed to config-weave for this machine's run.

Validation requires a non-empty play, a path that is a directory holding a playbook.wcl, a timeout greater than zero, and no variable set twice on one block. guest_dir and guest_bin must be absolute for the machine's guest family: a drive-letter path on Windows, a path starting with / on Linux (every container), and either where the family is unknown. On Windows guest_bin must also end in .exe. config-weave ships guest binaries for x86_64 only, so a playbook on a VM with another known arch is rejected unless it names a guest_bin.

var {}

One variable override for the enclosing playbook {}, passed to config-weave as --var name=value for this machine's run only.

wcl
var "tz" { value = "UTC" }
FieldTypeDefaultMeaning
nameutf8 (label)requiredVariable name; must be a WCL identifier. The inline block label.
valueutf8requiredValue, passed through verbatim. config-weave reads it as a WCL expression where it can (3 is an int, true a bool) and as a string otherwise.

Validation requires the name to be letters, digits and underscores, not starting with a digit, because config-weave binds each override as a let inside the guest.

on {}

An event handler, declared inside lab {}. The handler script runs when the named event fires; a failing handler is logged and never fatal. See Events and handlers and the event list.

wcl
on "vm.crashed" { run = "scripts/collect-dumps.ws" targets = ["dc01"] }
FieldTypeDefaultMeaning
eventutf8 (label)requiredEvent name to handle, for example vm.crashed; the inline block label.
runutf8requiredPath to the handler .ws file, relative to the lab root. Must exist and compile.
targetslist<utf8>noneVM or container names to restrict the handler to. Empty handles every occurrence.

Validation requires a known event name and a script that exists and compiles. targets may only be set on vm.*, container.* and snapshot.* events; a lab-wide event with targets is an error. A vm.* event may target only VMs and a container.* event only containers, and every target must exist.

examples/ad-lab/vmlab.wclwcl
on "vm.crashed"    { run = "scripts/collect-dumps.ws" }
on "host.disk_low" { run = "scripts/alert.ws" }