The vmlab.wcl schema

A complete reference of the vmlab.wcl (and host config.wcl) schema, reflected straight from src/config/schema.wcl / host_schema.wcl with WCL's reflection builtins (child_types / type_fields) and the wdoc type_table component — so it can never drift from the code. Each block lists its attributes (type, whether required, description), any nested blocks, and a worked example. Descriptions are the fields' @doc annotations.

§ 1`lab` block

PropertyTypeRequiredDescription
nameutf8yesLab name (DNS label, ≤63 chars); the inline block label
guiboolnoDefault for all VMs: open a VNC viewer on up (§11); VM gui overrides

Child blocks

SlotAcceptsMultipleDescription
segmentssegmentyesVirtual L2 network segments in this lab
vmsvmyesThe VMs in this lab
provisionsprovisionyeswscript provision scripts run on vmlab up, in declaration order
handlersonyesLifecycle event handlers (failures are logged, never fatal)
recordsrecordyesLab-wide static DNS entries (wildcards allowed) (§9.5)
sinkholessinkholeyesLab-wide DNS sinkholes (§9.9)

Example:

wcl
lab "demo" {
  gui = true                       // lab-wide default: show each guest's screen
  vm "box" {
    template = "x86_64/linux-modern"
    memory   = 2GiB
    nic { nat = true }
  }
}

§ 1.1`segment` (in `lab`)

PropertyTypeRequiredDescription
nameutf8yesSegment name (DNS label); unique per lab; the inline block label
subnetutf8noCIDR; auto-allocated as a /24 from the host pool if omitted (§9.4)
globalboolnoOwned by the supervisor and shared across labs (§9.2)
dhcpboolnoEnable DHCP (default true) (§9.4)
natboolnoEnable NAT/internet egress for this segment (default false) (§9.7)
mtui64noLink MTU (576–65535); default jumbo (9000) on nat/global, else 1500
routes_tolist<utf8>noNames of other segments to route to — daemon inter-segment routing opt-in (§9.6)

Child blocks

SlotAcceptsMultipleDescription
dnsdnsnoDNS service override: hand out another server, or opt out (§9.5)
connectconnectnoCross-host segment peer over TCP (PSK from host config) (§9.2)
routesrouteyesGuest routes pushed via DHCP option 121 (§9.6)
recordsrecordyesStatic DNS entries for this segment (wildcards allowed) (§9.5)
forwardsforwardyesHost→guest port forwards (§9.8)
block_rulesblockyesL3 block rules at the switch (§9.9)
redirect_rulesredirectyesL3 DNAT redirect rules (§9.9)
sinkholessinkholeyesDNS sinkhole rules (§9.9)

Example:

wcl
segment "corp" {
  subnet = "10.50.0.0/24"          // omit to auto-allocate a /24 from the host pool
  nat    = true                    // internet egress for this segment
  record { name = "dc01" ip = "10.50.0.10" }
}

`dns` (in `segment`)

PropertyTypeRequiredDescription
serverutf8noIPv4 of the DNS server to hand out via DHCP instead of the daemon
enabledboolnoHand out a DNS server at all (default true); false suppresses the DHCP option

Example:

wcl
dns { server = "10.50.0.10" }      // hand out a DC as the resolver via DHCP
dns { enabled = false }            // …or suppress DNS on the segment entirely

`connect` (in `segment`)

PropertyTypeRequiredDescription
hostutf8yesRemote supervisor host[:port] to bridge this segment with (required)

Example:

wcl
connect { host = "helios:9999" }   // bridge this segment to a peer supervisor (PSK)

`route` (in `segment`)

PropertyTypeRequiredDescription
destutf8yesDestination CIDR, e.g. 10.60.0.0/24 (required)
viautf8yesGateway IPv4 the route points at (required)

Example:

wcl
route { dest = "10.60.0.0/24" via = "10.50.0.254" }   // pushed via DHCP option 121

`record` (in `segment`)

PropertyTypeRequiredDescription
nameutf8yesDNS name to resolve; wildcards allowed, e.g. *.internal (required)
iputf8yesIPv4 address the name resolves to (required)

Example:

wcl
record { name = "srv" ip = "10.50.0.5" }     // wildcards OK: name = "*.internal"

`forward` (in `segment`)

PropertyTypeRequiredDescription
host_porti64yesHost port to listen on (1–65535); unique across the lab (required)
toutf8yesTarget as vm:port; the VM must be declared (required)
protoutf8noProtocol: tcp (default) | udp | both

Example:

wcl
forward { host_port = 13389 to = "dc01:3389" proto = "tcp" }

`block` (in `segment`)

PropertyTypeRequiredDescription
cidrutf8yesIPv4 CIDR to drop traffic to/from (required)
protoutf8noProtocol to scope the rule: tcp | udp | icmp
porti64noPort to scope the rule (1–65535); requires proto

Example:

wcl
block { cidr = "192.0.2.0/24" proto = "tcp" port = 443 }

`redirect` (in `segment`)

PropertyTypeRequiredDescription
fromutf8yesMatch destination as ip[:port] (required)
toutf8yesRewrite destination to ip[:port] (required)
protoutf8noProtocol to scope the rule: tcp | udp

Example:

wcl
redirect { from = "10.50.0.254:53" to = "10.50.0.10:53" proto = "udp" }

`sinkhole` (in `segment`)

PropertyTypeRequiredDescription
patternutf8yesDNS name pattern to sink; wildcards allowed (required)
modeutf8noResponse: nxdomain (default) | zero (resolve to 0.0.0.0)

Example:

wcl
sinkhole { pattern = "*.telemetry.com" mode = "nxdomain" }   // or mode = "zero"

§ 1.2`vm` (in `lab`)

PropertyTypeRequiredDescription
nameutf8yesVM name (DNS label); unique per lab; the inline block label
templateutf8yes<arch>/<name>[@<version>], scratch, or an OCI registry ref (required)
archutf8noArchitecture; required for scratch and registry references
profileutf8noGuest OS profile (hardware defaults); required for scratch
cpusi64novCPU count (> 0); inherited from template→profile if omitted
memorystd.ByteSizenoRAM as a byte size, e.g. 8GiB/512MiB; inherited if omitted
diskstd.ByteSizenoPrimary disk size, e.g. 64GiB — scratch VMs only (rejected on cloned VMs)
cdromutf8noPath to an ISO to attach as a CD-ROM (relative to lab root)
floppyutf8noPath to a floppy image to attach (relative to lab root)
depends_onlist<utf8>noVM names to wait for before this one (no cycles)
nestedboolnoEnable nested virtualisation (host CPU passthrough)
guiboolnoOpen a VNC viewer on up (§11); the VM always runs headless
displayutf8noQEMU display string; inherited from template→profile if omitted
firmwareutf8noFirmware: ovmf | seabios; inherited from template→profile
tpmboolnoEnable a TPM 2.0 device; inherited from template→profile
secure_bootboolnoEnable secure boot (OVMF only); inherited from template→profile
qemu_argslist<utf8>noRaw QEMU flags appended last — escape hatch (§5.2)

Child blocks

SlotAcceptsMultipleDescription
gpugpunoGPU acceleration (passthrough / virgl / vulkan)
nicsnicyesNetwork interfaces; no NICs = air-gapped (shares need ≥1 NIC)
extra_disksdiskyesAdditional disks beyond the primary disk
sharesshareyesSMB shared folders (require ≥1 NIC) (§7.5)
mediamediayesISO/floppy images built from a folder (§6.3)

Example:

wcl
vm "dc01" {
  template = "x86_64/windows-2025"
  cpus     = 4
  memory   = 8GiB
  nic   { segment = "corp" ip = "10.50.0.10" }
  share { host = "./src" guest = "D:\\src" }
}

`gpu` (in `vm`)

PropertyTypeRequiredDescription
modeutf8yesMode: passthrough | virgl | vulkan (required)
addressutf8noHost PCI address, e.g. 0000:01:00.0 — required for passthrough

Example:

wcl
gpu { mode = "passthrough" address = "0000:01:00.0" }   // or mode = "virgl" | "vulkan"

`nic` (in `vm`)

PropertyTypeRequiredDescription
segmentutf8noSegment name to attach to; required unless nat = true
natboolnoShorthand: attach to the per-lab built-in NAT segment (§9.7)
iputf8noStatic IPv4 (becomes a DHCP reservation); must be in the subnet, unique
macutf8noFixed MAC, e.g. 52:54:00:ab:cd:ef; generated and persisted otherwise
isolatedboolnoPort isolation: reach gateway/forwards but not segment neighbours (§9.1)

Example:

wcl
nic { segment = "corp" ip = "10.50.0.10" mac = "52:54:00:aa:bb:cc" }
nic { nat = true }                       // per-lab built-in NAT segment shorthand
nic { segment = "dmz" isolated = true }  // port isolation

`disk` (in `vm`)

PropertyTypeRequiredDescription
nameutf8yesDisk identifier; the inline block label
sizestd.ByteSizenoBlank disk size, e.g. 10GiB; one of size/from is required
fromutf8noFolder copied onto a fresh FAT filesystem; one of size/from is required

Example:

wcl
disk "data"      { size = 10GiB }         // extra blank disk
disk "formatted" { from = "./payload/" }  // folder copied onto a fresh FAT filesystem

`share` (in `vm`)

PropertyTypeRequiredDescription
hostutf8yesHost directory to share; must exist (required)
guestutf8yesGuest mount path, e.g. /mnt/src or D:\data (required)
readonlyboolnoMount read-only (default false)
smb1boolnoEnable the SMB1 dialect + auth relaxation for XP/2003-era guests
nameutf8noSMB share name; derived from the guest path if omitted

Example:

wcl
share { host = "./src"  guest = "/mnt/src" }
share { host = "~/data" guest = "D:\\data" readonly = true }
share { host = "./old"  guest = "X:" smb1 = true }   // legacy dialect for XP/2003

`media` (in `vm`)

PropertyTypeRequiredDescription
kindutf8yesImage kind: iso | floppy (required)
fromutf8yesSource folder built into the image; must exist (required)
labelutf8noVolume label for the image

Example:

wcl
media { kind = "iso"    from = "./unattend/" label = "CIDATA" }
media { kind = "floppy" from = "./drivers/"  label = "DRV" }

§ 1.3`provision` (in `lab`)

Provision script run during vmlab up (§10.4). Optional vms list scopes the script for depends_on satisfaction (§7.2).

PropertyTypeRequiredDescription
scriptutf8yesPath to the .ws file; must exist and compile; the inline label
vmslist<utf8>noVM names this script is scoped to (gates their depends_on) (§7.2)

Example:

wcl
provision "scripts/setup.ws" { }                     // runs on `vmlab up`, in order
provision "scripts/join.ws"  { vms = ["client01"] }  // scoped: gates depends_on

§ 1.4`on` (in `lab`)

Event handler binding (§8.2).

PropertyTypeRequiredDescription
eventutf8yesEvent name to handle, e.g. vm.crashed; the inline block label
runutf8yesPath to the handler .ws file; must exist and compile (required)

Example:

wcl
on "vm.crashed"    { run = "scripts/collect-dumps.ws" }
on "host.disk_low" { run = "scripts/alert.ws" }

§ 1.5`record` (in `lab`)

Static DNS entry (wildcards allowed in name) (§9.5).

PropertyTypeRequiredDescription
nameutf8yesDNS name to resolve; wildcards allowed, e.g. *.internal (required)
iputf8yesIPv4 address the name resolves to (required)

Example:

wcl
record { name = "srv" ip = "10.50.0.5" }     // wildcards OK: name = "*.internal"

§ 1.6`sinkhole` (in `lab`)

DNS sinkhole (§9.9): NXDOMAIN by default, or 0.0.0.0 with mode = "zero".

PropertyTypeRequiredDescription
patternutf8yesDNS name pattern to sink; wildcards allowed (required)
modeutf8noResponse: nxdomain (default) | zero (resolve to 0.0.0.0)

Example:

wcl
sinkhole { pattern = "*.telemetry.com" mode = "nxdomain" }   // or mode = "zero"

§ 2`template` block

Template definition (§6.1), buildable with vmlab template build.

PropertyTypeRequiredDescription
nameutf8yesTemplate name, e.g. linux-modern; the inline block label
archutf8yesArchitecture — selects the QEMU system emulator (required)
versionutf8yesVersion string, non-empty; name+arch+version is unique (required)
registryutf8noFull OCI repo to publish to / version-bump against (§6.4)
profileutf8noGuest OS profile (hardware defaults) for the build VM
cpusi64novCPU count for the build VM; inherited by clones
memorystd.ByteSizenoRAM for the build VM, e.g. 8GiB; inherited by clones
diskstd.ByteSizenoWorking disk size for the build, e.g. 64GiB; required for scratch source
displayutf8noQEMU display string for the build VM
firmwareutf8noFirmware: ovmf | seabios
tpmboolnoEnable a TPM 2.0 device
secure_bootboolnoEnable secure boot (OVMF only)
nestedboolnoEnable nested virtualisation for the build VM
guiboolnoWatch the build VM via a VNC viewer (§11)
qemu_argslist<utf8>noRaw QEMU flags for the build VM — escape hatch (§5.2)
first_bootutf8nowscript run on first instantiation of a clone, before ready

Child blocks

SlotAcceptsMultipleDescription
sourcesourcenoWhat the build starts from — exactly one of four forms (required)
mediamediayesISO/floppy images attached to the build (§6.3)
provisionsprovisionyesProvision scripts that drive the build
nicsnicyesNICs for the build VM (optional; the build VM may be air-gapped)
extra_disksdiskyesAdditional disks attached during the build

Example:

wcl
template "linux-modern" {
  arch    = "x86_64"
  version = "1.0"
  profile = "linux-modern"
  disk    = 20GiB                  // working disk size for the build
  source "iso" { url = "https://releases.ubuntu.com/.../x.iso" sha256 = "abc123…" }
  provision "scripts/install.ws" { }
}

§ 2.1`source` (in `template`)

Template build source (§6.1): exactly one of the four forms.

PropertyTypeRequiredDescription
kindutf8yesSource kind: iso | qcow2 | template | scratch; the inline label
pathutf8noLocal file path — iso/qcow2; mutually exclusive with url
urlutf8noRemote artefact URL — iso/qcow2; requires sha256
sha256utf8noSHA-256 of the remote artefact; required with url
fromutf8noSource template <arch>/<name>[@<version>] — kind template (layered build)

Example:

wcl
source "iso"      { path = "./isos/win11.iso" }           // local installer ISO
source "iso"      { url = "https://…" sha256 = "" }      // downloaded + verified
source "qcow2"    { path = "./base.qcow2" }               // existing disk as base
source "template" { from = "x86_64/linux-modern@1.0" }    // layered build
source "scratch"  { }                                     // blank disk

§ 2.2`media` (in `template`)

ISO/floppy image built from a folder (§6.3).

PropertyTypeRequiredDescription
kindutf8yesImage kind: iso | floppy (required)
fromutf8yesSource folder built into the image; must exist (required)
labelutf8noVolume label for the image

Example:

wcl
media { kind = "iso"    from = "./unattend/" label = "CIDATA" }
media { kind = "floppy" from = "./drivers/"  label = "DRV" }

§ 2.3`provision` (in `template`)

Provision script run during vmlab up (§10.4). Optional vms list scopes the script for depends_on satisfaction (§7.2).

PropertyTypeRequiredDescription
scriptutf8yesPath to the .ws file; must exist and compile; the inline label
vmslist<utf8>noVM names this script is scoped to (gates their depends_on) (§7.2)

Example:

wcl
provision "scripts/setup.ws" { }                     // runs on `vmlab up`, in order
provision "scripts/join.ws"  { vms = ["client01"] }  // scoped: gates depends_on

§ 2.4`nic` (in `template`)

PropertyTypeRequiredDescription
segmentutf8noSegment name to attach to; required unless nat = true
natboolnoShorthand: attach to the per-lab built-in NAT segment (§9.7)
iputf8noStatic IPv4 (becomes a DHCP reservation); must be in the subnet, unique
macutf8noFixed MAC, e.g. 52:54:00:ab:cd:ef; generated and persisted otherwise
isolatedboolnoPort isolation: reach gateway/forwards but not segment neighbours (§9.1)

Example:

wcl
nic { segment = "corp" ip = "10.50.0.10" mac = "52:54:00:aa:bb:cc" }
nic { nat = true }                       // per-lab built-in NAT segment shorthand
nic { segment = "dmz" isolated = true }  // port isolation

§ 2.5`disk` (in `template`)

Additional disk (§5.2): blank by size, or pre-formatted from a folder.

PropertyTypeRequiredDescription
nameutf8yesDisk identifier; the inline block label
sizestd.ByteSizenoBlank disk size, e.g. 10GiB; one of size/from is required
fromutf8noFolder copied onto a fresh FAT filesystem; one of size/from is required

Example:

wcl
disk "data"      { size = 10GiB }         // extra blank disk
disk "formatted" { from = "./payload/" }  // folder copied onto a fresh FAT filesystem

§ 3`host` block

PropertyTypeRequiredDescription
subnet_poolutf8noSegment auto-allocation pool (CIDR); default 10.213.0.0/16 (§9.4)
dns_suffixutf8noSuffix for auto-registered VM names; default vmlab.internal (§9.5)
dns_upstreamutf8noUpstream resolver ip[:port]; default: the host resolver
disk_low_percenti64nohost.disk_low watchdog threshold percent (0–100); default 10 (§8.1)
pskutf8noPre-shared key for cross-host segment links (§9.2)
viewerutf8noVNC viewer command; {} is replaced by the target (§11)
oci_chunk_sizestd.ByteSizenoOCI layer chunk size for template push; default 512MiB (§6.4)

Example:

wcl
host {
  subnet_pool      = "10.213.0.0/16"   // segment auto-allocation pool (default shown)
  dns_suffix       = "vmlab.internal"
  dns_upstream     = "1.1.1.1"
  disk_low_percent = 10
  viewer           = "vncviewer {}"    // {} = target
  oci_chunk_size   = 512MiB
}