Copybook parsing is a solved problem — everywhere except inside OutSystems. So I solved it there, and validated it against a real COBOL compiler. That's the whole series in two sentences; the rest is why the gap was worth closing and how I know the answer is right.
Start with the gap, because it's oddly specific. In the low-code platform I spend most of my working life in, every mainframe integration I have watched go in starts the same way: a person, a copybook, and a ruler.
Someone opens a .cpy file — a COBOL copybook, the little schema that describes the exact byte layout of a fixed-width record — and starts counting. This field is nine bytes. The next one starts at offset twelve. That one is packed decimal, so it is really only five bytes on disk even though it holds nine digits. They write those numbers down in a config file somewhere, wire up a parser around them, and ship it. It works. And then someone adds a field three months later, every offset after it shifts by one, and nobody notices until the numbers on a report quietly stop adding up.
I know this dance well, because I did it myself. Not for a client — for a toy.
A miniature of the whole problem
One of my side projects, CATALOG-74, is a deliberately anachronistic thing: it takes a modern expense-splitting app and rebuilds it the way a 1974 COBOL shop would have — batch jobs, flat files, nightly runs. To read those flat files back out in Node.js, I had to tell my code where every field lived. So I did what everyone does. I opened the copybooks, and I transcribed them by hand:
// Field specs mirror the copybooks byte-for-byte -- see // cobol/copybooks/*.cpy for the authoritative PIC clauses. const BALANCE_SPEC = [ { name: 'groupId', start: 0, len: 6, type: 'int' }, { name: 'userId', start: 6, len: 6, type: 'int' }, { name: 'totalPaid', start: 12, len: 9, type: 'int' }, { name: 'totalOwed', start: 21, len: 9, type: 'int' }, { name: 'netBalance', start: 30, len: 9, type: 'int' }, { name: 'sign', start: 39, len: 1, type: 'str' }, { name: 'asOf', start: 40, len: 14, type: 'int' }, ]CATALOG-74's
specs.js: a human read the copybook, and typed the layout back in by hand.
Look at the highlighted line. The copybook says TOTAL-PAID PIC 9(7)V99. To turn that into { start: 12, len: 9 }, I had to know a handful of things by heart: that 9(7) means seven digits, that the V99 tacks on two more after an implied decimal point that occupies no byte of its own, that seven plus two is nine characters of DISPLAY storage — and that the field before it ended at byte 12, so this one begins there. Nine bytes, offset twelve. I counted it out, and I typed it in.
It works. It is also completely absurd. That line is a hand transcription of information that already exists, in precise machine-readable form, in a file sitting one directory away. I copied a number out of a document, by eye, into another document — the single most error-prone thing a programmer can do — and the original was right there the whole time, waiting to be parsed.
But the original sitting right there wasn't the same as being able to use it — not from where I work. I had no way to read that copybook from inside the platform I ship in. That was the moment the real project announced itself. Not the expense app. The missing tool underneath it.
Solved everywhere except where I needed it
Here is the part I want to put on the table first, because it is the first thing a skeptical reader will reach for — and they would be right to. Copybook parsing is not an open problem. It is one of the most thoroughly solved problems in enterprise computing. The rules for turning a PIC clause into a byte width have not meaningfully changed in fifty years, and the tooling reflects that maturity. JRecord and cb2xml have parsed copybooks in Java for two decades — and, as it happens, the same author's tooling is what I later cross-check PICASSO's decoded values against. Cobrix does it at scale on Spark. There are libraries in Python and Ruby, connectors from the big cloud and ETL vendors, and — of course — every COBOL compiler ever written, which is the whole reason I could eventually check my work against one. Even .NET has a couple of copybook parsers sitting on NuGet, plus Micro Focus's commercial toolchain. So, no: I did not discover an untouched problem.
What I found was narrower, and more specific. None of that solved tooling is usable from where I was standing.
In the OutSystems Forge — the marketplace of connectors for the low-code platform I build in — there is no COBOL connector at all. There are connectors for Salesforce, for Twilio, for a dozen databases, for services that did not exist a decade ago. Search it for "cobol" or "mainframe" across its thousands of components and you get nothing. Reaching the mature tools from OutSystems means standing up a separate Java service or an ETL layer beside your app — a whole second moving part to run, secure, and justify — just to read a flat file. And the native .NET options that do exist are the opposite of reassuring: unmaintained, undocumented, mostly structure-only rather than validated data decoders, several with murky licensing or locked inside a vendor product. Not one is packaged for OutSystems.
The problem isn't unsolved. It's unsolved here — and the pieces lying around in .NET aren't ones you'd bet a financial record on.
So an OutSystems team that needs to read a mainframe extract does the rational thing under the circumstances: it reaches for the ruler and counts the bytes out by hand, in a custom Integration Studio extension, exactly the way I did in a toy. It reinvents a fragile transcription that the wider world solved decades ago but its own platform never handed it. That is the real gap — not "nobody can parse a copybook," but "you can't parse one from inside OutSystems without bolting on infrastructure, and the loose .NET parts aren't trustworthy enough to stake a financial record on." Unglamorous, specific, and worth closing properly, once.
What I set out to build
PICASSO — PICTURE Interpreter for COBOL, Assembling Structured Schemas for OutSystems — is my attempt to close that gap where it actually is: natively, in .NET, packaged for the platform that never had it. The goal is stated in a single sentence: point it at a .cpy file, and it derives the byte layout itself.
Not a config file you fill in by hand. Not a wizard that asks you where each field starts. You give it the copybook — the same text a mainframe compiler reads — and it works out every offset, length, type, implied decimal, packed-decimal width, and sign the way the compiler would, then decodes and encodes the actual fixed-width records against that layout. The seven-line hand-count above becomes a function call. And because it reads the source of truth directly, it cannot drift out of sync with it: add a field to the copybook, and the layout updates itself, because it was never a copy in the first place.
The proof I hold it to is deliberately blunt. I point PICASSO at CATALOG-74's own copybooks — the very ones I once transcribed by hand — and check that it reproduces the exact offsets I typed, byte for byte, no ruler involved. It does. And where it disagrees with my old hand-transcription, it turns out to be right and I was the one who cut a corner — but that is a story for a later post.
That instinct — never taking the parser's own word for the answer — is also the thing that most sets PICASSO apart from the .NET copybook parsers already out there. Most of them will hand you a layout; almost none can show you it is right. PICASSO's is cross-checked against a real COBOL compiler and round-tripped through a genuine 1990s mainframe extract, three independent ways that never consult PICASSO's own opinion of itself. That validation is Part 4, and it is the part I am proudest of.
Packaged up, PICASSO is built to become the OutSystems Integration Studio Extension the Forge never had: the COBOL connector, so that the next person with a mainframe extract and a deadline calls an action instead of reaching for the ruler.
A note on honesty, because it matters to me and it comes up later in this series: I identified this problem, set the scope, and cross-checked the layouts against a real COBOL compiler (GnuCOBOL) and round-tripped a genuine 1990s mainframe file — but the .NET engine itself was written by an AI (Claude) working under my direction. I think the division of labour is genuinely interesting, and I would rather tell you about it plainly than let you assume otherwise. Part 5 is entirely about what that collaboration was actually like.
Where this series is going
- The thing that should already existyou are here
- What's actually inside a copybook — and why a human counting characters gets it wrong
- Let the real files find the bugs: "fail loudly, never silently miscompute"
- Trust, but verify — proving a parser against a real COBOL compiler
- I didn't write a line of the C#: directing an AI to build it
- The last mile is a Windows GUI: the part you can't automate