Skip to content

Tera templates ​

mise renders Tera templates in config values, so a value can come from the project directory, the environment, [vars], or a command's output. These string templates are different from task templates, which share task definitions through extends.

Where templates are rendered ​

File or sectionWhat is renderedExtra context
mise.tomlString values in most sections, such as [env], [tools], [vars], [hooks], and [plugins]. Keys are not rendered, and the file must be valid TOML first.config_source
[bootstrap]Some values only; see bootstrap templatesNone
Task fields, such as runWhen the task runsusage, tools, task_source_files()
Dotfiles with mode = "template"The source file's contentSee the dotfiles page
.tool-versionsThe whole fileconfig_source
.miserc.tomlThe whole file, before it is parsedLimited; see below

Example ​

mise.toml
[env]
PROJECT_NAME = "{{ config_root | basename }}"
TERRAFORM_VERSION = "1.9"

[tools]
# an [env] value defined above
terraform = "{{ env.TERRAFORM_VERSION }}"
# NODE_VERSION from your shell, or 24 when it is unset
node = "{{ env.NODE_VERSION | default(value='24') }}"

In a project directory named myproj, mise env includes export PROJECT_NAME=myproj. Use config_root for paths inside the project: it stays at the project root when you run mise from a subdirectory, while cwd is the directory you ran mise from.

Syntax ​

Templates use Tera v2 syntax with three delimiters:

  • {{ ... }} for expressions
  • {% ... %} for statements such as if and for
  • {# ... #} for comments

Use a raw block to keep delimiters from being rendered:

text
{% raw %}
  Hello {{ name }}
{% endraw %}

This renders as Hello {{ name }}.

KindSyntaxExample
LiteralsStrings in '', "", or backticks; numbers; true, false; [lists]{{ ["a", "b"] }}
Attribute accessa.b, a["b"]{{ env.HOME }}
Math+ - * / %{{ (num_cpus() | int) * 2 }}
Comparison== != < <= > >={{ os() == "linux" }}
Logicand or not{{ a and not b }}
Concatenation~{{ config_root ~ "/bin" }}
Membershipin{{ os() in ["linux", "macos"] }}

Apply a filter with |, as in {{ name | lower }}. Call a function with named arguments, as in {{ exec(command="date") }}. Check a value with is, as in {% if path is dir %}. Filters can be chained. See Tera's expressions and control structures for the full syntax.

Tera v2 also has slices (parts[0:2], parts[-1]), spread ([first, ...rest], {...base, "key": value}), list comprehensions ([t.name for t in tools if t.active]), optional chaining (env?.NODE_ENV or "development"), and conditional expressions ("prod" if release else "dev").

Variables ​

VariableValueAvailable in
envEnvironment variables: the environment mise started with, plus [env] values resolved so farEverywhere
varsConfig variablesmise.toml and tasks
cwdThe directory mise runs inEverywhere
config_rootThe config root of the file: the project directory, even for .config/mise.toml. In [bootstrap.files] content templates, the directory that contains the declaring fileEverywhere
config_sourceAbsolute path of the config file the template is in, as reached; symlinks are not resolvedmise.toml values, .tool-versions, [env] directives, and a task's own env
mise_binPath of the running mise executableEverywhere except .miserc.toml
mise_pidProcess ID of the running miseEverywhere except .miserc.toml
mise_envList of the config environments you selected. Undefined when none is selected, so check it with {% if mise_env is defined %}Everywhere except .miserc.toml
xdg_cache_home, xdg_config_home, xdg_data_home, xdg_state_homeXDG base directoriesEverywhere
system_brew_prefixThe prefix of mise's built-in Homebrew support: /opt/homebrew on macOS, /home/linuxbrew/.linuxbrew elsewhereEverywhere
toolsActive tools that are installed (see below)Tasks, and [env] directives with tools = true
usageParsed task arguments; see Task argumentsTask run scripts

Task fields other than env, such as run and dir, and file task headers have config_root but not config_source.

config_source lets a shared config symlinked into conf.d add its own bin directory to PATH:

[env]
_.path = "{{ config_source | canonicalize | dirname }}/bin"

Leave out canonicalize to get the directory the file was reached through instead of the one it lives in.

tools maps each active tool to its installed active version. It is keyed by the name used in config, such as npm:prettier, and by the tool's name within its backend, such as prettier. Versions that are installed but not selected, and selected versions that are not installed, are left out.

  • When one version is active: tools.node.version is the resolved version, such as "24.1.0", and tools.node.path is the install path.
  • When several versions are active, such as python = ["3.14", "3.13"], the value is a list: tools.python[0].version, tools.python[1].path, and so on.

Functions ​

[] marks optional arguments.

FunctionReturnsDescription
exec(command, [cache_key], [cache_duration])stringRuns command with the default inline shell and returns its output without the trailing newline. See exec().
read_file(path)stringReturns a file's contents. Relative paths resolve from the template's directory, not from cwd; see exec().
read_json(path)anyParses a JSON file. See read_json().
read_yaml(path)anyParses a YAML file. See read_json().
read_toml(path)anyParses a TOML file. See read_json().
get_env(name, [default])stringReturns a variable from the environment mise started with, or default when it is unset. An empty value is returned as is. It does not see [env] values; prefer {{ env.NAME | default(value="...") }}.
arch([x64=...], [arm64=...])stringThe CPU architecture: x64, arm64, or another Rust architecture name. Pass a keyword named after an architecture to rename it, as in {{ arch(x64="amd64", arm64="aarch64") }}.
os([linux=...], [macos=...], [windows=...])stringThe operating system: linux, macos, or windows. Keywords rename it the same way, as in {{ os(macos="darwin") }}.
os_family()stringunix or windows
libc([gnu=...], [musl=...])stringThe C library of the host: musl or gnu. Linux is the only OS with a musl build, and the libc setting overrides detection. Keywords rename it, as in {{ libc(gnu="", musl="-musl") }}.
avx2([yes=...], [no=...])stringWhether the host CPU supports AVX2: yes or no. Every CPU other than x86-64 reports yes. Keywords rename it, as in {{ avx2(yes="", no="-baseline") }}.
num_cpus()stringThe number of CPUs. Use {{ (num_cpus() | int) * 2 }} for arithmetic.
choice(n, alphabet)stringn characters sampled from alphabet. choice(n=64, alphabet="0123456789abcdef") is a random 64-character hex string.
haiku([words], [separator], [digits])stringA random readable name such as blazing-shadow-46. Defaults: 2 words, -, 2 digits.
now([timezone])stringThe current date and time. The time zone defaults to UTC and accepts IANA names such as America/New_York. Format it with date: {{ now() | date(format="%Y") }}.
get_random(start, end, [seed])numberA random integer in a range. A seed makes the result repeatable.
range(end, [start], [step_by])listIntegers from start (default 0) up to, but not including, end.
throw(message)noneStops rendering with an error.
task_source_files([only_changed])listA task's source files. Tasks only.

exec() and read_file() ​

exec() runs, and relative read_file() paths resolve, in the directory of the config file that holds the template. For .config/mise.toml that is .config/, not the project root. In task run scripts it is the task's working directory (its dir, which defaults to the config root). It is never the directory you ran mise from. In [env], exec() also sees variables set by earlier [env] entries; elsewhere it sees the environment mise started with.

[env]
GIT_SHA = "{{ exec(command='git rev-parse --short HEAD') }}"
VERSION = "{{ read_file(path='VERSION') | trim }}"

Pass cache_key or cache_duration to cache the output under MISE_CACHE_DIR. The cache is keyed by the command, its directory, and cache_key, so changing cache_key runs the command again. cache_duration, such as "1h" or "1d", makes the cached output expire; without it, mise reuses the output until the cache is cleared:

[env]
AWS_ACCOUNT_ID = "{{ exec(command='aws sts get-caller-identity --query Account --output text', cache_key='aws-account', cache_duration='1d') }}"

Both functions need a trusted config. In safe mode (MISE_SAFE=1), they fail with an error instead of running, and neither is available in .miserc.toml.

WARNING

exec() runs every time its template renders, including under --dry-run. Use it only for commands without side effects. mise bootstrap --dry-run renders [bootstrap.hooks] commands with exec() disabled, so a hook command that calls exec() stops the preview with exec() is disabled during dry run. [vars] and [env] are still rendered when the config loads, so their exec() calls run even under --dry-run.

read_json(), read_yaml() and read_toml() ​

These read a file and parse it, so a template can use one value from a file that already records it instead of repeating it in mise.toml. Relative paths resolve like read_file(), from the directory of the config file.

Index into the result with ["key"] and [0]:

[tools]
node = "{{ read_json(path='package.json')['volta']['node'] }}"
pnpm = "{{ read_json(path='package.json')['packageManager'] | split(pat='@') | last }}"
pulumi = "{{ read_yaml(path='Pulumi.yaml')['requiredPulumiVersion'] }}"
mytool = "{{ read_toml(path='pyproject.toml')['tool']['mytool']['version'] }}"

Tera does not parse .key straight after a call. To read several values from one file, bind the result with set first, where .key works: {% set pkg = read_json(path='package.json') %}{{ pkg.volta.node }}.

  • A file that cannot be read or parsed fails with an error naming the function and the file.
  • A key that is not there is an error. Add | default(value="...") for a fallback.
  • TOML dates and times are strings.
  • YAML follows mise's other YAML reads: only true and false are booleans, so yes and on stay strings. An unquoted number such as 3.270 is a number and renders as 3.27, so quote versions in the file.

They need a trusted config, are refused in safe mode, and are not available in .miserc.toml, like read_file().

Filters ​

[] marks optional arguments. Tera's own filters also work; see the Tera documentation.

Strings ​

FilterDescription
quoteQuotes a string for a POSIX shell. Embedded single quotes use the '\'' form, so it's becomes 'it'\''s'.
lower, upperLowercase or uppercase
capitalizeUppercases the first character and lowercases the rest
titleCapitalizes each word: "foo bar" becomes Foo Bar
replace(from, to)Replaces every from with to
trim, trim_start([pat]), trim_end([pat])Removes whitespace, or pat, from both ends, the start, or the end: {{ "v1.2.3" | trim_start(pat="v") }} gives 1.2.3
truncate(length, [end])Cuts a string to length characters and appends end (default …)
split(pat)Splits a string into a list
regex_replace(pattern, rep)Replaces regular expression matches
default(value, [boolean])Returns value when the input is undefined. A defined but empty or false input is kept unless you pass boolean=true, as in {{ env.NAME | default(value="x", boolean=true) }}.
kebabcase, lowercamelcase, uppercamelcase, snakecase, shoutysnakecase, shoutykebabcaseConverts case: kebab-case, lowerCamelCase, UpperCamelCase, snake_case, SHOUTY_SNAKE_CASE, SHOUTY-KEBAB-CASE
slug, slugifyConverts a string to a URL-friendly slug
urlencode, urlencode_strictPercent-encodes a string for URLs; urlencode_strict encodes every character that is not alphanumeric
b64_encode([url_safe], [padded]), b64_decode([url_safe])Encodes or decodes base64
striptags, spacelessRemoves HTML tags, or whitespace between HTML tags

quote always quotes for POSIX shells, not for PowerShell or cmd. The one exception is a daemon run command on Windows, which pitchfork runs with cmd /C; there quote quotes for cmd.

Use quote when you insert a template value into a shell command. A quoted value can be joined with unquoted text in the same argument:

[tasks.create-config]
run = "touch {{ config_root | quote }}/generated.toml"

Paths ​

FilterReturnsDescription
absolutestringMakes a path absolute. The path does not have to exist.
canonicalizestringMakes a path absolute and resolves symlinks. Fails when the path does not exist.
dirnamestring/foo/bar/baz.txt becomes /foo/bar
basenamestring/foo/bar/baz.txt becomes baz.txt
extnamestring/foo/bar/baz.txt becomes txt, without a leading dot
file_stemstring/foo/bar/baz.txt becomes baz
file_sizenumberThe file's size in bytes
last_modifiednumberThe file's modification time, in seconds since the Unix epoch
join_pathstringJoins a list of path parts: {{ [config_root, "bar.txt"] | join_path }}

Hashing ​

FilterDescription
hash([algorithm], [len])Hashes a string with sha256 (default) or blake3. len truncates the result: {{ "foo" | hash(len=8) }}
hash_file([len])The BLAKE3 hash of the file at a path. len truncates the result.

Lists, numbers, and formatting ​

FilterDescription
first, lastThe first or last item of a list
join(sep)Joins a list into a string: {{ ["a", "b"] | join(sep=", ") }} gives a, b
lengthThe length of a string or list
reverseReverses a string or list
shuffle([seed])Shuffles a list
int, floatConverts a value to a number
absThe absolute value of a number
filesize_format, filesizeformatFormats a number of bytes as a readable size
date(format, [timezone])Formats a timestamp: {{ ts | date(format="%Y-%m-%d") }}. See the jiff format reference.
format(spec)Formats a value with Rust-style formatting
json_encode([pretty])Encodes a value as JSON

Tests ​

Use a test with is, as in {% if os() is starting_with(pat="mac") %}. Tests take named arguments. Tera's own tests also work; see the Tera documentation.

TestTrue when
definedThe variable is defined
string, numberThe value is a string, or a number
starting_with(pat), ending_with(pat), containing(pat)The value starts with, ends with, or contains pat
divisible_by(divisor)The number is divisible by divisor
matching(pat)The string matches the regular expression pat
dir, file, existsThe path is a directory, is a file, or exists
before(other, [inclusive]), after(other, [inclusive])The date is before or after other: {% if release_date is after(other="2026-01-01") %}
semver_matching(requirement)The version matches a semver requirement, as in {% if v is semver_matching(requirement=">=1.2") %}. Only meaningful for semver-shaped versions.

Templates in tasks ​

Task run scripts are rendered when the task runs, so they can use the tools map and the helpers below.

Task arguments ​

When a task has a usage spec, its run script can read the parsed arguments from usage. Keys are the argument and flag names in snake_case, so --dry-run is {{ usage.dry_run }}, the same name as the $usage_dry_run environment variable. Values are booleans for flags, strings, integers for count flags (count=#true, so -vvv gives 3), and lists for variadic arguments.

Values are not quoted for the shell. Double quotes around a template expression do not keep its value from becoming shell syntax, so pipe strings through quote:

[tasks.deploy]
usage = '''
arg "<environment>" help="Target environment"
flag "-v --verbose" help="Enable verbose output"
arg "[tags]" var=#true
'''
run = '''
printf 'env=%s\n' {{ usage.environment | quote }}
printf 'verbose=%s\n' {{ usage.verbose | str | quote }}
printf 'tag count=%s\n' {{ usage.tags | length | str | quote }}
{% for tag in usage.tags %}
printf 'tag=%s\n' {{ tag | quote }}
{% endfor %}
'''

mise run deploy prod -v a b prints:

text
env=prod
verbose=true
tag count=2
tag=a
tag=b

task_source_files() ​

task_source_files([only_changed]) returns the files matched by the task's sources, with globs and templates expanded. Patterns that match nothing are left out, and a task without sources gets an empty list.

[tasks.list-sources]
sources = ["src/**/*.ts", "package.json"]
run = '''
{% for file in task_source_files() %}
printf 'Processing: %s\n' {{ file | quote }}
{% endfor %}
'''

With only_changed=true, it returns only the sources modified since the task last succeeded, which keeps linters and formatters fast:

[tasks.lint]
sources = ["src/**/*.ts"]
run = "eslint{% for file in task_source_files(only_changed=true) %} {{ file | quote }}{% endfor %}"

It returns every source when the task has never succeeded, and when no source changed but the task runs anyway, such as with --force or because a dependency did work. A failed run does not reset the baseline, so the same files stay in the list until the task passes. Changes are detected by modification time, so touch and restored caches affect the result, as they do for task caching.

Secrets in a task's env experimental ​

Experimental

mise secrets are experimental. Enable them with experimental = true under [settings].

In a task's own env values, {{ secrets.NAME }} renders a mise secret when the task starts and grants that key to the task. secrets is not available anywhere else. See Compose values.

Parameterized snippets with components ​

A Tera component expands a snippet several times with different arguments inside one task's script:

[tasks.restart]
run = """
{% component restart(service, region="us-east-1") %}
aws ecs update-service --region {{ region | quote }} --cluster prod --service {{ service | quote }} --force-new-deployment
{% endcomponent %}
{{ <restart service="api" /> }}
{{ <restart service="worker" /> }}
{{ <restart service="api" region="eu-west-1" /> }}
"""

Each call expands to one command, which runs with the task's tools, environment, working directory, and shell. service is required and region defaults to us-east-1. String arguments use quotes; pass an expression in braces, such as region={vars.region}.

Define a component in the same template string that calls it; mise has no shared component library. A component defined in [vars] is rendered when the config loads, not when the task runs (see when vars are resolved). To share a whole task, use a task template; to share shell logic across scripts, source a shell file. Components need the default Tera v2 engine and do not work with tera_v1.

Templates in .miserc.toml ​

mise renders .miserc.toml before it reads mise.toml or settings, so only information from the operating system is available:

  • env: the environment mise started with
  • config_root: the directory that holds the .miserc.toml file
  • cwd
  • xdg_cache_home, xdg_config_home, xdg_data_home, xdg_state_home
  • system_brew_prefix
  • Functions that need no project context, such as arch(), os(), os_family(), libc(), avx2(), num_cpus(), choice(), and get_env()
  • All filters and tests

Not available: mise_env (which .miserc.toml itself sets), exec(), read_file(), read_json(), read_yaml(), read_toml(), mise_bin, mise_pid, vars, and tools.

~/.config/mise/miserc.toml
toml
# Stop the config search at your home directory
ceiling_paths = ["{{ env.HOME }}"]

# Ignore a config file under your XDG config directory
ignored_config_paths = ["{{ xdg_config_home }}/mise/shared.toml"]

A block that renders to nothing leaves empty lines, which TOML ignores, so conditionals work:

~/.config/mise/miserc.toml
text
{% if os() == "linux" %}
ceiling_paths = ["{{ env.HOME }}/work"]
{% endif %}

If a template fails to render, for example because of an undefined variable or a call to exec(), mise silently uses the file's raw, unrendered content. No warning is printed, because mise reads .miserc.toml before logging starts. To keep a literal {{, use a raw block.

Migrating from Tera v1 ​

mise renders templates with Tera v2. It still accepts most Tera v1 helpers, but since mise 2026.10.0 it warns when a template uses one, and mise 2027.4.0 removes them. Replace them as follows:

Tera v1 patternTera v2 replacement
value | trim_start_matches(pat="v")value | trim_start(pat="v")
value | trim_end_matches(pat="-beta")value | trim_end(pat="-beta")
items | slice(start=0, end=2)items[0:2]
[base] | concat(with="file.txt")[base, "file.txt"]
[...items] | concat(with=extra_items)[...items, ...extra_items]
items | map(attribute="name")[item.name for item in items]
items | filter(attribute="active")[item for item in items if item.active]
value | as_strvalue | str
value | escapevalue | escape_html
value | linebreaksbrvalue | newlines_to_br
value | addslashesan explicit replace(from=..., to=...)
value is divisibleby(divisor=3)value is divisible_by(divisor=3)
value is objectvalue is map
value | indent(prefix=">")value | indent(width=1), for spaces only
value | truncatevalue | truncate(length=255)
value | int(default=0), float(default=0), or int/float on input that is not a numberValidate the input first; Tera 2 errors instead of returning the default or 0
items | first, last, or nth(n=...) on an empty or short listCheck items | length first; Tera 2 returns null instead of ""

mise's unique filter still compares case-insensitively, as Tera v1 did, until 2027.4.0, when it becomes Tera 2's case-sensitive unique. unique(case_sensitive=...) warns and has no replacement that keeps case-insensitive matching.

now() takes [timezone] in Tera v2. Under tera_v1, the original now([timestamp], [utc]) signature applies instead.

Some Tera v1 behavior has no v2 equivalent: v2 fails on undefined variables and has no macros (use components). Until you can update such a template, set tera_v1 to render every template with Tera v1. In a shared mise.toml, set it as an environment variable, which older mise releases ignore instead of rejecting as an unknown setting:

[env]
MISE_TERA_V1 = true

Enabling tera_v1 prints a deprecation warning, and mise 2027.4.0 removes it. It does not apply to .miserc.toml, which mise renders before it reads settings.

MIT LicenseCopyright © 2026jdx.dev