Most developers reach for a Lambda function every time they need an LLM response, adding unnecessary complexity and latency. Step Functions can call Claude directly via the Bedrock SDK integration, turning a multi‑step AI workflow into a single, visual state machine. In this guide you’ll see how to build, run, and debug that workflow from Node.js 22.
Why Step Functions Makes AI Orchestration Simple
When you ask an LLM (large language model) to do something, the request is just the first step. Real‑world agents often need to
- split a big problem into smaller prompts,
- call the model many times in parallel,
- combine the answers, and
- store the result somewhere (for example, S3).
A naïve solution is to write a Lambda function that does all of this work. Each Lambda invocation adds a cold‑start delay, a separate IAM role, and a piece of code that you have to maintain.
Step Functions is a managed service that runs a state machine: a diagram of steps (states) that pass data from one to the next. The service itself takes care of retries, parallelism, and visual debugging. Because Step Functions can talk directly to other AWS services (called service integration), you can ask Claude (the Anthropic model hosted on Bedrock) to answer a prompt without ever writing a Lambda.
In plain English: Think of Step Functions as a conductor that tells each musician (service) when to play, instead of a soloist (Lambda) trying to play the whole orchestra alone.
Minimal “Hello Claude” state machine
Below is the tiniest possible state machine that sends a single prompt to Claude and returns the answer. The definition is written in Amazon States Language (ASL), a JSON dialect that describes each state.
{
"Comment": "A one‑step workflow that calls Claude via Bedrock.",
"StartAt": "AskClaude",
"States": {
"AskClaude": {
"Type": "Task",
"Resource": "arn:aws:states:::aws-sdk:bedrockruntime:invokeModel",
"Parameters": {
"ModelId": "anthropic.claude-v2",
"ContentType": "application/json",
"Accept": "application/json",
"Body.$": "$.prompt"
},
"End": true
}
}
}
- Task – a step that performs work.
-
Resource – the ARN that tells Step Functions to call the
InvokeModeloperation of the Bedrock SDK. -
$.prompt– a JSONPath that pulls thepromptfield from the state input.
When you start this execution with {"prompt":"{\"prompt\":\"Hello, Claude!\"}"}, the state machine hands the JSON to Bedrock, waits for the response, and finishes with the model’s output. No Lambda involved.
Setting Up a Claude InvokeModel Integration in the State Machine
Before you can run the workflow you need three things:
-
An AWS IAM role that lets Step Functions call
bedrock-runtime:InvokeModeland write to S3. -
A Bedrock model ID – for this article we use
anthropic.claude-v2. - A state machine definition that includes the task.
IAM role (short version)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"bedrock-runtime:InvokeModel",
"s3:PutObject"
],
"Resource": "*"
}
]
}
Attach the policy to a role named StepFunctionsClaudeRole and give the role to the state machine when you create it.
Full state machine with a final S3 write
The following definition does three things:
- Accepts a list of prompts.
- Calls Claude for each prompt in parallel (via a Distributed Map – covered later).
- Writes the aggregated summary to an S3 bucket.
{
"Comment": "Parallel Claude calls with result stored in S3.",
"StartAt": "ProcessPrompts",
"TimeoutSeconds": 300,
"Roles": "arn:aws:iam::123456789012:role/StepFunctionsClaudeRole",
"States": {
"ProcessPrompts": {
"Type": "Map",
"ItemsPath": "$.prompts",
"MaxConcurrency": 5,
"Iterator": {
"StartAt": "AskClaude",
"States": {
"AskClaude": {
"Type": "Task",
"Resource": "arn:aws:states:::aws-sdk:bedrockruntime:invokeModel",
"Parameters": {
"ModelId": "anthropic.claude-v2",
"ContentType": "application/json",
"Accept": "application/json",
"Body.$": "$"
},
"ResultPath": "$.response",
"End": true
}
}
},
"ResultPath": "$.responses",
"Next": "SummarizeAndStore"
},
"SummarizeAndStore": {
"Type": "Task",
"Resource": "arn:aws:states:::aws-sdk:s3:putObject",
"Parameters": {
"Bucket": "my-claude-output",
"Key": "summary.json",
"Body.$": "States.JsonToString($.responses)"
},
"End": true
}
}
}
- Map – runs the same sub‑workflow for each element of an array.
-
MaxConcurrency– caps how many child executions can run at once (the service limit is 10 000). -
ResultPath– tells Step Functions where to store the output of the child.
Tip: The
States.JsonToStringintrinsic function converts a JSON object to a string that S3 expects as the body.
Why the service integration matters
When you use the arn:aws:states:::aws-sdk:… prefix, Step Functions becomes a thin client for the underlying SDK. No extra code, no packaging, just declarative wiring. This reduces deployment friction and keeps the latency low because the call happens inside the AWS control plane.
Defining the Distributed Map for Parallel Prompt Processing
Imagine you have 100 customer tickets that each need a short summary from Claude. Processing them one after another would take minutes. A Distributed Map works like a kitchen line where multiple chefs (tasks) prepare dishes (responses) simultaneously.
Anatomy of a Map state
- ItemsPath – points to the array in the input that you want to iterate over.
- Iterator – the mini‑state machine that runs for each element.
- ResultPath – where the collection of results is placed in the parent state.
Example: 3‑prompt parallel run
// This file is only for illustration; the JSON above is the real definition.
import { SFNClient, CreateStateMachineCommand } from "@aws-sdk/client-sfn";
const client = new SFNClient({ region: "us-east-1" });
const definition = {
// (copy the JSON from the previous section)
};
await client.send(
new CreateStateMachineCommand({
name: "ClaudeParallelDemo",
definition: JSON.stringify(definition),
roleArn: "arn:aws:iam::123456789012:role/StepFunctionsClaudeRole",
type: "STANDARD" // STANDARD gives us the 25,000‑event history limit
})
);
- The
CreateStateMachineCommandregisters the workflow with Step Functions. - The
type: "STANDARD"tells the service to keep a full execution history (useful for debugging, but remember the 25 000‑event ceiling).
When you start this machine with:
{
"prompts": [
"{\"prompt\":\"Summarize the first ticket.\"}",
"{\"prompt\":\"Summarize the second ticket.\"}",
"{\"prompt\":\"Summarize the third ticket.\"}"
]
}
Step Functions spins up three parallel AskClaude tasks, each receiving its own prompt JSON. The results are collected under $.responses and later written to S3.
In plain English: A Map state is like a mail‑room sorter that hands each envelope to a separate clerk; the clerks work at the same time, and the sorter later gathers all the stamped envelopes back together.
Running the Workflow from Node.js with the AWS SDK
Now that the state machine exists, a developer needs a small script that triggers it and watches the outcome. The only SDK we need is @aws-sdk/client-sfn, the official JavaScript client for Step Functions.
Step‑by‑step script
// run-clair-workflow.ts
import {
SFNClient,
StartExecutionCommand,
DescribeExecutionCommand,
GetExecutionHistoryCommand
} from "@aws-sdk/client-sfn";
// 1️⃣ Create a client that talks to Step Functions in your region.
const sfn = new SFNClient({ region: "us-east-1" });
// 2️⃣ Build the input payload – an array of prompt objects.
const executionInput = {
prompts: [
JSON.stringify({ prompt: "Explain the difference between TCP and UDP." }),
JSON.stringify({ prompt: "Give a short recipe for banana bread." }),
JSON.stringify({ prompt: "Summarize the plot of *The Little Prince*." })
]
};
// 3️⃣ Start the execution. The ARN below is the ARN of the state machine you created earlier.
async function start() {
const startCmd = new StartExecutionCommand({
stateMachineArn: "arn:aws:states:us-east-1:123456789012:stateMachine:ClaudeParallelDemo",
input: JSON.stringify(executionInput) // Step Functions expects a JSON string
});
const startResp = await sfn.send(startCmd);
console.log("Execution started:", startResp.executionArn);
return startResp.executionArn!;
}
// 4️⃣ Poll until the execution finishes (simple exponential back‑off).
async function waitForCompletion(executionArn: string) {
const describeCmd = new DescribeExecutionCommand({ executionArn });
while (true) {
const { status, output } = await sfn.send(describeCmd);
if (status === "SUCCEEDED") {
console.log("✅ Finished!");
console.log("Final output (S3 key is stored there):", output);
break;
} else if (status === "FAILED" || status === "TIMED_OUT" || status === "ABORTED") {
console.error(`❌ Execution ended with status: ${status}`);
break;
}
// Not finished yet – wait a bit.
await new Promise(r => setTimeout(r, 2000));
}
}
// 5️⃣ Run the helper functions.
(async () => {
const arn = await start();
await waitForCompletion(arn);
})();
Explanation of each line
- Lines 1‑4 import the three commands we need: start, describe, and get history.
-
sfnis the client object that knows how to sign requests with your credentials. -
executionInputmirrors the shape expected by the state machine (promptsarray). -
StartExecutionCommandtells Step Functions to begin a new run; the response contains anexecutionArnthat uniquely identifies this run. - The
whileloop repeatedly asks for the execution status. When the status becomesSUCCEEDED, the loop stops.
Key takeaway: The Node.js code only talks to Step Functions; every heavy‑lifting (prompt → Claude → S3) happens inside the state machine itself.
Optional: Pulling the full execution history
If you hit the 25 000‑event limit, you can fetch the raw event log:
const historyCmd = new GetExecutionHistoryCommand({
executionArn,
maxResults: 1000, // keep under the service limit per call
reverseOrder: false
});
const { events } = await sfn.send(historyCmd);
console.log("Number of events:", events?.length);
The history is useful for spotting where a child task failed or for measuring latency per step.
Debugging, Observability, and Payload Limits
Step Functions gives you a visual console, but a few hidden limits can trip new users.
1. Execution‑history ceiling
A standard state machine stores up to 25 000 events per execution. Each Map iteration, each retry, and each service‑integration call adds events. If you process thousands of prompts in a single execution, you may exceed the ceiling and the console will truncate the view.
Solution: Break a huge batch into smaller executions, or use an Express workflow (which streams events to CloudWatch Logs instead of storing them).
2. 256 KB input/output cap
Every state receives an input and emits an output that must fit into 256 KB. Claude can generate responses larger than this, especially when you ask for long essays. If a response exceeds the limit, the task fails with a PayloadTooLarge error.
Work‑around: After the AskClaude task, add a Pass state that copies the raw response to an S3 bucket, then replace the state output with a tiny reference (the S3 key). Example:
"StoreLargeOutput": {
"Type": "Task",
"Resource": "arn:aws:states:::aws-sdk:s3:putObject",
"Parameters": {
"Bucket": "my-claude-output",
"Key.$": "States.Format('large/{}.json', $.response.id)",
"Body.$": "$.response.body"
},
"ResultPath": "$.s3Location",
"Next": "TrimOutput"
},
"TrimOutput": {
"Type": "Pass",
"Result": {
"s3Key.$": "$.s3Location.Key"
},
"End": true
}
Now downstream states only see the S3 key (a few bytes), staying under the limit.
3. Cold start for Lambda tasks
If you ever mix a Lambda task into the workflow, remember that the Lambda cold start adds to the overall latency. By keeping everything as service integration tasks, you avoid that extra waiting time.
4. Distributed Map concurrency ceiling
A single Map can launch up to 10 000 child executions concurrently. If you set MaxConcurrency too high, the service throttles you. Start with a modest number (e.g., 5‑20) and monitor CloudWatch metrics (StepFunctions/MapStateConcurrency).
5. Observability tips
- Enable Execution Logging to CloudWatch Logs; it gives you timestamps for each state transition.
- Use the Step Functions console to click into a failed state and see the exact request/response payload.
- Turn on X-Ray tracing for the service‑integration calls if you need end‑to‑end latency numbers.
Tip: When you see “Execution history truncated” in the console, it’s a signal that you’ve crossed the 25 000‑event line and should split the job.
The Takeaway
- Step Functions can call Bedrock’s Claude directly; no Lambda code is required.
- A Task state with the
aws-sdk:bedrockruntime:invokeModelARN acts as a thin client to the model. - The Map state provides built‑in parallelism, letting you process dozens or hundreds of prompts at once.
- All orchestration logic lives in a JSON definition, so the Node.js side only starts the execution and polls for status.
- Be aware of the 256 KB payload limit; large model outputs should be streamed to S3 or truncated.
- Execution history, concurrency, and cold‑start limits are real constraints—design your workflows to stay comfortably inside them.
With these pieces in place, you can build AI agents that scale, stay observable, and avoid the overhead of managing Lambda functions. Happy orchestrating!
Transparency notice
This article was written with the help of an AI system — Groq (GPT OSS 120B).
Published: 2026-10-22 · Primary focus: StepFunctions
All code blocks are intended to be correct and runnable, but please verify them
against the official docs for the tools mentioned before using in production.Find an error? Drop a comment — corrections are always welcome.
Top comments (0)