<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jay</title>
    <description>The latest articles on DEV Community by Jay (@jay_krshn_1a9ac493fadf8).</description>
    <link>https://dev.to/jay_krshn_1a9ac493fadf8</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3833075%2F18bf8437-37b3-43ad-a655-c80c1f26108e.jpg</url>
      <title>DEV Community: Jay</title>
      <link>https://dev.to/jay_krshn_1a9ac493fadf8</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jay_krshn_1a9ac493fadf8"/>
    <language>en</language>
    <item>
      <title>Diagnosing a CA 2E-generated program that returns clean, throws nothing, and still doesn't do the thing</title>
      <dc:creator>Jay</dc:creator>
      <pubDate>Thu, 03 Sep 2026 19:22:15 +0000</pubDate>
      <link>https://dev.to/jay_krshn_1a9ac493fadf8/diagnosing-a-ca-2e-generated-program-that-returns-clean-throws-nothing-and-still-doesnt-do-the-3mfa</link>
      <guid>https://dev.to/jay_krshn_1a9ac493fadf8/diagnosing-a-ca-2e-generated-program-that-returns-clean-throws-nothing-and-still-doesnt-do-the-3mfa</guid>
      <description>&lt;h1&gt;
  
  
  It Compiled. It Ran. Nothing Got Written.
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Diagnosing a CA 2E-generated program that returns clean, throws nothing, and still doesn't do the thing — and why calling it directly is not the same test as running it for real
&lt;/h2&gt;




&lt;p&gt;A compile error is, in a strange way, the easy case. The compiler stops you, points at a line, and you fix it before anyone downstream ever sees the problem. What actually costs people time on CA 2E work isn't the 1,600-message compile listing — it's the program that compiles clean, runs to normal completion, puts nothing in the job log worth reading, and simply doesn't do what it was generated to do. No message. No indicator. The job ends *COMPLETE. And the row you expected in the file isn't there.&lt;/p&gt;

&lt;p&gt;This is a longer piece than the others in this set, on purpose. The failure mode itself isn't complicated once you've seen it a few times, but the list of things that can produce it is long enough that "just look at the code" isn't useful advice, and I want to actually walk through each one rather than wave at them.&lt;/p&gt;

&lt;p&gt;As with the rest of this material — no real library, program, or field names. The scenario is generalized but the mechanics are not.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Somebody needs to test a function that a CA 2E model generated — say it inserts a record into a shipment history file given a handful of key values. Instead of running it through whatever normally triggers it — a menu option, a batch job, a display file transaction — they do the direct thing: find the generated program's object name, and call it straight from a command line, hand-typing the parameters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="kd"&gt;CALL&lt;/span&gt; &lt;span class="kd"&gt;PGM&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;GENLIB&lt;/span&gt;&lt;span class="na"&gt;/PGMNAME&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;PARM&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CODE1'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'99'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'VALUE1'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'VALUE2'&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs. No escape message. No &lt;code&gt;*ESCAPE&lt;/code&gt; in the job log, no dump, nothing red. Job ends normally. And then somebody queries the file it was supposed to write to, and the row isn't there.&lt;/p&gt;

&lt;p&gt;The instinct at this point is to assume the program is broken — there's a bug in the generated logic, or the action diagram has a condition that's wrong. Sometimes that's true. Far more often, in my experience, the program did exactly what it was told, and what it was told wasn't what production actually sends it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why calling the generated program directly isn't the same test
&lt;/h2&gt;

&lt;p&gt;Go back to how a CA 2E function actually gets invoked in production, and this stops being surprising. A generated function very often isn't called directly by whatever kicks it off — there's a CL program sitting in front of it, and that CL program is not just a thin pass-through. It's doing real work before the RPG program ever gets control: setting up file overrides so the RPG program's generic file references resolve to the correct member or the correct library, possibly starting commitment control for the unit of work, sometimes translating an external code into the internal key value the RPG logic actually expects, sometimes validating that the caller is even allowed to do this before bothering to call the program that does it.&lt;/p&gt;

&lt;p&gt;Skip the CL and call the RPG program directly, and you've skipped all of that. The program you're testing is real. The environment it's running in is not the environment it was designed to run in. This is the single most common reason a direct call "does nothing" — not because the logic is wrong, but because the test wasn't equivalent to the thing that actually happens in production, and nothing about a clean compile or a clean run tells you that distinction exists.&lt;/p&gt;

&lt;p&gt;I'd go as far as saying: if you're testing a generated function for the first time and you don't already know for certain that there's no CL wrapper involved, your first attempt should go through whatever the real entry point is, not around it. Confirm the real path reproduces the problem before you start debugging the RPG program in isolation. Otherwise you can spend an afternoon convinced a program is broken when the only thing broken is how you're calling it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hypothesis one: commitment control, and the rollback nobody asked for
&lt;/h2&gt;

&lt;p&gt;This is the one that's caught nearly everyone who's worked with journaled files under commitment control on IBM i at least once, and it's worth understanding properly rather than just knowing "sometimes this happens."&lt;/p&gt;

&lt;p&gt;If the file involved is journaled and the program (or something in the call stack above it) started commitment control for this unit of work, then a write to that file isn't final the moment the WRITE or UPDATE operation executes. It's pending, under a commitment definition, until something explicitly commits it. Here's the part that surprises people: if the program reaches last record indicator — &lt;code&gt;*INLR&lt;/code&gt; on — without an explicit COMMIT having been issued, the &lt;em&gt;default&lt;/em&gt; behavior is not to commit what's pending. It's to roll it back. Silently. No message says "rolling back your changes." The row you thought you wrote simply reverts, as far as any other job or query is concerned, and the job that did the writing ends completely normally, because from the system's point of view nothing went wrong — you just never told it to keep the change.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of thing a production CL wrapper handles and a bare, direct &lt;code&gt;CALL PGM&lt;/code&gt; doesn't, unless the RPG program itself manages its own commit boundary internally, which not all generated functions do — some are written assuming the caller owns the transaction boundary, which is a completely reasonable design choice for a function that's meant to be one step inside a larger multi-file unit of work, and a completely unreasonable thing to assume when you're calling it standalone from a command line to test it.&lt;/p&gt;

&lt;p&gt;How you actually check this, rather than guess:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Find out whether the file the function writes to is journaled at all. If it isn't, this hypothesis is dead immediately and you can skip to the next one.&lt;/li&gt;
&lt;li&gt;Find out whether commitment control was active for the job that made the call. This is a job-level attribute; a job that never started commitment control can't have this problem, no matter how the file is journaled.&lt;/li&gt;
&lt;li&gt;If both of those are true, the decisive test is simple: call it again, and this time issue an explicit commit yourself immediately afterward, in the same commitment scope, before the job ends. If the row shows up now, you've confirmed it, and the actual fix is either calling the function through whatever normally owns that commit boundary, or issuing the commit yourself as part of however you're driving the test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I've also seen a variant of this that's more confusing to diagnose: the row is genuinely there, briefly, mid-transaction, and query it from a second session under the wrong isolation level and you'll either see it (uncommitted read) or won't (isolated read waiting on a lock that never resolves because the first job already ended and rolled back). If you're troubleshooting this interactively from two sessions at once, be aware that what you observe depends on which isolation level the checking query uses, and that alone can make the same bug look intermittent when it isn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hypothesis two: the write landed somewhere you didn't check
&lt;/h2&gt;

&lt;p&gt;If the production CL wrapper applies a file override — pointing a generic file name in the RPG program at a specific library, or a specific member, or a specific test copy of a file that exists for exactly this kind of ad hoc testing — and you call the RPG program directly without that override in place, the program still runs. It still executes a WRITE. It just resolves the file reference through whatever's on your own job's library list and override environment instead, which may not be the same target at all. The row might genuinely have been written. Just not where you looked.&lt;/p&gt;

&lt;p&gt;This is worth checking before you conclude anything else, because it's fast to check and it's happened to me more than once: display the current file overrides in effect for your job before you call the program, and separately, check what the CL wrapper does when it runs normally — whether it applies an override of its own before invoking the RPG program. If it does, and you didn't replicate it, go look at the file your override-free call would have actually written to. Sometimes the row is sitting there exactly as expected, in a place nobody thought to check because nobody expected the write to land there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hypothesis three: the row exists, and the thing you queried can't see it
&lt;/h2&gt;

&lt;p&gt;Related, but a distinct cause: you're looking at the right file, and the write genuinely happened and committed cleanly, but you're checking it through a logical file or a view with select/omit criteria that filters that particular row out. This happens more than you'd expect on files that have several access paths built for different purposes — a logical built for an active-orders screen that specifically excludes anything already flagged complete, for instance, would never show you a row you just inserted with a completed status, even though the physical write was entirely successful.&lt;/p&gt;

&lt;p&gt;The fix here isn't a fix, it's just checking correctly: query the physical file directly, or a logical you know carries no select/omit restriction, before you conclude the write didn't happen at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hypothesis four: the parameters you typed weren't the parameters the program needed
&lt;/h2&gt;

&lt;p&gt;This one is specific to testing generated functions by hand, and it's easy to dismiss because "the call didn't error" feels like proof the parameters were fine. It isn't. A generated RPG program's parameter interface reflects exactly what the model's function definition says the parameters are — specific lengths, specific numeric types, zoned versus packed, a code that has to match one of a small set of valid values or the action diagram's own logic treats it as "not eligible" and takes a branch that does nothing on purpose.&lt;/p&gt;

&lt;p&gt;If a CL wrapper normally sits in front of this function, part of what it may be doing is exactly this translation — taking a friendlier input and converting it into the exact shape the RPG program expects, sometimes including a lookup against another file to resolve an external code into an internal key. Skip that, hand-type a value that's the wrong length, the wrong justification, or simply doesn't match what a downstream condition is testing for, and the program can run start to finish, hit a condition that evaluates false, and reach *INLR having done nothing — correctly, according to its own logic, because as far as it's concerned you asked for something that isn't eligible.&lt;/p&gt;

&lt;p&gt;The only reliable way to rule this in or out is to actually watch it happen: run the program under a source-level debugger, set a breakpoint just before the write operation you expect to fire, and look at the field values in front of you at that point. If you never reach that breakpoint, you've found your answer — some condition upstream of the write is evaluating false, and now you go find out why, with actual field values in front of you instead of guessing from the outside.&lt;/p&gt;




&lt;h2&gt;
  
  
  The order I'd actually check these in
&lt;/h2&gt;

&lt;p&gt;Not because every case is the same, but because this order goes from cheapest-to-rule-out to most-invasive, and there's no reason to fire up a debugger before you've confirmed the basics:&lt;/p&gt;

&lt;p&gt;First, reproduce it through the real caller, not around it. If the CL wrapper or the real entry point produces the same non-result, you've eliminated everything above that's specific to bypassing it, and you're looking at a genuine logic problem, not a test-environment problem. If the real caller works fine and only your direct call fails, stop looking at the RPG program's logic entirely — the problem is in what the direct call skipped, not in the function itself.&lt;/p&gt;

&lt;p&gt;Second, if a journaled file and commitment control are anywhere in the picture, rule that out explicitly with a deliberate commit before you trust a negative result at all.&lt;/p&gt;

&lt;p&gt;Third, check where the write actually went — overrides, library list, the member actually targeted — before assuming it went nowhere.&lt;/p&gt;

&lt;p&gt;Fourth, check what you're using to verify the result — a filtered access path can hide a row that's genuinely there.&lt;/p&gt;

&lt;p&gt;Fifth, and only once the first four are exhausted, get into the program itself with a debugger and watch the actual condition logic evaluate against the actual values it received.&lt;/p&gt;

&lt;p&gt;I've seen people reach for the debugger first, because it feels like the thorough, technical thing to do. It's usually the slowest way to solve this specific problem, because three of the four most common causes have nothing to do with the code the debugger would show you. The debugger answers "why did this condition evaluate the way it did." It has nothing to say about a rollback that happened after the program already ended cleanly, or a write that landed in a file you weren't looking at.&lt;/p&gt;




&lt;h2&gt;
  
  
  The broader point, if there is one
&lt;/h2&gt;

&lt;p&gt;A clean compile tells you the syntax is legal. A clean run with no error message tells you the operations that executed didn't fail. Neither one tells you the operations you expected actually executed, against the environment you expected, with the data you expected, and stayed committed after the job ended. Those are four separate claims, and a CA 2E-generated function — layered as it typically is behind a CL driver, sometimes behind commitment control, sometimes behind a translation step you never see unless you go looking for the wrapper — is exactly the kind of thing where testing it by calling the innermost piece directly quietly drops two or three of those four claims without telling you it did.&lt;/p&gt;

&lt;p&gt;If something "worked, no errors" and the result still isn't there, the honest first question isn't "what's wrong with the program." It's "did I actually test the thing that runs in production, or did I test a piece of it in an environment nothing else ever puts it in."&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Jaya Krushna Mohapatra is a Warehouse Management Systems Architect focused on enterprise integrations, IBM i modernization, and scalable backend systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>synon</category>
      <category>as400</category>
      <category>ca2e</category>
      <category>rpgle</category>
    </item>
    <item>
      <title>1,619 Compiler Messages, Four Real Causes: A CA 2E to RPGLE Compile Reference</title>
      <dc:creator>Jay</dc:creator>
      <pubDate>Wed, 02 Sep 2026 22:29:34 +0000</pubDate>
      <link>https://dev.to/jay_krshn_1a9ac493fadf8/1619-compiler-messages-four-real-causes-a-ca-2e-to-rpgle-compile-reference-egl</link>
      <guid>https://dev.to/jay_krshn_1a9ac493fadf8/1619-compiler-messages-four-real-causes-a-ca-2e-to-rpgle-compile-reference-egl</guid>
      <description>&lt;h1&gt;
  
  
  1,619 Compiler Messages, Four Real Causes: A CA 2E to RPGLE Compile Reference
&lt;/h1&gt;

&lt;h2&gt;
  
  
  What actually happens when a Synon model becomes a compiled RPGLE program — and how to read the wall of errors when it doesn't
&lt;/h2&gt;




&lt;p&gt;CA 2E (Synon) does not write RPG. It writes a model — files, functions, action diagrams — and a code generator turns that model into RPG source, then a separate step turns that source into a compiled object. Most of the confusion people have converting CA 2E work to RPGLE comes from not knowing where the seam between those two steps is. This is a reference for that seam: the path from model to object, what it looks like when it breaks, and how to read a compile listing with over a thousand messages in it without treating each one as a separate problem.&lt;/p&gt;

&lt;p&gt;No library names, program names, or field names below are real. They've been left out entirely rather than replaced with placeholders, because the mechanics and the error categories don't need them to make sense.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two things to know before you touch a model
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Generate and Create are different steps, and only one of them needs the model to be right.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Generate" turns a function's action diagram into RPG source and writes it to a member. It does not compile anything. "Create" compiles and binds that source into an object. CA 2E's menu lets you do both in one submitted job, which is convenient right up until that job fails, because the failure message tells you almost nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target HLL "RPG IV" does not mean free-format.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the part that surprises people coming from a "just convert it to RPGLE free" mindset. Setting a function's target high-level language to RPG IV makes CA 2E generate traditional fixed-format RPG — column-based calculation specs, sequence numbers, no free-format delimiters. It compiles as an RPGLE member type, and ILE will bind it like any other RPGLE program, but the source itself reads like an older RPG dialect with a newer header. If you want genuinely free-format output, that is a second, deliberate step after the generated source compiles clean — never before. Free-format conversion tools rewrite syntax; they don't fix logic. Feed one a program that doesn't compile and you get free-format syntax wrapped around the same bug, which is now harder to spot because it looks new.&lt;/p&gt;

&lt;p&gt;So the realistic pipeline is: model, then generate, then fixed-format RPG IV source, then compile clean, and only then — if you actually want free-format — convert the syntax. Not model straight to free-format RPGLE in one step. Nothing in the generator does that for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  The path from model to object
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Get the model on the library list.&lt;/strong&gt; The generation commands need to find both the model itself and the separate generation library it automatically maintains alongside it. If the underlying product library isn't available, you get a blunt message before you even get near your own code, something to the effect of the product library not being installed for high-level-language generation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Copy the model if you don't want to touch the real one.&lt;/strong&gt; There's a menu option specifically for commands that copy a model, and the command behind it lets you copy from one model to a new one while telling it not to create any objects — just the definitions. That gives you a sandbox to generate and compile in without touching anything the real model owns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Set the function's target language.&lt;/strong&gt; Inside the copied model, find the function you're working on and set its target high-level language to RPG IV. This is a setting on the individual function, not on the model as a whole — different functions in the same model can target different languages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Generate the function.&lt;/strong&gt; From the screen that lists a model's functions, there's an option to generate. This writes the RPG IV source member into the model's generation library, whether or not anything gets compiled afterward. The member existing is not evidence the program works. It's evidence the generator ran.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Submit the model create request.&lt;/strong&gt; There's a follow-on option that submits generation and compilation and binding together as one batch job.&lt;/p&gt;

&lt;p&gt;If that job finishes clean, you have a working object and nothing below this line matters. If it doesn't —&lt;/p&gt;




&lt;h2&gt;
  
  
  When the submit job dies, stop debugging the submit job
&lt;/h2&gt;

&lt;p&gt;The message you get back is short: a job identifier, and a note that it ended abnormally.&lt;/p&gt;

&lt;p&gt;That's it. No compiler message, no line number, no indication of which of the several things inside a generate-then-compile-then-bind job actually failed. You can chase this through job logs and the model's own error lists, and sometimes that's the right call if the model itself is broken — a missing access path, an undefined function reference. But most of the time, the model is fine and the generated source has a problem the compiler would tell you about immediately, if you asked the compiler directly instead of through that wrapper.&lt;/p&gt;

&lt;p&gt;You already have the source. The generate step wrote it, independent of whether the create step's compile succeeded. Go find it with the standard member-list utility, pointed at the generation library and the source file the generator writes to.&lt;/p&gt;

&lt;p&gt;The member is sitting there — generated, uncompiled or compiled-and-failed. Compile it directly from that member list. Now you're not waiting on a batch job to tell you it died. You're looking at the actual compiler output, the same listing you'd get for hand-written source. That's the pivot: the moment the black-box submit job fails, drop out of the model layer entirely and work with the generated source like it's just RPGLE, because at this point it is.&lt;/p&gt;

&lt;p&gt;This one compile threw:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Message Totals:
  Information (00) . . . . . . :    48
  Warning      (10) . . . . . . :     0
  Error        (20) . . . . . . :   995
  Severe Error (30+) . . . . . . :   576
  -----------------------------------
  Total  . . . . . . . . . . . :  1619
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sixteen hundred and nineteen messages on a program that, days earlier, had been generating and running fine somewhere else in the model's history. Nobody rewrote 1,619 things overnight. Something small broke, and the compiler reports every downstream consequence of that one thing as its own message.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reading the listing without reading 1,619 lines
&lt;/h2&gt;

&lt;p&gt;Don't start at the top. Two places matter first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The final summary&lt;/strong&gt;, shown above, tells you how bad it is in aggregate: mostly error and severe error, no warnings, means something structural, not a style nitpick.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The message-frequency section&lt;/strong&gt; — the same compile listing groups every message by its ID and gives you a count. This is the part almost nobody scrolls down to look at, and it's the part that turns 1,619 lines into an actual to-do list. In this listing, ten distinct message types accounted for everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A handful of messages saying that all record formats for an externally described file were ignored or dropped due to error, and the file itself was ignored.&lt;/li&gt;
&lt;li&gt;A handful more saying a file-name or record-name entry wasn't valid, so the input record was ignored.&lt;/li&gt;
&lt;li&gt;A handful saying a record format name in an externally described file was already defined, so the format was ignored.&lt;/li&gt;
&lt;li&gt;Then the bulk of the volume — hundreds of occurrences of "the name or indicator is not defined," hundreds more of "the field on the calculation specification is not numeric, so the specification is ignored," dozens of "the move operation has operands that are not compatible," dozens of "expression contains an operand that is not defined," and a couple of "the types on either side do not match in the EVAL operation."&lt;/li&gt;
&lt;li&gt;Rounding it out, a few hundred lower-severity messages about matching-fields and control-level entries defaulting to blank because they weren't set to valid values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two message types, roughly ten occurrences between them, are root causes. Everything else — the other 900-plus occurrences — is the compiler dutifully reporting that fields it can no longer see are, in fact, no longer defined. Once a record format gets dropped, every field from that format that the program touches throws its own "not defined," and every operation built on that undefined field throws another message about incompatible types or non-numeric operands. One dropped file produces a hundred symptoms. Fix the file, and the hundred symptoms go with it.&lt;/p&gt;

&lt;p&gt;This is the actual skill in triaging a CA 2E-generated compile: read the frequency table before the line-by-line, find the message types whose description describes a structural problem — a file, a format, a name resolution — fix those first, and recompile before touching anything the cascade produced. Chasing the cascade individually is how a five-hour fix turns into a two-day one.&lt;/p&gt;




&lt;h2&gt;
  
  
  The four root causes, described rather than named
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The library list at compile time isn't the library list the model's own job used
&lt;/h3&gt;

&lt;p&gt;The model's own submit job runs with its own library list, built from the model's configuration. When you compile the generated source yourself, you're using whatever library list your own session has — and if the physical and logical files the generated source references aren't resolvable from there, the compiler doesn't fail loudly. It fails quietly, reporting the file as ignored, and then reports every field from that file as undefined for the rest of the listing. This produces exactly the shape above: a couple of "file ignored" messages and hundreds of cascading "not defined" messages that look unrelated but all trace back to one file the compiler couldn't find.&lt;/p&gt;

&lt;p&gt;The fix is to put the actual data library — the one holding the physical and logical files the function touches, not just the model's own libraries — on the library list before compiling standalone. This is the single highest-leverage thing to check first when a compile like this looks catastrophic.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A record format collides with itself
&lt;/h3&gt;

&lt;p&gt;One message type in particular — a record format in an externally described file already defined, so the format gets ignored — shows up when the same format name resolves twice. Usually that's a leftover generated member from a prior attempt still sitting in the same source file, or two access paths on the same file generating under the same format name. Deleting the stale member and regenerating clean is faster than trying to patch around a duplicate definition.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The generated instruction references a name the file doesn't actually expose
&lt;/h3&gt;

&lt;p&gt;Somewhere in the generated code, a read-by-key operation was pointed at the generator's own internal working name for an access path, rather than the actual key-list name that the underlying logical file exposes externally. Most of the time these line up and you never notice — the generator's internal label and the file's real key-list name happen to match. When they don't, the fix is a one-line swap of the operand to the file's real key-list name. You'd never find this by reading "the name or indicator is not defined" in isolation, since that message fires on dozens of unrelated lines too. You find it by noticing this specific line sits right next to the file that came back "ignored," and checking the logical file's actual externally-defined key-list name against what the generator wrote into the calculation.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Fields exist in the table, not in the format the program is reading
&lt;/h3&gt;

&lt;p&gt;A program reads by key against a logical file — an index — rather than the physical file directly, which is normal; that's what indexes are for. But if a couple of fields exist on the underlying physical file and were never carried onto that particular logical file's externally described format, the generated program can reference them by name — because the model knows about the physical file — while the compiler, working only from the logical file's actual record format, correctly says they don't exist.&lt;/p&gt;

&lt;p&gt;There are two honest fixes here. Either change which file the program keys off of, to one that does carry those fields, or confirm — actually confirm, not assume — that the values are already being retrieved elsewhere in the program from a different file, and only then drop the reference. In the case that produced this: both fields were already being pulled from two other files the program had open. The reference through the index was redundant, not load-bearing, so removing it was correct. If it hadn't been redundant, the right fix would have been changing which file the read targets, not deleting the field.&lt;/p&gt;




&lt;h2&gt;
  
  
  An estimation table that actually holds up
&lt;/h2&gt;

&lt;p&gt;If you're scoping more than one or two of these, per-program time varies enormously by file usage, and a flat "X hours per program" estimate will be wrong in both directions. Track three numbers separately, because they don't correlate the way you'd expect: how long the conversion itself took, how long fixing the resulting compile errors took, and how much testing effort is still outstanding.&lt;/p&gt;

&lt;p&gt;The first program in a batch is not representative of the rest. It takes the longest on both conversion and compile-fixing, because you're still learning the model's conventions and still learning which message types are structural versus cascade. The second and third go faster — not because the code is simpler, but because you've already built the pattern library for what a "file ignored" cascade looks like versus a genuine logic gap. A program with heavier file usage, or a display file involved, will cost more again regardless of where it sits in the sequence. Budget the first one generously and use it to calibrate, not to extrapolate.&lt;/p&gt;




&lt;h2&gt;
  
  
  The reference version of all this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Generate writes source. Create compiles it. They can run together in one submitted job, but they fail independently — know which one broke.&lt;/li&gt;
&lt;li&gt;Target high-level language "RPG IV" gets you fixed-format RPG IV. Free-format is a separate, deliberate conversion step you run after the source compiles clean, never before.&lt;/li&gt;
&lt;li&gt;When a submitted create job ends abnormally, don't dig through the job log for a reason. Go straight to the generation library, find the source the generate step already wrote, and compile it yourself. You'll get real compiler diagnostics instead of a batch-job status.&lt;/li&gt;
&lt;li&gt;On a large error count, read the final summary and the message-frequency table before anything else. Look for structural message types — file and format resolution — before touching the hundreds of cascade messages they cause.&lt;/li&gt;
&lt;li&gt;A file reported as ignored, or a format reported as already defined, are library-list and stale-member problems, not code problems. Fix those first; most of the rest disappears on the next compile.&lt;/li&gt;
&lt;li&gt;A field the model knows about isn't necessarily a field the file format you're reading exposes. Read against the file that actually carries what you need, or confirm the value already exists elsewhere before you delete the reference.&lt;/li&gt;
&lt;li&gt;Effort on the first converted program tells you almost nothing about the tenth. Track conversion time and compile-fix time separately per program if you're estimating a batch.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Jaya Krushna Mohapatra is a Warehouse Management Systems Architect focused on enterprise integrations, IBM i modernization, and scalable backend systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>as400</category>
      <category>ca2e</category>
      <category>synon</category>
      <category>rpgle</category>
    </item>
    <item>
      <title>I Rebuilt Our FTP Layer on IBM i — And It Fixed Problems I Didn't Know We Had</title>
      <dc:creator>Jay</dc:creator>
      <pubDate>Wed, 29 Jul 2026 18:04:24 +0000</pubDate>
      <link>https://dev.to/jay_krshn_1a9ac493fadf8/i-rebuilt-our-ftp-layer-on-ibm-i-and-it-fixed-problems-i-didnt-know-we-had-5358</link>
      <guid>https://dev.to/jay_krshn_1a9ac493fadf8/i-rebuilt-our-ftp-layer-on-ibm-i-and-it-fixed-problems-i-didnt-know-we-had-5358</guid>
      <description>&lt;h2&gt;
  
  
  What happens when you replace twenty years of ad-hoc scripts with one framework
&lt;/h2&gt;




&lt;p&gt;Every IBM i shop I've ever worked with has the same thing tucked away somewhere: a folder of CL programs that do FTP.&lt;/p&gt;

&lt;p&gt;Not one. Not two. Dozens.&lt;/p&gt;

&lt;p&gt;Some of them were written when the current network admin was in high school. Some have the password sitting in clear text, one line below an FTP command. Some of them ran fine for fifteen years and nobody touched them because touching them is scarier than leaving them alone. A few of them died quietly overnight last Tuesday and nobody noticed until the morning report was missing.&lt;/p&gt;

&lt;p&gt;That's what this project started from. Not a grand modernization strategy. Not a compliance directive. Just the growing feeling that what we had was a very polite kind of disaster waiting to happen.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pile
&lt;/h2&gt;

&lt;p&gt;If you've lived this, you already know the shape of it. But for anyone who hasn't, here's what the "pile" actually looks like in a typical IBM i shop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clear-text passwords in CL source members. Anyone with read authority on the source file can see them.&lt;/li&gt;
&lt;li&gt;No audit trail. Who created the script? Who last changed the destination path? Nobody knows. You display the source and hope the comments are honest.&lt;/li&gt;
&lt;li&gt;No record of what ran when. If Tuesday's file didn't arrive, you're opening spool files and piecing together what happened from raw FTP output.&lt;/li&gt;
&lt;li&gt;No retries. If the remote server bounced you once, the job failed and you got an email at 3 a.m. — assuming the email worked.&lt;/li&gt;
&lt;li&gt;No recovery. If the FTP crashed mid-transfer, the production file was already half-overwritten. Good luck.&lt;/li&gt;
&lt;li&gt;No way to disable a broken interface without editing production source. So nobody disables anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everyone knew. Everyone had a story about "that one time." Nobody had the time to fix it all.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deciding to actually do it
&lt;/h2&gt;

&lt;p&gt;I started with what I thought was a small fix. One of our FTP pulls from a mainframe kept failing on Sundays. I opened the CL, fixed the timeout, went to save, and saw the password in the source. Not hashed. Not referenced. Not pulled from a data area. Just sitting there in plain CL, in a source file that half the shop could read because that's how the whole library was set up fifteen years ago.&lt;/p&gt;

&lt;p&gt;I fixed it. Then I opened the next CL. Same thing.&lt;/p&gt;

&lt;p&gt;I looked at the list. Dozens of these things. Interfaces to a mainframe, to UNIX boxes, to a cloud SFTP gateway, to a vendor's SFTP. All with their own idea of what "error handling" meant. All with their own password, right there in the source.&lt;/p&gt;

&lt;p&gt;You know that moment where you stop trying to fix the broken thing and start trying to replace the category of thing? That was that moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why FTP, and not an API
&lt;/h2&gt;

&lt;p&gt;The obvious question, if you don't live on IBM i: why keep FTP alive at all? Why not just replace it with REST calls, or a message queue, or literally anything from this decade?&lt;/p&gt;

&lt;p&gt;Because I don't control the other end.&lt;/p&gt;

&lt;p&gt;The counterparties on these transfers are a mainframe that's been running since before I started, a handful of UNIX boxes owned by other teams, a cloud SFTP gateway, and vendor systems whose integration options begin and end with "here's your SFTP login." None of them are going to stand up a REST API for me. Some of them can barely be asked to rotate a password. FTP/SFTP isn't a design choice on my side of the fence — it's the only door that exists on the other side.&lt;/p&gt;

&lt;p&gt;So the goal was never "get off FTP." It was "stop treating FTP as a place where anything goes." Where I &lt;em&gt;do&lt;/em&gt; control both ends — internal services, systems I own — I use APIs, and that's a different, easier problem. This project was specifically about the transfers where FTP is the only option and the only thing standing between "reliable" and "whatever that CL happened to do in 2011" is whether someone bothered to build a framework around it.&lt;/p&gt;

&lt;p&gt;That distinction mattered more than anything else in scoping this. I wasn't competing with modern integration patterns. I was making the legacy pattern I was stuck with behave like one.&lt;/p&gt;




&lt;h2&gt;
  
  
  What these transfers actually move
&lt;/h2&gt;

&lt;p&gt;It's worth being concrete about what's riding on this, because "FTP framework" sounds abstract until you see what's actually flowing through it.&lt;/p&gt;

&lt;p&gt;The ERP itself runs on the IBM i. Around it sits a mainframe that's still the system of record for a slice of transactional data predating the ERP, a warehouse management system on its own platform, EDI exchanges with vendors and trading partners, and a handful of internal reporting feeds. None of these systems talk to each other natively — the FTP layer is the connective tissue. Nightly extracts pulled in for the ERP to consume. Outbound files pushed out for the warehouse system to pick up. EDI documents exchanged with partners. Inbound price and inventory files landing from vendors.&lt;/p&gt;

&lt;p&gt;None of it is glamorous. All of it is load-bearing. If the inbound inventory feed doesn't land by 4 a.m., the warehouse is working from yesterday's numbers. If the outbound order file doesn't reach the vendor, nothing ships. This is the plumbing that makes otherwise-unrelated systems agree with each other.&lt;/p&gt;

&lt;p&gt;That's what made "just harden the FTP layer" worth three weeks of evenings. It wasn't a nice-to-have. It was the seam where every other system touched every other system.&lt;/p&gt;




&lt;h2&gt;
  
  
  The approach
&lt;/h2&gt;

&lt;p&gt;I wasn't trying to build a platform. I wasn't trying to win an architecture award. I wanted one thing: take all of this and consolidate it into a single framework that an operator can configure without touching source code, that logs everything, that encrypts credentials, and that doesn't silently half-overwrite production files when something goes wrong.&lt;/p&gt;

&lt;p&gt;Three layers. That's it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance&lt;/strong&gt; — a subfile UI. Operators define transfers by filling in fields: host, port, remote path, local file, pre/post commands, retry counts, retention. Passwords go in once, encrypted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution&lt;/strong&gt; — one program that reads the config, acquires a lock, runs the transfer, validates what came back, logs everything, and updates status.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Housekeeping&lt;/strong&gt; — scheduled retention cleanup. Archives age out automatically according to rules the operator set when they defined the interface.&lt;/p&gt;

&lt;p&gt;A small set of objects overall. A handful of physical files for the data model. Two service programs (one for encryption, one for logging). Two main programs, a retention job, and a CL wrapper for operator inquiries, plus display and printer files and a key data area.&lt;/p&gt;

&lt;p&gt;About three weeks of evenings, once the design settled. The hard part wasn't the code. The hard part was all the things I didn't realize needed to be solved.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pre and post commands — the escape hatch
&lt;/h2&gt;

&lt;p&gt;Not every interface is "connect, get file, disconnect." Some need something to happen right before or right after.&lt;/p&gt;

&lt;p&gt;A vendor drops a zipped file — it needs unzipping after it lands, before the ERP job that reads it ever runs. A file needs archiving under a different name before the transfer starts, because a downstream job is still reading the old one. Or the transfer is really just step one, and step two is submitting the ERP load job the moment the data is safely in staging, instead of waiting on a fixed schedule.&lt;/p&gt;

&lt;p&gt;Rather than hardcode any of that into the engine, each interface can define its own sequence of pre-commands and post-commands — plain CL, run through the same command executor, in whatever order the operator sequences them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pre-commands  (before FTP): unzip inbound archive, rename prior file out of the way
Post-commands (after FTP):  submit the ERP load job, notify downstream job scheduler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They're stored the same way the rest of the interface is configured — as sequenced rows against that interface — so adding a step doesn't mean touching a compiled program. It means adding a row.&lt;/p&gt;

&lt;p&gt;This is the piece that turned the framework from "a way to move a file safely" into "a way to move a file safely and then make something happen because it arrived." That second part ended up mattering as much as the first.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problems I didn't know I had
&lt;/h2&gt;

&lt;p&gt;This is the part that genuinely surprised me.&lt;/p&gt;

&lt;p&gt;I went in thinking: centralize the config, encrypt the passwords, add logging. Done.&lt;/p&gt;

&lt;p&gt;What I didn't expect was how many failure modes you uncover the moment you have one central place to look at them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jobs that crashed and left the world inconsistent
&lt;/h3&gt;

&lt;p&gt;The old scripts would copy from FTP output straight into production. If anything failed partway — network blip, disk full, somebody killing the job — you'd end up with a partial file in production and no clean way to tell what happened.&lt;/p&gt;

&lt;p&gt;The fix wasn't complicated, but it changed everything: always archive the current production file to a timestamped member first, always FTP into a dedicated staging file (never production), and only copy from staging to production at the end, as the last step. If anything fails before the final copy, production is untouched.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[archive current] → [clear staging] → [FTP into staging]
                  → [validate]       → [atomic swap to production]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The existence of staging changed the shape of the whole program. It meant I could validate before committing. It meant I could detect empty files. It meant I could compare record counts to the last run. It meant I could reject a transfer that looked wrong &lt;em&gt;before&lt;/em&gt; it ate the existing data.&lt;/p&gt;

&lt;p&gt;Stripped down to the shape of it (generic names, not our actual field names), the execution engine looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Never touch production directly — everything lands in staging first
archMbr = %char(%date(): *iso0) + %char(%time(): *iso0);

CPYF FROMFILE(prodFile) TOFILE(archLib/archMbr) MBROPT(*ADD);
CLRPFM FILE(stgFile);

ftpGet(host: user: password: remotePath: stgFile);

if recordCount(stgFile) = 0 and not allowEmpty;
  logAndHalt('E01: zero records received, expected data');
  return;
endif;

varPct = ((lastRecCount - recordCount(stgFile)) / lastRecCount) * 100;
if varPct &amp;gt; maxVarPct;
  logAndHalt('E02: record count dropped ' + %char(varPct) + '% vs last run');
  return;
endif;

// Only now does production ever get touched
CPYF FROMFILE(stgFile) TOFILE(prodFile) MBROPT(*REPLACE);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing exotic. The whole trick is ordering: archive first, validate in staging, and let production be the &lt;em&gt;last&lt;/em&gt; thing written, not the first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jobs that died without anyone knowing
&lt;/h3&gt;

&lt;p&gt;Every run now opens with an entry in a history table marked &lt;em&gt;running&lt;/em&gt;. When it finishes clean, it gets flipped to &lt;em&gt;complete&lt;/em&gt;. If it fails, it gets &lt;em&gt;failed&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Which means: if I see a row stuck in &lt;em&gt;running&lt;/em&gt; but the job isn't running anymore, something crashed. The next run of that interface detects the orphan, logs it, and moves on. No manual cleanup. No lingering locks. No mystery rows.&lt;/p&gt;

&lt;p&gt;It's a tiny pattern — one column and three status values — but it's the difference between "silent failure" and "the system tells you what happened."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- opening a run&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;ftp_history&lt;/span&gt;
   &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'R'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- closing it out cleanly&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;ftp_history&lt;/span&gt;
   &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'C'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end_ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- on startup, before doing anything else: find yesterday's ghosts&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_ts&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;ftp_history&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'R'&lt;/span&gt;
   &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;start_ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;HOUR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- anything this query returns didn't crash gracefully — it just crashed.&lt;/span&gt;
&lt;span class="c1"&gt;-- mark it 'F', log it, move on. No paging, no digging through spool files.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Jobs that retried forever
&lt;/h3&gt;

&lt;p&gt;The old scripts didn't retry. If the server bounced you once, the whole job was a failure.&lt;/p&gt;

&lt;p&gt;But sometimes the server just needs a second. So the new engine retries up to &lt;em&gt;N&lt;/em&gt; times (per-interface, operator-configurable). Fine.&lt;/p&gt;

&lt;p&gt;The interesting question was: what happens when retries run out?&lt;/p&gt;

&lt;p&gt;Before, the job would just end in failure. But what if the operator is at their desk right now and could tell you something useful? What if they know the remote server is under maintenance and they want to skip this one and keep the rest of the batch going?&lt;/p&gt;

&lt;p&gt;I wired it into the system operator message queue. When retries exhaust, the job sends an inquiry message: &lt;em&gt;Retry / Skip / Cancel.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;R&lt;/strong&gt;etry — reset the retry counter, try again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S&lt;/strong&gt;kip — move on to the next record in the batch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C&lt;/strong&gt;ancel — shut down the entire run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It sounds like an old-school pattern because it is one. But for an operator-monitored batch window, it's perfect. The job doesn't die unnecessarily, and it doesn't loop forever either.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interfaces that were broken for weeks
&lt;/h3&gt;

&lt;p&gt;The old scripts had no concept of "this interface is broken, stop trying." If a vendor changed their SFTP credentials on a Monday and nobody told you, your job would fail every night at 2 a.m. for a month, sending 30 failure emails. And on day 32 someone would finally read one.&lt;/p&gt;

&lt;p&gt;New rule: if an interface fails &lt;em&gt;N&lt;/em&gt; times in a row consecutively (configurable per-interface), auto-disable it. Send the escalation email once, not thirty times. The operator has to explicitly re-enable it, which means they have to &lt;em&gt;acknowledge&lt;/em&gt; what broke.&lt;/p&gt;

&lt;p&gt;It's a small change. It ended the 3 a.m. email storm inside the first month.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transfers that overwrote good data with bad data
&lt;/h3&gt;

&lt;p&gt;This is the one I'm most proud of catching.&lt;/p&gt;

&lt;p&gt;Every interface stores the record count from its last successful run. On the next run, after FTP completes but &lt;em&gt;before&lt;/em&gt; production is overwritten, the engine compares the new count to the last one.&lt;/p&gt;

&lt;p&gt;If the drop is greater than a configurable variance percentage (say, 50%), it halts and raises an error. Production is untouched. The operator gets called in to look at it.&lt;/p&gt;

&lt;p&gt;The number of times this has caught an upstream batch that only wrote half its data to the output dataset… I'm not going to tell you, because it's embarrassing that this used to just happen silently.&lt;/p&gt;

&lt;h3&gt;
  
  
  The audit trail we didn't have
&lt;/h3&gt;

&lt;p&gt;The maintenance UI writes an audit record for every add, change, delete, or restore — field-level, with before/after values, user, timestamp.&lt;/p&gt;

&lt;p&gt;Password changes are always logged as masked asterisks. The actual value is never, ever written anywhere other than the encrypted field in the config table. Not in the audit log. Not in error messages. Not in the spool. Not in the job log. Not in emails.&lt;/p&gt;

&lt;p&gt;(Credential handling is its own rabbit hole and its own article. For now: I use AES-256-CBC with the IBM i Cryptographic APIs, key stored in a data area with exclusive authority locked to the service program, and the plaintext exists in memory for exactly the duration of the login call and is cleared the moment login returns. If you want the details, see my next post.)&lt;/p&gt;




&lt;h2&gt;
  
  
  Operational shape, after
&lt;/h2&gt;

&lt;p&gt;The whole system compiles in a documented order — physical files first, then the key data area, then the two service programs, the CL wrapper, and finally the three main programs. Operators run it with simple calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;call the maintenance program              // open the UI
call the FTP engine with &lt;span class="o"&gt;(&lt;/span&gt;interface, code&lt;span class="o"&gt;)&lt;/span&gt; // run one specific transfer
call the FTP engine with &lt;span class="o"&gt;(&lt;/span&gt;interface, blank&lt;span class="o"&gt;)&lt;/span&gt;// run all transfers &lt;span class="k"&gt;for &lt;/span&gt;an interface
submit the FTP engine to a batch queue     // scheduled batch window
submit the retention cleanup on a schedule // nightly housekeeping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No source changes. No recompile. New interface? Add a row. Change a destination path? Change a field. Disable one temporarily? Flip a flag. Restore yesterday's archive? Option from the maintenance screen, auto-logged to audit.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd say to anyone thinking about this
&lt;/h2&gt;

&lt;p&gt;If you're on IBM i and you've got a pile of FTP scripts like the one I had, I'd gently suggest that you have more technical debt than you realize, and less work than you think it will be to pay it off.&lt;/p&gt;

&lt;p&gt;A few things I learned that were not obvious going in:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Staging is the whole game.&lt;/strong&gt; If production is never written to directly, half your failure modes stop being catastrophic and become "try again tomorrow."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A history table with a state column is cheaper than a monitoring system.&lt;/strong&gt; A &lt;em&gt;running&lt;/em&gt; row that never became &lt;em&gt;complete&lt;/em&gt; or &lt;em&gt;failed&lt;/em&gt; is a crash. That's it. You just discovered yesterday's broken job this morning instead of Friday.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operators know things your program doesn't.&lt;/strong&gt; A system-operator inquiry on exhausted retries sounds old-fashioned but is strictly better than either "fail silently" or "retry forever."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variance detection catches bad upstream jobs.&lt;/strong&gt; Not just your own problems. Your upstream batch also has bugs. Stop letting its bugs corrupt your files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-disable after N failures is a kindness.&lt;/strong&gt; To you, to your team, to the operators who are tired of deleting 30 failure emails every Monday.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is novel. Every pattern I used exists somewhere in the IBM i world, in some shop, written by somebody who already had the scar. The only thing that's new about this is that they're together, in one framework, running every FTP transfer we do.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it feels like, afterwards
&lt;/h2&gt;

&lt;p&gt;I'll tell you the thing I wasn't expecting.&lt;/p&gt;

&lt;p&gt;After the rollout, the first week, we had a transfer fail. Remote server was down. The retry exhausted. The inquiry went out. Operator replied &lt;em&gt;Skip&lt;/em&gt;. The run continued. The history table had one failed row and sixteen successful ones. The spool had a clean summary. The next morning, somebody logged in and saw the one red row, checked the remote status, clicked &lt;em&gt;Retry&lt;/em&gt; from the maintenance screen, and it was done.&lt;/p&gt;

&lt;p&gt;Nobody called me. Nobody sent a frantic email. No 3 a.m. page. The system told the operator what happened, they knew what to do, they did it.&lt;/p&gt;

&lt;p&gt;That's what "modernization" actually looks like, I think, on IBM i. Not replacing the platform. Not rewriting everything in Java. Just taking the rough edges off the things that have been rough for twenty years, using the tools that are already there, and giving the people who run the business a reason to stop dreading their inbox.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;If you're in an IBM i shop and the word "FTP" gives you a mild anxiety response, you're not alone, and it's probably fixable. The platform has everything you need — SQL services, cryptographic APIs, subfile UIs, system-operator messaging, data areas, service programs. You don't need a new stack. You just need to decide the pile is the problem, and that one framework is the answer.&lt;/p&gt;

&lt;p&gt;The credential encryption piece specifically — how I'm using the IBM i cryptographic APIs with AES-256-CBC, where the key lives, how it's rotated, and why you probably shouldn't write your own — is what I want to cover next. It's the part that should be easy and never quite is.&lt;/p&gt;

&lt;p&gt;For now: if anyone from my old shops is reading this and you still have my old FTP scripts running somewhere, I'm sorry. I've done better since.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I am an IBM i practitioner working on modernization, integration, and warehouse systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>as400</category>
      <category>ftp</category>
      <category>zos</category>
    </item>
    <item>
      <title># How I Encrypt and Use Credentials on IBM i</title>
      <dc:creator>Jay</dc:creator>
      <pubDate>Wed, 22 Apr 2026 17:37:00 +0000</pubDate>
      <link>https://dev.to/jay_krshn_1a9ac493fadf8/-how-i-encrypt-and-use-credentials-on-ibm-i-2h9d</link>
      <guid>https://dev.to/jay_krshn_1a9ac493fadf8/-how-i-encrypt-and-use-credentials-on-ibm-i-2h9d</guid>
      <description>&lt;h1&gt;
  
  
  How I Encrypt and Use Credentials on IBM i
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Because the password probably shouldn't be in the CL source
&lt;/h2&gt;




&lt;p&gt;I'm going to say something a little uncomfortable.&lt;/p&gt;

&lt;p&gt;If you work on IBM i and your shop has been around for a while, there is almost certainly a password sitting in clear text in a source member right now. Probably several. Probably an FTP password, maybe a database connection string, maybe a vendor API key. Maybe in a data area somewhere that doesn't have its authority locked down. Maybe in a CL that anyone with read authority on the library can display.&lt;/p&gt;

&lt;p&gt;Not because your team is careless. It's just how things were done, and nobody has had the time to go back and fix it.&lt;/p&gt;

&lt;p&gt;This post is the "how I finally fixed it" — specifically, how I'm storing and using credentials in a real production framework on IBM i, using the platform's own cryptographic APIs, without adding any new technology to the stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this is harder than it looks
&lt;/h2&gt;

&lt;p&gt;The first time you sit down to solve "encrypt the password," a handful of bad patterns suggest themselves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Base64 or a simple XOR&lt;/strong&gt; — not encryption. Someone with read access can decode it in their head.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"I'll just hash it"&lt;/strong&gt; — hashing is one-way. You can't log into FTP with a hash. You need the plaintext at the moment you use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party encryption library&lt;/strong&gt; — now you have a dependency, a license question, and a library running in your service program. You don't need any of that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardcode a key in source&lt;/strong&gt; — now the key is in the source. Same problem, one level deeper.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you actually need is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strong encryption at rest&lt;/strong&gt; — actual AES, not a scheme you made up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A key that lives somewhere separate from both the ciphertext and the source&lt;/strong&gt; — and with restrictive authority.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decryption only at the moment of use&lt;/strong&gt; — the plaintext exists in memory for as briefly as possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The plaintext never goes anywhere else&lt;/strong&gt; — not a log, not a spool, not an error message, not the system operator queue, not an audit record.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;IBM i has everything you need for all of this, and has for years. You just have to wire it up.&lt;/p&gt;




&lt;h2&gt;
  
  
  The tools IBM gave us
&lt;/h2&gt;

&lt;p&gt;IBM i has a set of cryptographic APIs. The two you care about are the encrypt and decrypt primitives — they let you encrypt a block of data with a specified algorithm and key, and decrypt it back. They support the standard modern algorithms (AES-128, AES-192, AES-256) in the standard modes (ECB, CBC, OFB, CFB, CTR). They're callable from RPGLE. They've been part of the platform for nearly two decades. They're fine.&lt;/p&gt;

&lt;p&gt;I'm using &lt;strong&gt;AES-256-CBC&lt;/strong&gt;. AES-256 because there's no reason not to in 2026. CBC because it's straightforward, well-understood, and the per-encryption IV property is exactly what we want for "each stored password has a different ciphertext even if two users chose the same password."&lt;/p&gt;

&lt;p&gt;You don't need to write the cipher yourself. You don't want to write the cipher yourself. The built-in APIs are the adult in the room.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where the key lives
&lt;/h2&gt;

&lt;p&gt;This is where most implementations get it wrong, so let me be deliberate about it.&lt;/p&gt;

&lt;p&gt;The key is &lt;strong&gt;not&lt;/strong&gt; in the source code. It's &lt;strong&gt;not&lt;/strong&gt; in a physical file. It's &lt;strong&gt;not&lt;/strong&gt; in the same library as the configuration table it encrypts. It's in an IBM i data area, authority-locked down.&lt;/p&gt;

&lt;p&gt;Three things are happening when you set it up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A 32-byte character data area.&lt;/strong&gt; AES-256 needs a 256-bit key, which is 32 bytes. Nothing fancy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exclusive authority to public.&lt;/strong&gt; Nobody gets to read this by default. Not end users, not developers, nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit grant to the service program's user profile.&lt;/strong&gt; The encryption service program runs under a specific profile that has read access to the data area. Nothing else does.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your service program uses program-adopted authority (the owner having read access to the key data area), you get the same effect — the service program can read the key, but the calling program and the calling user still can't.&lt;/p&gt;

&lt;p&gt;This is the kind of thing IBM i is genuinely good at, and we forget to use. Object-level authority on a single data area is a more rigorous access control than most cloud secret managers offer out of the box. It just looks unfamiliar because we're used to reading about third-party vault products instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  The encryption service program
&lt;/h2&gt;

&lt;p&gt;The heart of this is a tiny service program. Two exported procedures. That's the whole API.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encrypt&lt;/strong&gt; — takes clear-text (say, up to 64 characters, which is plenty for a password) and returns a larger field containing a 16-byte IV followed by the ciphertext.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decrypt&lt;/strong&gt; — takes the stored field back and returns the clear-text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole public surface area. Everything else is an implementation detail of those two procedures.&lt;/p&gt;




&lt;h2&gt;
  
  
  Inside Encrypt (sketch)
&lt;/h2&gt;

&lt;p&gt;The real file is a couple hundred lines. The shape is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dcl-proc Encrypt export;
  dcl-pi *n varchar(256);
    plaintext varchar(64) const;
  end-pi;

  // 1. Read the key from the data area
  //    (only this procedure, under its adopted authority,
  //    is allowed to do this)

  // 2. Generate a fresh IV (16 bytes, random)
  //    using the platform PRNG

  // 3. Build the algorithm descriptor for AES-256-CBC
  //    with this IV

  // 4. Build the key descriptor for our 32-byte key

  // 5. Call the encrypt primitive with plaintext,
  //    algorithm, key, result buffer

  // 6. Concatenate IV + ciphertext and return

end-proc;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to note:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The IV is fresh every call.&lt;/strong&gt; A new IV per encryption means that the same password encrypted twice produces two different ciphertexts. This is not optional; it's the security property CBC is trying to give you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The IV is stored alongside the ciphertext, not hidden.&lt;/strong&gt; The IV isn't secret — it just has to be unpredictable. Prepending it to the ciphertext means decryption doesn't need to know anything except the 32-byte key and the stored field itself.&lt;/p&gt;

&lt;p&gt;Decrypt is the mirror image: slice off the first 16 bytes as the IV, feed the rest into the decrypt primitive with the same key and algorithm, return the plaintext.&lt;/p&gt;




&lt;h2&gt;
  
  
  Using it safely at the call site
&lt;/h2&gt;

&lt;p&gt;This is the part that matters more than the crypto.&lt;/p&gt;

&lt;p&gt;Here's how the execution program — the thing that actually performs the FTP transfer — uses a stored credential:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// 1. Read the encrypted password from the config table

// 2. Decrypt — plaintext now exists in a local field

// 3. Use it immediately — the login call is the only thing
//    that sees it

// 4. Clear it from memory the instant we're done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four steps, in this order, no exceptions. The plaintext exists in memory for the duration of the login call and is cleared the moment login returns — whether the login succeeded or failed. The clear-text field never leaves this procedure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's deliberately missing:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No display of the plaintext. Obviously.&lt;/li&gt;
&lt;li&gt;No log line that includes the password. Also obviously. But I've seen this in the wild.&lt;/li&gt;
&lt;li&gt;No copy into a global field for "later use." There is no "later use."&lt;/li&gt;
&lt;li&gt;No error message that includes the password. If the login fails, the error is "login failed for user &lt;em&gt;X&lt;/em&gt;." That's it. The password is not the problem; the problem is whatever the server said back.&lt;/li&gt;
&lt;li&gt;No audit record with the plaintext. The audit table logs changes to the encrypted field as masked asterisks. Always. If you ever need to prove that a password &lt;em&gt;changed&lt;/em&gt; for compliance, you can see the timestamp and the user. You cannot see the value. That is the correct behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I could do one thing to make people's IBM i shops more secure, it would be: get everyone to stop logging the password "just for debugging, I'll remove it later." You won't. It'll be in a spool file for three years.&lt;/p&gt;




&lt;h2&gt;
  
  
  What about key rotation?
&lt;/h2&gt;

&lt;p&gt;You need a plan for this before you deploy, not after.&lt;/p&gt;

&lt;p&gt;My approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate a new 32-byte key. Store it somewhere &lt;em&gt;outside&lt;/em&gt; IBM i for the duration of the rotation (a password manager, a sealed envelope — whatever your compliance model allows).&lt;/li&gt;
&lt;li&gt;For each row in the config table: read the stored encrypted field, decrypt with the &lt;em&gt;old&lt;/em&gt; key, re-encrypt with the &lt;em&gt;new&lt;/em&gt; key, write back.&lt;/li&gt;
&lt;li&gt;Update the data area to the new value.&lt;/li&gt;
&lt;li&gt;Destroy the external copy of the new key.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This rotation is the kind of job that lives in a one-shot program that takes the old key and the new key as parameters and runs once. You throw it away after rotation. You don't leave it running anywhere.&lt;/p&gt;

&lt;p&gt;There's a more elegant version of this where you have a key ID column next to each ciphertext and can support multiple keys in flight during a rotation window. For most shops, the simple "rotate all at once" pattern is fine — it takes a couple of seconds even with thousands of rows.&lt;/p&gt;




&lt;h2&gt;
  
  
  What else I'd watch for
&lt;/h2&gt;

&lt;p&gt;A few things I've seen people get wrong, even with the encryption piece done correctly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The password in a job parameter.&lt;/strong&gt; If you submit a job with a clear-text password as one of its parameters, that parameter is visible in the job's submitted-jobs view and in the job log. Never pass a plaintext password as a parameter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The password in a temporary physical file.&lt;/strong&gt; "I'll just stage it in a work file for a second." No. Work files persist until they're cleared. If the job crashes, they persist longer. Don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The password in a user space.&lt;/strong&gt; User spaces are visible with the right authority. If you wouldn't write the password to a physical file, don't write it to a user space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adopted authority for the wrong thing.&lt;/strong&gt; If your service program adopts authority to read the key data area, make sure the programs that &lt;em&gt;call&lt;/em&gt; it don't also adopt that authority. The whole point is that the key is only visible to one narrow thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting the clear.&lt;/strong&gt; In RPG, local fields go out of scope when the procedure exits, and their memory will eventually be reused. "Eventually" is the issue — a failed login could leave the plaintext sitting in the activation group's memory until something else overwrites it. An explicit clear on the field as the last thing you do is cheap insurance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this is worth doing even if nobody asks for it
&lt;/h2&gt;

&lt;p&gt;No regulator is auditing your IBM i shop for clear-text FTP passwords. No CISO is going to find them unless they look. No application scanner is going to flag them because nothing runs against RPG source.&lt;/p&gt;

&lt;p&gt;This is the kind of problem you either fix because you care, or you don't fix at all.&lt;/p&gt;

&lt;p&gt;Here's the honest argument for doing it: the day someone &lt;em&gt;does&lt;/em&gt; ask — because of a breach, an audit, a new vendor, a merger — the fix will take an afternoon if you've already built the pattern, and it will take months if you haven't. The encryption service program in my current framework is a couple hundred lines of RPG. The data area is one command to create. The authority lockdown is one command. Every program that uses a credential already goes through the Encrypt / Decrypt procedures. If I need to rotate the key, it's one program and fifteen minutes.&lt;/p&gt;

&lt;p&gt;None of this is clever. None of it is new. IBM gave us these cryptographic APIs two decades ago and they've been sitting there, waiting.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;If you take one thing from this post, take this: &lt;strong&gt;passwords on IBM i should live in an encrypted field in a config table, encrypted with an AES-256 key stored in an authority-locked data area, decrypted by a small service program only at the moment of use, cleared from memory immediately after, and never written anywhere else.&lt;/strong&gt; That's the whole pattern.&lt;/p&gt;

&lt;p&gt;You don't need a vault. You don't need a new tool. You don't need to learn anything outside the platform. The cryptographic APIs are right there.&lt;/p&gt;

&lt;p&gt;A framework I've been working on — an FTP integration layer I rebuilt from a pile of ad-hoc scripts — uses exactly this pattern for every credential it stores. It's a couple hundred lines of RPG and a data area. It's the cheapest, most durable security improvement I've made to an IBM i codebase in years. (I'll write that one up in a follow-up post.)&lt;/p&gt;

&lt;p&gt;If you do this for one program, you'll end up doing it for more. And the next time someone opens a CL and sees a password in plain text, you'll notice, and you'll fix it.&lt;/p&gt;

&lt;p&gt;That's the real goal. Not the crypto. The habit.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Jaya Krushna Mohapatra is a Warehouse Management Systems Architect focused on enterprise integrations, IBM i modernization, and scalable backend systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>infosec</category>
      <category>programming</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Connected Claude to My IBM i - And It Changed How I Think About Legacy Modernization</title>
      <dc:creator>Jay</dc:creator>
      <pubDate>Thu, 26 Mar 2026 14:34:23 +0000</pubDate>
      <link>https://dev.to/jay_krshn_1a9ac493fadf8/i-connected-claude-to-my-ibm-i-and-it-changed-how-i-think-about-legacy-modernization-19i1</link>
      <guid>https://dev.to/jay_krshn_1a9ac493fadf8/i-connected-claude-to-my-ibm-i-and-it-changed-how-i-think-about-legacy-modernization-19i1</guid>
      <description>&lt;h2&gt;
  
  
  What happens when you give an AI direct access to a system most people have never heard of
&lt;/h2&gt;




&lt;p&gt;There's a weird disconnect in the AI conversation right now. Everyone's talking about coding assistants, AI-powered DevOps, intelligent dashboards — but almost all of it assumes you're running modern cloud infrastructure. Kubernetes, PostgreSQL, GitHub Actions. The usual stack.&lt;/p&gt;

&lt;p&gt;Nobody's talking about what happens when your critical business system runs on IBM i.&lt;/p&gt;

&lt;p&gt;I work with IBM i every day. Warehouses, supply chains, enterprise systems that process millions of transactions and have been running for decades. These systems aren't going anywhere. They're stable, they're fast, and they do exactly what they're supposed to do.&lt;/p&gt;

&lt;p&gt;But they've also been left out of the AI conversation entirely. And I kept wondering — does it have to be that way?&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with IBM i and modern tooling
&lt;/h2&gt;

&lt;p&gt;If you've worked with IBM i, you know the feeling. You're reading about some new tool or platform, nodding along, and then you hit the part where they assume everything lives in a REST API or a cloud database. And you think — okay, that doesn't apply to me.&lt;/p&gt;

&lt;p&gt;The irony is that IBM i systems often hold the most valuable data in an organization. Decades of transaction history. Real-time inventory positions. Production schedules that run 24/7. But getting that data out — or even just asking questions about it — still involves signing into a green screen, navigating menus, running queries manually, and interpreting raw output.&lt;/p&gt;

&lt;p&gt;It's not that the data isn't accessible. Db2 for i is a perfectly capable database. QSYS2 SQL Services have made an incredible amount of system information queryable through standard SQL. The access is there. But the experience of getting to it hasn't kept up with what's happening everywhere else.&lt;/p&gt;




&lt;h2&gt;
  
  
  When I discovered MCP
&lt;/h2&gt;

&lt;p&gt;Earlier this year I came across the Model Context Protocol — MCP for short. It's an open standard that lets AI assistants connect to external tools and data sources. Anthropic published it, and it's gained traction quickly. The idea is simple: you define tools that the AI can call, and the AI figures out when and how to use them based on what you're asking.&lt;/p&gt;

&lt;p&gt;The moment I understood how it worked, the wheels started turning.&lt;/p&gt;

&lt;p&gt;What if I could write a handful of tools — run a SQL query, list active jobs, check system status, browse the IFS — and expose them to Claude through MCP? Not building a chatbot. Not training a model on IBM i documentation. Just giving an AI the ability to reach into the system and pull back real data.&lt;/p&gt;

&lt;p&gt;So that's what I did.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it works (without the complexity you'd expect)
&lt;/h2&gt;

&lt;p&gt;The architecture is embarrassingly simple, which is part of what makes it powerful.&lt;/p&gt;

&lt;p&gt;You write a small Python server that runs on your local machine. This server connects to IBM i through ODBC — the same driver you probably already have installed if you use ACS (IBM i Access Client Solutions). The server defines tools as Python functions, each with a description that tells the AI what it does.&lt;/p&gt;

&lt;p&gt;That's the entire stack. Python on your PC, ODBC to IBM i, MCP to the AI. No changes on the IBM i side. No new programs to deploy. No RPG modifications. No service entries. Nothing.&lt;/p&gt;

&lt;p&gt;The secret sauce, if there is one, is QSYS2 SQL Services. IBM has been quietly building out an incredible set of SQL-accessible system functions over the last several technology refreshes. Active job info, job logs, spool files, IFS statistics, system values, user profiles, message queues, data areas — almost everything you'd normally access through CL commands or green screen menus is now available as SQL table functions.&lt;/p&gt;

&lt;p&gt;This means every tool in the MCP server is just a SQL query. Clean input, structured output. The AI gets JSON back instead of green-screen text, which it can actually interpret and reason about.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it feels like to use
&lt;/h2&gt;

&lt;p&gt;This is the part that genuinely surprised me.&lt;/p&gt;

&lt;p&gt;I expected it to be useful. A faster way to run queries, maybe. A convenience layer. What I didn't expect was how much it would change the way I interact with the system.&lt;/p&gt;

&lt;p&gt;You can ask things like:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Show me all libraries that start with PROD and tell me how many tables are in each one."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And it just does it. Runs the query, counts the results, gives you a formatted answer. No navigating to WRKLIB. No typing SQL into STRSQL. Just a question and an answer.&lt;/p&gt;

&lt;p&gt;Or:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"What's the system health looking like right now? Anything I should worry about?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It pulls CPU usage, memory, disk capacity, active job counts — and then interprets them. Not just raw numbers, but context. "CPU is at 23%, well within normal range. Disk usage on ASP 1 is at 71%, which is getting up there — you might want to keep an eye on that."&lt;/p&gt;

&lt;p&gt;The real magic happens when you chain things together in a conversation. You ask about active jobs for a specific user. Something looks odd. You say "check the job log for that one." It knows which job you mean from the previous response. Then you say "has this user had issues before? Check their message queue." And it does.&lt;/p&gt;

&lt;p&gt;That kind of continuity — where context carries forward naturally — is something you can't replicate with traditional tools. Every green screen interaction is stateless. You close the screen, the context is gone. Here, the AI holds onto the thread and builds on it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The tools that matter most
&lt;/h2&gt;

&lt;p&gt;After using this for a while, I've found that certain tools get used far more than others.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQL queries&lt;/strong&gt; are the backbone. Being able to say "find all orders from the last 48 hours where the quantity is over 500" and get results instantly — that alone would justify the setup. But when you combine it with the AI's ability to interpret and summarize, it becomes something different. You're not just querying data, you're having a conversation about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;System status&lt;/strong&gt; is surprisingly useful. I used to check WRKACTJOB and WRKSYSSTS a few times a day. Now I just ask. And the AI remembers what "normal" looked like from previous checks, so it can flag when something changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Job logs&lt;/strong&gt; are where the AI really shines. Reading job logs on IBM i is tedious — they're dense, full of informational messages mixed in with the important stuff. The AI is genuinely good at scanning through a job log and picking out the messages that matter. "There are 47 messages in this job log. Most are routine. But there's a CPF4131 at 14:23 indicating a file member not found, and a follow-up CPD0006 — that's likely your issue."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IFS browsing and file reading&lt;/strong&gt; is the one I didn't expect to use as much as I do. Being able to say "show me what's in /home/myuser/exports and read the most recent CSV" is just faster than navigating the IFS through any other method.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why QSYS2 is the unsung hero
&lt;/h2&gt;

&lt;p&gt;I want to spend a moment on this because I think it's underappreciated.&lt;/p&gt;

&lt;p&gt;IBM has been building QSYS2 SQL Services for years. Every technology refresh adds more. And the beauty of it is that it turns everything on the system into structured, queryable data. You don't need to parse command output. You don't need screen-scraping programs. You just write SQL.&lt;/p&gt;

&lt;p&gt;For this kind of AI integration, that's everything. The AI needs structured data to reason about. Give it a blob of green-screen text and it'll struggle. Give it a JSON array of job records with named fields and it'll do exactly what you want.&lt;/p&gt;

&lt;p&gt;If your shop hasn't explored what's available through QSYS2 lately, it's worth looking. The coverage now is extensive — far beyond what most people realize. It's one of the best things IBM has done for the platform in recent years, and projects like this show why.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this means for IBM i shops
&lt;/h2&gt;

&lt;p&gt;I'm not going to oversell this. It's not going to replace your operators. It's not going to automate your entire system. It's a tool.&lt;/p&gt;

&lt;p&gt;But it's a tool that addresses something I've seen at every IBM i shop I've worked with: the knowledge bottleneck.&lt;/p&gt;

&lt;p&gt;There are usually one or two people who really know the system. Who know which libraries matter, what the critical jobs are, where to look when something breaks. When those people are unavailable — or when they eventually leave — that knowledge walks out the door.&lt;/p&gt;

&lt;p&gt;An AI that can query the system, interpret results, and carry context across a conversation doesn't replace that expertise. But it makes it accessible to people who don't have it yet. A junior developer can ask "what are the biggest tables in PRODLIB?" and get an immediate, meaningful answer. A manager can check on system health without learning CL commands. A new team member can explore the system conversationally instead of reading documentation that may or may not be current.&lt;/p&gt;

&lt;p&gt;That accessibility matters. IBM i's biggest challenge has never been capability — it's been the perception that it's impenetrable. Anything that makes it more approachable is a win for the platform's long-term viability.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd build next
&lt;/h2&gt;

&lt;p&gt;This was a starting point. The obvious extensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source member browsing&lt;/strong&gt; — being able to read RPG or CL source through a conversation and have the AI explain what it does. Imagine onboarding new developers who can literally ask the AI "what does this program do?" while looking at the source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authority analysis&lt;/strong&gt; — "who has access to this file?" is a question that takes too long to answer today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PTF and system maintenance status&lt;/strong&gt; — turning system administration checks into conversations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-referencing&lt;/strong&gt; — "which programs use this file?" by querying object references through SQL Services.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The protocol makes this extensible. Adding a new capability is just adding a new function with a description. There's no framework overhead, no deployment complexity. The hardest part is writing good SQL, and if you're on IBM i, you're already doing that.&lt;/p&gt;




&lt;h2&gt;
  
  
  The bigger picture
&lt;/h2&gt;

&lt;p&gt;I started building this because I was curious. I kept building it because it genuinely made me more productive. But what excites me most is what it represents.&lt;/p&gt;

&lt;p&gt;For years, IBM i modernization has been framed as "move off the platform" or "rewrite everything." And both of those approaches are expensive, risky, and often unnecessary. The systems work. The data is valuable. The business logic is proven.&lt;/p&gt;

&lt;p&gt;What MCP shows is that you can bring modern capabilities to IBM i without changing IBM i. You don't need to rewrite your RPG. You don't need to migrate your database. You don't need to replace anything. You just need to build a bridge — a thin layer that translates between what the AI expects and what IBM i provides.&lt;/p&gt;

&lt;p&gt;QSYS2 SQL Services is one half of that bridge. MCP is the other. And the fact that you can connect them with a few hundred lines of Python — no middleware, no platform changes, no vendor contracts — is exactly the kind of pragmatic modernization that actually works in enterprise environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;I'm not naive about the limitations. There are security considerations — you need to be thoughtful about what you expose and to whom. There's the question of audit trails and compliance. And the AI will occasionally get a query wrong, just like any tool.&lt;/p&gt;

&lt;p&gt;But the potential here is real. And it's the kind of potential that doesn't require permission from a steering committee or a six-month project plan. It's a Saturday afternoon experiment that turns into something you use every day.&lt;/p&gt;

&lt;p&gt;If you're in an IBM i shop and you've been wondering where AI fits into your world, this might be the answer. Not a massive transformation initiative. Not a vendor platform. Just a conversation with your system that actually understands what you're asking.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Jaya Krushna Mohapatra is a Warehouse Management Systems Architect focused on enterprise integrations, IBM i modernization, and scalable backend systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ibmi</category>
      <category>ai</category>
      <category>modernization</category>
      <category>python</category>
    </item>
    <item>
      <title>I Connected Claude to My IBM i — And It Changed How I Think About Legacy Modernization</title>
      <dc:creator>Jay</dc:creator>
      <pubDate>Thu, 26 Mar 2026 09:05:47 +0000</pubDate>
      <link>https://dev.to/jay_krshn_1a9ac493fadf8/i-connected-claude-to-my-ibm-i-and-it-changed-how-i-think-about-legacy-modernization-24i0</link>
      <guid>https://dev.to/jay_krshn_1a9ac493fadf8/i-connected-claude-to-my-ibm-i-and-it-changed-how-i-think-about-legacy-modernization-24i0</guid>
      <description>&lt;h2&gt;
  
  
  What happens when you give an AI direct access to a system most people have never heard of
&lt;/h2&gt;




&lt;p&gt;There's a weird disconnect in the AI conversation right now. Everyone's talking about coding assistants, AI-powered DevOps, intelligent dashboards — but almost all of it assumes you're running modern cloud infrastructure. Kubernetes, PostgreSQL, GitHub Actions. The usual stack.&lt;/p&gt;

&lt;p&gt;Nobody's talking about what happens when your critical business system runs on IBM i.&lt;/p&gt;

&lt;p&gt;I work with IBM i every day. Warehouses, supply chains, enterprise systems that process millions of transactions and have been running for decades. These systems aren't going anywhere. They're stable, they're fast, and they do exactly what they're supposed to do.&lt;/p&gt;

&lt;p&gt;But they've also been left out of the AI conversation entirely. And I kept wondering — does it have to be that way?&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with IBM i and modern tooling
&lt;/h2&gt;

&lt;p&gt;If you've worked with IBM i, you know the feeling. You're reading about some new tool or platform, nodding along, and then you hit the part where they assume everything lives in a REST API or a cloud database. And you think — okay, that doesn't apply to me.&lt;/p&gt;

&lt;p&gt;The irony is that IBM i systems often hold the most valuable data in an organization. Decades of transaction history. Real-time inventory positions. Production schedules that run 24/7. But getting that data out — or even just asking questions about it — still involves signing into a green screen, navigating menus, running queries manually, and interpreting raw output.&lt;/p&gt;

&lt;p&gt;It's not that the data isn't accessible. Db2 for i is a perfectly capable database. QSYS2 SQL Services have made an incredible amount of system information queryable through standard SQL. The access is there. But the experience of getting to it hasn't kept up with what's happening everywhere else.&lt;/p&gt;




&lt;h2&gt;
  
  
  When I discovered MCP
&lt;/h2&gt;

&lt;p&gt;Earlier this year I came across the Model Context Protocol — MCP for short. It's an open standard that lets AI assistants connect to external tools and data sources. Anthropic published it, and it's gained traction quickly. The idea is simple: you define tools that the AI can call, and the AI figures out when and how to use them based on what you're asking.&lt;/p&gt;

&lt;p&gt;The moment I understood how it worked, the wheels started turning.&lt;/p&gt;

&lt;p&gt;What if I could write a handful of tools — run a SQL query, list active jobs, check system status, browse the IFS — and expose them to Claude through MCP? Not building a chatbot. Not training a model on IBM i documentation. Just giving an AI the ability to reach into the system and pull back real data.&lt;/p&gt;

&lt;p&gt;So that's what I did.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it works (without the complexity you'd expect)
&lt;/h2&gt;

&lt;p&gt;The architecture is embarrassingly simple, which is part of what makes it powerful.&lt;/p&gt;

&lt;p&gt;You write a small Python server that runs on your local machine. This server connects to IBM i through ODBC — the same driver you probably already have installed if you use ACS (IBM i Access Client Solutions). The server defines tools as Python functions, each with a description that tells the AI what it does.&lt;/p&gt;

&lt;p&gt;That's the entire stack. Python on your PC, ODBC to IBM i, MCP to the AI. No changes on the IBM i side. No new programs to deploy. No RPG modifications. No service entries. Nothing.&lt;/p&gt;

&lt;p&gt;The secret sauce, if there is one, is QSYS2 SQL Services. IBM has been quietly building out an incredible set of SQL-accessible system functions over the last several technology refreshes. Active job info, job logs, spool files, IFS statistics, system values, user profiles, message queues, data areas — almost everything you'd normally access through CL commands or green screen menus is now available as SQL table functions.&lt;/p&gt;

&lt;p&gt;This means every tool in the MCP server is just a SQL query. Clean input, structured output. The AI gets JSON back instead of green-screen text, which it can actually interpret and reason about.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it feels like to use
&lt;/h2&gt;

&lt;p&gt;This is the part that genuinely surprised me.&lt;/p&gt;

&lt;p&gt;I expected it to be useful. A faster way to run queries, maybe. A convenience layer. What I didn't expect was how much it would change the way I interact with the system.&lt;/p&gt;

&lt;p&gt;You can ask things like:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Show me all libraries that start with PROD and tell me how many tables are in each one."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And it just does it. Runs the query, counts the results, gives you a formatted answer. No navigating to WRKLIB. No typing SQL into STRSQL. Just a question and an answer.&lt;/p&gt;

&lt;p&gt;Or:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"What's the system health looking like right now? Anything I should worry about?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It pulls CPU usage, memory, disk capacity, active job counts — and then interprets them. Not just raw numbers, but context. "CPU is at 23%, well within normal range. Disk usage on ASP 1 is at 71%, which is getting up there — you might want to keep an eye on that."&lt;/p&gt;

&lt;p&gt;The real magic happens when you chain things together in a conversation. You ask about active jobs for a specific user. Something looks odd. You say "check the job log for that one." It knows which job you mean from the previous response. Then you say "has this user had issues before? Check their message queue." And it does.&lt;/p&gt;

&lt;p&gt;That kind of continuity — where context carries forward naturally — is something you can't replicate with traditional tools. Every green screen interaction is stateless. You close the screen, the context is gone. Here, the AI holds onto the thread and builds on it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The tools that matter most
&lt;/h2&gt;

&lt;p&gt;After using this for a while, I've found that certain tools get used far more than others.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQL queries&lt;/strong&gt; are the backbone. Being able to say "find all orders from the last 48 hours where the quantity is over 500" and get results instantly — that alone would justify the setup. But when you combine it with the AI's ability to interpret and summarize, it becomes something different. You're not just querying data, you're having a conversation about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;System status&lt;/strong&gt; is surprisingly useful. I used to check WRKACTJOB and WRKSYSSTS a few times a day. Now I just ask. And the AI remembers what "normal" looked like from previous checks, so it can flag when something changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Job logs&lt;/strong&gt; are where the AI really shines. Reading job logs on IBM i is tedious — they're dense, full of informational messages mixed in with the important stuff. The AI is genuinely good at scanning through a job log and picking out the messages that matter. "There are 47 messages in this job log. Most are routine. But there's a CPF4131 at 14:23 indicating a file member not found, and a follow-up CPD0006 — that's likely your issue."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IFS browsing and file reading&lt;/strong&gt; is the one I didn't expect to use as much as I do. Being able to say "show me what's in /home/myuser/exports and read the most recent CSV" is just faster than navigating the IFS through any other method.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why QSYS2 is the unsung hero
&lt;/h2&gt;

&lt;p&gt;I want to spend a moment on this because I think it's underappreciated.&lt;/p&gt;

&lt;p&gt;IBM has been building QSYS2 SQL Services for years. Every technology refresh adds more. And the beauty of it is that it turns everything on the system into structured, queryable data. You don't need to parse command output. You don't need screen-scraping programs. You just write SQL.&lt;/p&gt;

&lt;p&gt;For this kind of AI integration, that's everything. The AI needs structured data to reason about. Give it a blob of green-screen text and it'll struggle. Give it a JSON array of job records with named fields and it'll do exactly what you want.&lt;/p&gt;

&lt;p&gt;If your shop hasn't explored what's available through QSYS2 lately, it's worth looking. The coverage now is extensive — far beyond what most people realize. It's one of the best things IBM has done for the platform in recent years, and projects like this show why.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this means for IBM i shops
&lt;/h2&gt;

&lt;p&gt;I'm not going to oversell this. It's not going to replace your operators. It's not going to automate your entire system. It's a tool.&lt;/p&gt;

&lt;p&gt;But it's a tool that addresses something I've seen at every IBM i shop I've worked with: the knowledge bottleneck.&lt;/p&gt;

&lt;p&gt;There are usually one or two people who really know the system. Who know which libraries matter, what the critical jobs are, where to look when something breaks. When those people are unavailable — or when they eventually leave — that knowledge walks out the door.&lt;/p&gt;

&lt;p&gt;An AI that can query the system, interpret results, and carry context across a conversation doesn't replace that expertise. But it makes it accessible to people who don't have it yet. A junior developer can ask "what are the biggest tables in PRODLIB?" and get an immediate, meaningful answer. A manager can check on system health without learning CL commands. A new team member can explore the system conversationally instead of reading documentation that may or may not be current.&lt;/p&gt;

&lt;p&gt;That accessibility matters. IBM i's biggest challenge has never been capability — it's been the perception that it's impenetrable. Anything that makes it more approachable is a win for the platform's long-term viability.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd build next
&lt;/h2&gt;

&lt;p&gt;This was a starting point. The obvious extensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Source member browsing&lt;/strong&gt; — being able to read RPG or CL source through a conversation and have the AI explain what it does. Imagine onboarding new developers who can literally ask the AI "what does this program do?" while looking at the source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authority analysis&lt;/strong&gt; — "who has access to this file?" is a question that takes too long to answer today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PTF and system maintenance status&lt;/strong&gt; — turning system administration checks into conversations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-referencing&lt;/strong&gt; — "which programs use this file?" by querying object references through SQL Services.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The protocol makes this extensible. Adding a new capability is just adding a new function with a description. There's no framework overhead, no deployment complexity. The hardest part is writing good SQL, and if you're on IBM i, you're already doing that.&lt;/p&gt;




&lt;h2&gt;
  
  
  The bigger picture
&lt;/h2&gt;

&lt;p&gt;I started building this because I was curious. I kept building it because it genuinely made me more productive. But what excites me most is what it represents.&lt;/p&gt;

&lt;p&gt;For years, IBM i modernization has been framed as "move off the platform" or "rewrite everything." And both of those approaches are expensive, risky, and often unnecessary. The systems work. The data is valuable. The business logic is proven.&lt;/p&gt;

&lt;p&gt;What MCP shows is that you can bring modern capabilities to IBM i without changing IBM i. You don't need to rewrite your RPG. You don't need to migrate your database. You don't need to replace anything. You just need to build a bridge — a thin layer that translates between what the AI expects and what IBM i provides.&lt;/p&gt;

&lt;p&gt;QSYS2 SQL Services is one half of that bridge. MCP is the other. And the fact that you can connect them with a few hundred lines of Python — no middleware, no platform changes, no vendor contracts — is exactly the kind of pragmatic modernization that actually works in enterprise environments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;I'm not naive about the limitations. There are security considerations — you need to be thoughtful about what you expose and to whom. There's the question of audit trails and compliance. And the AI will occasionally get a query wrong, just like any tool.&lt;/p&gt;

&lt;p&gt;But the potential here is real. And it's the kind of potential that doesn't require permission from a steering committee or a six-month project plan. It's a Saturday afternoon experiment that turns into something you use every day.&lt;/p&gt;

&lt;p&gt;If you're in an IBM i shop and you've been wondering where AI fits into your world, this might be the answer. Not a massive transformation initiative. Not a vendor platform. Just a conversation with your system that actually understands what you're asking.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Jaya Krushna Mohapatra is a Warehouse Management Systems Architect focused on enterprise integrations, IBM i modernization, and scalable backend systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>as400</category>
      <category>ibmi</category>
      <category>mcp</category>
      <category>python</category>
    </item>
    <item>
      <title>When HTTPAPI Fails: A Practical SOAP Integration Workaround on IBM i</title>
      <dc:creator>Jay</dc:creator>
      <pubDate>Thu, 19 Mar 2026 04:25:43 +0000</pubDate>
      <link>https://dev.to/jay_krshn_1a9ac493fadf8/when-httpapi-fails-a-practical-soap-integration-workaround-on-ibm-i-58kb</link>
      <guid>https://dev.to/jay_krshn_1a9ac493fadf8/when-httpapi-fails-a-practical-soap-integration-workaround-on-ibm-i-58kb</guid>
      <description>&lt;p&gt;&lt;em&gt;A real-world approach to handling complex SOAP APIs when traditional IBM i tools fall short&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I've been doing integrations on IBM i for a while now. Most of the time, HTTPAPI handles everything I throw at it — REST, SOAP, whatever. It just works.&lt;/p&gt;

&lt;p&gt;But a few months ago, I hit a wall with a SOAP API that refused to play nice. And honestly, it took me longer than I'd like to admit to figure out why.&lt;/p&gt;

&lt;p&gt;This is what happened and how I ended up solving it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;p&gt;The setup was straightforward. Call a vendor's SOAP endpoint, send a request, get a response, process it in RPG. Standard stuff.&lt;/p&gt;

&lt;p&gt;Except the response kept coming back wrong.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connection was fine&lt;/li&gt;
&lt;li&gt;HTTP 200 every time&lt;/li&gt;
&lt;li&gt;But the response body was either empty or garbage&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ In some cases, the request returns HTTP 200 but the response isn’t usable. In other cases, the request may not return a proper response at all—especially when working with older SOAP-based APIs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When You Don’t Even Get a Proper Response
&lt;/h2&gt;

&lt;p&gt;In some scenarios, the issue goes beyond receiving an unusable response.&lt;/p&gt;

&lt;p&gt;With certain SOAP services—especially older or more rigid implementations—the request may fail before returning a meaningful HTTP response at all. This can show up as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connection resets
&lt;/li&gt;
&lt;li&gt;Timeouts
&lt;/li&gt;
&lt;li&gt;SSL handshake failures
&lt;/li&gt;
&lt;li&gt;Inconsistent or empty responses
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These situations are often harder to debug because there’s very little feedback to work with. The request appears to fail silently, and it’s not always clear whether the issue is with the request structure, the transport layer, or compatibility between systems.&lt;/p&gt;

&lt;p&gt;I spent a solid chunk of time convinced it was my SOAP envelope. Then I thought it was a namespace issue. Then maybe the headers. I kept tweaking things, and nothing changed.&lt;/p&gt;

&lt;p&gt;The frustrating part? When I tested the exact same request in Postman, it worked perfectly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The debugging spiral
&lt;/h2&gt;

&lt;p&gt;I went through all the usual steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rebuilt the SOAP envelope from scratch&lt;/li&gt;
&lt;li&gt;Double-checked every namespace and header&lt;/li&gt;
&lt;li&gt;Verified SSL certificates were in place&lt;/li&gt;
&lt;li&gt;Compared raw payloads character by character&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything matched. The request was identical. But HTTPAPI kept giving me bad responses while Postman returned clean data every time.&lt;/p&gt;

&lt;p&gt;At some point I had to accept that the problem wasn't my request — it was somewhere in how the request was being sent.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stepping back
&lt;/h2&gt;

&lt;p&gt;Once I stopped trying to force HTTPAPI to work, the answer became pretty obvious.&lt;/p&gt;

&lt;p&gt;The vendor's SOAP implementation was strict about certain HTTP behaviors — things like exact header ordering, specific TLS negotiation patterns, and chunked transfer encoding. HTTPAPI handles most of this fine for typical APIs, but this particular endpoint was picky in ways that were hard to control from RPG.&lt;/p&gt;

&lt;p&gt;So I asked myself: what if I just let something else handle the HTTP part?&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix: Java as a middle layer
&lt;/h2&gt;

&lt;p&gt;I wrote a small Java program. Nothing fancy — maybe 80 lines. Its only job is to make the SOAP call and write the response to a file.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RPG / CL
   ↓
Parameter file on the IFS
   ↓
Java program (handles the SOAP call)
   ↓
Vendor API
   ↓
Response file on the IFS
   ↓
Back to RPG for processing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RPG still runs the show. It writes out the parameters, kicks off the Java program, waits for the response, and processes it. The Java piece is just a bridge.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Java specifically
&lt;/h2&gt;

&lt;p&gt;I've seen people suggest Python or Node for this kind of thing, and those would work too. I went with Java because it's already on every IBM i, no extra setup needed.&lt;/p&gt;

&lt;p&gt;Java also gave me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Predictable TLS behavior (the JVM handles negotiation well)&lt;/li&gt;
&lt;li&gt;Full control over HTTP headers, including ordering&lt;/li&gt;
&lt;li&gt;Proper chunked encoding support&lt;/li&gt;
&lt;li&gt;Stack traces when things fail — which beats staring at a job log&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first time I ran it, the response came back clean. Same request that had been failing for days.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keeping it reusable
&lt;/h2&gt;

&lt;p&gt;I didn't want to hardcode anything, so the Java program reads from a parameter file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ENDPOINT=https://api.vendor.com/soap
SOAP_ACTION=SomeAction
PAYLOAD=&amp;lt;Soap:Envelope&amp;gt;...&amp;lt;/Soap:Envelope&amp;gt;
OUTPUT=/home/files/response.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Different API? Just change the file. The Java program doesn't care what it's calling.&lt;/p&gt;

&lt;p&gt;The response gets written in a simple format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP Response Code: 200
Response Body:
&amp;lt;Soap:Envelope&amp;gt;...&amp;lt;/Soap:Envelope&amp;gt;
STATUS: SUCCESS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RPG reads the status, grabs the body if it's good, and moves on. Clean and predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  This was part of what led me to try a different approach.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Looking back, I probably should have switched approaches sooner instead of spending so much time debugging HTTPAPI. The signs were there — identical requests working in other tools but failing from RPG.&lt;/p&gt;

&lt;p&gt;I've since used this same pattern for two other integrations that had similar quirks. It's become a go-to option when the standard approach doesn't cooperate.&lt;/p&gt;




&lt;h2&gt;
  
  
  When this makes sense
&lt;/h2&gt;

&lt;p&gt;I'm not saying stop using HTTPAPI. For most APIs, it's still my first choice.&lt;/p&gt;

&lt;p&gt;But if you're dealing with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SOAP endpoints that are strict about HTTP behavior&lt;/li&gt;
&lt;li&gt;Responses that work everywhere except from IBM i&lt;/li&gt;
&lt;li&gt;TLS issues you can't pin down&lt;/li&gt;
&lt;li&gt;Debugging that's hit a dead end&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;...it's worth considering a hybrid approach. Sometimes the best thing you can do is let each tool handle what it's best at.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;This wasn't a glamorous fix. No new framework, no architectural overhaul. Just a small Java program sitting between RPG and an API that wouldn't behave.&lt;/p&gt;

&lt;p&gt;But it solved a problem that had been eating up my time, and it's been solid since.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Note on Java Setup&lt;/strong&gt;  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make sure Java is properly available on your IBM i system.&lt;br&gt;&lt;br&gt;
A quick check is to run the &lt;code&gt;JAVA&lt;/code&gt; command from a CL prompt or QP2TERM to confirm it executes successfully.&lt;br&gt;&lt;br&gt;
Also ensure your classpath and IFS paths are correctly set, as misconfiguration here can quietly cause failures.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you've hit similar walls with SOAP on IBM i, I'd be curious to hear how you handled it. There's probably a dozen different ways to approach this — this just happened to be the one that worked for me.&lt;/p&gt;

</description>
      <category>ibmi</category>
      <category>java</category>
      <category>api</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
