Templating And Facts Reference

on this page

This page describes the template engine in internal/template/ and the facts model in internal/facts/.

Template Syntax

Preflight uses a lightweight Jinja-like placeholder form:

text
{{ vars.content_root }}
{{ facts.os.arch }}
{{ target.name }}
{{ env.PATH }}

Supported behavior:

  • dot-path lookups only
  • string substitution inside task names and parameter values
  • recursive rendering through nested maps and lists
  • boolean rendering for when:

Important limit:

  • The engine is intentionally small. It does not implement the full Jinja expression or filter language.

Namespaces

NamespaceMeaning
vars.*Merged variables
facts.*Gathered host facts
target.*Safe target metadata
env.*Gathered target environment variables
vars.preflight.*Built-in project metadata (see below)

Variable Precedence

vars.* exposes the merged result of the project, inventory, group, host, playbook, and CLI variable layers. The authoritative merge order lives in the inventory reference.

Undefined vars.* references are treated as errors so missing inventory, playbook, project, or CLI-provided values fail early instead of rendering as empty strings.

Built-In preflight Variables

The runner injects a preflight map into the project variable layer so playbooks can reference project metadata without repeating it in vars:

VariableMeaning
vars.preflight.projectproject field from preflight.yml
vars.preflight.environmentenvironment field from preflight.yml

These are injected above host vars in the merge order, so playbook vars and CLI --var flags can override them, but inventory, group, and host vars cannot.

Planning Versus Execution

The runner renders templates in two different contexts:

  • During plan, unknown facts.* and target.* expressions are preserved so planning stays pure.
  • During check and apply, the runner gathers facts and target metadata, then renders task names, when: conditions, and parameters for real execution.
  • Missing vars.* references fail in every phase because they indicate incomplete configuration, not unavailable runtime metadata.

That is why plan may still show {{ facts... }} placeholders while check and apply do not.

when: Conditions

when: uses the same template engine, then interprets the rendered result with truthy semantics:

  • False (task skipped): empty string, false, 0, no
  • True (task runs): anything else

This means when: works naturally for both boolean inputs and optional string inputs:

yaml
# gate on a boolean input
when: "{{ vars.winget_enabled }}"

# gate on an optional string — skips if computer_name was not provided
when: "{{ vars.computer_name }}"

Preflight does not currently support comparison or arithmetic expressions such as {{ vars.count > 1 }} in when:. Action authors should pass a boolean input or compute the decision inside a module-specific script.

Facts Shape

The facts command and execution-time fact gathering expose this structure:

FieldMeaning
facts.os.familywindows, linux, darwin, or unknown
facts.os.nameos-release ID on POSIX (e.g. ubuntu, rocky); friendly name on Windows
facts.os.versionos-release VERSION_ID on POSIX; raw version string on Windows
facts.os.buildNumeric build number (Windows-only; 0 elsewhere)
facts.os.archArchitecture
facts.os.hostnameHostname from OS facts
facts.os.package_managerapt or dnf on POSIX; empty on Windows and when none is found
facts.os.initsystemd on POSIX hosts running systemd; empty otherwise
facts.hostnameTop-level hostname
facts.disksList of disk objects
facts.envEnvironment-variable map

Absent signals

Every facts.os.* key is always present, even when its underlying signal is absent. An absent signal renders as an empty string (build as 0), never as a missing key. This lets playbooks branch on {{ facts.os.package_manager }} and similar without distinguishing “not detected” from “empty”:

yaml
# only install the systemd unit on hosts that actually run systemd
when: "{{ facts.os.init }}"
# only render this task on apt hosts via a vars boolean computed elsewhere
when: "{{ vars.is_apt_host }}"

On Windows the POSIX-only facts (package_manager, init) are always empty, and family is windows. build stays Windows-only (it is 0 on POSIX).

Because when: does not support comparison expressions, branch on a host’s package manager or family by rendering the fact and letting the playbook’s variable layer turn it into a boolean, or gate on the truthiness of the value itself:

Each disk entry includes:

FieldMeaning
pathDrive or filesystem path
total_gbTotal capacity in gigabytes
free_gbFree space in gigabytes
used_gbUsed space in gigabytes

Fact Gathering Behavior

Fact gathering is best-effort:

  • OS facts come from Target.Info(), which on POSIX hosts is backed by a single lazily-run, per-target detection probe (cached for the run). The probe roster is exactly: os-release ID/VERSION_ID, command -v apt-get / command -v dnf, test -d /run/systemd/system, plus hostname / uname -s / uname -m. Both Info() and the facts gatherer read the cached probe result, so there is no second detection path.
  • The probe is defensive per-signal: a missing source (e.g. no os-release on macOS) empties that field without failing the probe. Only a transport-level failure surfaces as an error through Info().
  • Windows disk and environment facts are collected through PowerShell.
  • Local non-Windows disk facts are gathered from the local filesystem.
  • Partial fact collection still returns a usable result set; individual failures are logged as warnings.

Safe Target Metadata

target.* intentionally exposes only non-secret transport metadata:

FieldMeaning
target.nameInventory host name
target.hostnameHostname used for safe template metadata
target.addressAddress or fallback host name
target.transportlocal, winrm, or ssh
target.portPort, when defined

Authentication fields such as passwords and private keys are not exposed through target.*.