Find Step Functions State Machines Without Logging or Tracing

Blue and yellow network cables plugged into the ports of a rack-mounted switch

Photo by Brett Sayles on Pexels

When Step Functions logging is not enabled, a state machine’s loggingConfiguration.level is OFF or missing. Find them by listing state machines and calling DescribeStateMachine on each, which also returns tracingConfiguration.enabled for X-Ray. It matters most for Express workflows: they keep no execution history, so without CloudWatch Logs a failed run leaves nothing to inspect.

State machines created through the API, the CLI or CloudFormation don’t send logs to CloudWatch Logs unless you configure it. For a Standard workflow you still get execution history in Step Functions. For an Express workflow, logging off means no record at all.

This example is for engineers who run workflows in production. The script reports every state machine’s type, log level, whether execution data is logged, the log group and X-Ray status, then flags the ones where Step Functions logging is not enabled. With --apply it turns logging on, and optionally tracing, for the state machines you name. For the difference between Standard and Express executions themselves, see the guide to starting a Step Functions execution with AWS SDK v3.

What do the Step Functions log levels record?

The level decides which execution history events are sent to CloudWatch Logs. ALL logs every event type and OFF logs none. The two in between:

Level What’s logged Good for
FATAL Only execution-level ends: ExecutionFailed, ExecutionAborted, ExecutionTimedOut Knowing that a run failed, not why
ERROR The FATAL events plus failures inside the run, such as FailStateEntered, LambdaFunctionFailed and MapIterationFailed A sensible production default
ALL Every event, including state entered and exited Express workflows you need to debug, at a higher log volume

Separately, includeExecutionData decides whether each event carries the state’s input, output and variables. That’s what makes logs useful for debugging, and also what puts customer data into CloudWatch. The OWASP Logging Cheat Sheet lists what should usually stay out of logs: access tokens, passwords, connection strings, encryption keys and sensitive personal data. The script flags payloads in logs so you can decide per workflow, and sets it to false when it enables logging.

Why do Express workflows need logging?

Standard workflows record execution history in Step Functions, and you can read it with GetExecutionHistory whether or not logging is on. Express workflows don’t record history, and GetExecutionHistory isn’t available for them. CloudWatch Logs is the only place their results show up. Log delivery is best effort, so if you need a guaranteed record of every Express run, write it to a data store from the workflow itself. When a Lambda task inside the workflow is what failed, the guide to investigating Lambda errors with CloudWatch picks up from there.

What does X-Ray tracing add?

With tracingConfiguration.enabled set, Step Functions sends traces to X-Ray for every sampled execution, so you can see where time goes across Lambda, SQS and other integrated services. By default X-Ray records the first request each second and 5% of additional requests. The state machine’s role needs xray:PutTraceSegments, xray:PutTelemetryRecords, xray:GetSamplingRules and xray:GetSamplingTargets.

What does the script do?

  1. Lists state machinespaginateListStateMachines for the Region.
  2. Describes each oneDescribeStateMachine returns type, roleArn, loggingConfiguration and tracingConfiguration.
  3. Flags findingsNO LOGS for level OFF (with a stronger note for Express), FATAL only, payloads in logs and no X-Ray.
  4. Resolves the log groupWith --apply, DescribeLogGroups finds the log group you named and uses its arn field, the form that ends in :* that Step Functions requires.
  5. Updates, if askedUpdateStateMachine with the existing roleArn, the new loggingConfiguration and, with --tracing, tracingConfiguration. It then describes the state machine again to confirm.

Prerequisites

  • Node.js 18 or later with tsx, plus @aws-sdk/client-sfn and @aws-sdk/client-cloudwatch-logs.
  • For --apply, a log group that already exists, ideally named under /aws/vendedlogs/states/, with a retention period. The script to set CloudWatch Logs retention for all log groups stops it from growing forever.
  • State machine execution roles that allow log delivery (see below). Step Functions expects the role in roleArn to have these permissions when you call UpdateStateMachine.

Which IAM permissions does it need?

The first two statements are for the report. The last two are only for --apply: iam:PassRole because the script passes the state machine’s role ARN back, and the CloudWatch Logs delivery actions, which Step Functions documents on "*" because they don’t support resource types.

step-functions-logging-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListStateMachines",
      "Effect": "Allow",
      "Action": "states:ListStateMachines",
      "Resource": "*"
    },
    {
      "Sid": "DescribeStateMachines",
      "Effect": "Allow",
      "Action": "states:DescribeStateMachine",
      "Resource": "arn:aws:states:*:111122223333:stateMachine:*"
    },
    {
      "Sid": "UpdateOnlyWithApply",
      "Effect": "Allow",
      "Action": ["states:UpdateStateMachine", "iam:PassRole"],
      "Resource": [
        "arn:aws:states:*:111122223333:stateMachine:*",
        "arn:aws:iam::111122223333:role/*"
      ],
      "Condition": { "StringEqualsIfExists": { "iam:PassedToService": "states.amazonaws.com" } }
    },
    {
      "Sid": "LogDeliveryOnlyWithApply",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogDelivery",
        "logs:GetLogDelivery",
        "logs:UpdateLogDelivery",
        "logs:DeleteLogDelivery",
        "logs:ListLogDeliveries",
        "logs:PutResourcePolicy",
        "logs:DescribeResourcePolicies",
        "logs:DescribeLogGroups"
      ],
      "Resource": "*"
    }
  ]
}

Each state machine’s execution role needs the same log delivery actions plus logs:CreateLogStream and logs:PutLogEvents, and the X-Ray actions above if you use --tracing. Narrow role/* to your state machine roles; the guide to reviewing IAM policies for least privilege covers PassRole in detail.

The script to find Step Functions without logging

find-step-functions-without-logging.ts

// find-step-functions-without-logging.ts
// Lists every Step Functions state machine in a Region with its CloudWatch Logs level, whether execution data
// is logged, and whether X-Ray tracing is on. Express workflows with logging OFF keep no execution history.
// --apply turns on logging (and optionally tracing) for the state machines you name.
// Usage:
//   npx tsx find-step-functions-without-logging.ts [--region us-east-1]
//   npx tsx find-step-functions-without-logging.ts --region us-east-1 --apply --names OrderFlow,Refunds \
//     --log-group /aws/vendedlogs/states/order-flow [--level ERROR] [--tracing]
import {
  SFNClient,
  DescribeStateMachineCommand,
  UpdateStateMachineCommand,
  paginateListStateMachines,
  type LogLevel,
} from "@aws-sdk/client-sfn";
import { CloudWatchLogsClient, DescribeLogGroupsCommand } from "@aws-sdk/client-cloudwatch-logs";

const args = process.argv.slice(2);
const flag = (name: string): string | undefined => {
  const i = args.indexOf(name);
  return i >= 0 ? args[i + 1] : undefined;
};
const region = flag("--region") ?? process.env.AWS_REGION ?? "us-east-1";
const apply = args.includes("--apply");
const tracing = args.includes("--tracing");
const names = (flag("--names") ?? "").split(",").map((n) => n.trim()).filter(Boolean);
const logGroupName = flag("--log-group");
const levelArg = (flag("--level") ?? "ERROR").toUpperCase();
if (!["ALL", "ERROR", "FATAL"].includes(levelArg)) {
  console.error("--level must be ALL, ERROR or FATAL");
  process.exit(1);
}
const level = levelArg as LogLevel;

const sfn = new SFNClient({ region });
const logs = new CloudWatchLogsClient({ region });

interface Row {
  StateMachine: string;
  Type: string;
  LogLevel: string;
  ExecutionData: string;
  LogGroup: string;
  XRay: string;
  Finding: string;
}

const errText = (err: unknown): string => (err instanceof Error ? `${err.name}: ${err.message}` : String(err));

// Step Functions needs the log group ARN that ends in ":*"; DescribeLogGroups returns it in the arn field.
async function logGroupArn(name: string): Promise<string> {
  const out = await logs.send(new DescribeLogGroupsCommand({ logGroupNamePrefix: name }));
  const group = out.logGroups?.find((g) => g.logGroupName === name);
  if (!group?.arn) throw new Error(`Log group ${name} not found in ${region}. Create it first.`);
  return group.arn;
}

async function main(): Promise<void> {
  const rows: Row[] = [];
  const arnsByName = new Map<string, { arn: string; roleArn?: string }>();
  for await (const page of paginateListStateMachines({ client: sfn }, {})) {
    for (const sm of page.stateMachines ?? []) {
      if (!sm.stateMachineArn || !sm.name) continue;
      const d = await sfn.send(new DescribeStateMachineCommand({ stateMachineArn: sm.stateMachineArn }));
      arnsByName.set(sm.name, { arn: sm.stateMachineArn, roleArn: d.roleArn });
      const lvl = d.loggingConfiguration?.level ?? "OFF";
      const dest = d.loggingConfiguration?.destinations?.[0]?.cloudWatchLogsLogGroup?.logGroupArn;
      const xray = d.tracingConfiguration?.enabled === true;
      const findings: string[] = [];
      if (lvl === "OFF") findings.push(d.type === "EXPRESS" ? "NO LOGS: no execution history at all" : "NO LOGS");
      if (lvl === "FATAL") findings.push("FATAL only: task failures not logged");
      if (lvl !== "OFF" && d.loggingConfiguration?.includeExecutionData) findings.push("payloads in logs");
      if (!xray) findings.push("no X-Ray");
      rows.push({
        StateMachine: sm.name,
        Type: d.type ?? "?",
        LogLevel: lvl,
        ExecutionData: d.loggingConfiguration?.includeExecutionData ? "yes" : "no",
        LogGroup: dest ? (dest.split(":log-group:")[1] ?? dest).replace(/:\*$/, "") : "-",
        XRay: xray ? "on" : "off",
        Finding: findings.join("; ") || "ok",
      });
    }
  }
  console.table(rows);
  const noLogs = rows.filter((r) => r.LogLevel === "OFF");
  const expressNoLogs = noLogs.filter((r) => r.Type === "EXPRESS");
  console.log(
    `${rows.length} state machines in ${region}: ${noLogs.length} with logging OFF ` +
      `(${expressNoLogs.length} Express), ${rows.filter((r) => r.XRay === "off").length} without X-Ray tracing.`,
  );

  if (!apply) {
    console.log("Report only: nothing was changed. Use --apply --names <a,b> --log-group <name> to turn logging on.");
    return;
  }
  if (!names.length || !logGroupName) {
    console.error("--apply needs --names and --log-group");
    process.exit(1);
  }
  const destination = await logGroupArn(logGroupName);
  for (const name of names) {
    const sm = arnsByName.get(name);
    if (!sm?.roleArn) {
      console.error(`Skipping ${name}: not found in ${region}`);
      continue;
    }
    try {
      await sfn.send(
        new UpdateStateMachineCommand({
          stateMachineArn: sm.arn,
          roleArn: sm.roleArn, // unchanged; UpdateStateMachine needs definition or roleArn
          loggingConfiguration: {
            level,
            includeExecutionData: false,
            destinations: [{ cloudWatchLogsLogGroup: { logGroupArn: destination } }],
          },
          ...(tracing ? { tracingConfiguration: { enabled: true } } : {}),
        }),
      );
      const after = await sfn.send(new DescribeStateMachineCommand({ stateMachineArn: sm.arn }));
      console.log(
        `Updated ${name}: logging ${after.loggingConfiguration?.level}, X-Ray ${after.tracingConfiguration?.enabled ? "on" : "off"}`,
      );
    } catch (err) {
      console.error(`Could not update ${name}: ${errText(err)}`);
      process.exitCode = 1;
    }
  }
}

main().catch((err) => {
  console.error(errText(err));
  process.exit(1);
});

How do you run it?

Terminal

npm install @aws-sdk/client-sfn @aws-sdk/client-cloudwatch-logs
npm install --save-dev tsx typescript @types/node

# Report only
AWS_PROFILE=readonly npx tsx find-step-functions-without-logging.ts --region us-east-1

# Log errors for OrderFlow to an existing log group and turn on X-Ray
AWS_PROFILE=platform-admin npx tsx find-step-functions-without-logging.ts --region us-east-1 \
  --apply --names OrderFlow --log-group /aws/vendedlogs/states/order-flow --level ERROR --tracing

Sample output

Output (report only)

┌─────────┬───────────────┬────────────┬──────────┬───────────────┬──────────────────────────────────┬───────┬──────────────────────────────────────────────────┐
│ (index) │ StateMachine  │ Type       │ LogLevel │ ExecutionData │ LogGroup                         │ XRay  │ Finding                                          │
├─────────┼───────────────┼────────────┼──────────┼───────────────┼──────────────────────────────────┼───────┼──────────────────────────────────────────────────┤
│ 0       │ 'OrderFlow'   │ 'STANDARD' │ 'OFF'    │ 'no'          │ '-'                              │ 'off' │ 'NO LOGS; no X-Ray'                              │
│ 1       │ 'ClickIngest' │ 'EXPRESS'  │ 'OFF'    │ 'no'          │ '-'                              │ 'off' │ 'NO LOGS: no execution history at all; no X-Ray' │
│ 2       │ 'Refunds'     │ 'STANDARD' │ 'ALL'    │ 'yes'         │ '/aws/vendedlogs/states/refunds' │ 'on'  │ 'payloads in logs'                               │
└─────────┴───────────────┴────────────┴──────────┴───────────────┴──────────────────────────────────┴───────┴──────────────────────────────────────────────────┘
3 state machines in us-east-1: 2 with logging OFF (1 Express), 2 without X-Ray tracing.
Report only: nothing was changed. Use --apply --names <a,b> --log-group <name> to turn logging on.

Names are illustrative. ClickIngest is the urgent one: an Express workflow with logging off has no execution history anywhere. OrderFlow is Standard, so its history exists in Step Functions, but failures aren’t in CloudWatch where your alarms and queries live. Refunds logs everything including payloads; if refunds carry card or personal data, set includeExecutionData to false. Once logs flow, the guide to running CloudWatch Logs Insights queries with SDK v3 helps you search them. Workflows are rarely the only gap: the same audit exists for API Gateway stages without access logging and for VPCs without flow logs.

Troubleshooting

  • InvalidLoggingConfiguration. The API reference only says the configuration is not valid. Check that the log group ARN ends in :*, that a destination is set whenever the level isn’t OFF, and that the execution role has the log delivery permissions.
  • The update fails once many state machines log to CloudWatch. CloudWatch Logs resource policies are limited to 5,120 characters, and to 10 per Region per account. Log groups under /aws/vendedlogs/states avoid growing the policy, which is why the console suggests that prefix.
  • MissingRequiredParameter. UpdateStateMachine needs definition or roleArn. The script always sends the current roleArn.
  • New executions don’t log right away. Executions already running keep the previous configuration, and ones started within a few seconds of the update may too.
  • Log entries look cut off. Large inputs and outputs are truncated to stay within CloudWatch Logs quotas; the event’s inputDetails and outputDetails say when.

Ask ChatWithCloud instead

To check without a script, ask ChatWithCloud “Which Step Functions state machines in us-east-1 have logging off or X-Ray tracing disabled?” It writes AWS SDK for JavaScript v2 code, runs it on your machine with your profile and summarizes the result. Keep it to read-only questions: changes run without a confirmation step, which ChatWithCloud’s security page explains along with what data leaves your machine.

Frequently asked questions

Is Step Functions logging enabled by default?

Not for state machines created with the API, CLI or CloudFormation, and not for Standard workflows created in the console. Express workflows created in the console are configured to log to CloudWatch Logs by default.

Where is Express workflow execution history?

Only in CloudWatch Logs, if logging is configured. Express workflows don’t record history in Step Functions, and GetExecutionHistory isn’t available for them.

Which log level should I use for Step Functions?

ERROR is a good default: it records failed executions and the failures inside them. Use ALL when you need every state transition, and think about includeExecutionData separately.

How much does Step Functions logging cost?

Step Functions logs are billed at the CloudWatch Logs vended logs rate, plus normal storage. Volume depends on the level, the number of executions and whether execution data is included.

Related guides

Ask your AWS account in plain English

Your first 15 runs are free, with no OpenAI key needed.

npx chatwithcloud