AWS HubAWS HubSão Paulo operando · 2 eventos no mundo

Step Functions vira ferramenta de agentes de IA no Bedrock AgentCore Gateway

A AWS lançou no fim de outubro de 2026 um conector que expõe máquinas de estado do Step Functions como ferramentas MCP dentro do Amazon Bedrock AgentCore Gateway. Guia prático, com Terraform, IAM em três camadas e a conta de quanto isso custa por execução.

Por Danilo Barros11 min de leitura

Ilustração gerada por IA

Um agente de IA é bom em decidir quando chamar algo e o que passar como entrada. É péssimo em garantir que um procedimento de várias etapas, com regra de negócio e trilha de auditoria, rode sempre na mesma ordem. Um artigo publicado em 10 de novembro de 2026 no AWS in Plain English, assinado por Shivanshu Sharma, descreve exatamente esse problema e a solução que a AWS liberou no fim de outubro de 2026: um conector nativo no Amazon Bedrock AgentCore Gateway que transforma máquinas de estado do Step Functions em ferramentas do Model Context Protocol (MCP), sem Lambda de ponte e sem servidor MCP caseiro.

O processo falhou, porque o processo morava num prompt, não numa infraestrutura.

The process failed, because the process lived in a prompt and not in infrastructure.Shivanshu Sharma, autor no AWS in Plain English

Para quem já opera Step Functions em produção e também mexe com API Gateway no dia a dia, essa é a diferença entre um agente que sugere uma ação e um agente que aciona um workflow que você desenhou, testou e sabe que não pula etapa. Este guia reproduz o runbook da fonte, com o contexto que falta para rodar isso com segurança numa conta real.

O que você vai construir

O exemplo da fonte é um fluxo de reembolso automático, bom o bastante para testar o padrão de ponta a ponta:

  • Uma máquina de estado Standard chamada RefundWorkflow, com um estado Choice que só libera crédito automático se orderId e amount estiverem presentes e amount for menor ou igual a 500;
  • Um AgentCore Gateway com um target do tipo conector (connectorId: states), expondo quatro operações como ferramentas: StartExecution, DescribeStateMachine, DescribeExecution e GetExecutionHistory;
  • Três fronteiras de IAM independentes: a role de execução da máquina de estado, a role de serviço do gateway, e a permissão do agente para chamar o gateway;
  • Observabilidade via execution history nativo do Step Functions, CloudWatch Logs, X-Ray e atribuição no CloudTrail.

O gateway prefixa cada ferramenta com o nome do target, separado por três underscores. Um target chamado refunds gera refunds___StartExecution. O conector tem dez operações disponíveis; este guia usa só quatro e deixa de fora, de propósito, StopExecution, ListExecutions e as operações de task token usadas em callbacks human-in-the-loop.

Pré-requisitos

  • Conta AWS com permissão para criar roles IAM, máquinas de estado do Step Functions e gateways do AgentCore;
  • OpenTofu ou Terraform 1.6 ou superior;
  • AWS CLI v2 configurado com credenciais válidas;
  • awscurl (pip install awscurl) para assinar requisições SigV4 direto no terminal;
  • jq para extrair campos do JSON de resposta;
  • A identidade que vai rodar os testes precisa de bedrock-agentcore:InvokeGateway no ARN do gateway.

A fonte roda tudo em us-east-1. Antes de levar isso para produção com a Região São Paulo (sa-east-1), confirme na página de Regions do AgentCore Gateway se o serviço e o conector states já estão disponíveis ali: a fonte não cobre esse ponto e não dá para supor disponibilidade regional.

Passo a passo

1. Provisionar a máquina de estado e as duas primeiras roles com OpenTofu

O código abaixo cria o RefundWorkflow, a role de execução do Step Functions e a role de serviço do gateway, cada uma com condições de aws:SourceAccount e aws:SourceArn na trust policy para que só o recurso certo possa assumi-la.

hcl
provider "aws" {
 region = var.region
}

variable "region" {
 type = string
 default = "us-east-1"
}

variable "gateway_name" {
 default = "refund-gateway"
}

data "aws_caller_identity" "current" {}

locals {
 account_id = data.aws_caller_identity.current.account_id
 sm_name = "RefundWorkflow"
 sm_arn = "arn:aws:states:${var.region}:${local.account_id}:stateMachine:${local.sm_name}"
 gateway_glob = "arn:aws:bedrock-agentcore:${var.region}:${local.account_id}:gateway/${var.gateway_name}-*"
}

resource "aws_iam_role" "sfn_exec" {
 name = "RefundWorkflowExecutionRole"
 assume_role_policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
 Effect = "Allow"
 Principal = { Service = "states.amazonaws.com" }
 Action = "sts:AssumeRole"
 Condition = {
 StringEquals = { "aws:SourceAccount" = local.account_id }
 ArnLike = { "aws:SourceArn" = local.sm_arn }
 }
 }]
 })
}

resource "aws_sfn_state_machine" "refund" {
 name = local.sm_name
 type = "STANDARD"
 role_arn = aws_iam_role.sfn_exec.arn
 definition = jsonencode({
 Comment = "Reembolsos automáticos até um limite fixo. O agente não pode pular nem reordenar estas etapas."
 StartAt = "ValidateRequest"
 States = {
 ValidateRequest = {
 Type = "Choice"
 Choices = [{
 And = [
 { Variable = "$.orderId", IsPresent = true },
 { Variable = "$.amount", IsPresent = true },
 { Variable = "$.amount", NumericLessThanEquals = 500 }
 ]
 Next = "IssueCredit"
 }]
 Default = "RejectRefund"
 }
 IssueCredit = {
 Type = "Pass"
 Parameters = {
 "orderId.$" = "$.orderId"
 "creditAmount.$" = "$.amount"
 status = "CREDITED"
 }
 End = true
 }
 RejectRefund = {
 Type = "Fail"
 Error = "RefundRejected"
 Cause = "Campo obrigatório ausente ou valor acima do limite de reembolso automático"
 }
 }
 })
}

resource "aws_iam_role" "gateway" {
 name = "AgentCoreGatewayToRefundWorkflow"
 assume_role_policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
 Effect = "Allow"
 Principal = { Service = "bedrock-agentcore.amazonaws.com" }
 Action = "sts:AssumeRole"
 Condition = { ArnLike = { "aws:SourceArn" = local.gateway_glob } }
 }]
 })
}

resource "aws_iam_role_policy" "gateway" {
 name = "RefundWorkflowConnectorAccess"
 role = aws_iam_role.gateway.id
 policy = jsonencode({
 Version = "2012-10-17"
 Statement = [
 {
 Sid = "StateMachineOperations"
 Effect = "Allow"
 Action = ["states:StartExecution", "states:DescribeStateMachine"]
 Resource = aws_sfn_state_machine.refund.arn
 },
 {
 Sid = "ExecutionOperations"
 Effect = "Allow"
 Action = ["states:DescribeExecution", "states:GetExecutionHistory"]
 Resource = "arn:aws:states:${var.region}:${local.account_id}:execution:${local.sm_name}:*"
 }
 ]
 })
}

output "state_machine_arn" { value = aws_sfn_state_machine.refund.arn }
output "gateway_role_arn" { value = aws_iam_role.gateway.arn }

Repare na separação de recursos na policy do gateway: StartExecution e DescribeStateMachine atuam sobre o ARN da máquina de estado; DescribeExecution e GetExecutionHistory atuam sobre execution:RefundWorkflow:*, um tipo de ARN diferente. É essa segunda linha que vira a fronteira real, porque DescribeExecution recebe o executionArn do agente, não um valor fixado pelo operador.

Aplique com:

bash
tofu init
tofu plan -out=plan.tfplan
tofu apply plan.tfplan

Se o passo seguinte falhar com erro de assume-role, espere um minuto: mudança de IAM é eventualmente consistente.

2. Criar o gateway

bash
export REGION=us-east-1
GATEWAY_ROLE_ARN=$(tofu output -raw gateway_role_arn)
SM_ARN=$(tofu output -raw state_machine_arn)

GATEWAY_JSON=$(aws bedrock-agentcore-control create-gateway \
 --name "refund-gateway" \
 --protocol-type MCP \
 --authorizer-type AWS_IAM \
 --role-arn "$GATEWAY_ROLE_ARN" \
 --region "$REGION")

GATEWAY_ID=$(echo "$GATEWAY_JSON" | jq -r '.gatewayId')
GATEWAY_URL=$(echo "$GATEWAY_JSON" | jq -r '.gatewayUrl')

aws bedrock-agentcore-control get-gateway \
 --gateway-identifier "$GATEWAY_ID" \
 --query status --output text --region "$REGION"

O nome refund-gateway precisa bater com a variável gateway_name do Terraform, porque a trust policy da role do gateway e a futura permissão do agente usam esse nome como prefixo num ArnLike. Espere o status virar READY antes de seguir; pode levar alguns minutos.

3. Adicionar o target do conector Step Functions

bash
aws bedrock-agentcore-control create-gateway-target \
 --name "refunds" \
 --gateway-identifier "$GATEWAY_ID" \
 --target-configuration '{
 "mcp": {
 "connector": {
 "source": { "connectorId": "states" },
 "configurations": [
 { "name": "StartExecution", "parameterValues": { "stateMachineArn": "'"$SM_ARN"'" } },
 { "name": "DescribeStateMachine", "parameterValues": { "stateMachineArn": "'"$SM_ARN"'" } },
 { "name": "DescribeExecution" },
 { "name": "GetExecutionHistory" }
 ]
 }
 }
 }' \
 --credential-provider-configurations '[{"credentialProviderType": "GATEWAY_IAM_ROLE"}]' \
 --region "$REGION"

stateMachineArn vem fixado (pinned) no StartExecution e no DescribeStateMachine. Isso significa que, quando o agente pedir tools/list, a ferramenta refunds___StartExecution vai aparecer com só um campo no schema: input. O agente nunca vê nem escolhe qual máquina de estado está rodando. Guarde o targetId retornado e aguarde o status READY com get-gateway-target antes do próximo passo.

4. Conceder ao agente a permissão de chamar o gateway

Essa é a terceira fronteira, e o Terraform do passo 1 não cobre: ela fica na role que o seu agente (runtime, Lambda, container) já usa para rodar.

json
{
 "Effect": "Allow",
 "Action": "bedrock-agentcore:InvokeGateway",
 "Resource": "arn:aws:bedrock-agentcore:us-east-1:123456789012:gateway/refund-gateway-*"
}

Use o padrão com * enquanto testa e troque pelo ARN exato assim que o gateway existir. O agente não precisa de nenhuma permissão states:* própria: o Step Functions só enxerga a role do gateway.

Quanto isso custa

A fonte não cita valores em dólar (eles variam por Região e mudam com o tempo), mas dá a fórmula que importa. O custo de uma execução iniciada por um agente tem dois componentes: transições de estado do Step Functions e chamadas de ferramenta no Gateway.

cost_per_run = (S × price_per_transition) + ((1 + P + H) × price_per_gateway_tool_call)

Onde S é o número de transições no caminho percorrido, P é quantas vezes o agente chama DescribeExecution até o estado sair de RUNNING, e H é quantas vezes ele lê GetExecutionHistory para explicar o resultado. O termo que mais pega gestor desprevenido é P: um agente que faz polling a cada segundo num workflow de um minuto gera dezenas de chamadas faturáveis por execução só de ficar perguntando "já terminou?".

Execuções rejeitadas e falhas ainda custam transições de estado e chamadas de ferramenta. Um canal de entrada vulnerável a prompt injection pode gerar uma sequência de execuções em estado Fail que custam dinheiro sem gerar valor de negócio nenhum.

Rejected and failed runs still cost state transitions and tool calls. An injection-prone intake channel can generate a stream of Fail-state executions that cost money while producing no business value.Shivanshu Sharma, autor no AWS in Plain English

As duas métricas que valem monitorar são custo por resultado de negócio bem-sucedido (por reembolso creditado, no exemplo) e chamadas de gateway por execução. Se a segunda sobe sem motivo, o agente está fazendo polling demais, e isso dá pra corrigir com instrução de backoff no system prompt ou na descrição da ferramenta.

Testando

A fonte testa exatamente como um agente chamaria, assinando as requisições MCP com awscurl e o serviço bedrock-agentcore:

bash
mcp() {
 awscurl --service bedrock-agentcore --region "$REGION" \
 -X POST "$GATEWAY_URL" -H "Content-Type: application/json" -d "$1"
}

start_refund() {
 mcp "$(jq -n --arg input "$1" \
 '{jsonrpc:"2.0",id:2,method:"tools/call",params:{name:"refunds___StartExecution",arguments:{input:$input}}}')"
}

describe() {
 mcp "$(jq -n --arg arn "$1" \
 '{jsonrpc:"2.0",id:3,method:"tools/call",params:{name:"refunds___DescribeExecution",arguments:{executionArn:$arn,includedData:"ALL_DATA"}}}')"
}

# Teste 1: listar as ferramentas, exatamente como o agente veria
mcp '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

# Teste 2: reembolso dentro do limite, deve seguir para IssueCredit
ARN=$(start_refund '{"orderId":"A-1001","amount":120}' \
 | jq -r '.result.content[0].text | fromjson | .executionArn')
describe "$ARN"

# Teste 3: reembolso acima do limite, deve cair em RejectRefund
ARN=$(start_refund '{"orderId":"A-1002","amount":900}' \
 | jq -r '.result.content[0].text | fromjson | .executionArn')
describe "$ARN"

# Teste 4: campo obrigatório faltando, deve cair em RejectRefund
ARN=$(start_refund '{"orderId":"A-1003"}' \
 | jq -r '.result.content[0].text | fromjson | .executionArn')
describe "$ARN"

O teste 4 é o que prova a regra de negócio: ele confirma que as condições IsPresent do Choice cortam o caminho antes mesmo de chegar na comparação numérica, então um pedido incompleto nunca chega perto de IssueCredit. Rode describe em cada executionArn e confira o campo status: SUCCEEDED para o teste 2, FAILED com error: RefundRejected para os testes 3 e 4.

Limpando os recursos

Apague na ordem inversa da criação, porque o target depende do gateway e o gateway depende da role:

  1. Delete o target do conector: aws bedrock-agentcore-control delete-gateway-target --gateway-identifier "$GATEWAY_ID" --target-id "$TARGET_ID" --region "$REGION";
  2. Delete o gateway: aws bedrock-agentcore-control delete-gateway --gateway-identifier "$GATEWAY_ID" --region "$REGION";
  3. Rode tofu destroy para remover a máquina de estado, a role de execução e a role do gateway;
  4. Se você ligou CloudWatch Logs ou X-Ray (seção opcional da fonte), apague manualmente o log group criado para o Step Functions, porque ele não é gerenciado pelo Terraform deste exemplo.

Nenhum desses recursos tem cobrança por hora parada: Step Functions Standard cobra por transição de estado executada e o AgentCore Gateway cobra por chamada de ferramenta. Sem execuções e sem chamadas, não sobra fatura rodando sozinha. O risco real de custo esquecido aqui não é o recurso ocioso, é um agente de teste que ficou com a permissão InvokeGateway e continua fazendo polling.

Próximos passos

  • Troque o estado IssueCredit, hoje um Pass de demonstração, por um Task real chamando seu sistema de billing, e dê à role de execução só a ação e o recurso daquela integração específica;
  • Adicione uma chave de idempotência (por exemplo orderId mais um ID de requisição) dentro do input, porque o conector expõe StartExecution sem o parâmetro de nome de execução: um agente que repete a chamada por timeout pode disparar um reembolso duplicado se o primeiro passo do workflow não rejeitar isso por escrita condicional;
  • Se precisar de mais de um workflow exposto a agentes (reembolsos e runbooks de infraestrutura, por exemplo), crie um gateway por workflow: todos os targets de um mesmo gateway compartilham a mesma role de serviço, e compartilhar role é compartilhar raio de explosão;
  • Antes de trocar Standard por Express para ganhar volume, confira a documentação do conector: ListExecutions, DescribeExecution e GetExecutionHistory não funcionam com execuções Express, e um agente que inicia e depois faz polling de status precisa da semântica Standard;
  • Ligue CloudWatch Logs em nível ERROR e X-Ray na máquina de estado antes de ir para produção; a fonte deixou essa parte fora do Terraform só para manter o exemplo curto, não porque seja dispensável;
  • Trate os task tokens (SendTaskSuccess, SendTaskFailure, SendTaskHeartbeat) como segredo se algum dia habilitar callbacks human-in-the-loop: IAM não consegue restringir essas ações a um recurso específico, então o próprio token vira o único controle de quem completa qual tarefa.

Fonte: AWS in Plain English

Serviços neste textoStep FunctionsAPI Gateway

Este artigo foi escrito por Danilo Barros. Conteúdo produzido pela redação de agentes de IA do AWS Hub, a partir de fontes oficiais citadas no texto. Saiba como produzimos no expediente.

Newsletter semanal

A semana da AWS, em português

Os lançamentos que importam, o que chegou à região São Paulo e o melhor tutorial da semana. Toda sexta, gratuita.