Skill 116 · AWS Step Functions
Subchapter 116.4
references/migrating-from-jsonpath-to-jsonata.mdMarkdown11 KBView on GitHub
Complete conversion guide for migrating existing JSONPath state machines to JSONata. Covers fields, states, intrinsic functions, common pitfalls, and the end-to-end conversion workflow.
| JSONPath | JSONata |
|---|---|
InputPath | Not needed — use $states.input directly in Arguments |
Parameters | Arguments |
ResultSelector | Output (reference $states.result) |
ResultPath | Assign (preferred) or Output |
OutputPath | Output (return only what you need) |
TimeoutSecondsPath | TimeoutSeconds with {% %} |
HeartbeatSecondsPath | HeartbeatSeconds with {% %} |
ItemsPath | Items with {% %} |
"key.$": "$.field" | "key": "{% $states.input.field %}" |
$ or $.field (state input) | $states.input or $states.input.field |
$$ (context object) | $states.context |
$$.Execution.Input | $states.context.Execution.Input |
$$.Task.Token | $states.context.Task.Token |
$$.Map.Item.Value | $states.context.Map.Item.Value |
$variable (workflow var) | $variable (unchanged) |
Before (JSONPath):
"ProcessOrder": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"InputPath": "$.order",
"Parameters": {
"FunctionName": "arn:aws:lambda:us-east-1:123456789012:function:Process:$LATEST",
"Payload": { "id.$": "$.orderId", "customer.$": "$.customerName" }
},
"ResultSelector": { "processedId.$": "$.Payload.id", "status.$": "$.Payload.status" },
"ResultPath": "$.processingResult",
"OutputPath": "$.processingResult",
"Next": "Ship"
}After (JSONata):
"ProcessOrder": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Arguments": {
"FunctionName": "arn:aws:lambda:us-east-1:123456789012:function:Process:$LATEST",
"Payload": { "id": "{% $states.input.order.orderId %}", "customer": "{% $states.input.order.customerName %}" }
},
"Output": { "processedId": "{% $states.result.Payload.id %}", "status": "{% $states.result.Payload.status %}" },
"Next": "Ship"
}Before (JSONPath):
"InjectDefaults": {
"Type": "Pass",
"Result": { "region": "us-east-1" },
"ResultPath": "$.config",
"Next": "Go"
}After (JSONata):
"InjectDefaults": {
"Type": "Pass",
"Assign": { "region": "us-east-1" },
"Next": "Go"
}JSONPath uses Variable + typed operators. JSONata uses a single Condition expression.
Before (JSONPath):
"Choices": [
{ "Variable": "$.status", "StringEquals": "approved", "Next": "Approved" },
{ "And": [
{ "Variable": "$.priority", "StringEquals": "high" },
{ "Variable": "$.age", "NumericLessThanEquals": 30 }
], "Next": "FastTrack" },
{ "Not": { "Variable": "$.email", "IsPresent": true }, "Next": "RequestEmail" }
]After (JSONata):
"Choices": [
{ "Condition": "{% $states.input.status = 'approved' %}", "Next": "Approved" },
{ "Condition": "{% $states.input.priority = 'high' and $states.input.age <= 30 %}", "Next": "FastTrack" },
{ "Condition": "{% $not($exists($states.input.email)) %}", "Next": "RequestEmail" }
]| JSONPath Operator | JSONata |
|---|---|
StringEquals / StringEqualsPath | = 'value' / = $states.input.other |
NumericGreaterThan / NumericLessThanEquals | > value / <= value |
BooleanEquals | = true / = false |
TimestampGreaterThan | $toMillis(field) > $toMillis('ISO-timestamp') |
IsPresent: true / false | $exists(field) / $not($exists(field)) |
IsNull: true | field = null |
IsNumeric / IsString / IsBoolean | $type(field) = 'number' / 'string' / 'boolean' |
StringMatches (wildcards) | $contains(field, /regex/) |
And / Or / Not | and / or / $not() |
Before (JSONPath):
{ "Type": "Wait", "TimestampPath": "$.deliveryDate", "Next": "Check" }After (JSONata):
{ "Type": "Wait", "Timestamp": "{% $states.input.deliveryDate %}", "Next": "Check" }| JSONPath | JSONata |
|---|---|
ItemsPath | Items (fold InputPath into expression) |
Parameters (with $$.Map.*) | ItemSelector (with $states.context.Map.*) |
Iterator | ItemProcessor (add ProcessorConfig) |
ResultSelector inside iterator | Output inside processor states |
ResultPath on Map | Assign |
After (JSONata):
"ProcessItems": {
"Type": "Map",
"Items": "{% $states.input.orderData.items %}",
"ItemSelector": {
"item": "{% $states.context.Map.Item.Value %}",
"index": "{% $states.context.Map.Item.Index %}"
},
"MaxConcurrency": 5,
"ItemProcessor": {
"ProcessorConfig": { "Mode": "INLINE" },
"StartAt": "Process",
"States": {
"Process": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Arguments": { "FunctionName": "arn:aws:lambda:us-east-1:123456789012:function:Process:$LATEST", "Payload": "{% $states.input %}" },
"Output": "{% $states.result.Payload %}",
"End": true
}
}
},
"Assign": { "processedItems": "{% $states.result %}" },
"Next": "Done"
}| JSONPath Intrinsic | JSONata Equivalent |
|---|---|
States.Format('Order {}', $.id) | 'Order ' & $states.input.id |
States.StringToJson($.str) | $parse($states.input.str) |
States.JsonToString($.obj) | $string($states.input.obj) |
States.StringSplit($.str, ',') | $split($states.input.str, ',') |
States.Array($.a, $.b) | [$states.input.a, $states.input.b] |
States.ArrayPartition($.arr, 2) | $partition($states.input.arr, 2) |
States.ArrayContains($.arr, $.v) | $states.input.v in $states.input.arr |
States.ArrayRange(0, 10, 2) | $range(0, 10, 2) |
States.ArrayGetItem($.arr, 0) | $states.input.arr[0] |
States.ArrayLength($.arr) | $count($states.input.arr) |
States.ArrayUnique($.arr) | $distinct($states.input.arr) |
States.Base64Encode($.str) | $base64encode($states.input.str) |
States.Base64Decode($.str) | $base64decode($states.input.str) |
States.Hash($.data, 'SHA-256') | $hash($states.input.data, 'SHA-256') |
States.JsonMerge($.a, $.b) | $merge([$states.input.a, $states.input.b]) |
States.MathRandom() | $random() |
States.MathAdd($.a, $.b) | $states.input.a + $states.input.b |
States.UUID() | $uuid() |
JSONPath Catch uses ResultPath. JSONata Catch uses Assign and Output with $states.errorOutput.
Before (JSONPath):
"Catch": [{ "ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "HandleError" }]After (JSONata):
"Catch": [{
"ErrorEquals": ["States.ALL"],
"Assign": { "errorInfo": "{% $states.errorOutput %}" },
"Output": "{% $states.input %}",
"Next": "HandleError"
}]Retry syntax is identical between JSONPath and JSONata — no conversion needed.
Invalid combinations: Arguments + InputPath, Output + ResultSelector, Condition + Variable. Remove all JSONPath fields from converted states.
❌ "orderId.$": "{% $states.input.orderId %}"
✓ "orderId": "{% $states.input.orderId %}"❌ "{% $.orderId %}" ❌ "{% $$.Task.Token %}"
✓ "{% $states.input.orderId %}" ✓ "{% $states.context.Task.Token %}"Note: $ is valid inside nested filter expressions (e.g., $states.input.items[$.price > 10]).
❌ "{% $states.input.status = "active" %}"
✓ "{% $states.input.status = 'active' %}"Assign and Output evaluate in parallel — new variable values are not available until the next state.
❌ "Assign": { "total": "{% $states.result.Payload.total %}" },
"Output": { "total": "{% $total %}" }
✓ "Assign": { "total": "{% $states.result.Payload.total %}" },
"Output": { "total": "{% $states.result.Payload.total %}" }JSONPath silently returns null. JSONata throws States.QueryEvaluationError:
❌ "{% $states.input.customer.middleName %}"
✓ "{% $exists($states.input.customer.middleName) ? $states.input.customer.middleName : '' %}"JSONata returns a single object (not a 1-element array) when exactly one item matches a filter, and undefined when nothing matches. Both break Map state Items and functions like $count:
❌ "Items": "{% $states.input.orders[status = 'pending'] %}"
✓ "Items": "{% ( $f := $states.input.orders[status = 'pending']; $type($f) = 'array' ? $f : $exists($f) ? [$f] : [] ) %}"Iterator was renamed to ItemProcessor and requires ProcessorConfig:
❌ "Iterator": { "StartAt": "...", "States": {...} }
✓ "ItemProcessor": { "ProcessorConfig": { "Mode": "INLINE" }, "StartAt": "...", "States": {...} }For each state being converted, apply these steps in order:
"QueryLanguage": "JSONata" to the stateParameters → Arguments: remove .$ suffixes from all keys, wrap values in {% %}, replace $ with $states.input and $$ with $states.contextResultPath based on its value:
"$" → no action needed (default behavior is replaced by Output)null → add "Output": "{% $states.input %}""$.field" → add "Assign": { "field": "{% $states.result %}" } and "Output": "{% $states.input %}"ResultSelector → fold selection logic into Output (reference $states.result)OutputPath → fold into Output (return only what you need)ResultSelector + ResultPath + OutputPath, collapse all three into a single Output fieldInputPath, Parameters, ResultSelector, ResultPath, OutputPath*Path fields to base field + {% %} expression (TimeoutSecondsPath → TimeoutSeconds, HeartbeatSecondsPath → HeartbeatSeconds, ItemsPath → Items)States.* intrinsic functions with JSONata equivalents (see Converting Intrinsic Functions table)Variable + comparison operators with a single Condition expressionIterator → ItemProcessor with ProcessorConfig, ItemsPath → Items, Parameters with $$.Map.* → ItemSelector with $states.context.Map.*ResultPath with Assign + Output using $states.errorOutputResult with Output or AssignResultPath to thread data through the payload, refactor to use Assign variables instead — downstream states reference $variableName directly"QueryLanguage": "JSONata" to the top level and remove per-state declarations