Go templates + Sprig: the templating you already know
OrchStep's template layer is plain Go templates plus the Sprig function library — default, regexFind, semverCompare and 100+ more — with JavaScript waiting for the logic that templates make ugly.
If you have written a Helm chart, a text/template in Go, or a GitHub Actions
expression, you already know most of OrchStep's template layer. There is no
bespoke DSL to learn. Interpolation is Go templates; the standard library is
Sprig v3, the same 100+ functions Helm
ships. When a value gets genuinely logic-heavy, you drop into JavaScript — but
not before.
This post is a tour of that layer: what {{ }} can do, the Sprig functions
worth memorizing, and the exact line where JS takes over.
The basics: {{ }} is Go templates
Anything in {{ }} is a Go template expression, evaluated against a single
merged vars namespace (workflow defaults:, task/step vars:, and --var
all merge into it):
steps:
- func: shell
do: echo "Deploying {{ vars.version }} to {{ vars.target }}"You also get the standard reference roots — {{ steps.build.image }} for a
prior step's output, {{ env.HOME }} for an OS variable, {{ loop.item }}
inside a loop.
Sprig: the functions that delete your shell hacks
Every function below is real and verified with orchstep eval (the expression
playground — try them yourself). The point is that the messy bits of a bash
script — defaulting a value, parsing output, comparing versions — become one
piped function call.
Defaults and fallbacks
The single most useful one. No more ${PORT:-8080} scattered through do:
blocks:
$ orchstep eval --lang template '{{ "" | default "fallback" }}'
fallback
$ orchstep eval --lang template '{{ coalesce "" "" "from-coalesce" }}'
from-coalesce
$ orchstep eval --lang template '{{ ternary "yes" "no" (eq "prod" "prod") }}'
yesExtracting values from command output
This is where Sprig earns its keep in an ops tool. regexFind pulls the first
capture group out of a step's stdout — replacing a grep | sed | cut pipeline:
$ orchstep eval --lang template '{{ "IMAGE=app:v1.2.3" | regexFind "IMAGE=(.+)" }}'
app:v1.2.3
$ orchstep eval --lang template '{{ regexFind "v([0-9]+)" "release-v42" }}'
42In a workflow it lives in outputs:, turning unstructured stdout into a named
value the next step can use:
- name: extract
func: shell
do: |
echo "Build complete"
echo "IMAGE=registry.io/my-service:v2.4.0"
outputs:
image: '{{ result.output | regexFind "IMAGE=(.+)" }}'
- name: deploy
func: shell
do: echo "deploying {{ steps.extract.image }}"Run it and image resolves to registry.io/my-service:v2.4.0 — the IMAGE=
prefix stripped, because regexFind returns the capture group, not the whole
match.
Strings, versions, and the rest
$ orchstep eval --lang template '{{ "my-service" | camelcase }}'
MyService
$ orchstep eval --lang template '{{ semverCompare ">=1.2.0" "1.5.0" }}'
trueThe full menu — upper/lower/trim, b64enc, sha256sum, toJson,
now | date "2006-01-02", list and dict helpers — is in the
Templates reference. If Sprig has it, OrchStep has it.
Where JavaScript takes over
Go templates are great for interpolation and one-shot functions. They get ugly
the moment you need real boolean logic — nested and/or/eq is hard to read.
So OrchStep evaluates un-wrapped expressions (no {{ }}) as JavaScript via
an embedded VM. The rule is exact and lives in one place: fully wrapped in
{{ }} means Go template; anything else means JavaScript.
$ orchstep eval '2 + 3 > 4'
true
$ orchstep eval --env production 'vars.require_approval === "true" && Number(vars.replicas) > 4'
trueJavaScript is what assert conditions, retry.when, and loop.until use. A
condition that would be a thicket of Go-template and calls reads like code:
- func: assert
args:
condition: 'steps.extract.image.includes("my-service") && vars.service.length > 0'
message: "image must reference the service"That assert passes in a real run — steps.extract.image is a JS string, so
.includes() and .length just work.
What you gained
| You used to write | Now it is |
|---|---|
${VAR:-default} in every block | {{ vars.x | default "..." }} |
grep ... | sed ... | cut ... | {{ result.output | regexFind "..." }} |
| hand-rolled version string compares | {{ semverCompare ">=1.2.0" vars.v }} |
unreadable nested and/or/eq | a plain JavaScript boolean expression |
| "what does this resolve to?" guesswork | orchstep eval '<expression>' |
The honest boundary
This is a templating layer, not a programming language. Go templates have no
local variables worth the name and the control flow is awkward; that is why
the JS escape hatch exists. If you find yourself writing a 20-line
{{ range }}/{{ if }} block inside a do:, that logic probably belongs in a
real script that the step calls — OrchStep orchestrates your tools, it does not
replace them. Use templates for wiring, JS for conditions, and a script for
anything that wants to be a program.
Where to go next
- Templates & Expressions — the full Sprig function tables
- JavaScript Expressions — the JS-first syntax guide
- Expression Playground —
orchstep eval, the fastest way to test any of this
Curious what a specific expression resolves to in your workflow? orchstep eval '<expression>' answers in milliseconds — no run required.
curl -fsSL https://orchstep.dev/install.sh | sh