| title | Templating Reference |
|---|---|
| description | Guide to Task's templating system — native Jinja (the default), the legacy Go text/template dialect, special variables, and available functions. |
| outline | deep |
Task renders the string values in a Taskfile with a templating engine so they
can be computed dynamically. Templates are written between double curly braces
{{ and }} (expressions) and, in Jinja, {% … %}
(statements).
Task supports two template dialects:
- Jinja (the default) — native minijinja, a Jinja2-compatible engine. This is the recommended dialect for new Taskfiles.
- Go
text/template(legacy, deprecated) — the original Task dialect, a limited subset of which is still supported for backwards compatibility.
The dialect is resolved per file:
-
If a file sets the top-level
templater:field, that wins:version: '3' templater: jinja # or: go
-
Otherwise the dialect is auto-detected from the file's syntax. Leading-dot access (
{{.VAR}}), Go control words ({{if}},{{range}}), and Go comments ({{/* … */}}) mark a file as Go; anything else (including a file with no templates) is treated as Jinja.Detection is textual: a dot that starts an identifier and is not preceded by a letter, digit,
_,),"or'marks the file as Go — even inside a string. A native Jinja file containing, say,{{ PATH | replace("/.git", "") }}is therefore misread and fails to translate. Settemplater: jinjaexplicitly on such a file.
A file's dialect governs every template string written in that file — its
vars: and env:, its tasks, the vars: it passes to an includes: entry,
and the caches: models it defines. It does not follow the include: a task in
an included file renders its own strings in its file's dialect, and a caches:
model it inherits in the dialect of the file that defined the model. That lets
a tree be migrated one file at a time.
Files that resolve to the Go dialect emit a one-time deprecation warning. Go template support will be removed in a future release. Convert a Taskfile with:
task --migrate # preview the Jinja conversion on stdout
task --migrate --write # rewrite the file in place and add `templater: jinja`To silence the deprecation warning in the meantime, set
TASK_NO_GO_DEPRECATION=1.
Variables are referenced by name — no leading dot:
version: '3'
templater: jinja
tasks:
hello:
vars:
MESSAGE: 'Hello, World!'
cmds:
- 'echo {{ MESSAGE }}'A variable that is not defined renders as an empty string.
Values are transformed with the pipe (|) filter syntax:
cmds:
- 'echo {{ NAME | upper }}' # JOHN DOE
- 'echo {{ MESSAGE | trim }}' # trims whitespace
- 'echo {{ MISSING | default("fallback") }}' # only when unset
- 'echo {{ EMPTY | default("fallback", true) }}' # also when emptyFilters can be chained: {{ CSV | splitList(",") | join(" ") }}.
Functions use parentheses:
cmds:
- 'echo {{ OS() }}/{{ ARCH() }}'
- 'echo {{ joinPath(ROOT_DIR, "bin", "app") }}'
- 'echo {{ env("HOME") }}'cmds:
- 'echo {% if CI %}github-actions{% else %}local{% endif %}'Comparisons and boolean logic use native operators (==, !=, <, >,
and, or, not, in):
cmds:
- '{% if OS() == "linux" and not DRY_RUN %}./deploy.sh{% endif %}'cmds:
- |
{% for name in ["alice", "bob", "charlie"] %}
echo "Hello {{ name }}"
{% endfor %}Jinja mode is native minijinja, so its full syntax is available — {% set %},
tests (is defined, is none, …), the standard filter set (see
Functions and filters below), and arithmetic
({{ 1 + 2 }}). See the
minijinja documentation
for the complete syntax.
::: warning
The Go text/template dialect is deprecated and only a subset is supported.
Prefer Jinja for new Taskfiles and migrate existing ones with task --migrate.
:::
Go templates reference variables with a leading dot and use Go's pipeline and control-flow syntax:
version: '3'
tasks:
hello:
vars:
MESSAGE: 'Hello, World!'
HAPPY: true
cmds:
- 'echo {{.MESSAGE}}'
- 'echo {{if .HAPPY}}:){{else}}:({{end}}'
- 'echo {{.NAME | trim | upper}}'Supported Go constructs:
- Interpolation and nested field access:
{{.VAR}},{{.MAP.KEY}}. - Conditionals:
{{if …}},{{else if …}},{{else}},{{end}}. - Pipelines and the mapped functions listed under Functions and filters.
- The builtins
and,or,not,eq,ne,lt,le,gt,ge,index,len,printf,print, and Go comments{{/* … */}}. - Parenthesised sub-expressions:
{{ regexReplaceAll "[^a-z]" (trunc 48 .TASK) "-" }}.
Not supported (these raise an error — migrate to Jinja instead): range and
with loops, and the wider slim-sprig function library that upstream Task
offered (list/dict/date/math/encoding helpers, uuid, spew, and so on). Any
{% … %} or {# … #} in a Go-dialect file is treated as literal text, exactly
as Go text/template would.
An action also ends at the first }}, even one inside a string literal, so
{{ .P | replace "}}" "" }} cannot be written directly — build the braces
from a variable, or use the Jinja dialect.
Task provides these variables in every template. They are the same in both
dialects — only the access syntax differs ({{ TASK }} in Jinja, {{.TASK}} in
Go). Examples below use Jinja.
| Variable | Type | Description |
|---|---|---|
CLI_ARGS |
string |
Extra arguments after --, as a single string |
CLI_ARGS_LIST |
list |
Extra arguments after --, shell-split into a list |
CLI_FORCE |
bool |
Whether --force or --force-all was set |
CLI_SILENT |
bool |
Whether --silent was set |
CLI_VERBOSE |
bool |
Whether --verbose was set |
CLI_ASSUME_YES |
bool |
Whether --yes was set |
tasks:
test:
cmds:
- cargo test {{ CLI_ARGS }} # task test -- --nocapture| Variable | Type | Description |
|---|---|---|
TASK |
string |
Name of the current task |
ALIAS |
string |
Alias used to call the task, otherwise the task name |
TASK_EXE |
string |
The task executable name or path |
| Variable | Type | Description |
|---|---|---|
ROOT_TASKFILE |
string |
Absolute path of the root Taskfile |
ROOT_DIR |
string |
Absolute path of the root Taskfile's directory |
TASKFILE |
string |
Absolute path of the current (included) Taskfile |
TASKFILE_DIR |
string |
Absolute path of the current Taskfile's directory |
TASK_DIR |
string |
Absolute path the task runs in |
USER_WORKING_DIR |
string |
Absolute path task was invoked from |
| Variable | Type | Description |
|---|---|---|
CHECKSUM |
string |
Checksum of the task's sources (available in status, and in the cache url/lock templates) |
tasks:
build:
sources: ['**/*.rs']
cache:
url: 'oci://registry.example.com/cache:{{ urlsafe(TASK) }}-{{ CHECKSUM }}'
cmds:
- cargo build --release| Variable | Type | Description |
|---|---|---|
ITEM |
any |
The current value when iterating with a command's for property (rename with as) |
tasks:
greet:
cmds:
- for: [alice, bob]
cmd: echo "Hello {{ ITEM }}"| Variable | Type | Description |
|---|---|---|
EXIT_CODE |
int |
The failed command's exit code — only in a defer, and only when non-zero |
| Variable | Type | Description |
|---|---|---|
TASK_VERSION |
string |
The running version of Task |
The functions below are provided by Task in both dialects. In Jinja they are
called as functions (joinPath(a, b)) or filters (value | trimPrefix("x")); in
Go they are called in pipeline/space-separated form (joinPath a b,
.VALUE | trimPrefix "x").
title, join, first and last are the exception: in Jinja the filter of
each of those names is Jinja's own, not Task's. default goes further — Task
registers no function of that name in Jinja at all, only minijinja's builtin
filter. See
sprig semantics after a pipe.
Most helpers that take a subject accept it in either position, but at opposite
ends: the function form takes it last (trimSuffix ".po" .ITEM), which is
what lets the pipeline form take it first (.ITEM | trimSuffix ".po"). printf
and print are function-only. regexReplaceAll is the one helper whose subject
moves rather than flips ends: in the middle as a function (sprig's order),
first as a Jinja filter — a Go pipe becomes the sprig-ordered call instead, see
sprig semantics after a pipe.
| Function | Description |
|---|---|
OS() |
The operating system (linux, darwin, windows, …) |
ARCH() |
The CPU architecture (amd64, arm64, …) |
numCPU() |
The number of CPUs available |
exeExt() |
The executable extension for the OS (.exe on Windows, else empty) |
env(name) |
The value of an environment variable, or empty if unset |
| Function | Description |
|---|---|
joinPath(a, b, …) |
Join and clean path segments |
base(path) |
The final path element |
dir(path) |
The parent directory |
ext(path) |
The file extension (including the dot) |
isAbs(path) |
Whether the path is absolute |
fromSlash(path) |
Convert / to the OS path separator |
toSlash(path) |
Convert the OS path separator to / |
| Function / filter | Description |
|---|---|
trim, trimAll(cutset), trimPrefix(prefix), trimSuffix(suffix) |
Trim whitespace or a given cutset/affix |
lower, upper, title† |
Change case |
contains(substr), hasPrefix(prefix), hasSuffix(suffix) |
Substring tests |
replace(old, new) |
Replace all occurrences |
trunc(n, s), s | trunc(n) |
First n characters (or last -n if negative) |
regexReplaceAll(pattern, s, repl), s | regexReplaceAll(pattern, repl) |
Replace all regex matches |
printf(format, …) |
Format a string: %s, %v, %q, %d, %%, with the - / 0 flags and a width (%-10s, %03d) |
print(…) |
Concatenate the operands, with a space between two neighbours when neither is a string |
quote(s), squote(s) |
Wrap in double / single quotes |
urlsafe(s) |
Percent-encode for use in URLs and cache keys |
catLines(s) |
Replace newlines with spaces |
splitLines(s) |
Split into a list of lines |
printf covers the verbs a Taskfile composes strings with. Any other verb
(%f, %x), a precision (%.2s), an argument its verb cannot render (a list
for %s, a string for %d), or an argument count that does not match the
format is an error rather than the %!d(string=x) marker Go writes into its
output. %s and %v are more permissive than Go the other way: they render a
number or a boolean, where Go writes a %!s(int=3) marker. print likewise
takes scalars only.
| Function / filter | Description |
|---|---|
splitList(sep, s), s | splitList(sep) |
Split a string into a list on sep |
join(sep, list)†, list | join(sep) |
Join a list into a string with sep |
first(list)†, last(list)†, list | first, list | last |
The first / last element |
len(x) |
Length of a list, map, or string |
splitArgs(s) |
Shell-split a string into an argument list |
index(coll, k…) |
Successive index/key lookups (index(MATCH, 0)) |
† In the Jinja dialect the filter spelling is minijinja's own; only the call form carries sprig's meaning.
| Filter | Description |
|---|---|
x | default(fb) |
fb when x is unset |
x | default(fb, true) |
fb when x is unset or empty ("", 0, false, an empty list) — sprig's rule |
Both Go spellings of sprig's default migrate to the second form; there is no
default function. See
sprig semantics after a pipe.
and, or, not, eq, ne, lt, le, gt, ge are available for the Go
dialect. In Jinja, use the native operators (==, !=, <, and, or,
not, in) instead. In Jinja default is a filter, not a function; the Go
dialect takes either spelling and both migrate to the filter — see below.
default, title, join, first and last mean something different in
slim-sprig than the minijinja filters of the same name. In a Go-syntax
Taskfile the sprig meaning wins, in both call and pipe position.
Jinja Taskfiles are not affected — {{ COUNT | default(10) }} there is
Jinja's own filter and still yields 0 for a COUNT of 0. For title,
join, first and last the Go dialect gets sprig's meaning by translating
the pipe into a call, {{ .P | join "," }} becoming join(",", P), so
task --migrate writes that call into the converted file and the migrated
Taskfile keeps rendering what it rendered as Go. Keep the call form when editing
one of those in a migrated file: rewriting it to P | join(",") switches to
Jinja's meaning.
default needs no call, because Jinja's own filter takes a second argument that
widens it to every empty value — exactly sprig's rule. Both Go spellings,
{{ .X | default "y" }} and {{ default "y" .X }}, translate to
{{ X | default("y", true) }}.
Write that form in a Jinja Taskfile whenever an empty value should take the
fallback, and the plain {{ X | default("y") }} when only an unset one should.
There is no default function: a sprig-ordered default("y", X) — carried by a
Taskfile written or migrated against task 4.1.0 / 4.1.1, which is also the form
this page used to recommend for hand-written Jinja — fails with
unknown function.
Pass that second argument positionally. Filter arguments are positional
throughout, and most filters reject a keyword outright —
{{ P | trimPrefix(prefix="dir/") }} is an error — but default reads its
second argument loosely enough that a keyword lands there as a value that is
always on, so {{ X | default("y", boolean=false) }} substitutes for an empty
X instead of leaving it: the opposite of what it says.
Where the two differ:
defaultsubstitutes its fallback for any empty value ("",0,false, an empty list), not only an undefined one. This is what the second argument to the filter selects.titleuppercases the first letter of every word and leaves the rest of the word alone, soHELLO worldbecomesHELLO Worldwhere Jinja'stitlegivesHello World.jointreats a non-list as a one-element list, so a string joins to itself instead of to its characters.firstandlastrender empty for a value they cannot iterate — a number, a boolean, an undefined variable — where Jinja's raise an error. A string still yields its first or last character.
The Go builtins printf and print translate to a call after a pipe for the
same reason — minijinja has no filter of either name, so a Jinja Taskfile calls
both in function position — and {{ .P | printf "%s.mo" }} migrates to
{{ printf("%s.mo", P) }}.
regexReplaceAll stays a call after a pipe for a different reason: sprig takes
its subject in the middle, so a Go pipe hands it the replacement slot and
{{ .P | regexReplaceAll "[.]po$" ".mo" }} substitutes inside .mo — odd, but
what a Go Taskfile renders, and what the migrated call
regexReplaceAll("[.]po$", ".mo", P) keeps. Task's own regexReplaceAll
filter takes the subject first ({{ P | regexReplaceAll("[.]po$", ".mo") }}),
which is what to write in a Jinja Taskfile.
Every other helper keeps one meaning in both dialects, and migrates to the
idiomatic filter form ({{ .X | trimSuffix ".po" }} becomes
{{ X | trimSuffix(".po") }}).
In the Jinja dialect, minijinja's built-in filters and functions are also
available — for example default, title, join, first, last, length,
reverse, sort, unique, map, select, int, float, tojson, and
urlencode, all with their standard Jinja meaning. See the
minijinja filter reference
for the full list.