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.

§ 1lab block

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

Child blocks

SlotAcceptsMultipleDescription
segmentssegmentyesVirtual L2 network segments in this lab
vmsvmyesThe VMs in this lab
containerscontaineryesOCI containers in this lab, each run in a micro-VM
handlersonyesLifecycle event handlers (failures are logged, never fatal)
recordsrecordyesLab-wide static DNS entries (wildcards allowed)
sinkholessinkholeyesLab-wide DNS sinkholes

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.1segment (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
globalboolnoOwned by the supervisor and shared across labs
dhcpboolnoEnable DHCP (default true)
natboolnoEnable NAT/internet egress for this segment (default false)
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

Child blocks

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

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.2vm (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; 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

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)
mediamediayesISO/floppy images built from a folder
webwebyesHTTP UIs served in the guest, proxied into the web console (require ≥1 NIC)
loginsloginyesIdentities a surface attaches to this VM as; without one every verb keeps the agent identity
provisionsprovisionyeswscript provision scripts run on vmlab up once this VM is ready, interleaved with its playbooks in declaration order
playbooksplaybookyesconfig-weave playbooks applied to this VM on vmlab up, interleaved with its provisions in declaration order

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
iputf8noStatic IPv4 (becomes a DHCP reservation); must be in the subnet, unique
gatewayboolnoMake this NIC the segment gateway; it must own the subnet's first usable address
macutf8noFixed MAC, e.g. 52:54:00:ab:cd:ef; generated and persisted otherwise
isolatedboolnoPort isolation: reach gateway/forwards but not segment neighbours

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
nameutf8noShare name; derived from the guest path if omitted
transportutf8noTransport: auto (default; virtiofs when host + guest support it, else SMB) | virtiofs | smb

Example:

wcl
share { host = "./src"  guest = "/mnt/src" }         // transport auto: virtiofs, else SMB
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" }

web (in vm)

PropertyTypeRequiredDescription
nameutf8yesPage name (DNS label); unique per machine; the inline block label
porti64yesGuest TCP port serving the HTTP UI (1–65535) (required)
pathutf8noInitial path opened in the console (default /)

Child blocks

SlotAcceptsMultipleDescription
authauthnoCredentials the proxy injects so the guest app's own login never prompts

Example:

wcl
web "admin" { port = 8080  path = "/manage" }   // proxied into the web console

auth (in web)

PropertyTypeRequiredDescription
methodWebAuthMethodyesMethod: :basic | :bearer | :header | :ntlm (IIS/AD integrated) | :form (cookie capture) (required)
usernameutf8noUsername — :basic, :ntlm, :form
passwordutf8noPassword — :basic, :ntlm, :form
domainutf8noNTLM domain, e.g. CORP:ntlm (optional)
tokenutf8noStatic bearer token — :bearer
headerutf8noHeader name, e.g. X-Api-Key:header
valueutf8noHeader value — :header
login_pathutf8noLogin request path, e.g. /login:form (required)
login_methodutf8noLogin HTTP method: POST (default) | GET:form
login_bodyutf8noLogin body template; {user}/{pass} are substituted and escaped — :form (required)
login_content_typeutf8noLogin body content type: application/x-www-form-urlencoded (default) | application/json:form
fail_redirectutf8noRedirect-Location substring that means 'not logged in' (401/403 always retrigger) — :form

Example:

wcl
auth { method = :basic  username = "admin" password = "s3cret" }
auth { method = :bearer token = "eyJ…" }
auth { method = :ntlm   username = "Administrator" password = "" domain = "CORP" }
auth { method = :form   username = "admin" password = ""
       login_path = "/login" login_body = "user={user}&pass={pass}" }

login (in vm)

PropertyTypeRequiredDescription
labelutf8yesIdentity label — what an SSH username selects it by, e.g. dev; unique per machine; the inline block label
userutf8yesGuest account to log on as, e.g. PROBE\dev (required)
passwordutf8noThe account's password, plainly; required on a Windows-family profile
elevatedboolnoRun the session elevated (default true); Windows-only — declaring it on a Linux-family profile is an error
defaultboolnoMake this the machine's default identity; implied when the machine declares exactly one login

Example:

wcl
// Machine-level identity (PRD §19.2). The label is what an SSH username
// selects; the secret is written plainly, because the lab's own provisioning
// created the account and already carries the same string.
// On a Windows-family profile every login needs a `password`. `elevated`
// (default true) selects the linked token, so one account can be declared
// twice at two elevations — the labels are what tell them apart, and they
// must be unique on a machine.
login "dev"      { user = "PROBE\\dev" password = "vmlab123!" default = true }
login "dev-user" { user = "PROBE\\dev" password = "vmlab123!" elevated = false }
// On a Linux-family profile `elevated` is a validation error, and the secret
// is only needed where something authenticates against it.
login "ops" { user = "ops" }

provision (in vm)

PropertyTypeRequiredDescription
scriptutf8yesPath to the .ws file; must exist and compile; the inline label

Example:

wcl
// Declared inside the vm/container it configures; runs once that machine is
// ready, at this position among its steps.
provision "scripts/setup.ws" { }

playbook (in vm)

PropertyTypeRequiredDescription
pathutf8yesPlaybook folder (contains playbook.wcl), relative to the lab root; the inline label
playutf8yesPlay name inside the playbook to run (required)

Child blocks

SlotAcceptsMultipleDescription
varsvaryesVariable overrides passed to config-weave for this machine's run

Example:

wcl
// Declared inside the vm/container it converges.
playbook "playbooks/domain" { play = "dc" }
playbook "playbooks/domain" {
  play = "member"
  var "domain"      { value = "corp.example.com" }
  var "member_name" { value = "APP01" }   // --var, this machine only
}

var (in playbook)

PropertyTypeRequiredDescription
nameutf8yesVariable name; must be a WCL identifier; the inline block label
valueutf8yesValue, 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 (required)

Example:

wcl
var "domain"  { value = "corp.example.com" }  // --var domain=corp.example.com
var "retries" { value = "3" }                 // config-weave reads this as an int
var "tag"     { value = "\"3\"" }               // quoted: forced to a string

§ 1.3container (in lab)

PropertyTypeRequiredDescription
ididentifiernoName used to connect the shape (a -> b) and to anchor others to it.
classlist<utf8>noStyle classes — text and SVG paint via the class system.
linkutf8noLink the shape to an in-site page (bare page name, or site:page). Wraps it in a clickable <a>; an unknown page fails the build like a bad prose link.
strokeutf8noOptional chrome — outline colour of the background rect that makes the group visible.
fillutf8noOptional chrome — fill colour of the background rect that makes the group visible.
paddingf64noInset between the chrome and the child shapes.
widthf64noDeclared interior width (when no layout/anchor sizes it).
heightf64noDeclared interior height (when no layout/anchor sizes it).
layoutsymbolnoLayout mode: :free (default, manual) / :grid / :layered / :force / :radial.
columnsi64noNumber of columns for :grid layout.
cell_widthf64noGrid cell width for :grid layout.
cell_heightf64noGrid cell height for :grid layout.
gapf64noGap between cells in :grid layout.
directionsymbolnoFlow direction for :layered: :top_to_bottom (default) / :left_to_right.
layer_gapf64noSpacing between ranks (layers) in :layered layout.
node_gapf64noSpacing between nodes within a rank in :layered layout.
iterationsi64no:force relaxation steps (default 300).
repulsionf64no:force node repulsion strength (default 9000).
link_distancef64no:force ideal edge-to-edge length (default 60).
gravityf64no:force centering pull (default 0.05).
seedi64no:force random seed for reproducible layouts (default 1).
hubidentifierno:radial hub shape id (defaults to the highest-degree shape).
radiusf64no:radial radius of the first ring (default: auto-fit to shape sizes).
ring_gapf64no:radial added radius per successive ring (default 120).
start_anglef64no:radial angle (radians) of the first shape on each ring (default -PI/2, i.e. top).
anchor_leftf64noFractional anchor (0–1) pinning the left edge to the parent box.
anchor_rightf64noFractional anchor (0–1) pinning the right edge to the parent box.
anchor_topf64noFractional anchor (0–1) pinning the top edge to the parent box.
anchor_bottomf64noFractional anchor (0–1) pinning the bottom edge to the parent box.
connect_pointslist<AnchorSide>noWhich sides (:left/:right/:top/:bottom) edges attach to.
iconutf8noIcon-badge icon (a pack.name).
icon_sizef64noIcon-badge size.
icon_posIconPosnoIcon-badge position (:center / :top_left / …).
icon_classlist<utf8>noIcon-badge style classes.
edgeslist<Edge>yesEdges connecting child shapes (a -> b).

Child blocks

SlotAcceptsMultipleDescription
childrenSvgBlockyesThe child shapes laid out by the container.

Example:

wcl
container "web" {
  image      = "nginx:1.27"              // docker.io shorthand; @sha256:… pins
  mode       = :workload                  // :workload (default) | :idle
  profile    = "container"               // micro-VM hardware floor (§5.3)
  memory     = 512MiB                    // overrides the profile; cpus likewise
  depends_on = ["db"]                    // VM or container names — one namespace
  nic    { segment = "corp" ip = "10.50.0.20" }
  env    { name = "MODE" value = "prod" }
  volume { name = "data" target = "/var/lib/data" }
  port   { host = 18080 container = 80 }
  healthcheck { command = ["curl", "-fsS", "http://localhost/"] interval = 5s }
}

nic (in container)

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

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

env (in container)

PropertyTypeRequiredDescription
nameutf8yesVariable name (required)
valueutf8yesVariable value (required)

Example:

wcl
env { name = "PGUSER" value = "app" }

volume (in container)

PropertyTypeRequiredDescription
hostutf8noHost path to bind-mount, relative to the lab root; one of host/name is required
nameutf8noNamed volume kept under the lab dir, shared by name, retained until lab destroy; one of host/name
targetutf8yesAbsolute mount path inside the container (required)
read_onlyboolnoMount read-only (default false)

Example:

wcl
volume { name = "data"  target = "/var/lib/data" }               // named, lab-scoped
volume { host = "./www" target = "/srv/www" read_only = true }   // host bind

port (in container)

PropertyTypeRequiredDescription
hosti64yesHost port to listen on (1–65535); unique across the lab (required)
containeri64yesContainer port to forward to (1–65535) (required)
protoutf8noProtocol: tcp (default) | udp | both

Example:

wcl
port { host = 18080 container = 80 proto = "tcp" }   // sugar for a segment forward

healthcheck (in container)

PropertyTypeRequiredDescription
commandlist<utf8>yesProbe command run inside the container (exec form); exit 0 = healthy (required)
intervalstd.DurationnoTime between probes, e.g. 10s (default 10s)
timeoutstd.DurationnoPer-probe timeout (default 5s)
retriesi64noConsecutive failures before unhealthy (default 3)
start_periodstd.DurationnoGrace period after start before failures count (default 10s)

Example:

wcl
healthcheck {
  command      = ["curl", "-fsS", "http://localhost/"]   // exit 0 = healthy
  interval     = 10s
  timeout      = 5s
  retries      = 3
  start_period = 10s
}

login (in container)

PropertyTypeRequiredDescription
labelutf8yesIdentity label — what an SSH username selects it by, e.g. dev; unique per machine; the inline block label
userutf8yesGuest account to log on as, e.g. PROBE\dev (required)
passwordutf8noThe account's password, plainly; required on a Windows-family profile
elevatedboolnoRun the session elevated (default true); Windows-only — declaring it on a Linux-family profile is an error
defaultboolnoMake this the machine's default identity; implied when the machine declares exactly one login

Example:

wcl
// Machine-level identity (PRD §19.2). The label is what an SSH username
// selects; the secret is written plainly, because the lab's own provisioning
// created the account and already carries the same string.
// On a Windows-family profile every login needs a `password`. `elevated`
// (default true) selects the linked token, so one account can be declared
// twice at two elevations — the labels are what tell them apart, and they
// must be unique on a machine.
login "dev"      { user = "PROBE\\dev" password = "vmlab123!" default = true }
login "dev-user" { user = "PROBE\\dev" password = "vmlab123!" elevated = false }
// On a Linux-family profile `elevated` is a validation error, and the secret
// is only needed where something authenticates against it.
login "ops" { user = "ops" }

web (in container)

PropertyTypeRequiredDescription
nameutf8yesPage name (DNS label); unique per machine; the inline block label
porti64yesGuest TCP port serving the HTTP UI (1–65535) (required)
pathutf8noInitial path opened in the console (default /)

Child blocks

SlotAcceptsMultipleDescription
authauthnoCredentials the proxy injects so the guest app's own login never prompts

Example:

wcl
web "admin" { port = 8080  path = "/manage" }   // proxied into the web console

auth (in web)

PropertyTypeRequiredDescription
methodWebAuthMethodyesMethod: :basic | :bearer | :header | :ntlm (IIS/AD integrated) | :form (cookie capture) (required)
usernameutf8noUsername — :basic, :ntlm, :form
passwordutf8noPassword — :basic, :ntlm, :form
domainutf8noNTLM domain, e.g. CORP:ntlm (optional)
tokenutf8noStatic bearer token — :bearer
headerutf8noHeader name, e.g. X-Api-Key:header
valueutf8noHeader value — :header
login_pathutf8noLogin request path, e.g. /login:form (required)
login_methodutf8noLogin HTTP method: POST (default) | GET:form
login_bodyutf8noLogin body template; {user}/{pass} are substituted and escaped — :form (required)
login_content_typeutf8noLogin body content type: application/x-www-form-urlencoded (default) | application/json:form
fail_redirectutf8noRedirect-Location substring that means 'not logged in' (401/403 always retrigger) — :form

Example:

wcl
auth { method = :basic  username = "admin" password = "s3cret" }
auth { method = :bearer token = "eyJ…" }
auth { method = :ntlm   username = "Administrator" password = "" domain = "CORP" }
auth { method = :form   username = "admin" password = ""
       login_path = "/login" login_body = "user={user}&pass={pass}" }

provision (in container)

PropertyTypeRequiredDescription
scriptutf8yesPath to the .ws file; must exist and compile; the inline label

Example:

wcl
// Declared inside the vm/container it configures; runs once that machine is
// ready, at this position among its steps.
provision "scripts/setup.ws" { }

playbook (in container)

PropertyTypeRequiredDescription
pathutf8yesPlaybook folder (contains playbook.wcl), relative to the lab root; the inline label
playutf8yesPlay name inside the playbook to run (required)

Child blocks

SlotAcceptsMultipleDescription
varsvaryesVariable overrides passed to config-weave for this machine's run

Example:

wcl
// Declared inside the vm/container it converges.
playbook "playbooks/domain" { play = "dc" }
playbook "playbooks/domain" {
  play = "member"
  var "domain"      { value = "corp.example.com" }
  var "member_name" { value = "APP01" }   // --var, this machine only
}

var (in playbook)

PropertyTypeRequiredDescription
nameutf8yesVariable name; must be a WCL identifier; the inline block label
valueutf8yesValue, 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 (required)

Example:

wcl
var "domain"  { value = "corp.example.com" }  // --var domain=corp.example.com
var "retries" { value = "3" }                 // config-weave reads this as an int
var "tag"     { value = "\"3\"" }               // quoted: forced to a string

§ 1.4on (in lab)

Event handler binding.

PropertyTypeRequiredDescription
eventutf8yesEvent name to handle, e.g. vm.crashed; the inline block label
runutf8yesPath to the handler .ws file; must exist and compile (required)
targetslist<utf8>noOptional VM/container names; empty handles every occurrence of the event

Example:

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

§ 1.5record (in lab)

Static DNS entry (wildcards allowed in name).

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.6sinkhole (in lab)

DNS sinkhole: 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"

§ 2template block

Template definition, 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
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
qemu_argslist<utf8>noRaw QEMU flags for the build VM — escape hatch
first_bootutf8nowscript run on first instantiation of a clone, before ready
agentboolnoBake the vmlab-agent service into the image (default true)

Child blocks

SlotAcceptsMultipleDescription
sourcesourcenoWhat the build starts from — exactly one of four forms (required)
mediamediayesISO/floppy images attached to the build
provisionsprovisionyesProvision scripts that drive the build
playbooksplaybookyesconfig-weave playbooks applied to the build VM, interleaved with provisions in declaration order; steps stream as structured build progress
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.1source (in template)

Template build source: 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.2media (in template)

ISO/floppy image built from a folder.

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.3provision (in template)

Provision script run during vmlab up, declared inside the vm/container it configures (or inside a template, for the build VM). It runs once, after that machine is ready, at its position among the machine's steps.

PropertyTypeRequiredDescription
scriptutf8yesPath to the .ws file; must exist and compile; the inline label

Example:

wcl
// Declared inside the vm/container it configures; runs once that machine is
// ready, at this position among its steps.
provision "scripts/setup.ws" { }

§ 2.4playbook (in template)

config-weave playbook applied on vmlab up (interleaved with the machine's provisions in declaration order) and runnable on demand via vmlab playbook check|apply. Declared inside the vm/container it converges, or inside a template for the build VM.

PropertyTypeRequiredDescription
pathutf8yesPlaybook folder (contains playbook.wcl), relative to the lab root; the inline label
playutf8yesPlay name inside the playbook to run (required)

Child blocks

SlotAcceptsMultipleDescription
varsvaryesVariable overrides passed to config-weave for this machine's run

Example:

wcl
// Declared inside the vm/container it converges.
playbook "playbooks/domain" { play = "dc" }
playbook "playbooks/domain" {
  play = "member"
  var "domain"      { value = "corp.example.com" }
  var "member_name" { value = "APP01" }   // --var, this machine only
}

var (in playbook)

PropertyTypeRequiredDescription
nameutf8yesVariable name; must be a WCL identifier; the inline block label
valueutf8yesValue, 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 (required)

Example:

wcl
var "domain"  { value = "corp.example.com" }  // --var domain=corp.example.com
var "retries" { value = "3" }                 // config-weave reads this as an int
var "tag"     { value = "\"3\"" }               // quoted: forced to a string

§ 2.5nic (in template)

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

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.6disk (in template)

Additional disk: 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

§ 3host block

PropertyTypeRequiredDescription
subnet_poolutf8noSegment auto-allocation pool (CIDR); default 10.213.0.0/16
dns_suffixutf8noSuffix for auto-registered VM names; default vmlab.internal
dns_upstreamutf8noUpstream resolver ip[:port]; default: the host resolver
disk_low_percenti64nohost.disk_low watchdog threshold percent (0–100); default 10
pskutf8noPre-shared key for cross-host segment links
trunk_porti64noTCP listen port for inbound cross-host segment trunks; default 13947
viewerutf8noVNC viewer command; {} is replaced by the target
fastpathutf8noNetwork fast path: auto (probe; default), off, sockmap, or afxdp
oci_chunk_sizestd.ByteSizenoOCI layer chunk size for template push; default 512MiB
config_weave_bin_dirutf8noDirectory holding config-weave guest binaries; default ~/.local/share/config-weave/bin
ssh_configutf8noFile vmlab writes its managed SSH block into; default ~/.ssh/config
workspace_max_filestd.ByteSizenoWorkspace syncer per-file size guard; a larger file is refused by name; default 256MiB

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
}