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.
Contents
Section titled “Contents”- Where templates are evaluated
- Two syntaxes: shorthand and full templates
- Render context per site
- Built-in
text/templatefunctions - Domain helper: appURL
- Sprout registries
- Command-scope resolvers
- Strict rendering (render packs)
- Common patterns
- Conventions and gotchas
- Further reading
Where templates are evaluated
Section titled “Where templates are evaluated”| Site | Syntax | Context | Notes |
|---|---|---|---|
info.yml — text, value, when | {{ ... }} | Resolved project config | See 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 config | Evaluated 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 config | See message builtin |
docker.yml — project_name | ${...} only | Resolved 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-valued | Strict 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.
${...} namespaces
Section titled “${...} namespaces”${...} resolves through namespaces; the first segment routes to a specific data source:
| Expression | Resolved 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.
Quoting templates inside YAML
Section titled “Quoting templates inside YAML”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 neededcmd: | 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 {{ }}.
Render context per site
Section titled “Render context per site”The data exposed to a template depends on the site. Field access uses dot syntax (.Project.Name).
Commands:
| Path | Contents |
|---|---|
.Raw | Merged workspace.yml + defaults.yml + local.yml as a nested map |
.Params | Resolved param values (map keyed by param name) |
.Context | Resolved context values (map keyed by context name) |
.Files | Resolved file artefacts (map keyed by file id; each has a .Path field) |
.Host.UID / .Host.GID | Host 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):
| Variable | Source |
|---|---|
.Project | project: block from workspace.yml |
.Service | canonical config identity — the root of the rendering service’s extends: chain (equals .Resolved when there is no extends chain) |
.Resolved | rendering identity — the map key of the service actually being rendered (the collision-policy winner) |
.ServiceCfg | effective service config after extends resolution |
.Runtime | merged runtime block (.Runtime.UseHTTPS, .Runtime.SPX.Path). Per-service ports / hosts live on each service entry (see .Services below). |
.Services | services 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. |
.Cfg | the 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 / .CommandGroups | the 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.
Built-in text/template functions
Section titled “Built-in text/template functions”The standard library exposes these out of the box. Full reference: pkg.go.dev/text/template#hdr-Functions.
| Function | Use |
|---|---|
eq, ne, lt, le, gt, ge | Comparison |
and, or, not | Boolean logic |
len | Length of string / slice / map |
index | Map / slice indexing |
printf | Formatted strings (Go format verbs) |
print, println | Concatenation |
html, js, urlquery | Escaping |
Control structures: {{ if }}, {{ range }}, {{ with }}, {{ define }} / {{ template }}.
# emit a flag only when a bool param is trueargv: - "{{ if .Params.fresh }}--fresh{{ end }}"
# nested if / else if / elseenv: LOG_LEVEL: |- {{ if eq .Params.profile "prod" }}error {{ else if eq .Params.profile "stage" }}warn {{ else }}debug{{ end }}
# range with indexenv: TAGS: "{{ range $i, $t := .Params.tags }}{{ if $i }},{{ end }}{{ $t }}{{ end }}"
# with / defaultcmd: "mariadb -u${vars.db.user}{{ with .Params.database }} -D{{ . }}{{ end }}"env: REGION: '{{ or .Params.region "us-east-1" }}'Trimming whitespace
Section titled “Trimming whitespace”{{- ... -}} 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 -}}"Domain helper: appURL
Section titled “Domain helper: appURL”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"Sprout registries
Section titled “Sprout registries”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.
| Registry | Examples | Description |
|---|---|---|
std | default, ternary, empty, coalesce | Defaults, conditionals, emptiness checks |
strings | hasSuffix, hasPrefix, toLower, toUpper, trim, replace, split | String manipulation |
numeric | add, sub, mul, div, max, min | Numeric operations |
slices | first, last, slice, join, reverse, uniq, has | List/array operations |
maps | keys, values, hasKey, pick, omit | Map/object operations |
regex | regexMatch, regexReplaceAll, regexSplit | Regular expression matching |
conversion | toInt, toFloat64, toString, toBool | Type conversion |
time | now, date, dateInZone, duration | Date/time operations |
filesystem | pathBase, pathDir, pathExt, pathClean, osBase, osDir | Path manipulation |
semver | semver, semverCompare | Semantic 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.
Command-scope resolvers
Section titled “Command-scope resolvers”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).
| Helper | Signature | Use |
|---|---|---|
resolve | resolve .Raw "vars.db.host" | Dot-path lookup in merged config. Equivalent to ${vars.db.host}. |
resolveMap | resolveMap .Params "name" | Key lookup in a flat map[string]any. Equivalent to ${param.name} / ${context.name}. |
resolveFile | resolveFile .Files "id" "path" | Subkey lookup in a resolved file artefact. Equivalent to ${files.id.path}. |
resolveGenerated | resolveGenerated .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.).
Strict rendering (render packs)
Section titled “Strict rendering (render packs)”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.
Common patterns
Section titled “Common patterns”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.
| Task | Snippet |
|---|---|
| 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 }} |
Conventions and gotchas
Section titled “Conventions and gotchas”-
Prefer
path*overos*for container paths.pathBase/pathDiruse forward-slash semantics;osBase/osDirfollow the host OS separator. Container paths should be predictable when rendered on macOS hosts — stick with thepath*variants unless you genuinely need OS-specific behaviour. -
dateis a filter, not a constructor. It takes a format string and atime.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 renderedwhen:value is truthy unless it equals"","false", or"0"(after trimming). Comparisons that return a Goboolrender 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.
Further reading
Section titled “Further reading”text/templatepackage docs — full Go template language reference- Action syntax —
{{ if }},{{ range }},{{ with }} - Built-in functions —
eq,printf,index, etc. - Pipelines and the
.cursor - Sprout registries — per-function documentation
- Go date/time formatting cheat sheet — reference layout
2006-01-02 15:04:05and common format strings