Defining a variable in Jaws Deploy is only half the job. A variable like ConnectionStrings:Default or Cache.Endpoint lives in the Jaws database, scoped to a workspace, project, environment, tag, or target. At deploy time you usually need that value to land inside a file on the target machine - an appsettings.json, a web.config, an .env, a YAML manifest, a plain text template. This guide explains the three mechanisms Jaws Deploy uses to push variable values into files, and how to configure the file lists they run against.
All three are configured on the Deploy package step template, under its Transformations property group. If you assemble your own step templates, the same building blocks are available as the Convert-JawsVariableReplace, Convert-JawsJsonHierarchicalVariableReplace, and Convert-JawsXmlTransform / Convert-JawsJsonTransform functions from the built-in JawsCommon module.
The three mechanisms
Where you configure this
Open the Deploy package step in your deployment process and expand the Transformations group. You will find three multi-line fields, each taking a newline-separated list of files:
- Config transforms -
[json|xml]: source => targettransform definitions. - Replace variables in files - files to run token-based
#{...}replacement against. - Replace variables in JSON files located based on hierarchical variable names - JSON files to run hierarchy-based replacement against.
All paths are relative to the package root (the extracted contents of your package), and all three fields support globbing. The step runs them in a fixed order with optional custom-script hooks in between - see [Processing order](#processing-order) below.
Listing and globbing files
Each Transformations field is a list, one entry per line. Blank lines are ignored and surrounding whitespace is trimmed, so you can format the list for readability.
Paths are resolved against the package root, then expanded as globs before any replacement happens. Globbing means you do not have to enumerate every file:
*matches any run of characters within a single path segment.**matches across directory boundaries (any depth).- A literal path with no wildcards matches exactly one file.
If a glob matches several files, the replacement runs against every match. If it matches nothing, that line is simply a no-op - which is also the most common reason a replacement "silently does nothing": the path is wrong relative to the package root, or the build did not include the file.
Examples of file list entries
Each line is relative to the package root. Use backslashes (Windows agents) consistently.
# exact file
appsettings.json
# every appsettings.{env}.json in the root
appsettings.*.json
# every web.config at any depth
**\web.config
# every .json under config\, any depth
config\**\*.json
# a single nested file
services\api\appsettings.Production.json
Token-based replacement
List a file under Replace variables in files and Jaws scans its text for #{...} tokens, replacing each one with the resolved variable value. It works on any text file - JSON, XML, YAML, .env, .conf, .ps1, an HTML template - because it is a pure text substitution.
A few rules worth knowing:
- Prefixed and bare names both work. User variables are stored internally with a
VAR.prefix. A variable you namedApiBaseUrlcan be referenced as either#{ApiBaseUrl}or#{VAR.ApiBaseUrl}. Use the bare form unless you need to disambiguate from a system parameter. - Names are restricted to a token charset. A token name may contain letters, digits, and the characters
. - _ :and spaces. Anything else ends the token, so#{My-Var.v2}is valid but#{a+b}is not treated as a token. - Nested / recursive resolution. If a variable value itself contains
#{...}, it is resolved too.installPath = C:\#{appName}\#{version}expands fully. Circular references are detected and fail the deployment rather than looping. - Unknown tokens are left untouched. If no variable matches, the literal
#{...}text stays in the file - a useful signal that something is misspelled or out of scope.
Before and after
File listed under "Replace variables in files". Variables resolved for the Production environment.
// appsettings.json (in the package, with tokens)
{
"ApiBaseUrl": "#{ApiBaseUrl}",
"ConnectionStrings": {
"Default": "Server=#{DbHost};Database=#{DbName};"
},
"FeatureFlags": { "NewCheckout": "#{Features.NewCheckout}" }
}
// after deployment to Production
{
"ApiBaseUrl": "https://api.example.com",
"ConnectionStrings": {
"Default": "Server=prod-sql-01;Database=shop;"
},
"FeatureFlags": { "NewCheckout": "true" }
}
Hierarchy-based replacement (JSON)
List a JSON file under Replace variables in JSON files located based on hierarchical variable names and Jaws takes a different approach: instead of looking for placeholders, it locates existing nodes by name. The variable name maps to a JSON path, where each colon (:) is one level of nesting.
To set the value of bar in { "foo": { "bar": 123 } }, create a variable named foo:bar with the new value. Jaws walks the document, finds the node at foo -> bar, and replaces it. No edit to the file's source is needed - the JSON you built ships unmodified and Jaws rewrites the matching nodes at deploy time.
More rules:
- Only user variables participate. Hierarchy replacement considers variables with the internal
VAR.prefix - i.e. the variables you defined. System parameters are ignored. - Missing nodes are skipped. If the JSON has no node at the variable's path, nothing happens for that variable. Hierarchy replacement never adds keys; it only updates ones that already exist.
- Scalars replace scalars. A string, number, or boolean value replaces the target value in place, preserving JSON type where possible.
- You can inject whole objects/arrays. If the target node is an array or object and your variable's value is a string of valid JSON, Jaws parses it and substitutes the parsed structure. If the value is not valid JSON in that case, the deployment fails with a clear error.
Variable names map to JSON paths
File listed under the hierarchical-replacement field. The JSON ships with real defaults - no tokens.
// appsettings.json (shipped as-is, no placeholders)
{
"Logging": { "LogLevel": { "Default": "Information" } },
"AllowedHosts": "*",
"Cors": { "Origins": ["http://localhost:3000"] }
}
// Jaws variables defined for Production:
// Logging:LogLevel:Default = "Warning"
// AllowedHosts = "app.example.com"
// Cors:Origins = ["https://app.example.com"] (value is valid JSON)
// after deployment to Production
{
"Logging": { "LogLevel": { "Default": "Warning" } },
"AllowedHosts": "app.example.com",
"Cors": { "Origins": ["https://app.example.com"] }
}
Processing order
The Deploy package step always runs the transformations in the same order, so you can layer them predictably. Between each stage there is a custom-script hook you can use for anything the built-in steps do not cover:
- Extract package to the work folder.
- Hook: After package is extracted.
- Config transforms (XDT / JSON merge) - restructure files first.
- Hook: After config transform is applied.
- Token replacement -
#{...}substitution across the listed files. - JSON hierarchy replacement - locate-and-replace by variable name.
- Hook: After variables are replaced in files.
- Copy to installation directory (optionally purging it first).
- Hook: After package is deployed.
Because token replacement runs before hierarchy replacement, a #{...} token can supply the value that a later hierarchy pass reads - but not the other way round. Keep that ordering in mind when a file is processed by more than one mechanism.
Common mistakes
- Wrong base path. All file lists are relative to the package root, not the agent working directory or the install directory. Match the folder layout inside the package.
- Glob matched nothing. A typo or a file the build excluded means the line is a silent no-op. Check the deployment log - each processed file is logged by name.
- Expecting hierarchy replacement to add keys. It only updates nodes that already exist. To introduce a brand-new section, use a config (JSON merge) transform instead.
- Secret values in plain files. Replacement writes the resolved value to disk in clear text. That is expected for config, but do not point replacement at files that get committed or shipped back to an artifact store.
- Forgetting the escape. A genuine
#{...}that belongs to another tool will be eaten by token replacement unless you write it as##{...}.