Variable substitution syntax

Octopus lets you reference variables in scripts, configuration files, and step settings, using a binding syntax powered by Octostache, Octopus’s open-source templating engine.

This page explains that syntax: how Octopus binds and evaluates a reference, how to compose a value from other variables, how to work with collections and conditionals, and how to reshape a value with a filter.

For the built-in variables you can reference, see System variables.

Variable substitution

Variable substitution is how Octopus replaces a reference in your text with the value of a variable at deployment or runbook run time. Understanding substitution lets you write one deployment process, script, or configuration file that adapts to each environment, target, or tenant instead of maintaining a copy for each.

You reference a variable by wrapping its name in #{ and }:

#{VariableName}

When Octopus runs the step, it evaluates the reference and replaces it with the variable’s value. Every value is a string, even when it looks like a number or a boolean, because Octopus stores and substitutes all variables as text.

Octopus resolves each reference using the most specifically scoped value that applies to the current deployment, so the same reference can produce a different value in Development than it does in Production.

Composite variables

A composite variable is a variable whose own value contains a reference to another variable, using the same #{...} binding syntax. Composing values this way lets you tame variable complexity by building a value out of simpler variables instead of maintaining a full string for every scope.

Given the variables:

NameValueScope
ServerSQLProduction, Test
DatabasePDB001Production
DatabaseTDB001Test
ConnectionStringServer=#{Server}; Database=#{Database}

Evaluating ConnectionString in Production yields Server=SQL; Database=PDB001. Binding to a value that isn’t defined for the current scope yields an empty string, so evaluating ConnectionString in a Dev environment (where neither Server nor Database is scoped) yields Server=; Database=.

Also read about common mistakes for variables for more information.

Escaping variable syntax

If the text undergoing substitution contains a literal sequence that looks like a reference but shouldn’t be replaced, such as #{NotToBeReplaced}, add an extra # to escape it:

ExpressionValue
##{NotToBeReplaced}#{NotToBeReplaced}

Escaping compounds with the length of the hash sequence. Given the variable Name with the value title, ###{Name} evaluates to #title: the leading ## collapses to a literal #, and the remaining #{Name} is substituted normally.

Variable references

A variable reference names the value you want Octopus to substitute, ranging from a plain variable name to a specific value selected out of a structured object. You can combine a reference with surrounding text to compose a new value:

#{Octopus.Environment.Name}-#{Octopus.Project.Name}

Some variables are structured, and you select a value from them using indexer notation, where the key inside the square brackets identifies the entry you want. Many system variables use this form to expose per-action, per-machine, or per-package values. For example, this reference selects the target roles for a specific action:

#{Octopus.Action[Website].TargetRoles}

The index itself can be another variable reference, which lets you look up a value dynamically rather than naming the key literally. Given the variables:

NameValue
MyPassword[Rob]passwordX
MyPassword[Steve]passwordY
MyPassword[Mary]passwordZ
UserNameMary

#{MyPassword[#{UserName}]} evaluates to passwordZ.

Output variables extend indexer notation with a second index for the machine that produced the value. An output variable set in one step is available to later steps using the action name and the machine name:

#{Octopus.Action[Website].Output[WEBSVR01].Package.InstallationDirectoryPath}

Collection variables

A collection variable holds many entries rather than a single value. Several system variables are collections, including the packages, builds, commits, and work items associated with a release, and the changes included in a deployment; see System variables for the full list. Knowing how to read a collection lets you work with all of them, because they share the same iteration and indexing syntax.

You iterate over a collection with an #{each} block. Octopus repeats the content between #{each} and #{/each} once per entry, binding each entry to the name you choose so you can reference its properties:

#{each package in Octopus.Release.Package}
    This release contains #{package.PackageId} #{package.Version}
#{/each}

Collections can be nested. An entry in one collection can itself hold a collection, which you iterate the same way:

#{each package in Octopus.Release.Package}
    #{each commit in package.Commits}
        - #{commit.CommitId}: #{commit.Comment}
    #{/each}
#{/each}

You can also select a single entry directly using indexer notation. Some collections are keyed by an identifier, such as a package ID:

#{Octopus.Release.Package[Acme.Web].Version}

Others are keyed by a zero-based integer index, so the first entry is at index 0:

#{Octopus.Release.Builds[0].BuildUrl}

A collection doesn’t have to come from a system variable. You can define your own keyed collection using multiple variables that share an index prefix, then iterate it the same way. Given the variables:

NameValue
Endpoint[A].Addresshttp://a.example.com
Endpoint[A].DescriptionPrimary
Endpoint[B].Addresshttp://b.example.com
Endpoint[B].DescriptionReplica
#{each endpoint in Endpoint}
 - #{endpoint} at #{endpoint.Address} is #{endpoint.Description}
#{/each}

A variable containing a comma-separated list also iterates directly, without needing an index at all:

#{each endpoint in "http://a.example.com,http://b.example.com"}
 - #{endpoint}
#{/each}

JSON variables

Octostache parses a JSON-formatted variable value natively, exposing its properties for substitution the same way a system-variable collection does. Given the variable Custom.MyJson with the value {Name: "t-shirt", Sizes: [{size: "small", price: 15.00}, {size: "large", price: 20.00}]}, #{Custom.MyJson.Name} evaluates to t-shirt, and #{Custom.MyJson.Sizes[0].price} evaluates to 15.00.

A JSON array or object iterates using the same #{each} syntax as any other collection:

#{each item in Custom.MyJson.Sizes}
 - #{item.size}: #{item.price}
#{/each}

When iterating a JSON object rather than an array, each entry exposes .Key and .Value properties for the object’s key and the value at that key.

Loop iteration variables

Inside an #{each} block, Octopus makes a set of variables available that report the position of the current entry in the collection being iterated. This topic lists them. All values are strings.

VariableDescription
Octopus.Template.Each.IndexThe zero-based index of the current entry in the iteration.
Octopus.Template.Each.FirstTrue if the entry is the first in the collection, otherwise False.
Octopus.Template.Each.LastTrue if the entry is the last in the collection, otherwise False.

Given the variable Endpoints with the comma-separated value SV1,SV2,SV3, the following template prints a marker on only the first and last entries:

#{each endpoint in Endpoints}
#{if Octopus.Template.Each.First}First: #{endpoint}#{/if}
#{if Octopus.Template.Each.Last}Last: #{endpoint}#{/if}
#{/each}

This produces:

First: SV1
Last: SV3

Calculations

Octopus supports basic arithmetic on variable values using the calc statement, so you can derive a value like an IP offset or a scaled count without pre-computing it yourself. Four operators are supported: addition (+), subtraction (-), multiplication (*), and division (/).

Given the variables:

NameValue
IPOffset[Primary]0
ScaleFactor12
Numbers10,20,30,40,50
  • 192.168.0.#{calc IPOffset[Primary] + 1} evaluates to 192.168.0.1
  • #{calc 22 * ScaleFactor} evaluates to 264
  • #{each i in Numbers}#{calc i + 5}#{/each} evaluates to 15 25 35 45 55

When a variable appears on the left-hand side of a divide or subtract operation, enclose its name in braces so the operator symbol isn’t parsed as part of the variable name: #{calc {IPOffset[Primary]} - 4}.

Conditionals

A conditional includes or excludes text based on a variable’s value, letting you vary a script or configuration file by context without maintaining a separate copy for each case. Octopus evaluates the condition when the step runs and keeps only the matching branch.

Two conditional statements are supported: if, which evaluates its content when the variable is truthy, and unless, which evaluates when the variable is falsy. A value is falsy if it’s undefined, an empty string, or (ignoring case and surrounding whitespace) False, No, or 0; every other value is truthy.

#{if VariableName}conditional statements#{/if}
#{unless VariableName}conditional statements#{/unless}

An #{if} block can include an #{else} branch for the case where the condition doesn’t hold:

#{if Octopus.Environment.Name == "Production"}
  ProductionValue
#{else}
  DefaultValue
#{/if}

Complex syntax

Beyond a truthy/falsy check, a conditional can compare values directly using == and !=, for example #{if Octopus.Environment.Name == "Production"}...#{/if}. Neither operand uses #{...} syntax inside the comparison — Octostache already evaluates each operand as a variable’s value there, and the same comparison works against another variable’s value instead of a literal: #{if Environment.LogLevel == Base.MaxLogLevel}...#{/if}.

Combining if and else lets you express an effective OR across more than two cases:

#{if Octopus.Environment.Name == "Development"}
  Do this if it's Development
#{else}
  #{if Octopus.Environment.Name == "Test"}
    Do this if it's Test
  #{else}
    Do this if it's neither
  #{/if}
#{/if}

It’s possible to use variable filters to help create both complex run conditions and variable expressions, but there are limitations to be aware of.

Using variable filters inline in the two conditional statements if and unless are not supported.

If you wanted to include a variable run condition to run a step only when the release had a prerelease tag matching my-branch, you might be tempted to use the VersionPreReleasePrefix extraction filter to write a condition like this:

#{if Octopus.Release.Number | VersionPreReleasePrefix == "my-branch"}true#{/if}

However, the evaluation of the statement would always return False as the syntax is not supported.

Instead, you need to create a variable that includes the variable filter you want to use. For this example, let’s assume it’s named PreReleaseBranch with the value:

#{Octopus.Release.Number | VersionPreReleasePrefix}

Once you have created your variable, you can use it in your run condition like this:

#{if PreReleaseBranch == "my-branch"}True#{/if}

Run conditions

A conditional expression can also control whether a step in a deployment process runs at all — for example, #{if Octopus.Environment.Name == "Production"}true#{/if} runs a step only in Production. See Conditions for how run conditions are configured on a step.

Filters

A filter transforms a variable’s value as Octopus substitutes it, so you can reshape a value for its destination without changing the underlying variable. Apply a filter with a pipe after the variable name:

#{Octopus.Release.Notes | Markdown}

Filters cover common transformations such as changing case, escaping values for HTML or JSON, and rendering Markdown. For the full set of filters and what each one does, see Variable filters.

Version notes

  • The calc operator is available from Octopus Server 2023.2.

Help us continuously improve

Please let us know if you have any feedback about this page.

Send feedback

Page updated on Thursday, August 6, 2026

Use Octopus docs with AI