Structured Logging in Node.js: How a Business ID Became My correlationId A developer solved a log correlation problem in AWS Step Functions by using the execution name as the correlation ID. Instead of generating a separate UUID, the engineer used the business case ID as the Step Functions execution name, making it available in every Lambda via the $$.Execution.Name context variable. This eliminated the need to manually pass identifiers between steps and simplified debugging when customers reported issues months later. I've been working in software for more than 15 years. This is the first piece I've decided to write about the job. I picked this topic because it was a small, real problem that cost me time more than once, until I finally stopped and fixed it properly. Quick disclaimer: this isn't distributed tracing or multi-service observability. It's a targeted fix for one specific problem: correlating logs within a single Step Function execution. If your scenario is different, the problem takes a different shape. What matters here is the reasoning, not the recipe. Quick context for anyone who doesn't work with serverless: AWS Step Functions is an orchestration service. You design a workflow as a state machine JSON , and each state usually triggers an AWS Lambda a function that runs on demand, no server to manage . In my case, a request kicks off a state machine execution that passes through several Lambdas in sequence: some do validation, others call external services, and one waits on a callback response before moving on. The fix turned out to be simpler than it sounds: a field Step Functions already offered for free, and I just wasn't using it. A request was processed 120 days ago. The customer calls: "Did you receive my request? I never heard back." You open the AWS Console. The Step Functions execution is already gone. Execution history only sticks around for 90 days, and even within that window, finding one specific execution among thousands is already a hassle. That leaves CloudWatch, with logs from every Lambda in the pipeline. You have one piece of data: the case ID. But every Lambda logged its own way: no standard, no correlation between them. To reconstruct what happened, you have to open each function's logs in the order the workflow probably ran, and piece it together by hand. It wasn't the error itself that hurt. It was the time lost just organizing the logs before you could actually start investigating. The pipeline wasn't huge, but it wasn't trivial either: a few decision steps with branches the path changed depending on the flow type , and a callback step a Lambda waiting on an external response before continuing . Each Lambda logged on its own, with console.log scattered through the code. The bare minimum identifier the case ID did show up in some logs, but there was no consistency across functions. One Lambda logged a plain string, another logged an object, and none of them followed the same format. The result: no real correlation. To reconstruct what had happened, you had to guess the workflow's likely execution order and open the log groups one by one, trying to piece the timeline together by hand. And the short retention window made things worse: without the execution history, all that was left were the raw CloudWatch logs, and since they had no shared structure, you couldn't use one to make up for the absence of the other. The one missing piece was exactly the link that would have made the two complete each other. One option would be to generate a fresh correlationId a UUID at the start of each execution and pass it manually through every Lambda. It's the more obvious solution, and it works. But in my case it didn't make sense: I already had the case ID, which identified the execution from start to finish. Creating a second ID just to correlate logs would mean keeping two identifiers for the same thing, and every investigation would turn into a translation exercise: find the case ID first, then look up its associated correlationId, and only then search the logs. It wasn't a complicated realization. AWS Step Functions already lets you name each execution name when it starts, and that name is available in every state in the workflow through the $$.Execution.Name context variable, with nothing to pass manually between steps. All I had to do was use what was already there: instead of generating a random UUID as the execution name, I used the case ID itself : js const input: StartExecutionCommandInput = { stateMachineArn: getStateMachineArn , name: data.id, // the case ID becomes the execution name input: JSON.stringify data , }; From there, any state in the state machine can access that ID with zero effort: "Parameters": { "data.$": "$", "correlationId.$": "$$.Execution.Name" } One single identifier, from start to finish: the same case ID that shows up in the database is what shows up in the logs of every Lambda in the pipeline. No extra UUID, no mapping table, no translation needed. The solution has a few pieces that fit together. The core is a createLogger layer, category function that returns a logger that's already "pre-configured," aware of where it's being called from. This eliminates the repetition of manual fields in every log call: export type LogLevel = 'info' | 'warn' | 'error'; export type LogLayer = 'handler' | 'service' | 'wrapper' | 'authorizer' | 'util'; export interface LogEntry { message: string; eventCode: string; category: string; layer: LogLayer; correlationId?: string; key: string : unknown; } const REDACTED KEYS = 'token', 'authorization', 'password', 'secret', 'apikey', 'x-api-key' ; const REDACTED VALUE = ' REDACTED '; export const redact = value: unknown : unknown = { if Array.isArray value return value.map redact ; if value == null && typeof value === 'object' { return Object.entries value as Record