Skip to content

Template

Render Go templates with structured data. action: template.render processes literal text from with.template or dynamically sourced text from with.template_ref as a Go text/template, then writes the result to stdout or a file. It is useful for generating configuration files, reports, or any text output from structured data without spawning a shell.

How It Works

  1. Template text comes from with.template or the scoped Dagu reference in with.template_ref.
  2. Data from with.data is passed to the template as the root context (.).
  3. The rendered output is written to stdout (capturable with output:), or to a file if with.output is set.

Dagu skips all value resolution (${env.NAME}, ${params.name}, command substitution, etc.) on a literal with.template body. The template engine is the sole evaluator, so value references in the template are preserved as-is. with.template_ref resolves one complete scoped reference once and uses its value as the template body; references inside that value are also preserved. Data values in with.data are resolved by Dagu before being passed to the template.

with Fields

FieldTypeRequiredDescription
dataobjectNoKey-value pairs accessible as {{ .key }} in the template. Values can be strings, numbers, lists, or nested objects.
outputstringNoFile path to write the rendered output to. If empty, output goes to stdout. Relative paths resolve against the step's working_dir.
templatestringOne of template or template_refLiteral template text rendered by the template executor.
template_refstringOne of template or template_refOne complete scoped Dagu reference that resolves to template text at runtime.

Basic Example

yaml
steps:
  - name: render
    action: template.render
    with:
      template: |
        {{ .greeting }}, world!
      data:
        greeting: hello
    output: RESULT

RESULT captures hello, world!.

Using Environment Values

Pass environment values through with.data using the scoped ${env.NAME} syntax, then reference the corresponding data key in the template:

yaml
env:
  - FOO: world

steps:
  - id: render
    action: template.render
    with:
      template: "Hello, {{ .foo }}!"
      data:
        foo: ${env.FOO}
    output: RESULT

RESULT captures Hello, world!.

Putting ${env.FOO} directly in with.template preserves it as literal text. Environment access is not exposed as a template function, so {{ env "FOO" }} is unavailable. See Variables Reference for other scoped value sources.

Using a Dynamic Template

Use with.template_ref when the template text itself comes from an environment value, parameter, constant, prior-step output, foreach value, or runtime context:

yaml
env:
  - MESSAGE_TEMPLATE: "Hello, {{ .name }}!"

steps:
  - id: render
    action: template.render
    with:
      template_ref: ${env.MESSAGE_TEMPLATE}
      data:
        name: Alice
    output: RESULT

RESULT captures Hello, Alice!.

template_ref must be exactly one canonical scoped reference, such as ${env.MESSAGE_TEMPLATE}, ${params.template}, or ${steps.fetch.outputs.template}. Bare names, mixed text, and setting both template and template_ref are rejected. The reference must resolve to a non-empty string when the step starts.

Resolution is single-pass. If the referenced value contains ${env.OTHER}, that text remains literal in the Go template:

yaml
params:
  - name: template
    default: 'Hello, {{ .name }}! ${env.OTHER}'

steps:
  - id: render
    action: template.render
    with:
      template_ref: ${params.template}
      data:
        name: Alice

This renders Hello, Alice! ${env.OTHER}.

Writing to a File

yaml
steps:
  - name: render
    action: template.render
    with:
      template: |
        # {{ .title }}
        Generated by Dagu.
      output: /tmp/report.md
      data:
        title: Monthly Report

When with.output is set, the rendered content is written atomically to that path. Parent directories are created automatically. Stdout remains empty.

Relative paths resolve against working_dir:

yaml
steps:
  - name: render
    action: template.render
    working_dir: /opt/reports
    with:
      template: "{{ .msg }}"
      output: subdir/output.txt
      data:
        msg: hello

This writes to /opt/reports/subdir/output.txt.

Using Data from Prior Steps

Data values are expanded by Dagu before the template runs, so declared step outputs work:

yaml
steps:
  - id: producer
    run: |
      printf 'name=%s\n' "Alice" >> "$DAGU_OUTPUT_FILE"
    outputs:
      - name: name

  - id: render
    action: template.render
    with:
      template: "Hello, {{ .name }}!"
      data:
        name: ${steps.producer.outputs.name}
    output: RESULT
    depends:
      - producer

RESULT captures Hello, Alice!.

Template Functions

The template executor provides Dagu-specific functions plus functions from slim-sprig's hermetic text-template function map. Dagu removes functions for environment access, network lookup, current time, random generation, and crypto key generation. The Dagu-specific functions use pipeline-compatible argument order, where the pipeline value is the last argument.

Dagu-specific functions

These override or extend slim-sprig with pipeline-friendly argument order:

FunctionSignatureDescription
splitsplit sep sSplit string s by separator sep. Returns []string.
joinjoin sep listJoin a list with separator sep. Accepts []string, []any, or any slice.
countcount vLength of a slice, map, array, or string.
addadd b aInteger addition: a + b. Pipeline: {{ 5 | add 3 }}8.
emptyempty vReturns true if the value is nil, empty string, or empty collection.
upperupper sUppercase string.
lowerlower sLowercase string.
trimtrim sTrim whitespace from both ends.
defaultdefault def valReturns def if val is empty/nil/zero; otherwise returns val.

Available slim-sprig functions

These non-overridden names come directly from slim-sprig and work as documented in the slim-sprig docs:

  • Misc: hello
  • Strings: adler32sum, cat, contains, hasPrefix, hasSuffix, indent, nindent, plural, quote, repeat, replace, sha1sum, sha256sum, splitList, splitn, squote, substr, title, toString, toStrings, trimAll, trimPrefix, trimSuffix, trimall, and trunc
  • Numeric and conversion: add1, atoi, biggest, ceil, div, float64, floor, int, int64, max, maxf, min, minf, mod, mul, round, seq, sub, toDecimal, until, and untilStep
  • Defaults and JSON: all, any, coalesce, compact, fromJson, mustCompact, mustFromJson, mustToJson, mustToPrettyJson, mustToRawJson, ternary, toJson, toPrettyJson, and toRawJson
  • Reflection: deepEqual, kindIs, kindOf, typeIs, typeIsLike, and typeOf
  • Paths and file paths: base, clean, dir, ext, isAbs, osBase, osClean, osDir, osExt, and osIsAbs
  • Encoding: b32dec, b32enc, b64dec, and b64enc
  • Collections and dictionaries: append, chunk, concat, dict, dig, first, get, has, hasKey, initial, keys, last, list, mustAppend, mustChunk, mustFirst, mustHas, mustInitial, mustLast, mustPrepend, mustPush, mustRest, mustReverse, mustSlice, mustUniq, mustWithout, omit, pick, pluck, prepend, push, rest, reverse, set, slice, sortAlpha, tuple, uniq, unset, values, and without
  • Flow control: fail
  • Regex: mustRegexFind, mustRegexFindAll, mustRegexMatch, mustRegexReplaceAll, mustRegexReplaceAllLiteral, mustRegexSplit, regexFind, regexFindAll, regexMatch, regexQuoteMeta, regexReplaceAll, regexReplaceAllLiteral, and regexSplit
  • URLs: urlJoin and urlParse

The names split, join, add, empty, lower, upper, trim, and default are available, but Dagu overrides the slim-sprig implementation with the behavior documented in the Dagu-specific table above.

Blocked functions

These function names are not available and will cause a template parse error if used:

  • Environment access: env, expandenv
  • Network I/O: getHostByName
  • Current time and date helpers: ago, date, dateInZone, dateModify, date_in_zone, date_modify, duration, durationRound, htmlDate, htmlDateInZone, mustDateModify, mustToDate, must_date_modify, now, toDate, and unixEpoch
  • Crypto key generation: buildCustomCert, derivePassword, genCA, genPrivateKey, genSelfSignedCert, and genSignedCert
  • Random generation: randAlpha, randAlphaNum, randAscii, randBytes, randInt, randNumeric, randString, and uuidv4

Missing Key Behavior

Templates use missingkey=error. Referencing a key not present in data causes the step to fail:

yaml
steps:
  - name: render
    action: template.render
    with:
      template: "{{ .undefined_key }}"  # Fails with execution error
      data:
        name: test

Use default to handle optional keys safely:

yaml
with:
  template: '{{ .name | default "Anonymous" }}'

Or use get for safe map access:

yaml
with:
  template: '{{ get .app "owner" | default "unknown" }}'

Complex Example

yaml
steps:
  - name: render-config
    action: template.render
    with:
      template: |
        app={{ .app.name | lower | replace " " "-" }}
        owner={{ get .app "owner" | default "unknown" }}
        domains={{ get .app "domains" | default (list "localhost") | uniq | sortAlpha | join "," }}
      data:
        app:
          name: My Service
          domains:
            - api.example.com
            - api.example.com
            - app.example.com
    output: RESULT

Output:

app=my-service
owner=unknown
domains=api.example.com,app.example.com

Dollar Sign Preservation

Because Dagu skips expansion on with.template, shell-style variables like ${BAR} and backtick expressions pass through unchanged:

yaml
steps:
  - name: render
    action: template.render
    with:
      template: |
        export FOO=${BAR}
        echo "{{ .name }}"
        value=`command`
      data:
        name: test
    output: RESULT

The output contains literal ${BAR} and `command`.

Pipeline Chaining

Functions compose naturally in pipelines:

yaml
with:
  template: '{{ "a,b,c" | split "," | join ";" }}'
# Result: a;b;c
yaml
with:
  template: '{{ .csv | split "," | count }}'
# With csv: "x,y,z" → 3
yaml
with:
  template: '{{ .domains | uniq | sortAlpha | join "," }}'
# slim-sprig list functions return []any; join accepts both []string and []any

Dagu is open source under the GNU General Public License v3.0.