<?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>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>
