Skip to content

Templates

Go templates are evaluated in info dashboard items, declarative commands, pipeline when: conditions, the message builtin, and the IDE / AI / git / workspace / config render packs. This page describes their syntax, context, and available functions. Info, commands, conditions, and message use the go-sprout function library. The ide / ai / git / workspace packs use strict Go text/template with built-in functions only; Sprout helpers and appURL are unavailable there. Workspace packs evaluate only .tmpl sources and copy other files byte-for-byte. Config packs use the ${...} shorthand substrate (lenient — absent → ""); see render config.

SiteSyntaxContextNotes
info.yml — text, 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.yml — when: type: template, expr:{{ ... }}Resolved project configEvaluated at plan time. See deploy
deploy.yml / lifecycle.yml / reset.yml — cmd, 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.yml — project_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, .Commands, .CommandGroups)Strict mode. See render/git.md
workspace/templates/ide/<pack>/**/*.tmpl{{ ... }}Render-pack context (.Project, .Service, .Resolved, .ServiceCfg, .Runtime, .Services, .Cfg, .Commands, .CommandGroups)Strict mode. See render/ide.md
workspace/templates/ai/<pack>/**/*.tmpl{{ ... }}Render-pack context (.Project, .Service, .Resolved, .ServiceCfg, .Runtime, .Services, .Cfg, .Commands, .CommandGroups)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
workspace/templates/workspace/<pack>/**/*.tmpl{{ ... }}Render-pack context (.Project, .Runtime, .Services, .Cfg, .Commands, .CommandGroups); .Service / .Resolved empty, .ServiceCfg zero-valuedStrict mode; other sources copied byte-for-byte. See render/workspace.md
params.*.default_from, context.*.from——Plain dot-paths only (no template expressions).

Two syntaxes: shorthand and full templates

Section titled “Two syntaxes: shorthand and full templates”

Two interpolation layers exist; their availability depends on the site in the table above.

${...} — 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. Additional function availability depends on the site; config packs document the ${...} substrate.

# 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, 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 / workspace, 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.
.Commands / .CommandGroupsthe project’s declared commands and authored command groups, plus the .ServiceCommands / .ServiceCommandGroups methods — see Declared command index

Workspace packs share this context without service selection: .Service and .Resolved are empty, and .ServiceCfg is zero-valued. .ServiceCommands and .ServiceCommandGroups return empty lists; .Commands and .CommandGroups contain the project-wide index.

IDE, AI, and workspace 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.

ide / ai / git / workspace packs never see a decrypted secret

Section titled “ide / ai / git / workspace packs never see a decrypted secret”

Because their outputs are usually tracked by git, those four renderers load a sanitized config: the same three-layer assembly, but with no decrypt pass at all. Every field a template can reach — .Cfg.Raw, .Cfg, .Project, .Runtime, .Services, .ServiceCfg — carries the committed ENC[age:…] marker where the real runtime config carries plaintext.

So a template that reads an encrypted value emits the ciphertext (already committed, harmless), never the plaintext — no path bookkeeping, no ambiguity for sequences or dotted keys. It is a structural guarantee rather than an allowlist, which is why the “avoid secret keys here” advice above is about diff noise, not about leaking.

Config packs (render config) are the opposite case: they render into the gitignored service hub dir with the real config, and additionally support whole-file .age sources — see render config.

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 -}}"

Available in info, commands, pipeline conditions, and message, but not ide / ai / git / workspace packs. 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 in info, commands, pipeline conditions, and message. The ide / ai / git / workspace packs use only built-in text/template functions; these registries are unavailable there.

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, uniq, hasList/array operations
mapskeys, values, hasKey, pick, omitMap/object operations
regexregexMatch, 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

The value goes last. Nearly every sprout function takes the map, list or string it operates on as the last argument (merge, mergeOverwrite and concat take the destination first), so they read naturally in a pipe: {{ $m | pick "a" }}, {{ $list | append "x" }}, {{ $name | regexReplaceAll "-" "_" }}. Templates written for Sprig’s argument order need rewriting — see Upgrading DWE.

Hermetic by construction. The helper set is built without any function that touches the environment, filesystem, network, or random/crypto sources. Sprout’s shuffle (random output) 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, render git, and .tmpl sources in render workspace parse templates with the missingkey=error option: 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}}

For render workspace, every selected pack is validated and rendered in memory before any writes. Other sources are copied byte-for-byte, so placeholders for another tool such as {{PLAN_FILE}} survive unchanged.

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

Patterns using Sprout helpers or appURL below apply to info / commands / pipeline conditions / message; ide / ai / git / workspace packs support only Go template built-ins.

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 {{ ... }} in commands is fine. They share the same context and render in one pass — ${...} is rewritten to template calls before parsing.