Variables and expressions

A ${…} template gets a value from a variable, an environment, a CLI input or an earlier result.

Dynomate resolves the templates of an operation immediately before it runs. If a template does not resolve, the operation fails before its AWS call.

Variable Sources

SourceValue typesSet with
Global environment Strings Global selector or --global-env.
Collection environment Strings Collection selector or --collection-env.
CLI input Strings --input NAME=VALUE.
Request variables Any DNML value [variables] in the request.
Operation results The result types An operation that succeeds. See Result roots.

Environment values and inputs are always strings, also true, 100 and empty values. For a number or a boolean, use a cast.

$ dynomate-cli requests run ./orders/find-and-update-order.dnml \
    --global-env development --collection-env local \
    --input REGION=ap-southeast-2 --input ORDER_ID=order-123

Request variables keep their TOML types:

[variables]
accountId = "account#42"
pageSize = 25
includeArchived = false
statuses = ["NEW", "PAID"]
  • Dynomate uses request variables as written. The value "${another}" is literal text, not the value of another.
  • Secret variables resolve as usual. Dynomate masks their values in results and diagnostics.

Precedence

If two sources give the same name, Dynomate uses the source that is lower in this list:

  1. Global environment
  2. Collection environment
  3. CLI --input
  4. Request [variables]
  5. Operation results, when the operations succeed

If a caller must give a value, do not declare it in [variables].

An operation result replaces an environment value or input with the same name, and the outcome gets a DNML_ROOT_SHADOWED warning.

Templates

A template is ${path} or ${cast(path, 'type')} in a string. A path is a root name, then map keys and list positions. The root is a variable or a normalized operation name:

# Variable
accountId = "${accountId}"

# Map key and list element of an earlier result
orderId = "${find_recent_order.items[0].orderId}"

# Nested map keys
city = "${load.item.address.city}"
  • .key selects a map key. Keys can contain Unicode and spaces, and Dynomate does not trim them.
  • [n] selects a list element, from 0. You cannot use an index on a set.
  • You cannot select a key that contains a dot, a bracket or }, but Dynomate keeps it when you reuse the parent map.

Templates work in all string values of an operation, also nested values. These are the exceptions:

  • name, dependsOn, enabled, onError and timeoutMs.
  • consistentRead, which accepts only true, false or no value. A template causes DNML_TYPE_MISMATCH.
  • The keys of expressionAttributeNames and expressionAttributeValues. Their values can use templates.

A map or list key, such as item, keyValues, keys or parameters, can be one template for the full value: item = "${load.item}".

Do not build expression text with templates. Use expressionAttributeValues for DynamoDB and executionParameters for Athena SQL.

An open ${ without an end, a malformed expression or an unknown function causes DNML_SYNTAX. A correct path with no value causes DNML_UNRESOLVED_TEMPLATE.

Whole Values and Interpolation

  • Whole value. If the string is only one template, the value keeps its type, for example a number, a map or a DynamoDB set.
  • Interpolation. If the string has other text, the value is a string. Numbers become decimal text. Booleans, null, lists and maps become compact JSON text.
[variables]
price = 42

[[dynamodb.put]]
name = "Write price"
tableArn = "arn:aws:dynamodb:ap-southeast-2:111122223333:table/Prices"

[dynamodb.put.item]
sku = "sku-1"
amount = "${price}"          # number 42
label = "Price: ${price}"    # string "Price: 42"

Dynomate resolves templates in one pass. Text from a template stays as it is, also if it contains ${ or $$.

Escaping

To write a literal $ before a template, or a literal ${, write $$. After TOML decodes the string, Dynomate reads it from left to right:

  1. $$ becomes one $.
  2. Dynomate resolves ${expression}.
  3. All other $ characters stay.

With the number variable price = 42:

String in the fileValue
"Cost: $$${price}"String "Cost: $42"
"$${price}"Literal string "${price}"
"$$$$"String "$$"
"$42"String "$42"
"${price}"Number 42

Result Roots

When an operation succeeds, later templates can read its result by its normalized name. dependsOn uses the exact name.

Operation nameIn dependsOnIn a template
Find recent order"Find recent order"${find_recent_order.items[0]}
Load"Load"${load.item}
  • Failed and skipped operations give no result, also with onError = "ignore". A template that reads their result does not resolve.
  • Result keys use camelCase, for example items and lastEvaluatedKey. The Operation reference gives each result.

Casts

DNML never changes a type automatically. A key that needs an integer rejects the string "100" with DNML_TYPE_MISMATCH. Use a cast:

limit = "${cast(PAGE_SIZE, 'integer')}"
scanIndexForward = "${cast(SCAN_FORWARD, 'boolean')}"
TargetAccepts
'string'A string, also an empty string. A number, as decimal text. A boolean, as "true" or "false".
'integer'An integer, or a whole number in the signed 64-bit range. A string of digits with an optional minus and no leading zeros. Fractions fail.
'number'A number, or a string in JSON number syntax, such as "1.5" or "2e3".
'boolean'A boolean, or the exact strings "true" and "false".
  • Write the target in lowercase and single quotes. Other targets, for example 'int', cause DNML_SYNTAX.
  • The first value in a cast must be a path, not a literal or a nested call.
  • No cast trims whitespace. " 100" is not an integer. An empty string converts only to 'string'.
  • You cannot cast maps, lists, sets, binary or null.

Typed Values

Dynomate converts plain values to DynamoDB types. It converts strings to S, numbers to N, booleans to BOOL, arrays to L and maps to M.

For other types, or to set the exact type, write a map with one type key:

[dynamodb.put.item]
id = "order-1"
total = { N = "12.50" }                 # exact decimal
tags = { SS = ["new", "priority"] }     # string set
thumbnail = { B = "aGVsbG8=" }          # binary (base64)
note = { NULL = true }                  # null
literalS = { M = { S = "a map entry named S" } }
Typed formPayload
{ S = "text" }A string.
{ N = "12.50" }A decimal number in a string, with full precision. { N = 12.5 } is not valid.
{ B = "aGVsbG8=" }A standard base64 string.
{ BOOL = true }A boolean.
{ NULL = true }Always true. Use it to write a null, because TOML has no null.
{ L = [ … ] }, { M = { … } }A list or a map of plain or typed values.
{ SS = ["a", "b"] }One or more unique strings.
{ NS = ["1", "2.5"] }One or more unique decimal strings or numbers.
{ BS = ["aGk="] }One or more unique base64 strings.

To write a usual map that has one of these names as its only key, put it in M: { M = { S = "..." } }.

Templates Inside Typed Values

Dynomate resolves the templates in a typed value, then it checks the payload. S and N need a string after they resolve. A payload that is not valid causes DNML_TYPED_VALUE before the AWS call.

[variables]
count = 5
numericText = "12.50"

# Valid: the cast produces the string "5"
countAsNumber = { N = "${cast(count, 'string')}" }
# Valid: numericText is already a decimal string
exact = { N = "${numericText}" }
# Invalid (DNML_TYPED_VALUE): count resolves to the number 5, not a string
broken = { N = "${count}" }

To reuse a number from a result, use a direct reference, such as amount = "${load.item.amount}". Do not put it in N.

All casts and escapes in one file:

version = "1.0"
name = "casts-and-escaping"
description = "Convert string inputs explicitly and compose literal dollars with interpolation."

[variables]
accountId = "account#42"
PAGE_SIZE = "100"
SCAN_FORWARD = "true"
numericText = "12.50"
count = 5
price = 42
templateLookingText = '${price}'

[defaults]
profileName = "commerce-dev"
region = "ap-southeast-2"

[[dynamodb.query]]
name = "Read orders"
tableArn = "arn:aws:dynamodb:ap-southeast-2:123456789012:table/Orders"
keyConditionExpression = "accountId = :accountId"
limit = "${cast(PAGE_SIZE, 'integer')}"
scanIndexForward = "${cast(SCAN_FORWARD, 'boolean')}"
consistentRead = true
expressionAttributeValues = { ":accountId" = "${accountId}" }

# The query receives integer 100 and boolean true. Without casts, these input
# strings would fail the fields' type checks; no automatic coercion occurs.

[[dynamodb.put]]
name = "Write resolved values"
tableArn = "arn:aws:dynamodb:ap-southeast-2:123456789012:table/ExampleValues"

[dynamodb.put.item]
id = "casts-and-escaping"
asInteger = "${cast(PAGE_SIZE, 'integer')}"
asNumber = "${cast(numericText, 'number')}"
asBoolean = "${cast(SCAN_FORWARD, 'boolean')}"
asString = "${cast(count, 'string')}"
stringAttribute = { S = "${cast(count, 'string')}" }
numberAttribute = { N = "${cast(count, 'string')}" }
exactDecimal = { N = "${numericText}" }

# A whole-value expression preserves its type. Both typed payload casts above
# return the string "5"; exactDecimal supplies the decimal string "12.50"
# directly to strict N, without an intermediate binary64 conversion.

cost = "Cost: $$${price}"       # string "Cost: $42"
escapedTemplate = "$${price}" # literal string "${price}"
fourDollars = "$$$$"          # string "$$"
literalAmount = "$42"         # string "$42"
wholeValue = "${price}"       # number 42
substitutedText = "${templateLookingText}" # literal string "${price}"

# Scan after TOML decoding: $$ emits $, ${...} resolves, other $ stays literal.
# Neither emitted nor substituted text is scanned again.
#
# Invalid examples (comments only, so this remains a valid document):
#   ${cast(PAGE_SIZE, 'String')} is DNML_SYNTAX: target names are lowercase.
#   Casting the string "1.5" to 'integer' is DNML_TYPE_MISMATCH.
#   Casting the string "NaN" to 'number' is DNML_TYPE_MISMATCH.
#   Casting the string "yes" to 'boolean' is DNML_TYPE_MISMATCH.
#   Casting a map, array, set, binary, or null to 'string' is DNML_TYPE_MISMATCH.
#   No cast trims whitespace; an integer string " 100 " is rejected.
#   { S = "${count}" } and { N = "${count}" } are DNML_TYPED_VALUE:
#   count resolves to a number, and these typed payloads require strings.

Lossless Reuse

A whole-value reference to an earlier result keeps each DynamoDB type and value exactly, at all levels:

  • Numbers keep their exact value, also past the limits of a 64-bit integer or float.
  • Binary, lists, null and sets keep their type.
  • A map with only a type name as its key, such as S, stays a map.

This applies to a full item, one attribute, a subtree, and a reused value in a new map. The display format of a result does not change the reused value.

Interpolation and casts are not lossless. In a longer string, binary becomes base64 text and a set becomes a JSON array.

version = "1.0"
name = "lossless-reuse"
description = "Reuse fetched items and selected attributes without changing DynamoDB types or values."

# SourceValues and ReplicaValues both have a string partition key named id.
# This example covers all ten DynamoDB attribute types. The 38-digit decimal
# cannot be copied faithfully through a binary64 number.

[defaults]
profileName = "commerce-dev"
region = "ap-southeast-2"

[[dynamodb.put]]
name = "Seed source"
tableArn = "arn:aws:dynamodb:ap-southeast-2:123456789012:table/SourceValues"

[dynamodb.put.item]
id = "all-types"
stringValue = { S = "text" }
preciseNumber = { N = "12345678901234567890.123456789012345678" }
binaryValue = { B = "AAEC/w==" }
booleanValue = { BOOL = true }
nullValue = { NULL = true }
listValue = { L = [ { N = "1.25" }, { B = "AA==" }, { NULL = true } ] }
mapValue = { M = { child = { NS = ["1", "2.5"] }, "customer.name" = "a literal dotted key" } }
stringSet = { SS = ["blue", "green"] }
numberSet = { NS = ["1", "2.5", "12345678901234567890123456789012345678"] }
binarySet = { BS = ["AA==", "/w=="] }
singletonTagMap = { M = { S = "a map entry named S" } }

[[dynamodb.get]]
name = "Load"
dependsOn = "Seed source"
tableArn = "arn:aws:dynamodb:ap-southeast-2:123456789012:table/SourceValues"
consistentRead = true
keyValues = { id = "all-types" }

[[dynamodb.put]]
name = "Copy item"
dependsOn = "Load"
tableArn = "arn:aws:dynamodb:ap-southeast-2:123456789012:table/ReplicaValues"
item = "${load.item}"

# Copy item preserves every attribute recursively: numbers remain exact, binary
# remains binary, sets remain sets, and singletonTagMap remains a map, not S.
# A displayed JSON representation does not determine the copied input types.

[[dynamodb.put]]
name = "Copy selected attributes"
dependsOn = "Load"
tableArn = "arn:aws:dynamodb:ap-southeast-2:123456789012:table/ReplicaValues"

[dynamodb.put.item]
id = "selected-attributes"
stringValue = "${load.item.stringValue}"
preciseNumber = "${load.item.preciseNumber}"
binaryValue = "${load.item.binaryValue}"
booleanValue = "${load.item.booleanValue}"
nullValue = "${load.item.nullValue}"
listValue = "${load.item.listValue}"
mapValue = "${load.item.mapValue}"
stringSet = "${load.item.stringSet}"
numberSet = "${load.item.numberSet}"
binarySet = "${load.item.binarySet}"
singletonTagMap = "${load.item.singletonTagMap}"
nestedNumberSet = "${load.item.mapValue.child}"
nestedBinary = "${load.item.listValue[1]}"

# Selected subtrees have the same lossless guarantee as the whole item.
# mapValue's literal customer.name key cannot be selected directly in v1, but
# it is valid data and is preserved when mapValue or the whole item is reused.
# Text interpolation and explicit casts deliberately convert values; this
# example uses only whole-value references for the copied data.