Step Function execution name format
I was looking at the execution history for a Step Functions state machine that is triggered daily by an EventBridge Scheduler schedule. The execution names caught my eye — they look like UUIDs, they're not UUIDv7, but there's clearly a pattern. It got me excited in the same way that noticing AWS access key IDs were similarly-formatted back in 2020. So of course I had to dig in.
Look at these execution names, and their corresponding timestamps. How could you not be intrigued?
4b69cbb7-4050-4cf0-a096-4a0307b92a1a 2026-03-31 12:01:30 UTC
4b69ca65-c050-4cf0-a096-4a0307b92a1a 2026-03-30 12:01:30 UTC
4b69c914-4050-4cf0-a096-4a0307b92a1a 2026-03-29 12:01:30 UTC
4b69c7c2-c050-4cf0-a096-4a0307b92a1a 2026-03-28 12:01:30 UTC
4b69c671-4050-4cf0-a096-4a0307b92a1a 2026-03-27 12:01:30 UTC
4b69c51f-c050-4cf0-a096-4a0307b92a1a 2026-03-26 12:01:30 UTC
4b69c3ce-4050-4cf0-a096-4a0307b92a1a 2026-03-25 12:01:30 UTC
4b69c27c-c050-4cf0-a096-4a0307b92a1a 2026-03-24 12:01:30 UTC
The last ten bytes are always the same. The second segment alternates between
4050 and c050. The first segment increments by a small amount each day.
These are clearly not random UUIDs. So what's going on?
The structure¶
It turns out these are UUID-shaped deterministic identifiers with an embedded timestamp. The 16 bytes break down like this:
Rendered as a UUID string:
SSTTTTTT-TTFF-4xxx-xxxx-xxxxxxxxxxxx
│ │ │ │ │
│ │ │ │ └── per-schedule constant (fingerprint)
│ │ │ └───── fixed byte
│ │ └─────── low byte of timestamp
│ └────────────── upper 3 bytes of timestamp
└──────────────── schedule-specific seed byte
The UUID version nibble is forced to 4 and the variant bits are set to RFC 4122,
so it looks like a standard UUID v4 at first glance. But the content is entirely
deterministic.
What timestamp?¶
The 4-byte timestamp is the scheduled fire time in unix seconds — not the
actual execution start time. My schedule uses cron(0 22 * * ? *) in the
Australia/Brisbane timezone, which is 12:00:00 UTC. The actual executions start
at 12:01:30 UTC (due to the FlexibleTimeWindow being set to FLEXIBLE with a
60-minute maximum), but the embedded timestamp is exactly 12:00:00 UTC every time.
At first I thought the timestamp was truncated to the hour. To test this, I deployed two additional schedules targeting the same state machine: one firing every 2 minutes and one every 3 minutes. This quickly disproved the truncation theory — the embedded timestamps had full second precision:
Schedule A (every 3 min): bucket deltas = exactly 180 seconds
Schedule B (every 2 min): bucket deltas = exactly 120 seconds
The daily schedule only appeared to be hour-truncated because the cron expression fires exactly on the hour. Also because I had forgotten I had set a flexible time window - oops.
The per-schedule fingerprint¶
Everything except the 4 timestamp bytes is constant for a given schedule. Different schedules get different fingerprints:
| Schedule | Seed byte | Fingerprint |
|---|---|---|
| Daily cron | 0x4b |
4cf0-a096-4a0307b92a1a |
| Every 3 min | 0xbd |
4afa-8e02-9f1a19cda44c |
| Every 2 min | 0x79 |
4970-9d19-dcca01bbea4b |
This means two different schedules firing at the exact same second will never
produce the same execution name. I confirmed this: the 2-min and 3-min schedules
both had their first invocation at the same second (09:41:57 UTC) and produced
different, non-colliding names.
Why does Amazon do this?¶
Idempotent delivery. EventBridge Scheduler has at-least-once delivery
semantics. If it accidentally delivers the same scheduled invocation twice
(retries, network hiccups, cosmic rays, etc.), the execution name will be
identical both times. Step Functions requires execution names to be unique, so
the second StartExecution call is silently rejected as a duplicate.
This is actually a really elegant design. Amazon needed to solve two problems at once: generate names that are unique across invocations but identical across retries of the same invocation. Embedding the scheduled fire time achieves both.
Monotonically increasing¶
There's a nice bonus: for a given schedule, the execution names are monotonically increasing in lexicographic order. The seed byte and fingerprint are constant, so the only varying part is the 4-byte big-endian timestamp split across the first two UUID segments. Lexicographic comparison hits the most significant timestamp bytes first (in segment 1), then the least significant byte (in segment 2), preserving the natural time ordering.
This makes them excellent DynamoDB range keys. If you're tracking schedule
executions in a DynamoDB table with the schedule ARN as the partition key and
the execution name as the range key, you get chronological ordering for free
with no additional sort keys or GSIs. You can query for the latest N executions
with ScanIndexForward: false, do time-range queries, and paginate — all because
the execution names happen to sort correctly.
I'm definitely going to do this. I used to have an intermediate Lambda whose sole job was to invoke the state machine with a UUIDv7 execution name (so it could be used like this), and now I can get rid of it. Note that this only works as long as you only have a single schedule invoking your state machine, and you don't make any manual invocations (at least not without following the pattern manually).
Decoding¶
Here's a Go function to extract the scheduled fire time from an execution name:
func decodeScheduledTime(executionName string) (time.Time, error) {
parts := strings.Split(executionName, "-")
bucketHex := parts[0][2:] + parts[1][:2]
bucket, err := strconv.ParseInt(bucketHex, 16, 64)
if err != nil {
return time.Time{}, err
}
return time.Unix(bucket, 0), nil
}
Should you rely on this?¶
This is entirely undocumented behaviour. I reverse-engineered it from a handful of executions and it could change tomorrow. AWS has made no promises here and presumably has no idea I'm writing this blog post.
On the other hand, Hyrum's Law tells us that "with a sufficient number of users of an API, all observable behaviors of the system will be depended on by somebody." I'm now that somebody. And if you've read this far, you might be too. So really, Amazon can't change it now — that would be a backwards-incompatible change, and we all know how Amazon feels about those. I'm sure this argument would hold up great in a support ticket.