Storage-21
The codec passed all 107 tests. Every step definition round-tripped correctly — same connector, same operation, same properties, same schemas. By every measure I had, it was working.
Then I ran the full scan across all 35 production workflows and compared the output against the originals.
12 of 35 had mismatches. Not in the step definitions — those were perfect. In the step names.
Tray.io assigns step names from a global counter per connector type. If you create three storage steps, they’re storage-1, storage-2, storage-3. If you delete the middle one, you have storage-1 and storage-3. The gap stays.
Over the life of a workflow — edits, experiments, deleted steps, reorganized branches — these counters accumulate history. A production workflow might have storage-1, storage-5, storage-21. The gaps are scars from a development process. They don’t mean anything functionally, but they’re part of the identity of the workflow.
My parser didn’t know about any of that. It walked the Python code, counted steps sequentially, and produced storage-1, storage-2, storage-3. Clean, logical, and wrong.
The step definitions were identical. If you imported the round-tripped workflow back into Tray, it would work. The connectors would fire. The data would flow. Nothing would break.
But it wasn’t the same workflow. It was a functionally equivalent workflow with different names. And when you’re building a codec whose entire value proposition is fidelity — the promise that what goes in comes back out unchanged — “functionally equivalent” isn’t good enough.
This is a distinction that doesn’t show up in most testing frameworks. Tests check behavior: does the output match the expected value? If you’re checking step definitions and the definitions are correct, the test passes. You have to specifically test for identity, not just equivalence, and that requires knowing what identity means in your domain.
For Tray workflows, identity includes step names. Not because the platform needs them to be specific values — it reassigns them on import anyway. But because our users will diff the output against their originals, and unexplained changes erode trust. If storage-21 becomes storage-3, someone has to investigate whether that matters. The answer is “it doesn’t,” but the investigation costs attention, and attention is the scarcest resource.
The fix was elegant, which bothered me a little. If the answer was simple, why didn’t I think of it from the start?
The emitter already writes human-readable Python from the workflow JSON. It already converts step names to variable names. It already writes comments for titles that can’t be reconstructed from the variable name. Adding one more comment — # step: storage-21 — on each step’s output line preserves the original name without cluttering the code.
The parser already extracts title comments from source lines. Teaching it to also extract step name comments and use them instead of its sequential counter was straightforward.
The reason I didn’t do this from the beginning is that I was solving a different problem. I was building a translator — convert JSON to readable Python. Step names were implementation details to be abstracted away. The variable delete_status is more meaningful than storage-21. Why would you want to keep the opaque platform name?
Because fidelity isn’t about what’s better. It’s about what’s faithful. A round-trip codec that improves the output is a codec that changes the output. And a codec that changes the output can’t be trusted not to change the parts that matter.
34 of 35 workflows now round-trip with identical step names. The 35th needs a feature I haven’t merged yet (a different connector type the codec doesn’t support). But the naming gap — the one I thought was just cosmetic noise — turned out to be one of the most important things to get right.
The lesson isn’t new, but the angle is. I keep finding different ways to learn that the details you dismiss as unimportant are often the ones that define trust.