AWS Step Functions skill for Codex
Workflows where every failure has somewhere to go. Codex writes state machines where each Task retries only transient errors, catches named business errors into a compensation path that keeps the original input, and has a timeout. It chooses Standard or Express from duration and delivery guarantees, uses direct service integrations instead of wrapper Lambdas, and tests individual states with the TestState API before deploying.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- aws-step-functions Installs to ~/.agents/skills/aws-step-functions/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install aws-step-functions The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project aws-step-functions Installs to .agents/skills/aws-step-functions/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/aws-step-functions.zipunzip -o aws-step-functions.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) aws-step-functions PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $aws-step-functions, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs and debugs AWS Step Functions state machines in Amazon States Language - Standard vs Express choice, JSONata or JSONPath data flow, Retry with backoff and jitter, Catch and compensation (saga) paths, timeouts and heartbeats, .sync and waitForTaskToken integrations, Map and Distributed Map, plus testing with the TestState API and redriving failed executions. Use when orchestrating multi-step workflows, adding error handling to a state machine, an execution fails or hangs, choosing Express vs Standard, or processing large S3 datasets in parallel.
Use it when
- Orchestrating several Lambdas, containers or AWS API calls
- Adding error handling, timeouts or human approval to a workflow
- An execution fails, hangs or hits the history limit
- Fanning out over a large S3 dataset
Use something else when
- You only need to route events between services: use aws-eventbridge
- The work is a simple queue with retries: use aws-sqs-sns
How to check the result
TestState in DEBUG mode shows correct output for success and failure inputs of each changed state, validate-state-machine-definition reports no diagnostics, and a dev execution exercises every new branch.
The mistake it prevents
Catching an error without keeping the original input, so the compensation step receives only the error and cannot undo the order.
Done well means: every Task has a Retry for transient errors and a Catch that leads to a defined outcome, every wait has a timeout, the workflow type matches its duration and delivery guarantee, payloads stay small, and each state’s behaviour was exercised with TestState before the whole machine was deployed.
Before changing anything
- Read
AGENTS.mdand find the definition: an.asl.jsonfile, a CDKDefinitionBody, a SAMDefinitionUri, or Terraform. Edit it there. - Note the type (
STANDARDorEXPRESS), the query language ("QueryLanguage": "JSONata"at the top or per state; JSONPath otherwise), and the role’s permissions. - For a failing workflow, read the failing execution (needs network and credentials):
aws stepfunctions list-executions --state-machine-arn $SM --status-filter FAILED --max-items 5
aws stepfunctions get-execution-history --execution-arn $EXEC --reverse-order --max-items 20 \
--query 'events[].{t:type,id:id,err:[taskFailedEventDetails.error,executionFailedEventDetails.error],cause:taskFailedEventDetails.cause}'
Standard vs Express
| Factor | Standard | Express |
|---|---|---|
| Max duration | 1 year | 5 minutes |
| Semantics | Exactly-once state transitions | At-least-once (async) or at-most-once (sync) |
.sync and waitForTaskToken | Yes | No |
| Pricing | Per state transition | Per request plus duration and memory |
| History | Visible in console/API for 90 days | CloudWatch Logs only (enable logging) |
| Redrive failed executions | Yes | No |
| Typical use | Business processes, human approval, long jobs | High-volume event processing, API-backing workflows, Distributed Map children |
Rule of thumb: anything that waits on people or long jobs, or must not run twice, is Standard. High-volume, short and idempotent is Express. A Standard parent can call Express children to combine both.
Error handling pattern
{
"QueryLanguage": "JSONata",
"StartAt": "ChargeCard",
"States": {
"ChargeCard": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Arguments": {
"FunctionName": "arn:aws:lambda:eu-west-1:111111111111:function:charge-card:live",
"Payload": "{% $states.input %}"
},
"Output": "{% $states.result.Payload %}",
"TimeoutSeconds": 30,
"Retry": [
{
"ErrorEquals": ["Lambda.ServiceException", "Lambda.AWSLambdaException",
"Lambda.SdkClientException", "Lambda.TooManyRequestsException"],
"IntervalSeconds": 1, "MaxAttempts": 3, "BackoffRate": 2, "JitterStrategy": "FULL"
},
{
"ErrorEquals": ["PaymentProviderUnavailable"],
"IntervalSeconds": 5, "MaxAttempts": 4, "BackoffRate": 2, "MaxDelaySeconds": 60
}
],
"Catch": [
{
"ErrorEquals": ["CardDeclined"],
"Output": "{% $merge([$states.input, {'error': $states.errorOutput}]) %}",
"Next": "NotifyDeclined"
},
{
"ErrorEquals": ["States.ALL"],
"Output": "{% $merge([$states.input, {'error': $states.errorOutput}]) %}",
"Next": "ReleaseInventory"
}
],
"Next": "ShipOrder"
}
}
}
Rules:
- Retry only errors that can succeed on retry: throttling, service errors, timeouts,
your own transient error names. Never retry
States.ALLblindly; a validation error retried 3 times is just slower failure. - Throw named errors from your code (a Lambda raising
class CardDeclined(Exception)surfaces as errorCardDeclined) so Retry and Catch can match them precisely. - Order matters: the first matching Retry and the first matching Catch win. Put specific
errors before
States.ALL, which must be last. - Keep the original input when catching. In JSONPath use
"ResultPath": "$.error"; in JSONata merge$states.errorOutputinto the input as above. Without this the catch handler receives only the error and loses the order data. - Every Task gets
TimeoutSeconds. The default is effectively unlimited, so a hung integration holds a Standard execution open for up to a year. - Callback tasks (
.waitForTaskToken) also getHeartbeatSeconds, and the worker sendsSendTaskHeartbeatwhile it runs.
Compensation (saga)
For multi-step business transactions, each step that has a side effect gets a matching undo state, and Catch routes to the undo chain for the steps already completed:
ReserveInventory -> ChargeCard -> ShipOrder -> Succeed
| | |
v v v
Fail <- ReleaseInventory <- RefundCard
Compensating actions must be idempotent and retried aggressively; a failed compensation should alert a human (SNS, ticket) rather than fail silently.
Integration patterns
| Pattern | Resource suffix | Use when |
|---|---|---|
| Request-response | arn:aws:states:::sns:publish | Fire and continue |
| Run a job and wait | arn:aws:states:::ecs:runTask.sync, glue:startJobRun.sync, states:startExecution.sync:2 | Step Functions polls the job for you |
| Callback | arn:aws:states:::sqs:sendMessage.waitForTaskToken | Human approval, external systems; pass $states.context.Task.Token |
| Any AWS API | arn:aws:states:::aws-sdk:dynamodb:putItem | No optimized integration exists; error names become DynamoDb.ConditionalCheckFailedException style |
Call AWS services directly from the state machine instead of wrapping each API call in a Lambda; fewer functions, fewer cold starts, fewer failure points.
Map and Distributed Map
| Items | Use |
|---|---|
| Up to a few hundred, small, in the state input | Inline Map with MaxConcurrency |
| Thousands to millions, from S3 (CSV, JSON, JSON Lines, object listing) | Distributed Map with ItemReader, Express child executions, ResultWriter to S3 |
Set MaxConcurrency to what downstream systems can take, and
ToleratedFailurePercentage so one bad record does not fail a million-item run. Batch
items (ItemBatcher) when per-item work is tiny.
Limits that shape designs
- State input/output payload: 256 KiB at time of writing. Pass S3 keys or IDs, not documents.
- Standard execution history: 25,000 events. Long loops must hand off to a child execution (or use Distributed Map) before hitting it.
- Variables (
Assign, JSONata or JSONPath) reduce the need to carry everything through every state’s output; prefer them to ever-growing payloads.
Data flow details for both query languages (Arguments/Output/Assign vs InputPath/Parameters/ResultSelector/ResultPath/OutputPath, context object fields and intrinsic functions) are in references/data-flow.md. Read it when writing or converting state input/output mappings.
Testing
- Unit-test the code behind each Lambda task normally.
- Test single states against the real service with the TestState API (needs network and credentials; it runs the state for real, so use non-production resources):
aws stepfunctions test-state \
--definition file://states/charge-card.json \
--role-arn arn:aws:iam::111111111111:role/sfn-test \
--input '{"orderId":"o-1","amount":42}' \
--inspection-level DEBUG
DEBUG shows the input and output after every data-flow step, which is how you debug mappings. Test Catch paths by feeding an input that makes the task fail. 3. Validate the full definition before deploying:
aws stepfunctions validate-state-machine-definition --definition file://order.asl.json \
--query 'diagnostics'
- Run an execution in a dev stack and check every branch you changed.
Operations
- Redrive a failed Standard execution from the failed state after fixing the cause:
aws stepfunctions redrive-execution --execution-arn $EXEC. This re-runs side effects from that state onward; confirm with the user first for production. - Publish versions and route an alias for safe rollouts of definition changes.
- Enable CloudWatch logging (
ALLwith execution data for Express in dev,ERRORin production if payloads contain personal data) and X-Ray tracing. - Alarm on
ExecutionsFailed,ExecutionsTimedOutandExecutionThrottled.
Gotchas
- The optimized
lambda:invokeintegration wraps the function result in{ "Payload": ..., "StatusCode": ... }. SelectPayloador downstream states read the wrapper. - Choice states without a
Defaultfail withStates.NoChoiceMatchedon unexpected input. - Mixing JSONPath and JSONata fields in one state is invalid; convert a state completely.
- The state machine role needs permission for every integration, including
states:StartExecutionplus EventBridge managed-rule permissions for.syncchild executions. - Changing a definition does not affect running Standard executions; they finish on the version they started with.
What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "AWS Step Functions"
short_description: "Workflows where every failure has somewhere to go"
default_prompt: "$aws-step-functions Add retries and a compensation path to this order workflow state machine" references/data-flow.md
# Step Functions data flow reference
Read this when writing or converting the input/output mapping of states. Pick one query
language per state; JSONata is the better default for new work because one `Output` field
replaces four JSONPath filters and it has real expressions.
## JSONata states
Expressions are strings wrapped in `{% ... %}`. Anything else is a literal.
| Field | Purpose | Replaces (JSONPath) |
|---|---|---|
| `Arguments` | Build the request sent to the task or the input of a Parallel/Map branch | `InputPath` + `Parameters` |
| `Output` | Shape what the state passes on; default is the task result | `ResultSelector` + `ResultPath` + `OutputPath` |
| `Assign` | Store variables for later states | (variables also work in JSONPath) |
| `Condition` | Boolean expression in a Choice rule | `Variable` + comparison operators |
| `Items` | Array for a Map state | `ItemsPath` |
Reserved variable `$states`:
| Expression | Value |
|---|---|
| `$states.input` | The state's raw input |
| `$states.result` | Task, Parallel or Map result (only in `Output` and `Assign`) |
| `$states.errorOutput` | Error and cause (only in `Catch` `Output`/`Assign`) |
| `$states.context` | Context object: `Execution.Id`, `Execution.StartTime`, `State.EnteredTime`, `State.RetryCount`, `Task.Token`, `Map.Item.Value` |
Examples:
```json
"Assign": { "orderId": "{% $states.input.order.id %}", "total": "{% $sum($states.input.order.lines.price) %}" },
"Output": "{% { 'orderId': $orderId, 'chargeId': $states.result.Payload.id } %}"
```
```json
{
"Type": "Choice",
"Choices": [
{ "Condition": "{% $total >= 1000 %}", "Next": "ManualReview" },
{ "Condition": "{% $states.input.country in ['US','CA'] %}", "Next": "DomesticShipping" }
],
"Default": "InternationalShipping"
}
```
Useful built-ins beyond standard JSONata: `$partition`, `$range`, `$hash`, `$random`,
`$uuid`, `$parse` (string to JSON). `$now()` and other non-deterministic functions are
evaluated when the state runs.
## JSONPath states
Processing order for a Task: `InputPath` selects part of the input, `Parameters` builds
the request (keys ending in `.$` take a path or intrinsic), the task runs,
`ResultSelector` reshapes the raw result, `ResultPath` places it into the original input,
`OutputPath` filters what is passed on.
```json
{
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": { "FunctionName": "charge-card", "Payload.$": "$.order" },
"ResultSelector": { "chargeId.$": "$.Payload.id" },
"ResultPath": "$.charge",
"OutputPath": "$",
"Catch": [{ "ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "Compensate" }],
"Next": "Ship"
}
```
- `"ResultPath": null` discards the result and passes the input through unchanged.
- `"ResultPath": "$"` (the default) replaces the whole input with the result; the common
cause of "where did my order data go".
- Context object: `$$.Execution.Id`, `$$.Task.Token`, `$$.Map.Item.Value`.
Intrinsic functions (JSONPath only): `States.Format('order-{}', $.id)`,
`States.StringToJson($.body)`, `States.JsonToString($)`, `States.Array(...)`,
`States.ArrayPartition($.items, 50)`, `States.ArrayLength($.items)`,
`States.MathAdd($.n, 1)`, `States.UUID()`, `States.Hash($.x, 'SHA-256')`,
`States.Base64Encode(...)`.
## Converting JSONPath to JSONata
| JSONPath | JSONata |
|---|---|
| `"Parameters": { "Payload.$": "$.order" }` | `"Arguments": { "Payload": "{% $states.input.order %}" }` |
| `"ResultSelector": { "id.$": "$.Payload.id" }` plus `"ResultPath": "$.charge"` | `"Output": "{% $merge([$states.input, {'charge': {'id': $states.result.Payload.id}}]) %}"` |
| `"ResultPath": null` | `"Output": "{% $states.input %}"` |
| `"Variable": "$.total", "NumericGreaterThanEquals": 1000` | `"Condition": "{% $states.input.total >= 1000 %}"` |
| `"ItemsPath": "$.lines"` | `"Items": "{% $states.input.lines %}"` |
| `States.Format('order-{}', $.id)` | `"{% 'order-' & $states.input.id %}"` |
| `$$.Task.Token` | `$states.context.Task.Token` |
Convert a whole state at a time, then run TestState with `--inspection-level DEBUG` on it
to compare the output with the old version.
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.