Blog › ICP guides
RPG developer on retainer: indicator model, ILE RPG subprocedures, %FOUND() semantics, and IBM i RPG on monthly retainer
October 3, 2026 · ~14 min read
An RPG developer was maintaining an IBM i (AS/400) inventory management system written in ILE RPG IV (free-format RPG). The system processed purchase orders and updated inventory records using CHAIN opcodes to perform keyed direct-access reads on an inventory data file named INVMAST. The developer had added a new code path to handle a backorder notification: immediately after a CHAIN to INVMAST that found the record (setting *IN50 = ‘1’), the new code called a subprocedure named CalcReorderPt to compute the reorder point before the found-indicator check. The bug: CalcReorderPt executed its own CHAIN opcode against a different file (VENDATA) to look up the vendor record. That CHAIN set *IN50 to ‘0’ (not found, because the first vendor record in the sequence was not present). When CalcReorderPt returned, the calling program’s IF *IN50 = ‘1’ branch was never taken — the found indicator from the original INVMAST CHAIN had been silently overwritten. Four inventory records that existed in INVMAST were not updated per batch run.
The RPG indicator model is the root cause. RPG indicators *IN01 through *IN99 are program-global boolean flags. They are not local to any procedure or subprocedure; any CHAIN, READ, or READE opcode executed anywhere in the program — including in called subprocedures — can overwrite indicators set by an earlier opcode in the calling procedure. The CHAIN opcode sets the “not found” indicator (in the Result Indicator positions, or in the *IN array slot specified in the opcode) based on the outcome of the most recent file operation for that opcode invocation. When CalcReorderPt executed its own CHAIN, the *IN50 indicator was unconditionally set based on the VENDATA lookup outcome, overwriting the *IN50 value that the calling procedure had just set from the INVMAST lookup. This behavior is correct per RPG program-cycle semantics: indicators are shared global state. There was no error, no exception, no job log message, and no abnormal termination. The batch job completed successfully. Four inventory records simply did not receive their updates.
The fix was to capture the found indicator immediately after the INVMAST CHAIN using the %FOUND built-in function and store it in a local variable: found = %FOUND(INVMAST). The %FOUND built-in returns a boolean (*ON/*OFF) reflecting whether the most recent file operation for the specified file found a record. Unlike the *IN indicator array, %FOUND is evaluated at the point of the statement and the result stored in a local variable is not affected by subsequent file operations in called subprocedures. After this fix, the calling procedure stored the find result before calling CalcReorderPt, then used the local variable rather than *IN50 in the conditional check. Four previously skipped inventory record updates per batch run: 4 → 0 via %FOUND() capture. The investigation — identifying that four records consistently failed to update each batch run, tracing the symptom to the indicator value at the conditional check, inserting a debug log of *IN50 before and after the CalcReorderPt call, and discovering the subprocedure’s CHAIN was overwriting the indicator — was four hours.
IBM i, AS/400, and ILE RPG: platform history and language architecture
IBM i (originally known as AS/400 at its 1988 launch, then iSeries, then System i before IBM settled on the IBM i branding) is a vertically integrated platform: the operating system (OS/400, later i5/OS, now IBM i) is tightly coupled to the hardware, the database (DB2 for i, integrated at the OS level), and the compiler toolchain. The machine interface (MI) abstracts the hardware from applications, which means RPG programs compiled on V3R2 in 1995 can often run unmodified on V7R5 in 2026 — a degree of binary and source compatibility unmatched by most platforms. IBM i is used predominantly in North American distribution, manufacturing, wholesale trade, financial services, and healthcare organizations that deployed AS/400 hardware in the 1990s and have continued to run the same applications — often maintaining and extending them with RPG developers on retainer — for thirty years. The RPG language has evolved across four major generations: RPG II (the original column-driven cycle language from the 1960s); RPG III (added structured control flow and indicator-free programming options); RPG/400 (RPG III for AS/400, with the same program-cycle semantics); and ILE RPG IV, introduced with OS/400 V3R1 in 1994, which introduced the Integrated Language Environment, subprocedures, prototyped procedure calls, and eventually full free-format syntax.
ILE RPG IV programs are compiled as modules (CRTRPGMOD), which are then bound into programs (CRTPGM) or service programs (CRTSRVPGM) using a binding directory. A service program is a shared library of exported procedures, analogous to a Unix shared object (.so) or a Windows DLL; programs bind to service programs at activation time. The activation group model determines the scope of resources — file opens, commitment definitions, storage — shared between programs and service programs: *CALLER activation groups inherit the calling program’s activation group and its open files and commitment context; *NEW activation groups create an isolated resource scope and clean up on return. File declarations in ILE RPG free-format use DCL-F (declare file); data structures use DCL-DS (declare data structure), which can be externally described from a physical file or logical file using the EXTNAME keyword, automatically generating subfields matching the database file’s field names and types. Procedures use DCL-PROC and END-PROC; the traditional RPG program cycle (automatic read-process-write loop driven by primary and secondary file designations in the F-spec) is still valid in ILE RPG but is rarely used in new development, which instead uses fully explicit mainline C-specs (calculation specifications) that control all file operations explicitly. The old-style fixed-format syntax is still valid and common in maintained AS/400 applications; the newer **FREE all-free directive at the top of a source member enables free-format for the entire source member without column restrictions.
The file access opcodes in RPG are the primary interface to DB2 for i. CHAIN performs a keyed direct-access read: given a key value, it reads the record with that key from a keyed (logical) file or a physical file with a key, and sets the not-found indicator if no record with that key exists. READ performs a sequential read of the next record in the current file position; READE reads the next record only if its key matches the specified key value (equal-key read), and sets the end-of-file indicator if the next record’s key does not match; READPE reads the prior record with the same key. SETLL (set lower limit) positions the file pointer to the first record with a key greater than or equal to the specified key, in preparation for a keyed sequential read; SETGT (set greater than) positions to the first record with a key greater than the specified key. WRITE, UPDATE, and DELETE are the DML opcodes for inserting, modifying, and removing records. The built-in functions %FOUND, %EOF, %ERROR, and %STATUS test the outcome of the most recent file operation for a specified file: %FOUND(INVMAST) returns *ON if the most recent CHAIN or READ against INVMAST found a record; %EOF(INVMAST) returns *ON if the most recent READ or READE against INVMAST reached end-of-file or end-of-key range. These built-in functions are the safe, modern alternative to the *IN indicator array for testing file operation results.
The *IN indicator array — *IN01 through *IN99, plus special indicators like *INLR (last record, triggers end-of-program cleanup), *INRT (return), and *INOF (overflow) — are fields in the Program Status Data Structure (PSDS), which is a global structure that exists for the lifetime of the program activation. Every procedure and subprocedure in the program shares the same PSDS and therefore the same indicator array. In fixed-format RPG IV, opcode indicator positions (columns 71–76 in the C-spec) specify which indicator slots to set based on operation outcome: a CHAIN with N50 in the not-found result indicator position sets *IN50 = ‘1’ when the record is not found and *IN50 = ‘0’ when it is found. In free-format RPG IV, the %FOUND and %EOF built-ins replace indicator positions entirely for file result testing, and %ind(*IN50) can still be used to explicitly read or write an indicator slot. The safe programming pattern — capture %FOUND(file) into a local procedure variable immediately after any file opcode, before calling any subprocedure that might execute file operations — eliminates indicator overwrite bugs entirely and is the recommended style in IBM i RPG programming references.
Typical RPG retainer work and what it looks like in a work log
The indicator-overwrite pattern is the most invisible category of RPG retainer work. The mechanism: a calling procedure sets a *IN indicator via a file opcode (CHAIN, READ, READE), then calls a subprocedure before testing that indicator. The subprocedure executes its own file opcode that writes to the same indicator slot. The calling procedure tests the indicator after the subprocedure returns and finds the subprocedure’s result rather than its own. No error is raised. No RPG exception is thrown. No IBM i job log entry is generated. The conditional branch is simply not taken, and the records that should have been updated are skipped silently. The bug is invisible on test data where all records exist in both files (because if the VENDATA CHAIN succeeds for all vendors, *IN50 after the subprocedure call is still ‘1’, accidentally matching the expected INVMAST found result). It only appears in production when at least one vendor record is absent from VENDATA, which sets *IN50 = ‘0’ and causes the INVMAST update branch to be skipped. Work log entry: “INVUPD: CHAIN INVMAST sets *IN50=‘1’ (found); CalcReorderPt calls CHAIN VENDATA; CHAIN VENDATA sets *IN50=‘0’ (not found for vendor); IF *IN50=‘1’ branch never taken; 4 inventory records not updated per batch; added found = %FOUND(INVMAST) immediately after first CHAIN; replaced IF *IN50=‘1’ with IF found; wrong skips: 4 → 0; 4h.”
READE loop runaway is the second most common RPG retainer pattern. A developer writes a READE loop over INVMAST for a key range — for example, reading all inventory records for a given warehouse code — but omits the %EOF check in the loop termination condition. The READE opcode reads the next record only if its key matches the search argument; when the key of the next record does not match (the end of the matching key range has been reached), READE sets the end-of-file indicator and the next iteration of an unguarded loop reads on into records with different key values. In a DOW loop that tests only a business-condition variable rather than %EOF(INVMAST), the loop continues reading and processing records with warehouse codes that are out of scope for the current batch run. The records have valid data; there is no I/O error; the updates applied to them are individually valid but contextually wrong. The fix: add DOW NOT %EOF(INVMAST) as the outer loop termination condition, or add IF %EOF(INVMAST); LEAVE; ENDIF immediately after the READE opcode before any processing logic. Work log entry: “INVBATCH: READE loop over INVMAST WHN-CODE key; missing %EOF check; loop continued into records with adjacent WHN-CODE values; 3 inventory records updated with wrong warehouse context; added IF %EOF(INVMAST); LEAVE; ENDIF after READE; wrong updates: 3 → 0; 2.5h.” The difficulty in diagnosing this pattern is that the wrong records are legitimate inventory records with the wrong warehouse code — they look like valid updates in the job output unless the analyst is specifically checking which records changed against which records were supposed to change.
Data structure field offset bugs arise when an externally described data structure is defined from a physical file using EXTNAME (or the equivalent E-spec keyword in fixed-format RPG IV), but a developer accesses a specific field by hardcoded subfield offset rather than by field name. The externally described DS automatically maps the database file’s field names, types, and lengths into RPG subfield declarations at compile time: if the physical file has fields ITEM-NO (10A), QTY-ON-HAND (7P 0), QTY-ON-ORDER (7P 0), and REORDER-PT (7P 0), the DS has subfields at positions 1–10, 11–14, 15–18, and 19–22 respectively. If the developer accesses REORDER-PT by hardcoding an offset of 19 rather than using the subfield name REORDER-PT, the code works correctly until a database administrator adds a new field before REORDER-PT in the physical file. After the PF is changed and recreated, the offset of REORDER-PT shifts, but the hardcoded offset in the RPG source does not. The program reads the wrong bytes as the reorder point, producing incorrect reorder calculations without any RPG error or exception. Work log entry: “INVUPD: INVMAST DS subfield REORDER-PT accessed by hardcoded offset 19; DBA added QTY-ALLOCATED field before REORDER-PT; offset shifted to 23; wrong values read for reorder point calculation; replaced offset reference with subfield name REORDER-PT in externally described DS; wrong-value count: all records in batch → 0; 2h.”
Entry-level RPG developers typically bill at $80–$145 per hour. Mid-level RPG programmers with ILE RPG free-format and subprocedure experience typically bill at $120–$210 per hour. Senior RPG developers with IBM i internals knowledge typically bill at $165–$305 per hour. Monthly retainer: $2,000–$3,500/mo for advisory (15–22h); $2,800–$6,500/mo for active maintenance.
Track RPG developer retainer hours without the status emails
When a 4-hour batch debug session traces four skipped inventory record updates to an RPG indicator overwrite where a subprocedure’s CHAIN silently reset *IN50 — a program-global boolean that every file opcode can overwrite — the work log must name the program, the file, the CHAIN opcode that set the indicator, the subprocedure that overwrote it, and the skipped-update count before and after. HourTab gives your RPG retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log naming the IBM i program, the ILE RPG file opcode, and the fix. No client login. No status emails. CSV in, URL out.
How HourTab tracks RPG developer retainer hours
RPG retainer work is invisible by the same mechanism that makes indicator overwrites dangerous: a subprocedure executing a file opcode silently modifies a program-global flag that the caller tested. On the test environment, the new code path involving CalcReorderPt was only exercised with vendor records that were present — *IN50 after the VENDATA CHAIN was ‘1’, which happened to match the INVMAST result so the conditional logic worked accidentally. In production, the missing vendor records set *IN50 to ‘0’ and the calling procedure’s inventory update was silently skipped. The batch job produced a normal completion message. The job log showed no errors. The output spool file reported the expected record counts for total records processed, because the INVUPD program counted every record it read via CHAIN — it did not separately count records that successfully reached the UPDATE opcode versus records where the IF *IN50 branch was not taken. The four missed updates were discovered only at end-of-week reconciliation, when the inventory quantities did not match the expected post-batch totals. The reconciliation analysis consumed an additional two hours on top of the four-hour diagnostic investigation.
Work log naming is what makes RPG retainer hours auditable: which RPG program (INVUPD), which file (INVMAST), which CHAIN opcode set the indicator (CHAIN INVMAST set *IN50=‘1’), which subprocedure overwrote it (CalcReorderPt), which file that subprocedure accessed (VENDATA), what the indicator was before and after the subprocedure call (*IN50 changed from ‘1’ to ‘0’), and the skipped-update count before and after the fix (4 → 0). HourTab gives RPG developers a public retainer dashboard URL they can send to their IBM i clients — distribution centers, manufacturing plants, and financial services organizations running IBM i in production — with hours used, hours remaining, and a work log that makes the invisible work visible without requiring client login or portal access. For related batch-job invisible-bug classes on legacy enterprise platforms, see COBOL retainers (similarly invisible batch update bugs in COBOL programs where file status codes after READ or REWRITE are not checked, producing silent wrong-record processing) and Natural retainers (Adabas READ HISTOGRAM mutation bugs where updating records inside a histogram loop causes descriptor-driven reordering that makes records appear twice in the same iteration — both are batch-job invisible-bug classes on legacy enterprise platforms where the absence of an error message is the most misleading diagnostic signal).
FAQ: RPG developer retainers
What does an RPG developer on retainer typically do?
An RPG developer on monthly retainer covers indicator-model audit (finding all CHAIN/READ/READE opcodes that set shared *IN indicators, confirming no called subprocedures execute conflicting file operations between indicator set and indicator test; replacing indicator positions with %FOUND/%EOF/%ERROR captured immediately after the opcode); ILE RPG free-format migration (converting fixed-format RPG III or RPG IV to /free or **FREE all-free format; modernizing CALL to CALLP with prototypes; replacing hard-coded indicator positions with %found/%eof); data structure alignment review (auditing externally described DS subfield positions after physical file changes; confirming all fields accessed by name not offset); READE loop termination (confirming all READE loops test %EOF(file) and exit when end-of-key-range is reached, preventing reads into non-matching records); and activation group management (confirming that called service programs and their file opens use appropriate activation groups — *CALLER vs *NEW — to avoid premature file closure or conflicting file handles across service program boundaries).
What RPG work is most commonly underlogged?
Indicator overwrite bugs are the most systematically underlogged RPG retainer work. The pattern: a subprocedure’s file opcode overwrites the caller’s found indicator; the symptom is that a conditional branch is never taken for a subset of batch records. The bug appears only when the called subprocedure fails to find its record (setting *IN to ‘0’); it is invisible on test data where all records are present. When all test vendor records exist, *IN50 after the VENDATA CHAIN is ‘1’, which accidentally matches the INVMAST found result and the conditional logic works. In production, missing vendor records set *IN50 to ‘0’ and the calling procedure’s inventory update is silently skipped. The 3–6 hour investigation finds no error message, no RPG exception, no abnormal termination — just four fewer records updated per batch run, which initially looks like a business-rules evaluation issue. The %FOUND() capture fix is one line; the diagnostic work is hours. The work log must name the program, the two CHAIN opcodes, the indicator slot, and the before-after skip count.
What are typical RPG developer retainer rates?
Entry-level RPG developers with experience in basic ILE RPG IV, fixed-format C-specs, and CHAIN/READ file operations typically bill at $80 to $145 per hour. Mid-level RPG programmers with experience in ILE RPG free-format, subprocedure design, service program binding, and indicator-free programming patterns typically bill at $120 to $210 per hour. Senior RPG developers with deep knowledge of IBM i internals, activation group management, externally described data structures, CRTPGM/CRTSRVPGM binding directories, and legacy RPG III modernization typically bill at $165 to $305 per hour. Monthly retainer ranges: $2,000 to $3,500 per month for advisory engagements covering indicator-model audit, batch program review, and performance optimization (15 to 22 hours per month); $2,800 to $6,500 per month for active maintenance including batch bug fixes, ILE RPG free-format migration, and subprocedure refactoring. Note: IBM i retainer rates are comparatively high due to the small talent pool and the critical nature of IBM i applications in distribution, manufacturing, and financial services.
What should an RPG developer retainer agreement include?
An RPG developer retainer agreement should specify: RPG version (RPG III, RPG/400, ILE RPG IV fixed-format, /free, **FREE all-free; major format differences affect which modernization tasks are in scope); IBM i OS version (V7R1 through V7R5; PTF level for relevant fixes to the RPG compiler and file system; OS version affects available built-in functions and free-format features); file access scope (whether the retainer covers reviewing indicator usage patterns across all programs that share a file, or only the specific programs where bugs are reported — indicator overwrites can originate in any subprocedure that accesses a shared file); service program and activation group scope (whether the retainer covers reviewing activation group isolation for file handles and indicators across called service programs — *CALLER vs *NEW activation groups affect file closure behavior and open data path sharing); physical and logical file change coordination (whether the retainer covers reviewing all programs that access a physical file when a new field is added — even at the end of a record — since externally described DS subfield positions may shift if field order changes or if the PF is recreated with a different layout); and migration scope (whether the retainer includes /free conversion, prototype addition, or module-to-ILE refactoring, which are each significant separate engagement categories requiring dedicated time budgets beyond standard maintenance hours).
How should RPG developer retainer hours be logged?
Log each RPG retainer session with specifics: for indicator overwrite bugs, the program name (INVUPD), the file name (INVMAST), the CHAIN opcode that set the indicator (*IN50=‘1’), the subprocedure name (CalcReorderPt), the file accessed by the subprocedure (VENDATA), the indicator overwrite mechanism (CHAIN VENDATA sets *IN50=‘0’), the fix (found = %FOUND(INVMAST) captured before CalcReorderPt call; IF found replaces IF *IN50=‘1’), the wrong-skip count (4 → 0), and the hours (4h). For READE loop runaway: the program name, the file name, the key value range being iterated, the missing %EOF check, the wrong-records-read count (3 → 0), the fix (DOW NOT %EOF(INVMAST) outer termination), and the hours (2.5h). For data structure offset bugs: the program name, the physical file name, the field name accessed by hardcoded offset rather than name, the offset before and after the field addition, the wrong-value count, the fix (access by field name in externally described DS), and the hours (2h). A log entry that says “fixed batch update bug, 4h” is not auditable. A log entry that names the program, the two CHAIN opcodes, the indicator slot overwritten, the subprocedure responsible, and the four-to-zero skip count is auditable and defensible.