Examples

Save these complete requests in a collection, then run them in the app or with the CLI.

The file name must match name. See Naming rules. Replace the profile names, account IDs and table ARNs with your values.

find-and-update-order.dnml

The running example: find, update and read back an order.

Operations: Query, Update item, Get item.

version = "1.0"
name = "find-and-update-order"
description = "Find the most recent order for an account, mark it reviewed, and read it back."

[variables]
accountId = "account#42"
pageSize = "1"

[defaults]
profileName = "commerce-dev"

[[dynamodb.query]]
name = "Find recent order"
tableArn = "arn:aws:dynamodb:ap-southeast-2:111122223333:table/Orders"
keyConditionExpression = "accountId = :accountId"
limit = "${cast(pageSize, 'integer')}"
scanIndexForward = false

[dynamodb.query.expressionAttributeValues]
":accountId" = "${accountId}"

[[dynamodb.update]]
name = "Mark reviewed"
dependsOn = "Find recent order"
tableArn = "arn:aws:dynamodb:ap-southeast-2:111122223333:table/Orders"
updateExpression = "SET #status = :status"

[dynamodb.update.keyValues]
accountId = "${accountId}"
orderId = "${find_recent_order.items[0].orderId}"

[dynamodb.update.expressionAttributeNames]
"#status" = "status"

[dynamodb.update.expressionAttributeValues]
":status" = "REVIEWED"

[[dynamodb.get]]
name = "Read back"
dependsOn = "Mark reviewed"
tableArn = "arn:aws:dynamodb:ap-southeast-2:111122223333:table/Orders"
consistentRead = true

[dynamodb.get.keyValues]
accountId = "${accountId}"
orderId = "${find_recent_order.items[0].orderId}"

chained-query-update.dnml

The same chain with a condition and a date-time value.

Operations: Query, Update item, Get item.

version = "1.0"
name = "chained-query-update"
description = "Locate the most recent order for an account and mark it reviewed."

[variables]
accountId = "account#42"
targetStatus = "REVIEWED"
pageSize = 10

[defaults]
profileName = "${AWS_PROFILE}"

[[dynamodb.query]]
name = "Find recent order"
tableArn = "arn:aws:dynamodb:ap-southeast-2:111122223333:table/Orders"
keyConditionExpression = "#accountId = :accountId"
limit = "${pageSize}"
scanIndexForward = false

[dynamodb.query.expressionAttributeNames]
"#accountId" = "accountId"

[dynamodb.query.expressionAttributeValues]
":accountId" = "${accountId}"

[[dynamodb.update]]
name = "Mark order reviewed"
dependsOn = "Find recent order"
tableArn = "arn:aws:dynamodb:ap-southeast-2:111122223333:table/Orders"
updateExpression = "SET #status = :status, reviewedAt = :now"
conditionExpression = "attribute_exists(orderId)"
returnValues = "all-new"

[dynamodb.update.keyValues]
accountId = "${accountId}"
orderId = "${find_recent_order.items[0].orderId}"

[dynamodb.update.expressionAttributeNames]
"#status" = "status"

[dynamodb.update.expressionAttributeValues]
":status" = "${targetStatus}"
":now" = 2026-09-05T03:12:41Z

[[dynamodb.get]]
name = "Read back"
dependsOn = ["Mark order reviewed"]
tableArn = "arn:aws:dynamodb:ap-southeast-2:111122223333:table/Orders"
consistentRead = true

[dynamodb.get.keyValues]
accountId = "${accountId}"
orderId = "${find_recent_order.items[0].orderId}"

casts-and-escaping.dnml

All four casts, typed values and literal dollar signs.

Operations: Query, Put item.

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.dnml

Copy items between tables with no loss of type or precision.

Operations: Put item, Get item.

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.

transfer-order-ownership.dnml

Move an order to another account in one transaction with a version check.

Operations: Get item, Write transaction, PartiQL statement.

version = "1.0"
name = "transfer-order-ownership"
description = "Atomically move an order with the documented item shape."

# Preconditions for this example:
# Orders has string keys accountId (partition) and orderId (sort).
# The source item exists and has exactly these five required fields:
# accountId, orderId, and status are strings; version is a positive integer;
# payload is a map containing arbitrary application data.
# fromAccount and toAccount differ. Every concurrent update to these items
# increments version, so a changed source fails the transaction's condition.
# Additional top-level fields are not copied; place application data in payload.

[variables]
fromAccount = "account#42"
toAccount = "account#77"
orderId = "order#1049"

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

[execution]
allowDestructive = true

[[dynamodb.get]]
name = "Load order"
tableArn = "arn:aws:dynamodb:ap-southeast-2:123456789012:table/Orders"
consistentRead = true
keyValues = { accountId = "${fromAccount}", orderId = "${orderId}" }

[[dynamodb.transactWrite]]
name = "Move order"
dependsOn = "Load order"
tableArn = "arn:aws:dynamodb:ap-southeast-2:123456789012:table/Orders"
actions = [
  {
    action = "put",
    conditionExpression = "attribute_not_exists(accountId)",
    item = {
      accountId = "${toAccount}",
      orderId = "${load_order.item.orderId}",
      status = "${load_order.item.status}",
      version = "${load_order.item.version}",
      payload = "${load_order.item.payload}",
    },
  },
  {
    action = "delete",
    keyValues = { accountId = "${fromAccount}", orderId = "${orderId}" },
    conditionExpression = "attribute_exists(accountId) AND #status <> :locked AND #version = :version",
    expressionAttributeNames = { "#status" = "status", "#version" = "version" },
    expressionAttributeValues = {
      ":locked" = "LOCKED",
      ":version" = "${load_order.item.version}",
    },
  },
]

# The destination condition belongs to the put itself. The put and delete use
# different keys, so each item has only one action in this transaction.

[[dynamodb.executeStatement]]
name = "Verify"
dependsOn = "Move order"
statement = 'SELECT "accountId", "orderId", "status" FROM "Orders" WHERE "accountId" = ? AND "orderId" = ?'
parameters = ["${toAccount}", "${orderId}"]
consistentRead = true

table-lifecycle.dnml

Create, fill, scan, truncate and delete a table on DynamoDB Local.

Operations: Create table, Import items, Scan, Truncate table, Delete table.

version = "1.0"
name = "table-lifecycle"
description = "Recreate the dev Orders table from seed data, verify, then tear down."

[execution]
onError = "stop"
allowDestructive = true

[defaults]
profileName = "local"
region = "ap-southeast-2"
endpointUrl = "http://localhost:8000"

[[dynamodb.createTable]]
name = "Create table"
tableArn = "arn:aws:dynamodb:ap-southeast-2:000000000000:table/Orders-dev"
billingMode = "pay-per-request"
ifNotExists = true
partitionKey = { name = "accountId", type = "S" }
sortKey = { name = "orderId", type = "S" }
timeToLive = { attribute = "expiresAt" }
globalSecondaryIndexes = [
  {
    name = "status-index",
    partitionKey = { name = "status", type = "S" },
    sortKey = { name = "orderId", type = "S" },
    projection = "keys-only",
  },
]

[[dynamodb.import]]
name = "Seed items"
dependsOn = "Create table"
tableArn = "arn:aws:dynamodb:ap-southeast-2:000000000000:table/Orders-dev"
conflictMode = "overwrite"

[dynamodb.import.source]
type = "inline"
items = [
  {
    accountId = "account#1",
    orderId = "order#1001",
    status = "NEW",
    total = { N = "19.99" },
    tags = { SS = ["gift", "priority"] },
  },
  { accountId = "account#1", orderId = "order#1002", status = "SHIPPED", total = 5 },
]

[[dynamodb.scan]]
name = "Count seeded"
dependsOn = "Seed items"
tableArn = "arn:aws:dynamodb:ap-southeast-2:000000000000:table/Orders-dev"
indexName = "status-index"
maxPages = 0

[[dynamodb.truncate]]
name = "Clear table"
dependsOn = "Count seeded"
tableArn = "arn:aws:dynamodb:ap-southeast-2:000000000000:table/Orders-dev"
segments = 4
guard = { expectTableName = "Orders-dev", expectItemCountAtMost = 1000 }

[[dynamodb.deleteTable]]
name = "Drop table"
dependsOn = "Clear table"
tableArn = "arn:aws:dynamodb:ap-southeast-2:000000000000:table/Orders-dev"
ifExists = true
guard = { expectTableName = "Orders-dev" }

ttl-control.dnml

Shows that ttlEnabled and enabled are different keys.

Operations: Update time to live.

version = "1.0"
name = "ttl-control"
description = "Distinguish the TTL setting from whether an operation runs."

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

[[dynamodb.updateTimeToLive]]
name = "Disable TTL"
tableArn = "arn:aws:dynamodb:ap-southeast-2:123456789012:table/Orders"
attribute = "expiresAt"
ttlEnabled = false

# enabled is omitted, so it defaults to true: execute and disable TTL.
# The required TTL field is distinct from the common operation enabled control.

[[dynamodb.updateTimeToLive]]
name = "Skipped TTL activation"
enabled = false
tableArn = "arn:aws:dynamodb:ap-southeast-2:123456789012:table/Orders"
attribute = "expiresAt"
ttlEnabled = true

# This operation is skipped because the common enabled field is false.
# With enabled omitted or true, ttlEnabled = true would enable TTL and the
# operation would be classified as destructive.

import-export.dnml

Export orders to a snapshot, then import from three sources.

Operations: Export items, Import items.

version = "1.0"
name = "import-export"
description = "Export production orders to a local snapshot, then load a curated subset into staging."

[variables]
prodAccount = "111122223333"
stagingAccount = "444455556666"

[execution]
allowDestructive = true

[[dynamodb.export]]
name = "Export production"
profileName = "commerce-prod"
tableArn = "arn:aws:dynamodb:ap-southeast-2:${prodAccount}:table/Orders"
snapshot = true
filterExpression = "#status <> :archived"
target = { type = "file", path = "exports/", format = "dynamodb-jsonl" }

[dynamodb.export.expressionAttributeNames]
"#status" = "status"

[dynamodb.export.expressionAttributeValues]
":archived" = "ARCHIVED"

[[dynamodb.import]]
name = "Load staging from export"
dependsOn = "Export production"
profileName = "commerce-staging"
tableArn = "arn:aws:dynamodb:ap-southeast-2:${stagingAccount}:table/Orders"
conflictMode = "skip-existing"
concurrency = 4
source = { type = "file", path = "${export_production.outputPath}", format = "jsonl" }

[[dynamodb.import]]
name = "Load fixtures from S3"
profileName = "commerce-staging"
tableArn = "arn:aws:dynamodb:ap-southeast-2:${stagingAccount}:table/Orders"
conflictMode = "overwrite"
source = { type = "s3", bucket = "commerce-fixtures", key = "orders/staging-fixtures.csv", region = "ap-southeast-2", format = "csv" }

[[dynamodb.import]]
name = "Mirror reference table"
profileName = "commerce-staging"
tableArn = "arn:aws:dynamodb:ap-southeast-2:${stagingAccount}:table/Products"

[dynamodb.import.source]
type = "dynamodb-table"
profileName = "commerce-prod"
tableArn = "arn:aws:dynamodb:ap-southeast-2:${prodAccount}:table/Products"
segments = 8

athena-report.dnml

An Athena query with text, number and date parameters.

Operations: Athena query.

version = "1.0"
name = "athena-report"
description = "Find paid orders using positional Athena execution parameters."

[defaults]
profileName = "analytics"
region = "ap-southeast-2"
workgroup = "dynomate"

[[athena.query]]
name = "Paid orders"
database = "commerce"
maxRows = 500
timeoutMs = 300000
sql = """
SELECT order_id, total, order_date
FROM orders
WHERE status = ?
  AND total >= ?
  AND order_date >= ?
"""
executionParameters = ["'PAID'", "100", "CAST('2026-09-01' AS DATE)"]

# Parameters bind to the three unquoted ? placeholders from left to right.
# Each TOML string contains a SQL literal or expression; DNML does not add SQL
# quotes. 'PAID' includes the required text quotes, 100 is a numeric SQL literal,
# and CAST(...) supplies a date. PREPARE and EXECUTE are not required.
# This SQL CAST is distinct from DNML's ${cast(...)} expression.

collection.dnml

The marker file that makes a folder a collection.

See Collection marker.

# Collection marker (collection.dnml). One of these per collection directory.
version = "1.0"
type = "dynomate-collection"
created = 2026-09-05T00:00:00Z

[metadata]
description = "Order service runbooks"
author = "Platform team"

[metadata.tags]
service = "orders"