Skip to content

Templates

Go templates (with the go-sprout function library) are evaluated across multiple DWE surfaces: info dashboard items, declarative commands, pipeline when: conditions, the message builtin, and the IDE / AI / git / config render packs. This page is the single reference for the template engine, the available helpers, and the conventions shared by every site. Note that the config render pack diverges from the other render kinds: it uses the ${...} shorthand substrate (lenient — absent → ""), not the strict {{ ... }} syntax of the ide/ai/git packs.

SiteSyntaxContextNotes
info.ymltext, value, when{{ ... }}Resolved project configSee info.md
workspace/commands/cmd, argv, workdir, compose_args, env, messages.*, confirmation_text, files.*.path/candidates, workflow steps[].with[<key>] / steps[].when${...} and {{ ... }}Command context (.Raw + .Params + .Context + .Files + .Host)See commands/
deploy.yml / lifecycle.yml / reset.ymlwhen: type: template, expr:{{ ... }}Resolved project configEvaluated at plan time. See deploy
deploy.yml / lifecycle.yml / reset.ymlcmd, the string leaves of with, check, timeout, and shell when: cmd:${...} only (known heads)Merged project config (.Raw)Rendered once at plan-resolution time, not at execution time — before the step is displayed, hashed, or run. See Templates in step fields
message builtin — text:{{ ... }}Resolved project configSee message builtin
docker.ymlproject_name${...} onlyResolved project config (.Raw lookups)Dot-path lookups (no {{ }} logic). See docker.md
workspace/templates/git/<pack>/**/*.tmpl{{ ... }}Render-pack context (.Project, .Service, .Resolved, .ServiceCfg, .Runtime, .Services, .Cfg)Strict mode. See render/git.md
workspace/templates/ide/<pack>/**/*.tmpl{{ ... }}Render-pack context (.Project, .Service, .Resolved, .ServiceCfg, .Runtime, .Services, .Cfg)Strict mode. See render/ide.md
workspace/templates/ai/<pack>/**/*.tmpl{{ ... }}Render-pack context (.Project, .Service, .Resolved, .ServiceCfg, .Runtime, .Services, .Cfg)Strict mode. See render/ai.md
workspace/templates/config/<pack>/**${...}Resolved project config (.Raw) + curated ${services.<name>...} subset + ${generated.<name>}Lenient (absent → ""). See render/config.md
params.*.default_from, context.*.fromPlain dot-paths only (no template expressions).

Two syntaxes: shorthand and full templates

Section titled “Two syntaxes: shorthand and full templates”

Two interpolation layers exist; both are evaluated by the same engine.

${...} — shorthand lookups. Compact, no logic. Used in command definitions and docker.yml’s project_name. The compiler rewrites each ${...} into an equivalent {{ ... }} expression at parse time.

{{ ... }} — full Go text/template. Conditionals, loops, pipelines, helper functions. Available everywhere templates are evaluated.

# Mixed in a single string (command site)
path: "${param.dump_dir}/${param.database}{{ if .Params.dump_date }}_{{ now | date \"2006-01-02\" }}{{ end }}.sql.gz"

Rule of thumb: use ${...} for plain lookups; reach for {{ ... }} whenever you need a condition, a comparison, a default, a string transform, or a pipeline.

${...} resolves through namespaces; the first segment routes to a specific data source:

ExpressionResolved as
${vars.db.user}Dot-path into the merged DWE config (Raw)
${param.<name>}Resolved param value
${context.<name>}Resolved context value
${files.<id>.path}Absolute path of a resolved file artefact
${host.uid} / ${host.gid}Effective UID/GID (1000:1000 on macOS, real values on Linux)
${generated.<name>}Per-service value harvested into .dwe/generated.yml (config render packs only; absent → ""). See render/config.md

Anything whose head is a merged-config root key (project, services, vars, exports, compose, update, bridge, state, schema_version, …) is treated as a dot-path lookup against Raw. This is a whitelist, not “anything not otherwise matched”: an unrecognized head — a shell-style ${HOME}/${PATH}, a stray dollar sign, a typo, or a stale pre-strict-root bare dot-path like ${databases.main} — is left as a literal ${...} instead of silently collapsing to "". This matters most in pipeline cmd:, which now renders (see the table above): a shell variable such as ${CONTAINER} in a docker inspect command reaches sh unchanged rather than being swallowed. Prefer ${vars.*} for user-defined config values stored under the vars: block in YAML — the strict root rejects free-form top-level keys, so vars: is their single home. A known head whose remaining path does not resolve (e.g. a typo under vars:) renders to ""dwe validate catches this case for pipeline steps (see config.template_refs). A literal $$ passes through unchanged.

A reference must carry a dot-path to count as one. Every namespace form above is dotted (${vars.db.host}, ${project.name}, ${host.uid}, ${files.<id>.path}), so a head-only ${host} / ${files} / ${services} is treated as a shell variable that happens to collide with a namespace name and is left literal, exactly like ${HOME}. ${args} is the single bare form the syntax defines and keeps its meaning (see commands/directives.md).

One documented exception: docker.yml’s project_name field uses a separate, stricter resolver (resolveVarTemplate) that predates this whitelist — it has no namespace restriction (any dot-path into Raw resolves) but errors on an unresolved path instead of leaving it literal, since a broken compose project name must fail loudly rather than silently pass an unresolved ${...} string to docker compose -p.

The string between {{ }} is the same in every YAML form — only the wrapping changes:

# double-quoted scalar: inner " must be escaped as \"
path: ".dwe/logs/{{ now | date \"2006-01-02\" }}.log"
# single-quoted scalar: no escaping needed (recommended for templates)
path: '.dwe/logs/{{ now | date "2006-01-02" }}.log'
# literal block scalar: no escaping needed
cmd: |
echo "{{ now | date "2006-01-02" }}"

Prefer single-quoted ('...') scalars for one-line templates whose body contains ". Reserve double-quoted ("...") for strings that need YAML’s \n/\t escape sequences. The \| you may see inside this page’s tables is markdown-cell escaping for the rendered docs — your YAML always uses a plain | inside {{ }}.

The data exposed to a template depends on the site. Field access uses dot syntax (.Project.Name).

Commands:

PathContents
.RawMerged workspace.yml + defaults.yml + local.yml as a nested map
.ParamsResolved param values (map keyed by param name)
.ContextResolved context values (map keyed by context name)
.FilesResolved file artefacts (map keyed by file id; each has a .Path field)
.Host.UID / .Host.GIDHost UID/GID strings

Info, pipelines, message builtin: the resolved project config — addressed via the same dot syntax as the render-pack .Cfg below (e.g. .Project.Name, ((index .Services "main").Port "http"), (index .Services "catalog").Enabled).

Render packs (git / ide / ai, strict):

VariableSource
.Projectproject: block from workspace.yml
.Servicecanonical config identity — the root of the rendering service’s extends: chain (equals .Resolved when there is no extends chain)
.Resolvedrendering identity — the map key of the service actually being rendered (the collision-policy winner)
.ServiceCfgeffective service config after extends resolution
.Runtimemerged runtime block (.Runtime.UseHTTPS, .Runtime.SPX.Path). Per-service ports / hosts live on each service entry (see .Services below).
.Servicesservices keyed by name. Use (index .Services "<name>") to fetch; per-entry helpers .Port "<port-name>" / .Host "<host-name>" / .PortScheme "<port-name>" (returns "" if no override) / .EffectiveScheme "<port-name>" .Runtime.UseHTTPS (returns "http" / "https" resolved through the per-port → service → runtime precedence chain). Type-filtered subsets via .AppServices / .ToolServices / .InfraServices.
.Cfgthe merged project config (advanced). .Cfg.Raw is the post-merge config tree (services.* is injected from per-service service.yml files). Dot syntax (.Cfg.Raw.git.project_prefix) works only for identifier-safe keys; use {{ index .Cfg.Raw "my-key" }} for keys with hyphens, dots, leading digits, etc. Prefer the dedicated fields above for common cases.

IDE and AI packs render into tracked project files. Avoid consuming developer-local or secret keys via .Cfg.Raw in those templates — values from local.yml will produce per-developer diffs. Git hooks render under .git/hooks/ (gitignored) and are not subject to this constraint.

The standard library exposes these out of the box. Full reference: pkg.go.dev/text/template#hdr-Functions.

FunctionUse
eq, ne, lt, le, gt, geComparison
and, or, notBoolean logic
lenLength of string / slice / map
indexMap / slice indexing
printfFormatted strings (Go format verbs)
print, printlnConcatenation
html, js, urlqueryEscaping

Control structures: {{ if }}, {{ range }}, {{ with }}, {{ define }} / {{ template }}.

# emit a flag only when a bool param is true
argv:
- "{{ if .Params.fresh }}--fresh{{ end }}"
# nested if / else if / else
env:
LOG_LEVEL: |-
{{ if eq .Params.profile "prod" }}error
{{ else if eq .Params.profile "stage" }}warn
{{ else }}debug{{ end }}
# range with index
env:
TAGS: "{{ range $i, $t := .Params.tags }}{{ if $i }},{{ end }}{{ $t }}{{ end }}"
# with / default
cmd: "mariadb -u${vars.db.user}{{ with .Params.database }} -D{{ . }}{{ end }}"
env:
REGION: '{{ or .Params.region "us-east-1" }}'

{{- ... -}} strips surrounding whitespace. Useful when a multi-line {{ if }} block is rendered into a single shell argument:

cmd: |-
echo "{{- if .Params.verbose -}}verbose{{- else -}}quiet{{- end -}}"

The only project-specific helper. Builds a URL from host, port, HTTPS flag, and optional path. The port is omitted when it matches the scheme default (80 for http, 443 for https).

Signature: appURL host port useHTTPS [path]

# App hostname + app port (proxied via the main service)
value: '{{ appURL ((index .Services "main").Host "web") ((index .Services "main").Port "http") .Runtime.UseHTTPS }}'
# → "http://laravel.localhost" or "https://laravel.localhost"
# Tool hostname + main reverse-proxy port (tool routed via the main app, not the tool's direct port)
value: '{{ appURL ((index .Services "adminer").Host "web") ((index .Services "main").Port "http") .Runtime.UseHTTPS "/login" }}'
# → "http://adminer.localhost/login"

The following registries from go-sprout are available everywhere templates are evaluated.

RegistryExamplesDescription
stddefault, ternary, empty, coalesceDefaults, conditionals, emptiness checks
stringshasSuffix, hasPrefix, toLower, toUpper, trim, replace, splitString manipulation
numericadd, sub, mul, div, max, minNumeric operations
slicesfirst, last, slice, join, reverse, uniqList/array operations
mapskeys, values, has, pick, omitMap/object operations
regexpregexMatch, regexReplaceAll, regexSplitRegular expression matching
conversiontoInt, toFloat64, toString, toBoolType conversion
timenow, date, dateInZone, durationDate/time operations
filesystempathBase, pathDir, pathExt, pathClean, osBase, osDirPath manipulation
semversemver, semverCompareSemantic version operations

Hermetic by construction. The helper set is built without any function that touches the environment, filesystem, network, or random/crypto sources. Sprout’s shuffle (math/rand seeded from crypto) and hello (debug stub) are deliberately removed.

For full per-function documentation see the sprout registries reference.

Four additional helpers are available only inside workspace/commands/ templates. They accept raw maps and walk dot-paths, returning "" for any missing key (no template error).

HelperSignatureUse
resolveresolve .Raw "vars.db.host"Dot-path lookup in merged config. Equivalent to ${vars.db.host}.
resolveMapresolveMap .Params "name"Key lookup in a flat map[string]any. Equivalent to ${param.name} / ${context.name}.
resolveFileresolveFile .Files "id" "path"Subkey lookup in a resolved file artefact. Equivalent to ${files.id.path}.
resolveGeneratedresolveGenerated .Generated "app_key"Per-service harvested value (config render pass). Equivalent to ${generated.app_key}.

These exist so the ${...} shorthand can be expanded to portable Go-template form, and so authors can reach raw config when the dotted .Raw.<x>.<y> style is awkward (keys with dots, numeric keys, etc.).

render ide, render ai, and render git parse templates with {{.Option "missingkey=error"}} semantics: a typo like {{.Servic.Name}} aborts the entire pack render rather than writing <no value> to disk. Guard genuinely optional fields with {{if ...}}:

{{if .ServiceCfg.CLI.Workdir}}WORKDIR={{.ServiceCfg.CLI.Workdir}}{{end}}

Other sites (info, commands, pipeline conditions, message) use lenient rendering — a missing key resolves to <no value> or empty string, never an error.

TaskSnippet
Current date{{ now | date "2006-01-02" }}
Current datetime{{ now | date "2006-01-02_15-04-05" }}
Path basename{{ .Params.script_path | pathBase }}
Path directory{{ .Params.script_path | pathDir }}
Default / fallback{{ .Value | default "N/A" }} or {{ or .Params.region "us-east-1" }}
Conditional value{{ if eq .State "ready" }}Ready{{ else }}Not ready{{ end }}
Empty-guarded block{{ with .Params.database }} -D{{ . }}{{ end }}
Join list{{ join "," .Params.tags }}
Raw config lookup{{ resolve .Raw "vars.db.host" }} (commands only)
Build URL{{ appURL ((index .Services "main").Host "web") ((index .Services "main").Port "http") .Runtime.UseHTTPS }}
  • Prefer path* over os* for container paths. pathBase / pathDir use forward-slash semantics; osBase / osDir follow the host OS separator. Container paths should be predictable when rendered on macOS hosts — stick with the path* variants unless you genuinely need OS-specific behaviour.

  • date is a filter, not a constructor. It takes a format string and a time.Time, not the other way around:

    • {{ now | date "2006-01-02" }}
    • {{ date "2006-01-02" }} ✗ (no time value)

    The format string uses Go’s reference time Mon Jan 2 15:04:05 MST 2006 — see Go date/time formatting cheat sheet.

  • when: truthiness. A rendered when: value is truthy unless it equals "", "false", or "0" (after trimming). Comparisons that return a Go bool render as "true"/"false"; comparisons that return an integer-like value (e.g. lengths) render as decimal strings.

  • No env, FS, network, or randomness. Templates are evaluated in a hermetic FuncMap by design. If a template needs project state, surface it through the resolved project config (info / pipelines) or through a context.<name>: from: <dot.path> declaration (commands).

  • Mixing ${...} and {{ ... }} is fine. They share the same context and render in one pass — ${...} is rewritten to template calls before parsing.