Why rewriting AS/400 programs in a newer language is not IBM i modernization

Almost every IBM i modernization deck I've seen has the same slide: convert the COBOL to free-format RPGLE, and then we'll be in good shape.

It sounds responsible. One language dates from 1960, and the other has dcl-proc and looks like something you could show a new hire. Consultants like the idea, steering committees like it, and the estimate fits on one line.

I liked it too, until I converted a few of those programs, saw what happened in production, and stopped treating the rewrite as the real work.

The COBOL isn't the problem. The problem is the twenty years of warehouse rules that live inside it. Changing the syntax doesn't move those rules anywhere. It just makes them harder to see, because now the code looks like it was written last year.

What the conversion pitch gets right

I'm not going to pretend COBOL is pleasant to work in.

A typical IBM i shop has a mix of everything: RPG III in fixed columns, RPG IV with /free and /end-free still in it, RPGLE that's genuinely free-format, CL that should have been a program, CA 2E (Synon) generated COBOL or RPG that nobody is supposed to edit, and a few real COBOL programs that have been allocating inventory, posting invoices or exploding bills of material since before the current team was hired.

For the work I do now, free-format RPGLE really is a better language:

  • Subprocedures with real parameters instead of a 100-line PERFORM
  • Service programs you can export and bind
  • Embedded SQL that's pleasant to write
  • Data structures, dcl-pi, %trim, %timestamp
  • Code a Python or Java developer can learn to read in an afternoon

If I'm writing a new program on this platform, it's free-format RPGLE with SQL, in a service program, with a procedure interface. I don't need convincing on that.

What I do question is the idea that a better language makes a better system. Most COBOL-to-RPGLE projects are sold as if those were the same thing.

What a "successful" conversion actually produces

Even a careful syntax conversion gives you RPGLE that still thinks like COBOL:

  • PERFORM becomes a subroutine that nobody gave parameters to.
  • Indicators survive as named indicators, which isn't much of an improvement.
  • The file is still read the COBOL way, except now it's a chain that doesn't mean what the original START and READ NEXT meant.
  • Packed decimals get a different definition and a quiet change in rounding.
  • COPY books become /copy members that still describe a 1998 record layout.
  • GO TO turns into nested if blocks that nobody wants to touch.
  • The program now looks modern, so the next person assumes its behavior was designed rather than inherited.

It compiles and it looks like RPG. It still has the same implicit FIFO ordering, the same status code that means two things, the same journal side effect, and the same "never run this on a Tuesday before 6 a.m." warning, except someone deleted that comment because free-format didn't have a column for it.

That isn't modernization. It's the same program in a different outfit.

A small example

I'll keep this generic, with no client names or real files. The pattern is the same in every warehouse ERP I've worked on.

A COBOL program finds the next open order line for a SKU in a warehouse and marks it picked. It's done this correctly for years, where "correctly" means by the business's definition, which isn't always the textbook one.

Simplified, the COBOL looks like this:

 MOVE WS-SKU TO OL-SKU MOVE WS-WHSE TO OL-WHSE START ORD-LINE-FILE KEY IS NOT LESS THAN OL-KEY READ ORD-LINE-FILE NEXT RECORD PERFORM UNTIL WS-FILE-STATUS NOT = "00" OR OL-SKU NOT = WS-SKU OR OL-WHSE NOT = WS-WHSE IF OL-STAT = "O" MOVE OL-ORD-QTY TO OL-PCK-QTY REWRITE ORD-LINE-REC EXIT PERFORM END-IF READ ORD-LINE-FILE NEXT RECORD END-PERFORM 

A conversion that's trying to be helpful produces this:

dcl-f ordlinf keyed usage(*update); chain (sku: whse) ordlinf; if %found(ordlinf) and olstat = 'O'; olpckq = olordq; update ordlinf; endif; 

It looks like a clean port, but it isn't. Here's what changed.

It only looks at one record. The COBOL starts at the first line for that SKU and warehouse and keeps reading until it finds one with an open status or runs out of lines for that SKU. The RPG chain reads the first line for that SKU and warehouse and stops there. If that first line is already picked, the RPG version does nothing, even though the next line is open. The faithful RPG equivalent is setll followed by reade in a loop. I've seen the chain version go into production because it compiled and the first test SKU only had one line.

What 'O' means. In the original shop, 'O' meant open. Later it also came to mean "on hold pending credit review" when a flag in a related table was set. The COBOL didn't mention that; a different program handled it. A port that reads 'O' as open picks held lines, and the warehouse finds out three weeks later through chargebacks.

Which line comes first. The COBOL relies on key order to approximate FIFO. That might be insertion order, line number or a date buried in the key, depending on how the DDS was set up in 2004. Anyone rewriting it has to know which, or the "next" line stops being the oldest one.

The lock. A COBOL REWRITE after a read on a file opened for I-O is holding a record lock. If two pickers go for the same line, one of them waits. In the RPG version, if commitment control is handled loosely, or the logic moves to a web API with no locking, both pickers can take the same line.

The side effect nobody wrote down. The original REWRITE writes to a journal that an EDI job reads to build 856 ASNs. That isn't in the COBOL comments, and it won't be in the RPG comments either. The day someone replaces the RPG update with a REST handler that writes to a different table, the ASN feed goes quiet, and the customer bills you for it the next month.

None of these are COBOL problems, and dcl-s doesn't fix any of them.

The conversion bugs I keep seeing

These get through code review because they look like style changes.

Packed decimal definitions. PIC S9(7)V99 COMP-3 doesn't automatically become packed(9:2). Truncation and rounding on money and quantities won't break the compile. They'll show up in a cycle count.

COPY books versus DDS. IBM i COBOL often copies the record layout from DDS. RPG's dcl-f does too, until someone "cleans up" the file spec and defines the fields by hand. Then there are two opinions about whether a quantity is 7,2 or 9,2.

File status versus %error and %found. COBOL shops check WS-FILE-STATUS. RPG shops check %found and sometimes forget %error. A record-lock timeout, a mapping error and "file not open" all look like "not found" if you only check one.

PERFORM ranges versus procedures. COBOL PERFORM PARA-A THRU PARA-Z is dangerous. A conversion that turns each paragraph into a subroutine and calls them in order keeps the danger. A conversion that "simplifies" the range into one procedure can quietly drop the paragraph in the middle that updated the audit file.

Indicators that were really return codes. RPG people like to joke about indicators, but COBOL people hid the same thing in 88 levels and file status. Collapsing both into a boolean called found loses the third state: found, but not usable.

Programs that are only correct at certain times. Some jobs are correct only because of when they run: before replenishment, after the nightly build, never during a physical inventory. The COBOL doesn't encode that; the job scheduler does. Rewrite the program and leave the schedule, and the timing dependency is still there. Rewrite it and put it behind an API that a web picker can call at 2 p.m., and you've created a kind of bug the original author never had to think about.

When I do convert

I still convert COBOL to free-format RPGLE, but as a deliberate decision for a specific program, not as a modernization strategy.

I convert when at least two of these are true:

  1. The program is going to change anyway. A new allocation rule, a new warehouse, a new unit of measure. If I have to go inside it, I'd rather come out with a procedure I can test.
  2. I need an ILE boundary. Something else, whether that's RPG, a web service, a Python job or an MCP tool, needs to call this logic with parameters, not by submitting CALL PGM(ALC001) PARM(...) and reading a data area afterwards.
  3. Nobody left can maintain it, and it isn't stable. Stable COBOL with no owner is still safer than a rewrite. Unstable COBOL with no owner is how you end up on a four-hour call at 3 a.m.
  4. The actual goal is embedded SQL or a service program. Not "it should look modern," but "this rule should be callable, logged and have a procedure interface."

When I do convert, I don't start with the syntax.

I start with a characterization test. For a given SKU, warehouse, time of day and credit flag: which record does it pick, what does it write, what journal entries appear, and which downstream job runs? I capture that, then write the RPGLE to reproduce the captured behavior rather than to mirror the COBOL line by line.

Converting line by line copies the bugs you can see and loses the behavior you can't.

What I do instead

My default is to wrap the program, not rewrite it. In practice, on IBM i, around COBOL and the RPG and CA 2E code it lives next to, that means a few different things.

1. Leave stable batch programs alone

If a COBOL program has posted the same way for fifteen years, the business trusts the numbers, and the only complaint is that it's COBOL, I don't touch it. I make sure it's under change management, the object matches its source, and the job has a name someone can find. That's already more useful than a rewrite nobody in the warehouse asked for.

2. Put a procedure in front of it before replacing it

An RPGLE service program can call a COBOL program. It isn't elegant, but it's easy to undo.

dcl-pr allocateLine extpgm('ALC001'); sku char(15) const; whse char(3) const; rc packed(3:0); end-pr; dcl-proc allocateOpenLine export; dcl-pi *n ind; sku char(15) const; whse char(3) const; end-pi; dcl-s rc packed(3:0); allocateLine(sku: whse: rc); return (rc = 0); end-proc; 

Now the web API, the MCP tool, the Python job and any new RPGLE all call allocateOpenLine, and none of them know ALC001 exists. If I ever rewrite the COBOL, I change one procedure instead of six callers.

It's the same idea as the FTP framework I rebuilt: stop letting every caller reach into the messy part directly, and give it a single entry point.

3. Extract the rule, not the whole program

Sometimes what you need isn't the whole 2,400-line COBOL program in RPG. You just need the rule for which line is eligible.

That rule can sometimes be written as SQL over the same tables:

select ol_order, ol_line, ol_ord_qty from ordlinf where ol_sku = :sku and ol_whse = :whse and ol_stat = 'O' and not exists (select 1 from creditflg where cf_order = ordlinf.ol_order and cf_on_hold = 'Y') order by ol_line fetch first 1 row only 

If that query returns the same line as the characterization test, you've extracted the rule. The COBOL can keep running the overnight batch while the interactive path uses the SQL, and you didn't have to rewrite 2,400 lines to build a picker screen. The query only finds the line. Locking it and writing the pick are separate steps, and the test has to cover those too.

If the query doesn't match the test, you've learned that the COBOL is doing something the tables don't show. Finding that out is the real modernization work, and the rewrite can wait until you understand it.

4. Build the API around the program, not through it

Most of the COBOL I still see sits inside an ERP that the rest of the company wants to talk to: a WMS, AutoStore, a carrier, a web order system, an agent. Those callers don't need the program to be RPGLE. They need a contract: inputs, outputs, idempotency and a status they can retry on.

So I put the contract on the outside, as REST, a data queue or a table the COBOL already knows how to read. The COBOL stays the system of record until the contract is dull and stable. Once it is, it's safe to replace what's behind it.

5. Generated code is a different problem

If the COBOL was generated by CA 2E, don't hand-convert it. The logic doesn't really live in the COBOL. It lives in the model's action diagram, and the COBOL is just one rendering of it (the glossary post explains the terms). Hand-porting that COBOL gives you an RPG program that has drifted away from its model and can't be safely regenerated.

There are two sound options. If the model is intact, change the function's target language and regenerate it as RPG, so the new code comes from the same design as the old. My next post covers that case. Or retire the function from the model on purpose and maintain the RPG from then on, which Fix It in the Model or Fix It in the Source? goes through. What you shouldn't do is tidy up the generated COBOL by hand and call it a rewrite.

The decision I actually use

When someone asks whether we should convert a COBOL program, I go through these questions in order, and the first yes decides it.

Question If yes
Was it generated (CA 2E / Synon / another CASE tool)? Don't hand-convert it. Regenerate from the model, or retire it from the model deliberately.
Has it been stable for years, with no change requested? Leave it. Put a procedure in front if callers need one.
Do we understand the rule well enough to write a characterization test? If not, we're not converting anything yet. We're still reading.
Does it need to be callable from more than one language, job or API? Build the service program wrapper first. Convert the inside later.
Are we changing the rule anyway, and is the source the only place it's written down? Now we convert, to free-format RPGLE with SQL, with tests that match production behavior.

Notice what isn't on the list: "the new developers don't like COBOL." That's a real staffing problem, but it isn't a reason to quietly change how allocation works.

You can train someone to read the COBOL, wrap it so they don't have to, or convert it with tests. Those are three different projects, and mixing them up is how you end up with RPGLE nobody trusts and COBOL nobody is allowed to delete.

What this has to do with "getting off the AS/400"

People still search for "AS/400," even though the platform is IBM i now. The instinct behind the search is the same: the language looks old, so the platform must be what needs replacing.

I've written about connecting an LLM to IBM i without rewriting any RPG, and about replacing a pile of FTP CL programs with one framework instead of replacing FTP. This is the same argument applied to the language.

You don't modernize a warehouse by translating IDENTIFICATION DIVISION into dcl-proc. You modernize it by making the rules visible, callable and hard to run with the wrong meaning of status 'O'.

Sometimes that means free-format RPGLE. Sometimes it means SQL over the file the COBOL already uses. Sometimes it means a 40-line service program in front of a COBOL program you haven't opened in a year. And sometimes it means leaving the overnight job exactly where it is, because it's the only thing in the building that still knows how a customer from 2009 wants their ASN.

The platform isn't going anywhere. What matters is whether the next person can change a rule without guessing.

What I'd tell anyone thinking about this

  1. chain isn't the same as START and READ NEXT. Partial keys are where quiet allocation bugs come from. Use setll and reade, or the conversion isn't finished.
  2. Free-format doesn't change behavior. If the pull request is mostly indentation and dcl-s, you've spent your risk budget on appearance.
  3. Don't let the batch and interactive paths drift apart. If COBOL still runs the overnight job and SQL serves the picker, both need to pass the same test. Two implementations of FIFO is how you end up with two different inventories.

Wrapping up

I used to think converting COBOL to free-format RPGLE was the grown-up modernization move on IBM i. It's a language change. Sometimes it's the right one, but it's the wrong default.

What has actually worked for me: leave stable COBOL alone, put a procedure in front of the COBOL that other systems need, pull the rule out into SQL when I can show it matches the old behavior, and only convert when I'm changing the rule anyway and have a test that defines what correct looks like.

If your shop has a slide that says "migrate COBOL to RPGLE" as though that were the whole project, the real project is underneath it: the allocation rule, the status code with two meanings, the journal the EDI job reads, the generated function nobody will regenerate, and the packed field that holds money on Tuesday and a quantity on Wednesday.

Rewrite those once you understand them. Until then, wrap them. The platform won't mind, but the warehouse will notice if you get it wrong.

Jaya Krushna Mohapatra is a Warehouse Management Systems Architect focused on enterprise integrations, IBM i modernization, and scalable backend systems.