Session 023 — Step Functions: Parallel, Map, data flow between states and error handling
Estimated duration: 60 minutes
Prerequisites: session-022-stepfunctions-standard-express-states
Objective
By the end, you will be able to use Parallel for simultaneous execution of independent branches and Map for iterating over arrays (inline vs distributed mode), master the InputPath, OutputPath, ResultPath and Parameters fields to control data flow between states without surprises, and implement Retry with exponential backoff and Catch by error type with redirection to a fallback state.
Context
[FACT] The previous session covered the basic Step Functions states (Task, Choice, Wait, Succeed, Fail) and the differences between Standard and Express Workflows. This session advances to the three mechanisms that make Step Functions useful in real production scenarios: structured parallelism (Parallel), iteration over collections (Map), and declarative failure handling (Retry + Catch).
[CONSENSUS] The data flow between states — controlled by the InputPath, Parameters, ResultSelector, ResultPath, and OutputPath fields — is consistently pointed out by the community as the most confusing part of Step Functions. Most bugs in real workflows come from developers who don't understand the application order of these filters, resulting in data being silently lost or overwritten. Mastering this pipeline is as important as knowing the state types.
[FACT] The Map state gained a second mode of operation — Distributed — which allows up to 10,000 parallel iterations running as independent child executions. This mode was introduced in 2022 and is the foundation for large-scale data processing pipelines directly in Step Functions, without the need for external tools like AWS Glue for simple orchestration.
Main concepts
1. Parallel State — concurrent branches with aggregated output
[FACT] The Parallel state executes multiple branches (sub-workflows) simultaneously. Each branch is a complete mini state machine with StartAt and States. Step Functions waits for all branches to reach a terminal state before advancing to the Parallel's Next.
Parallel State
───────────────────────────────────────────────────────────────────────
┌──── Branch 1 ─────┐
Input ──► Parallel ─┤ ├──► Output (array[branch1, branch2, branch3])
├──── Branch 2 ─────┤
└──── Branch 3 ─────┘
(todos rodam ao mesmo tempo)
(espera o mais lento)
[FACT] The output of Parallel is always an array with one element per branch, in the order they were declared. The output of each branch is the output of its last state.
"EnriquecerPedido": {
"Type": "Parallel",
"Branches": [
{
"StartAt": "BuscarCliente",
"States": {
"BuscarCliente": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "BuscarCliente",
"Payload.$": "$"
},
"ResultSelector": { "cliente.$": "$.Payload" },
"End": true
}
}
},
{
"StartAt": "BuscarEstoque",
"States": {
"BuscarEstoque": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "BuscarEstoque",
"Payload.$": "$"
},
"ResultSelector": { "estoque.$": "$.Payload" },
"End": true
}
}
}
],
"ResultPath": "$.enriquecimento",
"Next": "ProcessarPedidoEnriquecido"
}
After the Parallel above, $.enriquecimento will be:
[
{ "cliente": { "nome": "...", "credito": 5000 } },
{ "estoque": { "sku": "P001", "disponivel": 12 } }
]
[FACT] If any branch fails with an unhandled error within the branch, the entire Parallel state fails immediately (the other branches are cancelled). The Parallel can have its own Retry and Catch fields to handle failures from any branch.
[CONSENSUS] A common pitfall is expecting each branch to receive a different input. In reality, all branches receive the same input — the effective input of the Parallel state (after InputPath/Parameters). To pass different data to each branch, transform the input within the first state of each branch.
2. Map State — iteration over arrays (Inline vs Distributed)
[FACT] The Map state iterates over an array (in the input or from an external source) and executes a sub-workflow for each item. The two modes have radically different characteristics:
┌──────────────────┬───────────────────────────┬────────────────────────────────┐
│ Dimensão │ Inline Map │ Distributed Map │
├──────────────────┼───────────────────────────┼────────────────────────────────┤
│ MaxConcurrency │ Até 40 │ Até 10.000 │
├──────────────────┼───────────────────────────┼────────────────────────────────┤
│ Fonte dos itens │ Array no input JSON │ Array JSON, CSV ou lista S3 │
├──────────────────┼───────────────────────────┼────────────────────────────────┤
│ Execução filho │ Inline (mesmo execution) │ Child workflow (execução própria│
├──────────────────┼───────────────────────────┼────────────────────────────────┤
│ Execution history│ Embutido no pai │ Separado por iteração │
├──────────────────┼───────────────────────────┼────────────────────────────────┤
│ Resultados │ Array no output do Map │ Pode escrever em S3 │
├──────────────────┼───────────────────────────┼────────────────────────────────┤
│ Tolerância falha │ ToleratedFailureCount/ │ ToleratedFailureCount/ │
│ │ Percentage │ Percentage │
└──────────────────┴───────────────────────────┴────────────────────────────────┘
Map Inline
"ProcessarItens": {
"Type": "Map",
"ItemsPath": "$.itens",
"ItemSelector": {
"item_id.$": "$$.Map.Item.Value.id",
"indice.$": "$$.Map.Item.Index",
"contexto.$": "$.contexto_global"
},
"MaxConcurrency": 10,
"ToleratedFailurePercentage": 20,
"Iterator": {
"StartAt": "ProcessarUmItem",
"States": {
"ProcessarUmItem": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "ProcessarItem",
"Payload.$": "$"
},
"ResultSelector": { "resultado.$": "$.Payload" },
"End": true
}
}
},
"ResultPath": "$.resultados",
"Next": "Finalizar"
}
[FACT] Important Map fields:
| Campo | Descrição |
|---|---|
ItemsPath |
JsonPath para o array no input. Default: $ (input inteiro deve ser array) |
ItemSelector |
Constrói o input de cada iteração. $$.Map.Item.Value = item atual; $$.Map.Item.Index = índice |
MaxConcurrency |
Máximo de iterações paralelas. 0 = sem limite (até 40 em Inline) |
ToleratedFailureCount |
Número absoluto de falhas toleradas antes do Map falhar |
ToleratedFailurePercentage |
Percentual de falhas toleradas (0-100) |
[FACT] $$.Map.Item.Value and $$.Map.Item.Index are references to the Context Object specific to Map — accessible only within ItemSelector. $$ accesses the execution context; $ accesses the item already transformed by ItemSelector.
Map Distributed
"ProcessarCSVGrande": {
"Type": "Map",
"ItemReader": {
"Resource": "arn:aws:states:::s3:getObject",
"ReaderConfig": {
"InputType": "CSV",
"CSVHeaderLocation": "FIRST_ROW"
},
"Parameters": {
"Bucket": "meu-bucket",
"Key": "dados/pedidos.csv"
}
},
"MaxConcurrency": 1000,
"ToleratedFailurePercentage": 5,
"ItemSelector": {
"linha.$": "$$.Map.Item.Value"
},
"ItemBatcher": {
"MaxItemsPerBatch": 10,
"MaxInputBytesPerBatch": 262144
},
"Iterator": {
"StartAt": "ProcessarLote",
"States": {
"ProcessarLote": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "ProcessarLote",
"Payload.$": "$"
},
"End": true
}
}
},
"ResultWriter": {
"Resource": "arn:aws:states:::s3:putObject",
"Parameters": {
"Bucket": "meu-bucket",
"Prefix": "resultados/"
}
},
"Next": "Finalizar"
}
[FACT] ItemBatcher is exclusive to Distributed mode: it groups multiple items from the array into a single input for each iteration, reducing the number of invocations and increasing efficiency for workloads with many small items.
[FACT] ResultWriter writes the results of all iterations to S3 instead of returning them inline in the Map output — essential when the volume of results would exceed the 256KB state payload limit.
3. Data pipeline between states — order matters
[FACT] Step Functions applies five filters in sequence before passing control to the next state. Understanding this order is critical to avoid losing data or corrupting the flow:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PIPELINE DE DADOS EM UM ESTADO
Raw Input (output do estado anterior)
│
▼
┌──────────┐
│InputPath │ Filtra o input bruto. Default: "$" (tudo).
│ │ null → passa {} vazio para Parameters.
└────┬─────┘
│ Effective Input
▼
┌────────────┐
│ Parameters │ Constrói o payload enviado ao serviço.
│ │ Chaves com ".$" são referências JsonPath no Effective Input.
└─────┬──────┘
│ Task Input
▼
╔═════════╗
║ SERVIÇO ║ Lambda / DynamoDB / SQS / etc.
╚═════╤═══╝
│ Raw Result (response do serviço)
▼
┌────────────────┐
│ ResultSelector │ Filtra/reshapes o resultado bruto.
│ │ Chaves com ".$" são referências no Raw Result.
└───────┬────────┘
│ Selected Result
▼
┌────────────┐
│ ResultPath │ Onde gravar o Selected Result no Effective Input.
│ │ "$" → substitui o Effective Input inteiro.
│ │ null → descarta o resultado, passa Effective Input.
│ │ "$.x"→ adiciona/sobrescreve o campo "x".
└──────┬─────┘
│ Effective Output
▼
┌────────────┐
│ OutputPath │ Filtra o Effective Output antes de enviar.
│ │ Default: "$" (tudo). null → passa {}.
└──────┬─────┘
│
▼
Input do próximo estado
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[FACT] Concrete example to solidify each step:
// Input bruto recebido pelo estado:
// { "pedido": { "id": "P001", "valor": 1500 }, "contexto": { "usuario": "U42" } }
{
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
// 1. InputPath: usa só o campo "pedido" como effective input
"InputPath": "$.pedido",
// effective input: { "id": "P001", "valor": 1500 }
// 2. Parameters: constrói o payload para a Lambda
"Parameters": {
"FunctionName": "ValidarPedido",
"Payload": {
"pedido_id.$": "$.id",
"valor.$": "$.valor",
"timestamp.$": "$$.Execution.StartTime"
}
},
// task input para Lambda: { "pedido_id": "P001", "valor": 1500, "timestamp": "2026-06-22T..." }
// 3. [Lambda executa e retorna]:
// { "StatusCode": 200, "Payload": { "aprovado": true, "score": 95 } }
// 4. ResultSelector: pega só o que interessa do resultado da Lambda
"ResultSelector": {
"aprovado.$": "$.Payload.aprovado",
"score.$": "$.Payload.score"
},
// selected result: { "aprovado": true, "score": 95 }
// 5. ResultPath: grava no effective input ORIGINAL (InputPath=$.pedido)
// ATENÇÃO: ResultPath é aplicado sobre o effective input (após InputPath),
// não sobre o raw input.
"ResultPath": "$.validacao",
// effective output: { "id": "P001", "valor": 1500, "validacao": { "aprovado": true, "score": 95 } }
// 6. OutputPath: envia apenas o necessário
"OutputPath": "$.validacao",
// output para próximo estado: { "aprovado": true, "score": 95 }
"Next": "VerificarAprovacao"
}
[FACT] Critical point frequently confused: ResultPath is applied on the effective input (output of InputPath), not on the original raw input. If InputPath filtered to $.pedido, then the effective input no longer has $.contexto. When using ResultPath: "$.validacao", the result is added to the filtered $.pedido, not to the original complete input. To preserve the complete input, avoid using InputPath or use InputPath: "$".
4. Error Handling — Retry and Catch
[FACT] Error handling in Step Functions is declarative: you specify, within the state, how to react to errors — without external code or retry lambdas. Retry is always evaluated before Catch.
Built-in error codes
[FACT] Step Functions defines a set of reserved errors with the States. prefix:
States.ALL — wildcard: casa com qualquer erro
States.TaskFailed — a Task retornou falha
States.Timeout — TimeoutSeconds excedido
States.HeartbeatTimeout — HeartbeatSeconds excedido sem heartbeat
States.NoChoiceMatched — Choice sem Default e nenhuma regra casou
States.ResultPathMatchFailure — ResultPath não pôde ser aplicado
States.ParameterPathFailure — referência JsonPath falhou em Parameters
States.BranchFailed — branch de Parallel falhou
States.ExceedToleratedFailureThreshold — Map ultrapassou tolerância de falha
States.Runtime — erro de runtime não classificado
External service errors follow the convention <Service>.<ErrorType>, for example:
- Lambda.ServiceException — Lambda internal error
- Lambda.TooManyRequestsException — throttling
- Lambda.AWSLambdaException — function execution error
- DynamoDB.ProvisionedThroughputExceededException
Retry — exponential backoff with jitter
[FACT] The Retry array is evaluated in order. The first ErrorEquals that matches is applied. Specific errors must come before States.ALL.
"ProcessarPagamento": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": { "FunctionName": "ProcessarPagamento", "Payload.$": "$" },
"Retry": [
{
"ErrorEquals": ["Lambda.TooManyRequestsException", "Lambda.ServiceException"],
"IntervalSeconds": 1,
"MaxAttempts": 5,
"BackoffRate": 2.0,
"MaxDelaySeconds": 30,
"JitterStrategy": "FULL"
},
{
"ErrorEquals": ["PagamentoTemporarioIndisponivel"],
"IntervalSeconds": 10,
"MaxAttempts": 3,
"BackoffRate": 1.5
},
{
"ErrorEquals": ["States.ALL"],
"IntervalSeconds": 5,
"MaxAttempts": 2,
"BackoffRate": 1.0
}
],
"Catch": [ ... ],
"Next": "PagamentoAprovado"
}
[FACT] Retry fields:
| Campo | Obrigatório | Default | Descrição |
|---|---|---|---|
ErrorEquals |
Sim | — | Array de códigos de erro que ativam esta regra |
IntervalSeconds |
Não | 1 | Espera inicial antes do primeiro retry |
MaxAttempts |
Não | 3 | Total de tentativas (0 = sem retry) |
BackoffRate |
Não | 2.0 | Multiplicador aplicado a cada retry |
MaxDelaySeconds |
Não | sem limite | Cap no intervalo máximo entre retries |
JitterStrategy |
Não | NONE |
FULL adiciona jitter aleatório (0 a intervalo calculado) |
[FACT] Interval calculation with backoff and FULL jitter:
Tentativa 1: random(0, 1) segundos (IntervalSeconds=1, BackoffRate=2)
Tentativa 2: random(0, 2) segundos
Tentativa 3: random(0, 4) segundos
Tentativa 4: random(0, 8) segundos → se MaxDelaySeconds=10, cap em 10
Tentativa 5: random(0, 10) segundos
[CONSENSUS] JitterStrategy: FULL is recommended when multiple parallel executions can fail simultaneously (e.g., multiple Lambdas hitting the same throttled service). Without jitter, all retries happen at the same intervals, creating load waves — the phenomenon called thundering herd.
Catch — fallback after retries are exhausted
[FACT] After all retries of a rule fail, Catch is evaluated. The Catch array is also evaluated in order — first match wins.
"Catch": [
{
"ErrorEquals": ["PedidoDuplicado", "LimiteCredito"],
"ResultPath": "$.erro_negocio",
"Next": "TratarErroDenegocio"
},
{
"ErrorEquals": ["States.Timeout"],
"ResultPath": "$.erro_timeout",
"Next": "NotificarTimeout"
},
{
"ErrorEquals": ["States.ALL"],
"ResultPath": "$.erro",
"Next": "FallbackGenerico"
}
]
[FACT] When Catch is triggered, Step Functions injects into the next state an error object with the fields Error and Cause. The ResultPath controls where this object is inserted:
// Sem ResultPath no Catch (ou ResultPath: "$"):
// Input do estado de fallback = { "Error": "Lambda.ServiceException", "Cause": "..." }
// (o input original é completamente substituído)
// Com ResultPath: "$.erro":
// Input do estado de fallback = { ...input_original..., "erro": { "Error": "...", "Cause": "..." } }
// (input original preservado + erro adicionado)
[CONSENSUS] Always use ResultPath in Catch to preserve the original context. Without it, the fallback state receives only the error message, without the order/customer/etc. data that would be needed to log, compensate, or notify.
Complete flow: Retry → Catch
Estado Task é invocado
│
▼ erro ocorre
┌──────────────┐
│ Retry[0] │ → Verifica ErrorEquals → Match?
│ ErrorEquals │ Sim → aguarda IntervalSeconds → reinvoca → erro → Retry[1]?
│ MaxAttempts │ → sucesso → Next
└──────┬───────┘
│ MaxAttempts esgotados
▼
┌──────────────┐
│ Retry[1] │ → próximo retry rule
└──────┬───────┘
│ todos os retries esgotados
▼
┌──────────────┐
│ Catch[0] │ → Verifica ErrorEquals → Match → Next (fallback state)
│ Catch[1] │
└──────┬───────┘
│ nenhum Catch casou
▼
Execução FAILED
Practical example
Scenario: Invoice processing pipeline. Receives an array of invoices (NFes), validates each one in parallel with fiscal and customer enrichment, and processes the result.
{
"Comment": "Pipeline de processamento de notas fiscais",
"StartAt": "ProcessarNFes",
"States": {
"ProcessarNFes": {
"Type": "Map",
"ItemsPath": "$.notas",
"ItemSelector": {
"nota.$": "$$.Map.Item.Value",
"indice.$": "$$.Map.Item.Index"
},
"MaxConcurrency": 20,
"ToleratedFailurePercentage": 10,
"Iterator": {
"StartAt": "EnriquecerNF",
"States": {
"EnriquecerNF": {
"Type": "Parallel",
"Branches": [
{
"StartAt": "ValidarFisco",
"States": {
"ValidarFisco": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "ValidarFisco",
"Payload.$": "$.nota"
},
"ResultSelector": { "fiscal.$": "$.Payload" },
"Retry": [
{
"ErrorEquals": ["Lambda.TooManyRequestsException"],
"IntervalSeconds": 2,
"MaxAttempts": 3,
"BackoffRate": 2,
"JitterStrategy": "FULL"
}
],
"End": true
}
}
},
{
"StartAt": "BuscarDadosCliente",
"States": {
"BuscarDadosCliente": {
"Type": "Task",
"Resource": "arn:aws:states:::dynamodb:getItem.sync:2",
"Parameters": {
"TableName": "clientes",
"Key": {
"cnpj": { "S.$": "$.nota.emitente_cnpj" }
}
},
"ResultSelector": { "cliente.$": "$.Item" },
"Catch": [
{
"ErrorEquals": ["DynamoDB.ResourceNotFoundException"],
"ResultPath": "$.erro_cliente",
"Next": "ClienteNaoEncontrado"
}
],
"End": true
},
"ClienteNaoEncontrado": {
"Type": "Pass",
"Result": { "cliente": null },
"End": true
}
}
}
],
"ResultSelector": {
"fiscal.$": "$[0].fiscal",
"cliente.$": "$[1].cliente"
},
"ResultPath": "$.enriquecimento",
"Next": "VerificarFraude"
},
"VerificarFraude": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.enriquecimento.fiscal.situacao",
"StringEquals": "IRREGULAR",
"Next": "BloquearNF"
}
],
"Default": "RegistrarNF"
},
"RegistrarNF": {
"Type": "Task",
"Resource": "arn:aws:states:::dynamodb:putItem.sync:2",
"Parameters": {
"TableName": "notas_processadas",
"Item": {
"id": { "S.$": "$.nota.chave_acesso" },
"fiscal": { "S.$": "States.JsonToString($.enriquecimento.fiscal)" },
"status": { "S": "PROCESSADA" }
}
},
"ResultPath": null,
"Retry": [
{
"ErrorEquals": ["DynamoDB.ProvisionedThroughputExceededException"],
"IntervalSeconds": 1,
"MaxAttempts": 5,
"BackoffRate": 2,
"MaxDelaySeconds": 20,
"JitterStrategy": "FULL"
}
],
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"ResultPath": "$.erro_registro",
"Next": "FalhaRegistro"
}
],
"End": true
},
"BloquearNF": {
"Type": "Fail",
"Error": "NFIrregular",
"Cause": "Nota fiscal com situação fiscal irregular"
},
"FalhaRegistro": {
"Type": "Fail",
"Error": "FalhaRegistro",
"CausePath": "$.erro_registro.Cause"
}
}
},
"ResultPath": "$.resultados",
"Next": "GerarRelatorio"
},
"GerarRelatorio": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "GerarRelatorio",
"Payload": {
"total.$": "States.ArrayLength($.resultados)",
"resultados.$": "$.resultados"
}
},
"ResultPath": null,
"End": true
}
}
}
CDK — Parallel, Map and error handling in Python
from aws_cdk import (
Stack, Duration,
aws_stepfunctions as sfn,
aws_stepfunctions_tasks as tasks,
aws_lambda as lambda_,
aws_dynamodb as dynamodb,
)
class NFWorkflowStack(Stack):
def __init__(self, scope, construct_id, **kwargs):
super().__init__(scope, construct_id, **kwargs)
tabela = dynamodb.Table.from_table_name(self, "Tabela", "notas_processadas")
fn_validar = lambda_.Function.from_function_name(self, "FnValidar", "ValidarFisco")
fn_relatorio = lambda_.Function.from_function_name(self, "FnRelatorio", "GerarRelatorio")
# ── Branches do Parallel ──────────────────────────────────────
validar_fisco = tasks.LambdaInvoke(
self, "ValidarFisco",
lambda_function=fn_validar,
payload=sfn.TaskInput.from_json_path_at("$.nota"),
result_selector={"fiscal": sfn.JsonPath.object_at("$.Payload")},
).add_retry(
errors=["Lambda.TooManyRequestsException"],
interval=Duration.seconds(2),
max_attempts=3,
backoff_rate=2,
jitter_strategy=sfn.JitterType.FULL,
)
cliente_nao_encontrado = sfn.Pass(
self, "ClienteNaoEncontrado",
result=sfn.Result.from_object({"cliente": None}),
)
buscar_cliente = tasks.DynamoGetItem(
self, "BuscarCliente",
table=tabela,
key={"cnpj": tasks.DynamoAttributeValue.from_string(
sfn.JsonPath.string_at("$.nota.emitente_cnpj")
)},
result_selector={"cliente": sfn.JsonPath.object_at("$.Item")},
).add_catch(
cliente_nao_encontrado,
errors=["DynamoDB.ResourceNotFoundException"],
result_path="$.erro_cliente",
)
enriquecer = sfn.Parallel(
self, "EnriquecerNF",
result_selector={
"fiscal": sfn.JsonPath.object_at("$[0].fiscal"),
"cliente": sfn.JsonPath.object_at("$[1].cliente"),
},
result_path="$.enriquecimento",
)
enriquecer.branch(validar_fisco)
enriquecer.branch(buscar_cliente)
# ── Fallbacks e estados terminais ────────────────────────────
bloquear_nf = sfn.Fail(
self, "BloquearNF",
error="NFIrregular",
cause="Nota fiscal com situação fiscal irregular",
)
falha_registro = sfn.Fail(
self, "FalhaRegistro",
error="FalhaRegistro",
)
# ── Registrar NF ──────────────────────────────────────────────
registrar_nf = tasks.DynamoPutItem(
self, "RegistrarNF",
table=tabela,
item={
"id": tasks.DynamoAttributeValue.from_string(
sfn.JsonPath.string_at("$.nota.chave_acesso")),
"status": tasks.DynamoAttributeValue.from_string("PROCESSADA"),
},
result_path=sfn.JsonPath.DISCARD,
).add_retry(
errors=["DynamoDB.ProvisionedThroughputExceededException"],
interval=Duration.seconds(1),
max_attempts=5,
backoff_rate=2,
max_delay=Duration.seconds(20),
jitter_strategy=sfn.JitterType.FULL,
).add_catch(
falha_registro,
errors=["States.ALL"],
result_path="$.erro_registro",
)
# ── Choice ────────────────────────────────────────────────────
verificar_fraude = sfn.Choice(self, "VerificarFraude") \
.when(
sfn.Condition.string_equals("$.enriquecimento.fiscal.situacao", "IRREGULAR"),
bloquear_nf,
) \
.otherwise(registrar_nf)
# ── Iterator do Map ───────────────────────────────────────────
iterator = enriquecer.next(verificar_fraude)
# ── Map principal ─────────────────────────────────────────────
processar_nfes = sfn.Map(
self, "ProcessarNFes",
items_path=sfn.JsonPath.string_at("$.notas"),
item_selector={
"nota": sfn.JsonPath.object_at("$$.Map.Item.Value"),
"indice": sfn.JsonPath.number_at("$$.Map.Item.Index"),
},
max_concurrency=20,
tolerated_failure_percentage=10,
result_path="$.resultados",
).iterator(iterator)
gerar_relatorio = tasks.LambdaInvoke(
self, "GerarRelatorio",
lambda_function=fn_relatorio,
result_path=sfn.JsonPath.DISCARD,
)
definition = processar_nfes.next(gerar_relatorio)
sfn.StateMachine(
self, "NFWorkflow",
state_machine_name="ProcessamentoNF",
definition_body=sfn.DefinitionBody.from_chainable(definition),
state_machine_type=sfn.StateMachineType.STANDARD,
)
Common pitfalls
Pitfall 1 — ResultPath in Catch without preserving the original input causes context loss
The mistake: The developer configures a Catch without ResultPath (or with ResultPath: "$"). In the fallback state, the entire input is the object { "Error": "...", "Cause": "..." } — the order, the customer, the execution context: everything is gone.
Why it happens: The default behavior of absent ResultPath in Catch is "$", which replaces the entire effective input with the error object. It's the same behavior as Task, except here the "result" is the error.
How to avoid: Always use "ResultPath": "$.erro" (or any reserved field) in Catch. The fallback state will receive the original input intact plus the $.erro field with { "Error": "...", "Cause": "..." }.
// Errado — perde contexto
"Catch": [{ "ErrorEquals": ["States.ALL"], "Next": "Fallback" }]
// Correto — preserva contexto
"Catch": [{ "ErrorEquals": ["States.ALL"], "ResultPath": "$.erro", "Next": "Fallback" }]
Pitfall 2 — Using InputPath when you meant Parameters, losing input fields
The mistake: The developer wants to pass only $.pedido_id to the Lambda. They use "InputPath": "$.pedido_id". The result: the Lambda receives only the ID string (e.g., "P001"), not an object. Furthermore, the effective input for the rest of the pipeline (ResultPath, etc.) is now also just that string.
Why it happens: InputPath filters the entire effective input — if the selected value is a string, the effective input becomes a string. Parameters should be used to build a structured payload without losing the input.
How to avoid:
- Use InputPath only when you want to select a sub-tree of the input that will become the new effective input (keeping the object).
- Use Parameters when you want to build a new object with selected fields from the input.
// Errado: effective input vira string "P001"
"InputPath": "$.pedido_id"
// Correto: effective input vira { "id": "P001", "flag": true }
"Parameters": {
"id.$": "$.pedido_id",
"flag": true
}
Pitfall 3 — Placing States.ALL before specific errors in Retry or Catch
The mistake: The Retry or Catch array starts with { "ErrorEquals": ["States.ALL"], ... }. This causes all errors — including those that had specific handling planned — to fall into the generic rule. Subsequent rules are never evaluated.
Why it happens: Both Retry and Catch evaluate rules in order and stop at the first match. States.ALL matches any error, so it blocks everything that comes after.
How to avoid: Always place specific errors first, States.ALL last:
// Errado
"Retry": [
{ "ErrorEquals": ["States.ALL"], "MaxAttempts": 3 },
{ "ErrorEquals": ["Lambda.TooManyRequestsException"], "MaxAttempts": 10 }
]
// Correto
"Retry": [
{ "ErrorEquals": ["Lambda.TooManyRequestsException"], "MaxAttempts": 10, "BackoffRate": 2 },
{ "ErrorEquals": ["Lambda.ServiceException"], "MaxAttempts": 5, "BackoffRate": 1.5 },
{ "ErrorEquals": ["States.ALL"], "MaxAttempts": 2, "BackoffRate": 1 }
]
Reflection exercise
You are modeling a data import workflow from a CSV file stored in S3. The file has ~50,000 rows. For each row, your team needs to: (1) enrich with data from an external API (with 200ms SLA and 1,000 req/s rate limit), (2) validate business rules, and (3) persist to DynamoDB.
Question: How would you choose between Map Inline and Map Distributed for this case? What would be the appropriate MaxConcurrency to respect the external API's rate limit? Where would you place the ResultWriter and why? Also describe how you would configure Retry for step 1 (call to the rate-limited external API) and Catch for step 3 (DynamoDB failure), so that rows with persistence errors are marked for reprocessing without cancelling the processing of the remaining rows.
Resources for further study
-
AWS Step Functions — Map State
URL: https://docs.aws.amazon.com/step-functions/latest/dg/state-map.html
Entry point for the Map state documentation, with links to both modes (Inline and Distributed). The "Inline Mode" section details ItemsPath, ItemSelector, and MaxConcurrency; the "Distributed Mode" section covers ItemReader, ItemBatcher, and ResultWriter with CSV and S3 examples. -
Processing input and output in Step Functions
URL: https://docs.aws.amazon.com/step-functions/latest/dg/concepts-input-output-filtering.html
Complete reference for the InputPath → Parameters → ResultSelector → ResultPath → OutputPath pipeline. Includes visual examples of data state at each step, with tables showing the before/after of each filter. -
Handling errors in Step Functions workflows
URL: https://docs.aws.amazon.com/step-functions/latest/dg/concepts-error-handling.html
Complete list ofStates.*error codes, Retry semantics (fields, BackoffRate, JitterStrategy, MaxDelaySeconds), and Catch (evaluation order, interaction with ResultPath). Includes examples of states with both configured.