Skip to main content

Modernizing IBM i Applications

Published: 10 July 2026; last updated: 14 October 2026

Abstract

This publication covers modern IBM i development practices including RPG modernization with free-format syntax and ILE modularity, data-centric programming with SQL table definition and database design, database re-engineering strategies, character encoding and CCSID fundamentals, test-driven development with automation, and performance optimization tools for Db2 for i. The content addresses developers and technical professionals working with IBM i systems, providing practical guidance on modernizing legacy applications, implementing data-centric architectures, ensuring proper character encoding, establishing automated testing frameworks, and optimizing database performance through systematic analysis and index management.

Authors

Tim Rowe, Brad Bentley, Steve Bradshaw, Jim Buck, Andrew Clark, Andrew Youens, Scott Forstie, Ludovic Gasc, Shankar Giduturi, Wim Jongman, Sébastien Julliand, Gaurav Kumar, Marius le Roux, Benoit Marolleau, Kent Milligan, Ryan Moeller, Rahul Pareek, Het Patel, Michael Pavlak, Pascal Polverini, Marina Schwenk, Vikas Sonwal, Todd Stewart, Jeff Ticker, Khushi Wadhwa, Carol Woodbury, Jia Tian Zhong and George Van Eaton

  • Modern RPG
    • What defines modern RPG?
    • What is the business value of modern RPG?
    • How do I transition to modern RPG?
    • What is ILE modularization?
    • What are RPG program design best practices?
    • What are RPG coding best practices.
    • How do I measure and reduce complexity?
    • What is cyclomatic complexity?
    • What is dead code and why remove it?
    • How do I modularize RPG programs?
    • Why Use Fully Free Format RPG
    • How do I write fully free format RPG?
    • How do I convert to free format?
    • How do I use embedded SQL in RPG programs?
    • How do I use modern RPG data structures and arrays
    • How do I process JSONs within RPGLE programs?

Modern RPG

What defines modern RPG?

Modern RPG represents the current, preferred way IBM i developers write, organize, and maintain RPG programs to keep applications reliable, readable, and adaptable. It builds on the RPG IV foundation while incorporating free-format syntax, ILE modularity, data-centric techniques, and clear coding practices that align with how professionals work in 2026. These characteristics separate maintainable, future-ready code from legacy patterns that increase defects and slow down enhancements. The following topics are described in this section:

  • Core language and syntax foundation
  • Free-format presentation
  • ILE modularity and organization
  • Data-centric practices and built-in functions
  • Coding style and readability
  • Interface and integration support
  • Modernization spectrum and incremental approach
  • Role of AI-assisted tools in 2026

Core language and syntax foundation

Modern RPG requires the RPGLE source type (or FREE for fully free-format members since IBM i 7.1). Older member types such as RPG, RPG36, or RPG38 lack subprocedures, service programs, qualified data structures, and most built-in functions introduced since V5R1. Without these features, developers stay trapped in outdated patterns that raise defect rates and make routine maintenance unnecessarily difficult.

Free-format presentation

Free-format syntax eliminates the rigid column positions that made older RPG source error-prone and hard to read. With the FREE directive (IBM i 7.1 and later), the entire member starts in column 1, allowing natural indentation, longer descriptive names, and complex expressions without alignment issues. Shops that adopt free format experience fewer entry mistakes, faster code reviews, and easier comprehension by team members.

ILE modularity and organization

Modularity through the Integrated Language Environment (ILE) provides the structural foundation for modern RPG applications. Developers extract reusable business logic—such as pricing rules, tax calculations, customer validation, and inventory checks—into service programs that export procedures. These procedures bind directly into any program or other service program that requires them, eliminating the need to duplicate logic across standalone programs or copy source code between members.

In a typical IBM i shop, ILE modularity delivers concrete, day-to-day advantages:

  • Single source of truth for business rules. A change to a pricing formula, discount logic, or validation rule occurs in one procedure and immediately affects every caller—no searching dozens of programs to locate every instance.
  • Reuse across multiple interfaces without modification. The same CalcTax procedure serves a 5250 order-entry screen, a REST API endpoint, a mobile app service, a batch update job, or an integration with an external system.
  • Focused and reliable unit testing. Individual procedures can be tested in isolation using simple test harnesses or modern unit-test frameworks, rather than requiring a full program cycle with specific input files and database states.
  • Better dependency management and isolation. Activation groups and binding directories control resource sharing and prevent the side effects (unintended global variable changes) that plague monolithic programs.
  • Lower risk and faster evolution. When business requirements change—adding a new discount type, compliance check, or rounding rule—the update happens once in the service program, shrinking testing scope, deployment effort, and the chance of inconsistent behavior across the application.

Within individual modules, subprocedures replace traditional subroutines for internal code organization. Subprocedures offer:

  • Parameter passing by value or reference
  • Local variables with proper scope (no accidental global interference)
  • Structured control flow with meaningful return values
  • Clearer debugging because behavior depends only on inputs and local state

These capabilities remove many longstanding problems with EXSR and global variables, such as unexpected side effects, hard-to-trace bugs, and fragile code that breaks when one part changes.

Data-centric practices and built-in functions

Modern RPG favors database strengths and built-in functions to handle data efficiently. Developers use embedded SQL for result-set processing, multi-table joins, and aggregates when DB2 optimization provides clear advantages over native READ/CHAIN loops. Built-in functions replace custom routines for routine tasks:

  • String handling: %TRIM, %REPLACE, %SCAN, %XLATE
  • Date/time operations: %DATE, %DIFF, %SUBDT, %TIMESTAMP
  • JSON/XML processing: DATA-INTO and DATA-GEN (since IBM i 7.3)

Qualified data structures (LIKEDS, LIKEREC) and dynamic arrays (%ALLOC, %REALLOC) produce clearer, less error-prone code than positional fields or fixed-length limits. These techniques cut code volume, improve performance through indexes and constraints, and bring RPG closer to industry-standard data practices.

Coding style and readability

Modern coding style makes code self-explanatory. Developers use descriptive names (OrderTotal, CustNumber instead of OTOT, CUSNBR), qualify subfields (CustRec.Name instead of CUSTNM), group logic with blank lines and block comments, and replace numbered indicators with named indicators or direct conditions. Consistent indentation and logical flow reduce mental effort during maintenance and debugging, especially in mixed teams that include developers familiar with other languages.

Interface and integration support

Modern RPG supports multi-channel delivery through RPG Open Access handlers and service-oriented design. Open Access enables custom I/O for browser, mobile, or API front ends while preserving core business logic. RESTful services built around ILE procedures expose rules externally without duplication. This approach extends the life of existing applications and enables new delivery channels.

Modernization spectrum and incremental approach

Modern RPG exists on a practical continuum. A program can be meaningfully modern even if it retains some native I/O, provided it uses free-format logic, extracts reusable procedures, and avoids numbered indicators. The recommended path begins with small, high-value steps:

  • Convert calculations to free format
  • Extract internal subprocedures from subroutines
  • Move shared logic into service programs
  • Replace custom routines with built-in functions
  • Adopt embedded SQL for complex data access

Each step improves readability, reduces defects, and simplifies future work.

Role of AI-assisted tools in 2026

In 2026, AI-assisted tools such as IBM Bob analyze RPG source in real time within editors. They suggest replacements for outdated constructs (MOVE to assignment, indicator logic to structured blocks), recommend qualified structures for legacy records, explain legacy sections, and generate modern free-format equivalents from descriptions. This assistance speeds incremental modernization without requiring complete rewrites.

Modern RPG combines free-format clarity, ILE modularity, data-centric techniques, and readable style to produce code that is easier to understand, test, extend, and hand off. These characteristics lower maintenance costs, reduce production incidents, shorten enhancement cycles, and support multi-channel strategies while protecting decades of business logic.

Key topics covered:

  • Defining characteristics of modern RPG organized into clear categories with practical, shop-focused explanations
  • Significantly expanded ILE modularity subsection with bulleted lists for benefits and subprocedure advantages to improve readability
  • Emphasis on incremental modernization as an achievable, step-by-step process

Connections for future sections:

What is the business value of modern RPG?

Modern RPG brings direct, practical advantages to IBM i shops that affect budgets, project delivery, operational stability, and the ability to support new business requirements. These benefits emerge from the combination of free-format syntax, ILE-based modularity, structured programming techniques, embedded SQL, and modern data handling features such as DATA-INTO and DATA-GEN. The section explains why these changes matter in real-world IBM i environments and how they translate into measurable outcomes for IT teams and the businesses they support.

The following topics are described in this section:

  • Reduced ongoing maintenance costs
  • Faster development cycles and shorter time-to-market
  • Improved system reliability and lower production risk
  • Easier recruitment, onboarding, and retention of developers
  • Greater flexibility to support multiple user interfaces and channels
  • Protection of long-term investment through incremental modernization
  • Lower total cost of ownership for core business applications

Reduced ongoing maintenance costs

Many IBM i shops spend 60–80% of their programming effort fixing, enhancing, or simply understanding existing RPG applications. Legacy code—often written in fixed format, using numbered indicators, monolithic procedures, and duplicated business rules—creates this burden. Every change requires extensive searching to locate every instance of a rule, followed by careful testing to avoid breaking unrelated logic.

Modern RPG addresses the root causes:

  • Service programs centralize business rules (pricing, discounts, credit checks, tax calculations) so a single update applies everywhere the rule is used.
  • Free-format logic with qualified data structure names (Cust.OrderTotal instead of ORDTOT) and meaningful procedure names makes intent obvious at a glance.
  • Removing dead code and lowering cyclomatic complexity (see What is cyclomatic complexity?) reduces the surface area that must be reviewed during changes.
  • Shops that refactor high-impact modules into this style commonly report 30–50% reduction in maintenance effort within 12–18 months.

The savings compound over time. A rule change that once took two weeks of analysis, coding, and regression testing now takes a day or two because the logic lives in one well-documented place.

Faster development cycles and shorter time-to-market

Business requirements change constantly—new sales channels, regulatory updates, partner integrations, or competitive features. Legacy RPG slows response because developers spend more time deciphering code than writing new functionality.

Free-format RPG changes that dynamic:

  • Syntax resembles modern languages (Java, C#, Python), so developers familiar with those environments adapt quickly.
  • Fully free-format RPG (available since IBM i 7.3, fully supported in 7.4 and later) eliminates column constraints, making complex expressions, nested IFs, and long procedure names natural to write.
  • Subprocedures with clear parameters and return values replace tangled subroutines, so new features build on stable, testable building blocks.
  • Tools such as Rational Developer for i, Git-based source control, and AI assistants like IBM Bob (introduced in IBM i 7.5 and enhanced in 7.6) accelerate editing, refactoring, and debugging.

Teams adopting these practices report 40–60% faster completion of enhancements and new functionality. Release cycles that once spanned months shrink to weeks, letting the business respond to opportunities before competitors do.

Improved system reliability and lower production risk

Production incidents in legacy systems often trace back to side effects introduced during maintenance. Long procedures with many branches, global variables, and indicator-based control flow make it difficult to predict behavior after a change.

Modern RPG reduces these risks in several ways:

  • Subprocedures encapsulate logic with explicit interfaces, limiting scope and side effects.
  • Unit testing becomes practical when code is modular and dependencies are clear.
  • Embedded SQL replaces record-at-a-time loops with set-based operations that are easier to verify for correctness and often perform better.
  • Dead code removal eliminates paths that no longer execute but still complicate understanding and testing (see What is dead code and why remove it?).
  • Structured error handling with MONITOR blocks and procedure-level error parameters replaces reliance on *PSSR or global indicators.

The outcome is fewer defects reaching production, shorter recovery times when issues do occur, and greater confidence when deploying changes to mission-critical applications.

Easier recruitment, onboarding, and retention of developers

The pool of developers experienced with fixed-format RPG and traditional techniques continues to shrink. Many candidates view legacy RPG as outdated and prefer environments that use contemporary practices.

Modern RPG bridges that gap:

  • Free-format code looks and reads more like other enterprise languages.
  • Modular ILE structures (service programs, subprocedures) align with object-oriented and service-oriented thinking taught in modern curricula.
  • Developers ramp up in weeks instead of months because they recognize familiar patterns: functions with parameters, qualified names, inline expressions.
  • Existing staff experience less frustration and burnout when maintaining readable, well-organized code.

This shift reduces the risk of key-person dependency and lowers the hidden cost of relying on a small number of long-tenured RPG specialists.

Greater flexibility to support multiple user interfaces and channels

Businesses increasingly need their core applications to serve green-screen users, web portals, mobile apps, APIs, and partner systems.

Modern RPG enables this without duplicating logic:

  • Service programs act as a single source of truth for business rules.
  • The same procedures can be called from interactive 5250 programs, REST web services, or even external systems via integrated web services.
  • JSON support through DATA-INTO and DATA-GEN simplifies integration with modern front ends and cloud services.
  • Open Access handlers allow existing display-file logic to drive browser or mobile interfaces with minimal change.

This reuse protects investment in proven business logic while opening new revenue streams and customer touchpoints.

Protection of long-term investment through incremental modernization

Few shops can afford to rewrite entire applications. Modern RPG supports a low-risk, incremental path:

  • Convert logic to free format one program or module at a time.
  • Extract reusable rules into service programs without disrupting existing callers.
  • Gradually replace legacy file I/O with embedded SQL where performance or clarity improves.
  • Use IBM Bob (available in 2026 on IBM i 7.6) for real-time suggestions that replace MOVE with EVAL, indicators with structured logic, or fixed-format declarations with modern equivalents.

This approach delivers value early while steadily moving the codebase toward full modernity.

Lower total cost of ownership for core business applications

When these benefits combine, the overall picture is compelling:

  • Maintenance effort drops significantly.
  • New features reach production faster.
  • Production incidents decrease.
  • Talent becomes easier to attract and retain.
  • The same logic supports multiple channels without duplication.
  • The platform’s native strengths—security, reliability, scalability—remain intact.

For IBM i shops in manufacturing, distribution, finance, or healthcare, modern RPG turns a potential liability (aging code) into a strategic asset. It preserves decades of domain knowledge encoded in RPG while aligning development practices with current expectations.

The result is applications that are less expensive to own, faster to evolve, and ready to support whatever business direction comes next.

For practical guidance on beginning this transition in your own shop, see How do I transition to modern RPG?.

How do I transition to modern RPG?

Transitioning to modern RPG is a practical, step-by-step process that lets any IBM i shop improve code quality, reduce maintenance effort, and increase flexibility without halting business operations or requiring a complete rewrite. The previous section explained the business value: lower long-term costs, faster response to new requirements, easier onboarding of developers familiar with contemporary languages, and stronger support for modern integration points. This section provides a realistic roadmap for making that transition in controlled increments so that benefits appear early and risks stay low.

The guiding principle is to apply modern practices wherever code is already being changed—whether for bug fixes, new features, or regulatory updates—and to use those same techniques on all new development. Over multiple maintenance cycles, the entire application gradually adopts free-format syntax, modular structure, and cleaner logic. This approach makes modernization achievable even in shops with limited resources or large legacy codebases.

The following topics are described in this section:

  • Taking stock of the current codebase
  • Starting with free-format calculations
  • Shifting from subroutines to subprocedures
  • Moving shared logic into service programs
  • Replacing legacy patterns as opportunities arise
  • Incorporating supporting practices and tools

Taking stock of the current codebase

Begin with a quick, honest assessment of the existing RPG sources. Examine:

  • Which programs still use older source member types such as RPG, RPG36, or RPG38
  • The extent of numbered indicator usage
  • How much business logic is duplicated across programs
  • The ratio of subroutines to subprocedures
  • Reliance on the traditional RPG cycle

Focus first on programs that change frequently or contain core business rules. These deliver the fastest return on effort. Less frequently touched utilities or archival reports can wait. This inventory helps set realistic priorities without trying to boil the ocean.

Starting with free-format calculations

The single most accessible improvement is moving calculation logic into free format. Free-format calculations have been supported for many years and immediately improve readability while reducing common syntax mistakes.

On programs that need maintenance, convert the calculation specifications first, the **FREE directive on releases that support fully free-format sources. Test the change thoroughly in the normal development cycle, then promote it.

The free-format version eliminates rigid column positions, allows longer, more descriptive names, and supports natural operators and expressions. Within a few maintenance cycles, most active programs shift to free-format logic. New programs should use free format from the start.

Shifting from subroutines to subprocedures

Subroutines were effective for many years, but subprocedures provide better organization, explicit parameter interfaces, and proper variable scoping. Begin by converting one well-defined subroutine at a time—especially those that perform a single task such as calculating a total or validating a field.

The conversion makes dependencies clear through parameters instead of relying on global variables. After each conversion, compile, test, and deploy as part of the normal change process. Over time, the program becomes more modular and easier to understand without a disruptive rewrite.

Moving shared logic into service programs

The greatest long-term benefit comes from eliminating duplicated code. Identify business rules that appear in multiple places—tax calculations, credit checks, pricing adjustments, date formatting—and extract them into service programs.

Group related procedures into a module, compile it, and bind it into a service program object. Programs that need the logic bind to the service program rather than carrying their own copy. A single change to the service program immediately corrects or enhances behavior across the entire application.

Start with one or two high-value functions that are used widely and are likely to change over time. As confidence grows, expand the service program with additional procedures. This practice is one of the strongest drivers of reduced maintenance cost and improved consistency.

Replacing legacy patterns as opportunities arise

Whenever a program is opened for enhancement or correction, replace outdated elements with modern equivalents:

  • Numbered indicators with named indicators or structured control blocks
  • MOVE and MOVEL operations with EVAL, %CHAR, %EDITC, or other built-ins
  • CALL statements with CALLP for bound procedures
  • Cycle-based logic with linear main procedures

These replacements are low-risk when made in small, tested increments. Tools such as IBM Bob, available in 2026 with IBM i 7.6, can review code in real time and suggest these modern replacements directly in the editor. For example, Bob can propose converting a fixed-format MOVE to an EVAL or replacing indicator logic with an ON-EXCEPT block, significantly reducing the manual effort required.

Incorporating supporting practices and tools

Modern RPG development benefits from editors that understand free-format syntax and ILE binding structures. Source control systems track changes reliably. Unit testing frameworks verify behavior after refactoring. These supporting practices become routine as the team works with free-format and modular code.

The transition does not demand perfection from the beginning. Each maintenance window applies one or more of these techniques. Over months or years, the application moves steadily from legacy patterns to modern RPG practices. The result is code that is easier to read, test, extend, and integrate with new technologies.

For more information on the modular design principles that support subprocedures and service programs, see What is ILE modularization?.

What is ILE modularization?

ILE modularization changes how RPG applications are structured and maintained. Traditional RPG programs are often large, standalone units with duplicated logic, global variables, and subroutines that make changes risky and time-consuming. The Integrated Language Environment (ILE) lets developers break applications into smaller, reusable pieces—modules and service programs—that encapsulate specific responsibilities. This approach reduces duplication, isolates changes to a single place, and allows the same business logic to serve multiple interfaces without modification.

In a working IBM i shop, modularization delivers measurable benefits. A pricing rule or tax calculation lives in one service program; update it once, and every calling program benefits without recompilation (as long as the interface stays compatible). Service programs load once and stay resident, lowering overhead. Binding catches interface errors at creation time. Activation groups control resource scope. These features make code easier to test, debug, and extend—critical when the same rules support 5250 applications, web services, APIs, or mobile front ends.

The following topics are described in this section:

  • Core ILE concepts: modules, programs, and service programs
  • Subprocedures as the foundation of modular code
  • Building and exporting procedures in service programs
  • Binding and activation groups in modular designs
  • The program entry procedure requirement
  • Practical organization patterns for modular applications

Core ILE concepts: modules, programs, and service programs

An ILE module is the unit produced by the CRTRPGMOD command. Modules used should be cycle-less using the NOMAIN keyword in the control specification. NOMAIN modules are ideal for reusable components because they avoid unnecessary cycle overhead.

Modules alone are not executable. Bind them into a program object (*PGM) with CRTPGM or into a service program (*SRVPGM) with CRTSRVPGM. A program object has a single entry point and runs as a standalone unit. A service program functions as a shared library: it exports procedures that bound programs call, with the code loaded once and shared across callers.

Service programs provide the primary reuse benefit. Change exported logic once, and all bound programs see the update immediately, provided the procedure prototype remains unchanged. This eliminates the need for copy/paste or /COPY members that require recompiling every consumer when rules change.

Subprocedures as the foundation of modular code

Subprocedures replace subroutines in modular RPG. Subroutines rely on global variables and cannot be exported cleanly. Subprocedures offer local scope, typed parameters, return values, and export capability.

A subprocedure starts with DCL-PROC and ends with END-PROC. The procedure interface (DCL-PI) defines parameters and return type. Add EXPORT to make the subprocedure reusable from other modules.

Here is an exported subprocedure that calculates age in years:

**free ctl-opt nomain; // Prototype (typically in a /COPY member) dcl-pr AgeInYears int(10); BirthDate date(*iso) const; end-pr; // Implementation dcl-proc AgeInYears export; dcl-pi *n int(10); BirthDate date(*iso) const; end-pi; dcl-s Today date(*iso); dcl-s Years int(10); Today = %date(); Years = %diff(Today : BirthDate : *years); return Years; end-proc;

This subprocedure is self-contained and reusable. A caller invokes it simply:

CustAge = AgeInYears(CustRec.BirthDate);

Subprocedures promote small, focused units that are straightforward to understand and test.

Building and exporting procedures in service programs

Service programs collect related subprocedures into a reusable library. They let you package business logic once and call it from multiple programs, green-screen or otherwise.

Follow these steps to create one:

  1. Write the subprocedures in a source member, marking those for external use with EXPORT.

Here is a basic example of a reusable utility procedure in fully free-format RPG:

**free ctl-opt nomain; // Prototype (place in a /COPY member for callers) dcl-pr FormatPhone varchar(20); PhoneNum char(10) const; end-pr; // Exported implementation dcl-proc FormatPhone export; dcl-pi *n varchar(20); PhoneNum char(10) const; end-pi; dcl-s Formatted varchar(20); if PhoneNum = *blanks; return ''; endif; Formatted = '(' + %subst(PhoneNum:1:3) + ') ' + %subst(PhoneNum:4:3) + '-' + %subst(PhoneNum:7:4); return Formatted; end-proc;

This procedure formats a 10-digit phone number with parentheses and a hyphen. NOMAIN keeps it cycle-less and lightweight for reuse.

  1. Compile the source to a module object: CRTRPGMOD MODULE(REDBOOKCDE/PHONEUTILS) SRCFILE(REDBOOKCDE/QRPGLESRC) DBGVIEW(*SOURCE)

  2. Define the export list in binder source (strongly preferred over EXPORT(*ALL) for signature stability):

    STRPGMEXP PGMLVL(*CURRENT) EXPORT SYMBOL("FormatPhone") ENDPGMEXP

    This controls exactly which procedures are visible to callers and helps manage version compatibility.

  3. Create the service program: CRTSRVPGM SRVPGM(REDBOOKCDE/PHONEUTILS) MODULE(REDBOOKCDE/PHONEUTILS) EXPORT(*ALL) ACTGRP(*CALLER)

A caller program bound to this service program accesses the procedure naturally:

CustPhone = FormatPhone(CustRec.Phone);

Use binding directories to manage dependencies cleanly. : Create the Binding Directory CRTBNDDIR BNDDIR(REDBOOKCDE/APPBNDDIR) TEXT('Redbook Binding Directory')

Add the service program once ADDBNDDIRE BNDDIR(REDBOOKCDE/APPBNDDIR) OBJ((REDBOOKCDE/PHONEUTILS))

Then reference the directory when building programs:

CRTBNDRPG PGM(REDBOOKCDE/CUSTMAINT) SRCFILE(REDBOOKCDE/QRPGLESRC) BNDDIR(REDBOOKCDE/APPBNDDIR) DFTACTGRP(*NO)

This approach centralizes dependencies and keeps compile commands short.

Binding and activation groups in modular designs

Binding resolves procedure calls at creation time using prototypes and binding directories. Use DFTACTGRP(*NO) for modern ILE programs to avoid the default activation group.

Activation groups control runtime isolation:

  • ACTGRP(*CALLER) — Service program runs in the caller's activation group; simplest and most common choice.
  • Named activation group (e.g., ACTGRP(REDBOOKCDE)) — Enables persistence across calls; useful for caching.
  • ACTGRP(*NEW) — Creates a fresh group per call for strict isolation, but with higher overhead.

Select *CALLER for most service programs unless persistence or separation is required. Activation groups also govern commitment control and open data path behavior in modular database access.

The program entry procedure requirement

Every *PGM object requires a program entry procedure (PEP)—the starting point for execution. Cycle-main modules (compiled without NOMAIN) provide a PEP automatically through the RPG cycle. NOMAIN modules (containing only subprocedures) do not provide a PEP by default.

Attempting to bind only NOMAIN modules into a *PGM triggers CPD5D1A: "No program entry procedure was found." Service programs do not need a PEP—they exist as libraries.

To create a program:

  • Include at least one module with a main procedure (cycle-main or MAIN(proc-name)).
  • Specify ENTMOD(*FIRST) on CRTPGM if needed to designate the entry module.

Practical organization patterns for modular applications

Organize modular RPG effectively:

  • Group procedures by domain: utility service programs (DATEUTILS, STRINGUTILS) and business service programs (ORDERPROC, CUSTRULES).
  • Use NOMAIN on all service program modules.
  • Store prototypes in /COPY members for consistent interfaces.
  • Avoid global variables; pass data explicitly through parameters.
  • Limit procedures to a single responsibility (ideally one screen or less).
  • Document exported procedures in prototypes.
  • Test subprocedures independently (consider RPGUnit).

These patterns make large applications comprehensible. Developers locate logic quickly in well-named service programs instead of monolithic code.

ILE modularization makes RPG a flexible, component-based language that preserves existing investments while supporting modern needs. For transitioning legacy code to this structure, see How do I transition to modern RPG?. For syntax that enables cleaner subprocedures, see Why Use Fully Free Format RPG.

What are RPG program design best practices?

Any good program has a solid design behind it. Without a solid program design, you could be caught by scope creep that interferes with your program completion, implement bugs or have an incomplete project. There are several good practices you can put in place. Before we can talk about best practices, we need to first understand high level vs low level design.

High level design vs low level design

High Level Design (HLD) is the "what and why" the big picture architecture and approach without getting into the details of implementation. For example, the beginning statement would look like this: "We will modernize the order entry system by converting RPG OPM programs to ILE, replacing flat files with DB2 SQL tables, and exposing business logic as service programs consumable by a web front end."

It covers things like the inital business request, system architecture, component relationships, technology choices, data flow between systems, and integration points. A business analyst or project manager can read it and understand the scope. High level designs usually also includes an estimate and possible costs for a project. Most of the time this is to get signoff from the business to proceed with the request.

Low Level Design (LLD) is the "how" the detailed technical specification that a developer actually builds from. For example, you would include the overarching request from the high level design: "We will modernize the order entry system by converting RPG OPM programs to ILE, replacing flat files with DB2 SQL tables, and exposing business logic as service programs consumable by a web front end." This is to ensure that the developer understands the business request. You then will move on to the actual changes.

"The ORDENTRY program will be refactored into three service programs: ORDSV01 (validation), ORDSV02 (pricing), ORDSV03 (fulfillment). The existing ORDHDR and ORDDTL physical files will be replaced with SQL tables with the following columns, keys, and constraints... The API will accept JSON via HTTP POST to endpoint /orders using the YAJL library for parsing..."

It covers specific program names, data structures, field definitions, procedure signatures, error handling logic, and API contracts. Only a senior developer/developer needs to read it.

Ideally you want to have the low level and high level into two separate documents. This ensures that if you have to get sign off that the person reviewing it is not bogged down with low level design details that they dont care about.

Document the business requirements first

Any new program or modification comes with a business request. You must first document and verify what the business is asking you for. When a request comes in ask questions, make sure you and the person requesting the change understands what is being asked of you. Keep a high level design document for that change for you to keep track of what is being asked of you. This does not mean create a new document for every single change that comes in but, that you create the design document for any significant change to an existing program or process.

Document existing business logic.

A new program or modification could effect existing logic. You need to get a handle of what is being effected by this change and document it first. You will add to the high level document that you created when you were given the business request. You want to look at what are the existing database access, inputs and outputs and logic that could be deprecated by the new logic.

Understand the database

Most requests will have some data element to it. This could be having to modify an existing table, add a new view, add a new table, create new sql procedures or functions etc. You want to make sure you understand the data (if applicable) that is being written and what needs to be adjusted. Before you can create any low level design, get a handle of what the data looks like today and what it needs to look like in the future.

Understand inputs and outputs

The next phase of the design is understanding and identifying what your program(s), procedures, sql etc input and outputs are going to be. You need to understand whether you need to add a parameter to a program, call out to a service program procedure and what it needs, call out to the database and what it needs. You need to understand the flow of how the program(s), procedures, sql etc are going to work and how each call in the callstack is going to work together.

Decide on the architecture.

Not every change is going to be a standalone program. Most often than not, you are binding to a service program to call its methods, calling sql procedures or using functions within embedded sql. You need to decide whether you are creating a new service program(s), standalone programs, sql procedures, integration with open source, the use of open source projects like YAJL or open source utilities that exist to make your programming easier.

Complete the design

At this point, you should understand what is being asked of you and you know what architecture you are using, what your database access, inputs and outputs, existing logic and new logic you need to add is going to be. You need to now put that together in a way that a developer can understand. This would look like how the low level design looks above.

"The ORDENTRY program will be refactored into three service programs: ORDSV01 (validation), ORDSV02 (pricing), ORDSV03 (fulfillment). The existing ORDHDR and ORDDTL physical files will be replaced with SQL tables with the following columns, keys, and constraints... The API will accept JSON via HTTP POST to endpoint /orders using the YAJL library for parsing..."

You can also break it up into phases "Phase 1 refactoring ORDENTRY into three service programs ORDSV01 (validation), ORDSV02 (pricing), ORDSV03 (fulfillment)" "Phase 2 replace existing ORDHDR and ORDDTL tables with sql tables orderHeader and OrderDetail using the same fields as previous tables ensuring the use of long name and existing short names for easy interchanability" "Phase 3 map out the database to the api request and response" "Phase 4 add calls to the tested api request and response to ORDSV01 service program in the getRequest and getResponse method"

You are not limited to how many phases you have but, this helps to not only break the work into more manageable pieces but, adds support for easier integration and unit testing.

What are RPG coding best practices.

Coding best practices are key to any program that you develop. When you implement best practices in your shop, you enable your team to be consistent enough without squashing creativity that makes sure one developer can look at anyone's code developed by any memeber of the team.

Dont hard code.

Hardcoding was a practice done by developers in the past. You will encounter it but, dont ever add to it. Imagine what you will have to do if you inherited a series of programs that checked whether they were on the production system or not. The developer before you decided to add logic that checked for the system name/serial number of the IBM i you are developing on. Now imagine needing to switch systems and that system name/serial number changes. You now have a ton of programs that wont work in production because the system or serial number was changed. You never want to hard code a value, if you need to add logic lets say to check whether you are on the production system or not, create a utility service program that has a procedure that returns a boolean, that will tell you if you are on production or not by doing a lookup to a utility table that has the system name soft coded. You want to softcode as much as possible.

API keys, login information.

Todays RPG programs wont be limited to languages native on IBM i. You will most likely integrate with newer technologies via APIs and they will be secured either by using a secret key, user name and password. You dont ever want to hard code those credentials in your programs. Put them in a database file that has limited access and masking so you cant see the values in plain text.

User profiles, emails.

It was very common for a long time to hard code user profiles and/or email addresses for notifications. Now you have employees who leave and their profiles have to be left for one reason or another, emails that will start bouncing once they are deactivated. Create a email table and soft code any notification email addresses and create a lookup program to do the work.

Business logic.

Business logic like item numbers, customer numbers, warehouses etc. You will see them hard coded in many places. There are many different ways to handle this logic and the conditioning you need to do, instead of logic like

if ( warehouse = 1 ); itemNumber = 'ABC123'; endif;

You can create a procedure in a service program that will do the work. The procedure could look like this.

dcl-proc isWarehouseOne; dcl-pi *n ind; warehouse int( 5 ); end-pi; dcl-s count int( 5 ); exec sql select count( * ) into :count from warehouseMast where whs = :warehouse and whs = 1; // returns true if count > 0, otherwise false. return ( count > 0 ) ; end-proc; Now instead of the if block above, you have if ( isWarehouseOne( whsNum )); item = whsOneItem(); endif;

The action of hard coding key business logic that you know will be used in multiple places is a hard habit to break. You want to create service programs and procedures that will keep logic in one place.

The list of things not to hard code is vast but, the gist of it is softcode where you can, avoid hard coding like the plague and you will create less work for yourself and others in the future.

Dont be ambiguous with naming

Having naming conventions that require you to have to stop and think to understand what they mean needs to go away. For some they might think if they figure out IT12 = Item12 that they are like a hacker in a hacker movie but the reality is, you wont remember what IT12 means 3 weeks after your change goes live, let alone what another developer has to look at your code. Be purposeful with your names. If you have an item number variable, make that variable make sense. It could be itemNumber, itmNum, ItemNum etc. You want to be able to understand what the variable is by a quick glance.

Naming is not limited to just variables. Procedures, variables, constants are all up for grabs. You want to be able to look at something and know exactly what its doing. For procedures use getXX for returns, setXX for setting something with no return values, is for booleans, has if is does not make sense. For example getItemNumbers, setItemNumbers, isItemNumberActive, hasItemBalance all tell you exactly what the procedure is doing.

Dont write subroutines

Subroutines are logic that is just in a seperate spot in the program and can be called from multiple places. They use global variables and are hard to scope for that reason, use internal procedures to your program or service program procedures if available and applicable.

Limit global variables

Global variables can be touchy but, you need to be purposeful with the use of them. In main RPG programs, global variables are a big part of process but, that prevents you from limiting their scope. Or worse, put a variable (not template, explained below) in a copy book and its global use now just jumped from one program to many. If you have a new RPG program, scope out the logic to use internal procedures with local variables. For example instead of using dcl-s itemDescription varchar( 50 ); at the beginning of your program, create a procedure that uses local variables.

dcl-proc getItemDescription; dcl-pi *n varchar( 50 ); itemNumber int( 5 ); end-pi; dcl-s itemDescription varchar( 50 ); exec sql select itmDesc into :itemDescription from myItemTable where itemNum = :itemNumber; return itemDescription; end-proc;

Use service programs procedures when you can where you can

The rule of thumb is if you need the same logic in more than one place then it needs to go into a service program procedure. The first time you are developing the code and it exists in one place only then you can have it as mainline code or an internal procedure to that program but, as soon as you need that logic, extract the code and put it into a service program procedure.

Make use of error handling

RPG gives you monitor/end-mon with on-excp. It is pretty typical in generalized program to have some error handling in place, its a good practice to make use of monitor blocks to prevent your program from having a hard crash.

Comments should make sense

How many times have you read code that has limited, out of date or meaningless comments or worse no comments at all. Program and procedures should have an explanation; this may be as short as a sentence or as long as a few paragraphs. Explain its interactions with the outside world and/or how it should be used; don’t explain its actions with pseudocode. Use proper English grammar.

Be Consistant

Consistency is key, come up with a internal or team standard that makes sense. How you name your variables, comment your code, indent your code etc. Should all be consistant. Its very easy to indent one block of code two places in and the next three places in for no reason other than you just coded it that way. Find what works for you and what your teams standards and make sure to keep applying it in all your coding.

Use like or template where applicable

Template is a important keyword you can apply to variables. They make the template variable unchangeable but, you are able to define another variable like the template you created.

// template variable dcl-s myItemTemplate varchar( 50 ) template; -> dcl-s item like myItemTemplate;

like is a powerful keyword, it helps you define variables, procedure return values, input values etc like something else. This is helpful when you know you are going to have the same variable in multiple places and instead of setting the length in all places, you can have a template variable and reference that variable in all of the places you need to.

Conclusion

Coding best practices can vary based on your teams standards. If standards dont exist then create some for yourself and keep with them using the best practices that were given here. For some places the standard and best practice is to use SQL only. While most will agree with that standard, there are some places where that cannot be a hard and fast rule. Also note that there are best practices that wont ever change like dont hardcode but best practices that can change like how you create a data structure today might not look the same 10 years from now. Best practices can change like the technology we work with, just make sure to change with it.

How do I measure and reduce complexity?

Measuring and reducing complexity ranks among the most practical actions IBM i shops take to keep RPG code maintainable over the long term. Complex code especially legacy monolithic programs with deep nesting and tangled control flow drives up testing effort, increases defect risk, and slows down enhancements. By applying quantifiable metrics and targeted refactoring, developers identify trouble spots early and systematically improve the codebase. In 2026, with IBM i 7.6 and modern tooling, this process becomes far more efficient and integrated into daily work. This section explains the primary metrics, how they apply to RPG, available measurement tools, and concrete steps to lower complexity while preserving business logic.

Understanding cyclomatic complexity

Cyclomatic complexity, developed by Thomas McCabe in 1976, counts the number of independent execution paths through a unit of code. In RPG, each control structure that creates a branch—IF, SELECT/WHEN, FOR, DOW, DOU, MONITOR, and even certain built-in function outcomes—adds to the path count. The base value starts at 1 for a linear block of code, and each decision point adds one.

A simple subprocedure with one IF and one FOR loop typically shows complexity 3. Nested conditions push the number higher quickly: a procedure with three nested IF statements and a SELECT group can easily reach 10 or more. In legacy RPG programs, especially those written before ILE and free-format became common, complexity often climbs well above 30 in large main procedures, making thorough testing impractical and changes risky.

The practical target for individual procedures sits between 10 and 15. Scores above 20 signal code that is hard to understand at a glance and difficult to test completely. Values over 50 indicate serious maintenance debt. Keeping complexity low improves reliability, speeds up debugging, and makes it easier for new developers—many of whom know Java, Python, or JavaScript—to contribute effectively. High complexity directly correlates with higher defect rates and longer onboarding times in IBM i environments.

Complementary metrics

Cyclomatic complexity provides the core insight into control-flow risk, but other metrics round out the assessment.

Halstead metrics measure program volume and difficulty by counting distinct operators and operands. In RPG, this includes operation codes (EVAL, CALLP, IF), built-in functions (%CHAR, %SCAN), and fields. High Halstead volume often accompanies dense, hard-to-read code.

The Maintainability Index combines cyclomatic complexity, lines of code, and comment density into a single score (0–100, higher is better). Modern RPG code should aim for 70 or above.

RPG-specific indicators include:

  • Percentage of fixed-format statements (ideally approaching 0 percent)
  • Count of obsolete or legacy constructs (MOVE without %CHAR, CALL without prototypes, numbered indicators)
  • In-line declarations (result-field lengths or *LIKE DEFINE scattered throughout logic)
  • Heavy use of numbered indicators (01–99), which obscure decision logic and inflate perceived complexity

Tracking these alongside cyclomatic complexity reveals migration progress and highlights areas where readability improvements yield the biggest gains.

Tools for measuring complexity

Several tools help collect these metrics on IBM i.

Static analysis products such as ARCAD Observer, Fresche X-Analysis, and SourceMeter deliver detailed reports on cyclomatic complexity, Halstead values, and RPG-specific issues across entire applications. Open-source options, including older RPG Code Metrics analyzers, provide basic counts for decision density, fixed-format usage, and obsolete opcodes.

Dynamic tools like iTrace complement static analysis by showing which paths actually execute during real workloads, helping prioritize refactoring on frequently run complex sections.

IBM Bob, integrated into VS Code for IBM i 7.6, brings real-time intelligence directly into the editing experience. Bob analyzes RPG code on the fly and specifically addresses cyclomatic complexity through these features:

  • Performs automated structural and complexity analysis, calculating McCabe cyclomatic complexity (and Halstead metrics) for procedures and modules
  • Flags high-complexity sections in real time within the editor, highlighting procedures or changed code that exceed safe thresholds
  • Identifies low maintainability linked to elevated complexity scores during code reviews
  • Offers contextual refactoring suggestions to reduce complexity, such as extracting logic into new subprocedures, flattening nested conditions, or replacing legacy control structures with modern equivalents, complete with explanations of the expected impact on testability
  • Aggregates complexity metrics across source members or projects to guide modernization prioritization
  • Reviews uncommitted changes immediately, integrating complexity checks into the daily workflow
  • Supports modernization-focused modes that pair complexity metrics with recommendations for moving from fixed-format to free-format RPG and adopting ILE modularization

These capabilities make IBM Bob particularly effective for gradual, low-risk modernization of existing codebases.

Practical refactoring techniques

The most reliable way to reduce complexity involves decomposing large units into smaller, single-responsibility procedures.

Begin with extraction: move a block of complex logic—validation, calculation, or error handling—into its own subprocedure. A 400-line monolithic procedure with multiple nested IFs and loops often splits into 8–12 smaller units, each dropping below complexity 10. This preserves behavior while shrinking path counts dramatically.

Fully embrace ILE modularity. Place reusable logic in service programs bound by reference, eliminating duplicated decision structures across programs. Prototyped interfaces replace older CALL operations, reducing parameter-related complexity.

Convert to fully free-format RPG wherever possible. Free-format syntax encourages cleaner expressions (using %EQUAL instead of indicator checks, %XLATE instead of loops) and naturally lowers both actual and perceived complexity.

Refactor control flow aggressively:

  • Replace deep nesting with early returns or consolidated SELECT/WHEN groups
  • Eliminate unnecessary loops by shifting logic to SQL set-based operations (covered in Chapter 8)
  • Use modern built-ins to simplify string and date handling

After each change, run regression tests—preferably automated with RPGUnit—to confirm correctness. Monitor metrics before and after to quantify progress.

Building and using a complexity dashboard

Create a simple dashboard in a spreadsheet, database file, or reporting tool. Capture per-module metrics: cyclomatic complexity, lines of code, maintainability index, free-format percentage, and obsolete construct count. Update after each sprint or modernization wave.

Review the dashboard in team stand-ups or planning sessions. Establish rules such as no new code above complexity 15 and targets for reducing legacy modules by 20 percent per quarter. The data highlights training gaps (for example, overuse of nesting) and demonstrates concrete value—lower defects, faster enhancements, easier handoffs.

Systematic measurement and reduction of complexity, supported by tools like IBM Bob, turns a daunting legacy codebase into one that is responsive and future-ready. These practices build on the modular foundation described in What is ILE modularization? and set the stage for dead code elimination (What is dead code and why remove it?).

What is cyclomatic complexity?

Cyclomatic complexity provides an objective measure of how complicated a program's control flow really is. Introduced by Thomas J. McCabe in 1976, this metric counts the number of linearly independent paths through a section of code—typically a procedure or module. In RPG terms, it tells you how many different ways the logic can execute based on decisions, loops, and conditions. Lower values indicate simpler, more predictable code that is easier to understand, test, and maintain. Higher values signal potential trouble spots where bugs hide and future changes become risky.

In a modern IBM i shop, cyclomatic complexity matters because legacy RPG programs often accumulate nested IFs, multiple WHEN groups in SELECT, DO loops with embedded conditions, and scattered branching logic. These structures multiply execution paths quickly. A single procedure with a complexity of 30 or more can require dozens of test cases to achieve reasonable coverage, and even then, subtle defects slip through. When maintenance programmers encounter high-complexity code, they hesitate to make changes, leading to workarounds, duplicated logic, or patches that increase technical debt further.

The practical value of tracking cyclomatic complexity lies in guiding refactoring decisions. During modernization efforts, teams use this metric to prioritize which procedures or service programs to break apart first. A high score flags code that should be split into smaller, focused subprocedures—each responsible for a single decision branch or loop. This refactoring reduces risk, improves unit testability, and makes the overall application more responsive to business changes. Tools that compute the metric automatically help enforce team standards: many organizations set a target of 10 or less per procedure, with anything above 20 requiring review before promotion.

How cyclomatic complexity is calculated

The formal definition uses a program's control flow graph, where nodes represent blocks of sequential code and edges represent transfers of control (jumps caused by conditions, loops, or calls). The McCabe formula is:

Cyclomatic complexity (CC) = E - N + 2P

  • E = number of edges (control transfers)
  • N = number of nodes (sequential code blocks)
  • P = number of connected components (usually 1 for a single procedure)

For most RPG subprocedures, which have a single entry and exit, P = 1, so the formula simplifies to E - N + 2. In practice, developers rarely draw graphs. Instead, they count decision points (predicate nodes) and add 1:

CC ≈ number of decision points + 1

In RPG IV and later, each of the following adds one to the count:

  • Each IF statement (regardless of nesting level)
  • Each ELSEIF (or WHEN in a SELECT group)
  • Each DO / DOW / DOU / FOR loop
  • Each AND / OR in a complex condition (some analyzers count these separately)
  • CASxx operations in older code
  • MONITOR / ON-ERROR blocks (each ON-ERROR adds a path)

A simple procedure with no decisions has CC = 1. Adding one IF increases it to 2 (true and false paths). Nesting another IF inside raises it to 3, and so on.

Consider a basic validation routine:

**free ctl-opt main('ValidateOrder'); dcl-proc ValidateOrder; dcl-pi *n; OrderQty packed(5:0) const; StockAvail packed(5:0) const; Price packed(9:2) const; end-pi; dcl-s IsValid ind; dcl-s TotalPrice packed(11:2); IsValid = *on; if OrderQty <= 0; IsValid = *off; elseif StockAvail < OrderQty; IsValid = *off; endif; TotalPrice = OrderQty * Price; if TotalPrice > 10000.00; IsValid = *off; endif; if not IsValid; // log error or throw exception endif; return; end-proc;

This procedure has three decision points: two in the first IF-ELSEIF chain (treated as two branches) and one more IF for the total price check. Cyclomatic complexity is therefore 4 (3 decisions + 1 base path). Testing needs to cover four scenarios: valid order, zero quantity, insufficient stock, and high total price.

Now examine a more complex version with nested logic:

**free ctl-opt main('ProcessPayment'); dcl-proc ProcessPayment; // ... declarations ... if PaymentType = 'CC'; if CardNumberValid(CardNumber); if Amount <= CreditLimit; if AuthorizeCC(CardNumber: Amount); ProcessOrder(); else; SendDecline(); endif; else; SendLimitExceeded(); endif; else; SendInvalidCard(); endif; elseif PaymentType = 'ACH'; if BankAccountValid(Account); if FundsAvailable(Account: Amount); TransferACH(); else; SendNSF(); endif; else; SendInvalidAccount(); endif; else; SendUnknownType(); endif; // ... cleanup ... end-proc;

Here, the outer IF-ELSEIF creates two main branches, but each contains nested IFs. Counting decisions: outer IF (1), inner IF for CC (2), another for limit (3), authorize (4); elseif branch has two more IFs (5,6). Total decisions ≈ 6–8 depending on exact analyzer. Complexity easily reaches 7–9, meaning 7–9 independent paths. This single procedure now demands extensive testing, and a small change (such as adding a fraud check) risks breaking unrelated paths.

Interpreting complexity scores

McCabe's original guidance suggested:

  • 1–10: simple, low risk
  • 11–20: moderate complexity, manageable with care
  • 21–50: high complexity, refactor recommended
  • Over 50: very high, serious maintenance risk

In RPG modernization projects, teams often adopt stricter targets. A service program procedure with CC > 15 typically gets flagged for splitting. The Software Engineering Institute and many quality standards recommend keeping individual modules under 10–15 to ensure reliable testing and low defect rates.

High complexity correlates strongly with defect density. Studies show that procedures above 20 have significantly higher bug rates because paths interact in unexpected ways. In IBM i environments, where many programs run unattended for years, hidden defects in high-complexity code can cause costly production failures when data patterns change.

Using cyclomatic complexity in practice

During code reviews, calculate or display CC for each subprocedure. Many modern tools integrated with Rational Developer for i, VS Code extensions, or open-source analyzers provide this automatically. When a score exceeds the threshold, refactor by:

  • Extracting nested logic into separate subprocedures
  • Replacing deep nesting with early returns or guard clauses
  • Converting complex SELECT/WHEN groups to lookup tables or polymorphic calls
  • Simplifying conditions with helper procedures

As discussed in How do I measure and reduce complexity?, tracking CC over time shows progress in a modernization effort. A downward trend in average procedure complexity indicates cleaner, more maintainable code.

Cyclomatic complexity is not perfect—it ignores data complexity and external calls—but it provides a reliable, language-independent starting point for identifying trouble spots. In modern RPG, keeping procedures simple enough that their complexity stays low makes unit testing practical, onboarding faster, and long-term maintenance far less painful. Every shop should incorporate this metric into its quality gates to prevent legacy-style spaghetti logic from creeping back in.

What is dead code and why remove it?

Dead code consists of program statements, blocks, subroutines, or procedures that no longer execute or affect the program's behavior in any production scenario. In IBM i RPG applications, these unused fragments accumulate over years of maintenance and represent a form of technical debt that silently increases maintenance costs and modernization effort.

This section defines dead code in the RPG context, explains how it typically appears in legacy and modernizing codebases, and outlines the practical reasons to identify and eliminate it. Removing dead code improves readability, reduces risk during changes, and prepares the program for refactoring, modularization, and free-format conversion.

The following topics are described in this section:

  • Common forms of dead code in RPG programs
  • How dead code accumulates in IBM i applications
  • Risks of keeping dead code in the source
  • Benefits of systematic removal

Common forms of dead code in RPG programs

Dead code takes several recognizable forms in RPG source members:

  • Unreachable logic after a condition that never evaluates true in practice (for example, an IF branch based on a discontinued business rule)
  • Subroutines that no longer receive control because calling logic now uses procedures or different flows
  • Calculations or data moves whose results are never referenced (computed but unused)
  • Commented-out blocks left from previous versions or debugging sessions
  • Entire procedures or copybook sections included but never called in the current application
  • Legacy error-handling paths for conditions that modern database constraints or input validation prevent from occurring

In fixed-format RPG III and RPG/400 programs, dead code often hides in indicator-driven logic or in subroutines chained through GOTO or EXSR statements that modern control structures have replaced.

How dead code accumulates in IBM i applications

Several common practices lead to dead code buildup in long-lived applications:

  • Business processes evolve or are replaced, but old conditional paths remain “just in case”
  • Time pressure leads developers to copy-paste code from older programs, introducing unused sections
  • Temporary patches or workarounds are added without removing the original logic after the fix is permanent
  • Multiple programmers maintain the same source over decades, each adding layers without full visibility into prior code
  • Feature additions or bug fixes create alternative paths, leaving the original path intact but dormant
  • Commented code is retained for historical reference or fear of breaking something undocumented

These patterns are especially common in monolithic programs that handle multiple related functions in a single source member.

Risks of retaining dead code

Leaving dead code in place creates several ongoing problems:

  • Inflates perceived program complexity, making the active logic harder to understand at a glance
  • Increases cognitive load for developers reading or modifying the code
  • Distorts static analysis metrics such as line count and cyclomatic complexity
  • Raises the chance of reactivating dormant code during refactoring, introducing defects
  • Slows compilation, source navigation, and code reviews
  • Obscures the true business intent of the program
  • Complicates onboarding of new developers unfamiliar with legacy RPG patterns
  • Wastes memory and activation resources (minor but measurable in high-volume environments)

In modernization projects, dead code inflates the scope of work: tools, conversions, and refactoring efforts process unnecessary logic.

Benefits of removing dead code

Eliminating confirmed dead code delivers clear, practical advantages:

  • Sharper focus on active business logic, improving readability and maintainability
  • Reduced time spent understanding and navigating source during debugging and enhancements
  • Lower risk when making changes, as developers no longer second-guess irrelevant sections
  • More accurate test coverage metrics and complexity measurements
  • Faster compile times and quicker loading in modern IDEs such as Rational Developer for i
  • Smaller, cleaner codebases that are easier to hand off or modularize into service programs
  • Significant reduction in modernization effort—many legacy programs contain 20–40% dead or obsolete code
  • Alignment with modern RPG principles of clarity, self-documentation, and minimalism

Teams that routinely remove dead code report fewer defects during refactoring, faster feature delivery, and easier adoption of free-format RPG and ILE practices.

Cautions when removing dead code

Removal requires careful validation because much dead code is runtime-dependent rather than statically unreachable. Relying solely on static analysis risks deleting logic that executes only under rare conditions. Dynamic analysis—executing the program in a representative environment with comprehensive test scenarios—is essential for confidence.

The safest approach follows these steps:

  • Identify candidates through tracing or coverage tools
  • Comment out suspect sections first and monitor for behavioral changes
  • Perform regression testing with business users validating key scenarios
  • Delete only after no issues surface over time

This disciplined process minimizes risk while steadily cleaning the codebase.

In modern RPG, dead code stands in direct opposition to the goals of maintainability and future-readiness. By systematically identifying and removing it, IBM i developers create code that is easier to understand, test, and extend. This cleanup step pays dividends throughout the modernization journey, especially when moving toward What is ILE modularization? or reducing complexity (see How do I measure and reduce complexity? and How do I modularize RPG programs?). Modern AI tools like IBM Bob can easily help identify and remove dead code.

How do I modularize RPG programs?

Modularizing RPG programs means breaking large, tightly coupled monolithic applications into smaller, independent, reusable components that communicate through well-defined interfaces. This approach, made practical by the Integrated Language Environment (ILE), reduces maintenance effort, minimizes defects when changes occur, eliminates redundant code across applications, and makes unit testing far more feasible. In a typical IBM i shop, modularization turns a single 5,000-line order-entry program into a main driver that calls shared business logic housed in service programs—logic that the same shop can then reuse for web APIs, mobile back-ends, or batch reporting without duplication.

The following topics are described in this section:

  • Moving from subroutines to subprocedures
  • Organizing logic into modules
  • Packaging reusable procedures in service programs
  • Creating a module for the main application logic and binding it to service programs
  • Accelerating the process with IBM Bob

Move from subroutines to subprocedures

Legacy RPG programs organize internal reusable code with subroutines (BEGSR/ENDSR). Subroutines operate in the global scope of the program, share all variables, and rely on indicators or global fields to pass information. This design creates hidden dependencies: changing a variable in one subroutine can break unrelated logic elsewhere. Subprocedures solve these problems by providing local scope, typed parameters, return values, and explicit interfaces.

Begin the transition by identifying a self-contained subroutine. Determine what data it needs as input and what it must produce as output. Move those values into parameters and return the result instead of modifying globals.

Consider a typical subroutine that validates an order total against credit limits:

C CHKCRD BEGSR C IF ORDAMT > CRTLIM C MOVE 'Y' CREDIT_ERR C ENDIF C ENDSR

This code depends on global fields ORDAMT, CRTLIM, and CREDIT_ERR. Any other part of the program can accidentally change these fields before or after the subroutine runs.

Replace it with a subprocedure:

**free // Source member: CREDIT_H.RPGLE (prototype and implementation example) // Usually placed in a /COPY member named CREDIT_H.RPGLE for prototypes ctl-opt nomain; dcl-pr CheckCreditLimit ind; OrderAmount packed(11:2) const; CreditLimit packed(11:2) const; end-pr; dcl-proc CheckCreditLimit export; dcl-pi *n ind; OrderAmount packed(11:2) const; CreditLimit packed(11:2) const; end-pi; return (OrderAmount > CreditLimit); end-proc;

Call the subprocedure like this:

if CheckCreditLimit(Order.Total : Customer.CreditLimit); // handle credit limit exceeded endif;

The subprocedure is now self-contained. It cannot be affected by unrelated code, and its behavior is predictable based only on the parameters passed. This change alone makes debugging easier and enables isolated testing of the credit-check logic.

Organize logic into modules

A module (*MODULE) is a compiled object that contains one or more subprocedures. When created with the NOMAIN keyword, the module has no main procedure and exists solely to provide reusable code.

Group related subprocedures into a single module based on responsibility. For example, create a module named CUSTUTIL for all customer-related operations:

**free // Source member: CUSTUTIL.RPGLE in REDBOOKCDE/QRPGLESRC ctl-opt nomain; dcl-proc GetCustomerName export; dcl-pi *n varchar(50); CustNo zoned(6:0) const; end-pi; // retrieve and format name logic here return 'John Doe'; end-proc; dcl-proc ValidateCustomerStatus export; dcl-pi *n ind; CustNo zoned(6:0) const; end-pi; // status check logic here return *on; end-proc;

Compile the source to a module:

CRTRPGMOD MODULE(REDBOOKCDE/CUSTUTIL) SRCFILE(REDBOOKCDE/QRPGLESRC) SRCMBR(CUSTUTIL)

Modules become the basic unit of reuse. Keep each module focused: one module for customer functions, another for order calculations, another for date utilities. Cohesive modules with 5–20 subprocedures strike a good balance between manageability and reduced overhead.

Package reusable procedures in service programs

Service programs (*SRVPGM) collect one or more modules into a single object that multiple applications can bind to and share at runtime. This is the key to eliminating copy-paste duplication of business rules across green-screen, web, and API programs.

Create the service program from the module using the simplest form (which exports all procedures by default):

CRTSRVPGM SRVPGM(REDBOOKCDE/CUSTSRV) MODULE(REDBOOKCDE/CUSTUTIL) TEXT('Customer utility service program')

This command produces a service program that exports every exported subprocedure from the module. For introductory modularization, this approach avoids extra complexity while still delivering the core benefit: shared, callable business logic.

Creating a module for the main application logic and binding it to service programs

In a fully modular ILE application, even the entry-point logic belongs in a module. The ORDENTRY module contains the main procedure that drives the application and calls subprocedures from service programs. Create ORDENTRY as a module with a main procedure (specified via the MAIN keyword), then bind it to the service program when creating the final *PGM object.

Here is the ORDENTRY module source. The DSPly operation sends an information message to the message queue of the user running the program (typically QSYSOPR or the user's own message queue). You can view these messages by typing DSPMSG at a command line. Prototypes are shown inline for this example; in practice, move them to a separate prototype member.

**free // Source member: ORDENTRY.RPGLE in REDBOOKCDE/QRPGLESRC ctl-opt main(Entry); dcl-pr GetCustomerName varchar(50); CustNo zoned(6:0) const; end-pr; dcl-pr ValidateCustomerStatus ind; CustNo zoned(6:0) const; end-pr; dcl-s CustomerNumber zoned(6:0) inz(123456); dcl-s CustName varchar(50); dcl-s Msg varchar(52); // Main entry procedure dcl-proc Entry; CustName = GetCustomerName(CustomerNumber); if ValidateCustomerStatus(CustomerNumber); Msg = 'Valid customer: ' + CustName; dsply Msg '*REQUESTER'; else; dsply 'Invalid customer status' '*REQUESTER'; endif; end-proc;

Compile ORDENTRY to a module:

CRTRPGMOD MODULE(REDBOOKCDE/ORDENTRY) SRCFILE(REDBOOKCDE/QRPGLESRC) SRCMBR(ORDENTRY)

Create the final program by binding the ORDENTRY module to the CUSTSRV service program:

CRTPGM PGM(REDBOOKCDE/ORDENTRY) MODULE(REDBOOKCDE/ORDENTRY) BNDSRVPGM(REDBOOKCDE/CUSTSRV) ACTGRP(*CALLER)

This ORDENTRY module focuses on application flow while delegating business rules to the service program. The DSPly operation with '*REQUESTER' sends messages to the interactive user's message queue, making them visible with DSPMSG. In production code, replace DSPly with a display file or dedicated message handling for more robust output.

Binding by reference to service programs keeps program objects small and allows the service program to be updated independently (as long as the exported procedures remain compatible). This separation is especially valuable in shops that deliver changes frequently.

Incremental modernization and practical tips

Start modularization on high-value areas: pricing logic, tax calculations, credit checks, or address validation—routines duplicated in multiple programs. Extract one routine at a time into a subprocedure, move it to a module, build the service program, then create and bind the entry-point module.

Adopt qualified data structures inside subprocedures to avoid name collisions and improve readability.

Monitor binding errors during testing to catch interface mismatches early. Use DSPPGMREF and DSPMOD to document dependencies.

Modular RPG code becomes the foundation for cleaner design, easier testing, faster onboarding of new developers, and flexible deployment across interfaces. The effort pays off quickly in reduced defects and faster response to business requests.

Accelerating modularization with IBM Bob

IBM Bob, available since IBM i 7.6, brings AI assistance directly into the VS Code-based IDE and significantly speeds up the modularization process described in this section. Bob understands full RPG programs—including legacy fixed-format, ILE structures, subroutines, data flows, and dependencies—and can analyze large monolithic sources to identify modularization opportunities.

Highlight a subroutine in your editor and ask Bob to extract it as a subprocedure. Bob automatically:

  • Identifies input and output fields.
  • Converts globals to typed parameters (using CONST where safe).
  • Generates the DCL-PROC wrapper, local variables, and return value.
  • Produces a matching prototype for /COPY inclusion.
  • Suggests meaningful names if the original variables are cryptic.

For broader refactoring, provide a prompt like "Analyze this order-entry program and extract all pricing logic into a new service program named PRICING." Bob maps the relevant subroutines, consolidates duplicated code, generates the module source, and updates the main program to call the new service program via prototyped calls.

Bob also performs impact analysis before changes, showing which programs reference the affected logic, and generates basic unit-test stubs or integrates with RPGUNIT. This reduces manual risk when extracting high-value routines.

In practice, Bob turns weeks of careful manual extraction into hours of assisted work with review steps. Shops report faster progress on modularization projects because Bob handles repetitive pattern recognition and boilerplate while preserving business intent. Use Bob in combination with the incremental approach: start with one duplicated routine, let Bob propose the extraction, review and test, then bind the result into existing applications.

For background on the ILE concepts that enable this approach, see What is ILE modularization?.

Why Use Fully Free Format RPG

For decades, RPG has been a cornerstone of application development on IBM i. However, traditional fixed-format RPG, rooted in its punch-card era constraints, imposes rigid column-based rules that make code difficult to read, write and maintain. With the introduction of fully free format RPG in IBM i 7.1, developers are no longer bound by these limitations. Instead, they can write code in a modern, flexible syntax that closely resembles contemporary programming languages like Java, C#, and Python.

This evolution is not just about syntax, it represents a fundamental shift in how developers can approach coding on IBM i, unlocking new levels of productivity, maintainability, and integration capabilities. By removing the burden of column positioning and enabling clear declarations (ctl-opt, dcl-s, dcl-ds, dcl-pr), natural indentation, and longer meaningful variable names, it allows developers to focus on business logic rather than formatting rules. This leads to cleaner code that is easier to understand and maintain.

Better readability

Readable code is cheaper to maintain. Most of the life of an IBM i application developer is spent enhancing, debugging, and maintaining existing code rather than writing new code. Fully free format RPG improves readability in several ways:

  • It eliminates position-based syntax and most column-related mistakes.
  • It allows for meaningful variable names such as OrderTotal or TransactionStatus, making the code self-explanatory.
  • It supports natural indentation which enhances the visual structure of the code and makes it easier to follow the flow of logic.
  • It enables the use of modern constructs like if/else, for loops, and procedure calls that are familiar to developers from other languages.

Consider the difference:

Fixed Format:

C FLD1 IFEQ 'VALUE' C EXSR SUBR1 C ELSE C EXSR SUBR2 C ENDIF

Fully Free Format:

if FLD1 = 'VALUE'; exsr SUBR1; else; exsr SUBR2; endif;

The free format version is immediately recognizable to developers familiar with modern programming languages.

Easier Maintenance and lower risk

Fully free format does not change the business rules in a program, but it makes them easier to understand and modify. When code is clearer, developers can make changes with more confidence. Reviews are faster, debugging is simpler and the chance of introducing new bugs is reduced. This leads to lower maintenance costs and faster turnaround times for enhancements and fixes. This is very important for IBM i applications, which often have long lifecycles and are maintained by multiple developers over time.

Stronger fit with modern development practices

Modern RPG encompasses fully free format syntax combined with advanced language features that align with contemporary programming practices. Key characteristics include:

  • ILE modularity: Service programs, exported procedures, and subprocedures are easier to define and organize.
  • Built-in Functions: Operations such as %trim, %date, %timestamp and %replace read more naturally in free format.
  • Qualified Data Structures: Modern declaration are clearer and less error-prone than older position-based definitions.
  • Integration Capabilities: Enables seamless integration of SQL, JSON, and web services in free form source.
  • IDE integration: Leverages modern compilation and binding techniques withing the ILE environment or IDEs like VS Code, which provide features such as content assist, syntax checking, and debugging for RPG development.

Easier onboarding for new developers

One of the biggest practical benefits of fully free format RPG is that it lowers the entry barrier for new developers. Programmers having knowledge of modern programming languages such as Java, C#, or Python will find the free format syntax more familiar and easier to learn. This can help organizations attract new talent to work on their IBM i applications and reduce the time required for training and onboarding. The modern syntax allows new developers to quickly become productive and contribute to the codebase without struggling with the quirks of fixed-format RPG.

Better tooling and source control integration

Fully free format RPG is better suited for modern development tools and source control systems. The clear syntax and structure of free format code make it easier to use with editors like VS Code and version control systems like Git, enabling better collaboration among developers. It also makes code reviews more efficient. Additionally, modern IDEs can provide enhanced features such as syntax highlighting, code completion, and refactoring tools that work more effectively with free format code. This leads to improved developer productivity and a more efficient development workflow.

Practical modernization path

Another very good reason to use fully free form is that adoption can be gradual. Developers do not need to rewrite their entire fixed-format codebase at once. New development can be done in free format while existing fixed-format code continues to work without modification. Over time, as developers become more comfortable with the new syntax, they can choose to convert existing code to free format using available conversion tools or as part of regular maintenance activities. This allows organizations to modernize their applications incrementally without disrupting ongoing development or maintenance efforts.

IBM continues to enhance fully free format RPG with new features and capabilities. By adopting this format, developers ensure their code remains current and can take advantage of future language improvements. Fixed-format RPG, while still supported, is unlikely to receive major new features.

Conclusion

Fully free format RPG is worth using because it improves readability, lowers maintenance risk, aligns with modern development practices, allows easier knowledge transfer and makes it easier to onboard new developers. It helps developers spend less time wrestling with formatting rules and more time focusing on business logic. The shift to fully free format is not just about syntax, it's about embracing modern development practices that will serve your organization for years to come. Not only shoud free format there are great options for moving from fixed to free tools like IBM Bob can esily help you with this trasistion. The best part, the core function of the code does not change, just the readability to the developer.

How do I write fully free format RPG?

What This Covers

This procedure covers how to write RPGLE programs in fully free format — no fixed columns, no /FREE and /END-FREE delimiters, no H-specs. Just code.

What It Is

IBM added fully free format RPG to the platform years ago, and on 7.5 and 7.6 it's the default way to write new programs. Before fully free, "free format" still required the /FREE and /END-FREE delimiters inside a source member that was fundamentally column-based. Fully free format drops all of that — your code starts in column 1, there are no positional rules, and the compiler control options move into CTL-OPT.

Three eras of RPG source layout:

EraColumn RulesSpec Letters (H, F, D, C, P)Delimiter RequiredFixed formatColumns 6–80 matter. Factor 1, opcode, factor 2 in specific positionsYes — every line starts with a spec type in column 6NoMixed free formatFree-form C-specs between /FREE and /END-FREEH, F, D specs still positional; only C-specs go freeYes — /FREE and /END-FREEFully free formatNo column restrictions. Code starts in column 1No — replaced by CTL-OPT, DCL-F, DCL-S, DCL-DS, DCL-PR, DCL-PI, DCL-PROCNo

The key difference: in fully free format, the source member type is still RPGLE, but the first line must start in column 1 (not column 6). If the compiler sees anything in columns 1–5 on the first line, it knows it's fully free. If column 6 has a spec letter, it falls back to fixed/mixed parsing.

One gotcha that trips people up: you can't mix fully free and fixed format in the same source member. It's one or the other. You can /COPY or /INCLUDE fixed-format copy members into a fully free source — the compiler handles the switch per include — but the main source itself must be consistent.

Why It Matters

Fixed format RPG is the #1 reason new developers bounce off the platform. Show a Java or Python developer a fixed-format RPG source member and they'll assume the language hasn't changed since 1988. Show them fully free format and they'll see something that looks like a modern procedural language — because it is.

There's a practical side too. Fixed format means you're debugging column alignment problems instead of business logic. A misplaced character in column 7 vs. column 6 turns a valid statement into a compiler error that says nothing useful. Fully free format eliminates that entire category of mistakes.

And if your shop uses Git (you should), fixed-format diffs are painful. Every line looks like it changed because someone's editor reformatted the columns. Fully free format diffs show actual code changes.

How It Works

We'll convert a small fixed-format RPG program to fully free format, piece by piece.

The fixed-format original — a program that reads a customer record by ID and returns the name:

H DFTACTGRP(*NO) ACTGRP(*CALLER) FCUSTMAST IF E K DISK D GetCustName PR EXTPGM('GETCUSTNM') D custId 10A CONST D custName 50A D GetCustName PI D custId 10A CONST D custName 50A C custId CHAIN CUSTMAST C IF %FOUND(CUSTMAST) C EVAL custName = CMNAME C ELSE C EVAL custName = *BLANKS C ENDIF C RETURN

A. Replace H-spec with CTL-OPT

Fixed format:

H DFTACTGRP(*NO) ACTGRP(*CALLER)

Fully free:

**FREE CTL-OPT DFTACTGRP(*NO) ACTGRP(*CALLER);

**FREE must be the very first line — column 1, nothing before it. CTL-OPT replaces the H-spec. Every fully free statement ends with a semicolon — no exceptions. You'll forget the semicolon on CTL-OPT at least once. The compiler error for it is unhelpful.

ACTGRP(*CALLER) means this program runs inside the caller's activation group — correct for called programs. For service programs and main-procedure programs, use a named activation group instead (ACTGRP('MYAPP')). Getting this wrong means your *INLR logic and file closes don't behave the way you expect.

B. Replace F-spec with DCL-F

Fixed format:

FCUSTMAST IF E K DISK

Fully free:

DCL-F CUSTMAST KEYED USAGE(*INPUT);

The column-encoded attributes (IF, E, K, DISK) become named keywords. KEYED replaces the K in column 35. USAGE(*INPUT) replaces I in column 17. DISK is the default device type — you can drop it. The E (externally described) is assumed for physical and logical files.

C. Replace D-specs with DCL-PR, DCL-PI, DCL-S, DCL-DS

Fixed format:

D GetCustName PR EXTPGM('GETCUSTNM') D custId 10A CONST D custName 50A D GetCustName PI D custId 10A CONST D custName 50A

Fully free:

DCL-PR GetCustName EXTPGM('GETCUSTNM'); custId CHAR(10) CONST; custName CHAR(50); END-PR; DCL-PI GetCustName; custId CHAR(10) CONST; custName CHAR(50); END-PI;

Data types change from positional notation to keyword notation. 10A becomes CHAR(10). 7P 2 becomes PACKED(7:2). 10S 0 becomes ZONED(10).

EXTPGM is fine for calling existing programs, but for new development prefer EXTPROC with a service program — it avoids the activation group boundary crossing, eliminates the inter-program call overhead, and keeps related procedures packaged together in a *SRVPGM object.

Quick reference for the common type conversions:

Fixed FormatFully FreeNotes10ACHAR(10)50A VARYINGVARCHAR(50)7P 2PACKED(7:2)10S 0ZONED(10)10I 0INT(10)20U 0UNS(20)8S 0 (date)DATEUse actual date type instead of zonedNINDIndicator*POINTER

D. Replace C-specs with free-form operations

Fixed format:

C custId CHAIN CUSTMAST C IF %FOUND(CUSTMAST) C EVAL custName = CMNAME C ELSE C EVAL custName = *BLANKS C ENDIF C RETURN

Fully free:

CHAIN custId CUSTMAST; IF %FOUND(CUSTMAST); custName = CMNAME; ELSE; custName = *BLANKS; ENDIF; RETURN;

Drop EVAL — it's implicit for assignments. IF, ELSE, ENDIF all get semicolons. Indentation is yours to choose (the compiler doesn't care), but pick a style and stick with it.

CHAIN is the right choice for a single-row keyed fetch — it's direct, fast, and %FOUND gives you a clean true/false without parsing SQLCODE. Reserve SELECT INTO for cases where you need SQL features: joins, views, computed columns, or filtering beyond a simple key lookup.

E. The complete fully free program

**FREE CTL-OPT DFTACTGRP(*NO) ACTGRP(*CALLER); DCL-F CUSTMAST KEYED USAGE(*INPUT); DCL-PR GetCustName EXTPGM('GETCUSTNM'); custId CHAR(10) CONST; custName CHAR(50); END-PR; DCL-PI GetCustName; custId CHAR(10) CONST; custName CHAR(50); END-PI; CHAIN custId CUSTMAST; IF %FOUND(CUSTMAST); custName = CMNAME; ELSE; custName = *BLANKS; ENDIF; RETURN;

**FREE on line 1 is optional on 7.5 and 7.6 — the compiler detects fully free automatically if column 1–5 has content.

F. Using embedded SQL in fully free format

If your program uses embedded SQL, the Exec SQL block works the same in fully free:

**FREE CTL-OPT DFTACTGRP(*NO) ACTGRP(*CALLER); DCL-PI GetCustName; custId CHAR(10) CONST; custName VARCHAR(50); END-PI; Exec SQL SELECT CMNAME INTO :custName FROM CUSTDB.CUSTMAST WHERE CMCUSTID = :custId; IF SQLCODE = 0; // custName is populated — return to caller ELSE; custName = ''; ENDIF; RETURN;

No change in SQL syntax — just the RPG wrapper around it is free format. The Exec SQL statement doesn't need a semicolon after SQL, but the SQL statement itself ends with a semicolon.

If your shop has moved source out of QRPGLESRC members and into IFS stream files, compile with CRTBNDRPG SRCSTMF('/home/dev/src/GETCUSTNM.rpgle'). VS Code with the Code for IBM i extension edits IFS source directly and submits the compile — no more green-screen SEU or RDi required. That's the target state for a modernized development workflow.

Key Takeaways

  • Fully free format is the standard for new RPG development — every new program should use it
  • You can't mix fixed and fully free in the same source member — pick one; /COPY members can be a different format
  • **FREE on line 1 is optional on 7.5 and 7.6 — include it anyway for clarity and intent
  • Drop EVAL — assignments are just variable = value;
  • Every statement ends with a semicolon — including CTL-OPT, IF, ELSE, ENDIF, RETURN
  • Start new programs in fully free — converting existing programs is a nice-to-have, not a priority; don't rewrite working code just for the format
  • RDi and VS Code (with Code for IBM i) both support fully free — syntax highlighting, content assist, and error checking all work

How do I convert to free format?

Converting fixed-format RPG IV source members to fully free-format RPG removes the decades-old column constraints, produces code that reads more naturally, reduces visual noise, and aligns programs with current IBM i language standards. Starting with IBM i 7.2, fully free-format RPG allows every line—declarations, control options, calculations, and comments—to begin in column 1 and continue without positional restrictions. This style uses clear keywords such as ctl-opt, dcl-f, dcl-s, dcl-ds, dcl-pr, and dcl-pi, along with straightforward assignment and expression syntax. The conversion preserves program behavior while making the source significantly easier to maintain, review, and extend.

For IBM i shops running 7.2 or later, this change delivers immediate practical value. Developers scan logic faster, spot errors more readily, and spend less time fighting syntax quirks. New team members adapt quickly, especially those familiar with contemporary languages. The codebase also becomes fully prepared for modern features such as DATA-INTO, %JSON functions, advanced array handling, and cleaner embedded SQL integration. Many shops find that converting to fully free-format RPG yields one of the highest returns on modernization effort because the change applies broadly with low risk when approached systematically.

The following topics are described in this section:

  • Preparing the source member
  • Performing automated conversion
  • Manual cleanup and modern refactoring
  • Dealing with I- and O-specifications
  • Testing and verification
  • Reaching a high-quality modern state with IBM Bob assistance

Preparing the source member

Confirm the member uses type RPGLE (or SQLRPGLE when embedded SQL is present). If the source still contains any fixed-format RPG IV elements, make a working copy—typically by appending _FREE to the member name—or rely on source control to preserve the original.

No special preparation beyond a reliable backup is required for IBM i 7.2 and later releases, as fully free-format RPG is natively supported without additional PTFs or directives in these versions. The ** FREE directive (case-insensitive) placed in column 1 signals the compiler to use fully free-format mode and is the recommended way to begin modern sources.

Performing automated conversion

Several tools automate the bulk transformation from fixed-format RPG IV to fully free format.

In VS Code with the Code for IBM i extension and appropriate RPG language support, open the member, select the relevant lines or the entire file, and invoke the free-format conversion command (via context menu or command palette). These tools rewrite calculation specifications as natural expressions, convert legacy opcodes to modern equivalents (ADD to +=, compatible MOVE to direct assignment), and restructure conditional and looping logic.

For conversions across large applications or entire libraries, third-party IBM reseller tools such as ARCAD Transformer RPG or Fortra RPG Modernization Suite provide batch processing with consistent, configurable results. These often allow rules for qualifying data structures, renaming indicators, and applying naming standards during the initial pass.

The latest option for converting code to modern free format is IBM Bob, an AI based Vscode centered developmemt partner. Bob does a great job of updating the source from old hard to read Fixed format to a modern RPG based view. Bob can also help with converting iSpec and oSpec to the proper new equilivants.

Automated conversion usually produces compilable code, though manual review remains essential to achieve current best practices.

Manual cleanup and modern refactoring

Start the source with the ** FREE directive in column 1. In IBM i 7.2 and later, this single line enables fully free-format mode across the entire member—no /FREE or /END-FREE directives are needed or used in modern code.

Rewrite declarations using contemporary keywords:

  • Control specifications become ctl-opt lines.
  • File declarations become dcl-f.
  • Standalone fields and constants become dcl-s or dcl-c.
  • Data structures become dcl-ds.
  • Prototypes and procedure interfaces become dcl-pr and dcl-pi.

Example — legacy standalone field:

D ItemPrice S 9P 2

Modern equivalent:

dcl-s ItemPrice packed(9:2);

Example — legacy qualified data structure:

D SalesHdr DS QUALIFIED D OrderNbr 8P 0 D CustNbr 6P 0 D ShipDate D

Modern equivalent:

dcl-ds SalesHdr qualified; OrderNbr packed(8:0); CustNbr packed(6:0); ShipDate date; end-ds;

Refactor calculations to expressive statements. Chain operations where logical, use built-in functions such as %trim, %upper, %char, and %editc, and replace indicator-driven logic with structured IF, SELECT/WHEN, DOW, and DOU blocks.

Replace numbered indicators with named boolean fields:

dcl-s ValidOrder ind; dcl-s InputError ind;

Dealing with I- and O-specifications

Input specifications (I-specs) and output specifications (O-specs) remain fixed-format; fully free RPG does not provide free-form alternatives. Retain them where necessary, or refactor the program to reduce or eliminate dependence on them.

Shift program-described files to externally described files (DDS or SQL DDL) whenever possible. For output processing, migrate to embedded SQL result sets, DATA-GEN for JSON or CSV, or Open Access handlers for modern presentation layers. Encapsulate any remaining I/O specifications inside small procedures to limit their scope.

Testing and verification

Compile the converted member with CRTBNDRPG or CRTRPGMOD and bind it into the same program or service program structure as before. Execute thorough testing: unit tests for procedures, integration tests for modules, and full regression tests for the application.

Validate key results—database updates, spool files, display file behavior—against the original version. Use iTrace to confirm execution coverage remains consistent. Optionally compare cyclomatic complexity metrics before and after (see What is cyclomatic complexity?); the improved structure of free-format code often lowers perceived complexity.

Reaching a high-quality modern state with IBM Bob assistance

IBM Bob, the AI-assisted development tool integrated into VS Code for IBM i, significantly accelerates conversion and improves final quality. Bob can:

  • Convert highlighted fixed-format code to fully free-format syntax on request.
  • Suggest modern equivalents for legacy operations (for example, replacing MOVE with edit words using %editc or %char).
  • Recommend qualified data structures and named indicators to eliminate numbered indicators.
  • Identify subroutines suitable for conversion to subprocedures and generate the necessary skeletons.
  • Apply consistent formatting, logical blank-line separation, and descriptive naming conventions.
  • Flag potential issues such as type mismatches introduced during opcode replacement.

Developers typically run automated conversion first, then use Bob iteratively to refine declarations, expressions, and structure—often producing cleaner, more maintainable code in substantially less time.

After refinement, adopt these final practices:

  • Select clear, meaningful names for variables and procedures.
  • Qualify data structures to prevent field name collisions.
  • Replace subroutines with subprocedures.
  • Eliminate obsolete opcodes and traditional indicators.
  • Use consistent indentation and blank lines to separate logical blocks.

Converting to fully free-format RPG in IBM i 7.2 and later represents a foundational modernization step. The daily improvements in readability, maintainability, and developer productivity make the effort worthwhile for any shop maintaining RPG applications.

How do I use embedded SQL in RPG programs?

What This Covers

This procedure covers embedded SQL in RPGLE — host variables, cursors, error handling, dynamic SQL with parameter markers, NULL indicators, and transaction control. If you're writing new RPG and hitting the database, embedded SQL is how you do it.

What It Is

Embedded SQL lets you write SQL statements directly inside your RPGLE source. The SQL precompiler extracts them, builds access plans, and wires the results into RPG host variables. No CALL to a stored procedure, no API — the SQL is inline with your program logic.

Two compilation paths:

CommandWhat It DoesWhen to UseCRTBNDRPGCompiles RPG with embedded SQL, creates a bound programStandard for programs that contain SQLCRTSQLRPGISQL precompile + RPG compile in one stepLegacy path — still works, but CRTBNDRPG is the modern default

On 7.5 and 7.6, CRTBNDRPG handles the SQL precompile automatically. Use it.

Why It Matters

Native RPG file I/O (CHAIN, READ, SETLL) works fine for keyed single-row access against a physical file. But the moment you need a join, a computed column, an aggregate, or filtering beyond a simple key — you're fighting the language instead of using it.

Embedded SQL gives you the full power of Db2 for i inside your RPG program. You get:

  • Joins across multiple files in a single statement
  • GROUP BY, HAVING, ORDER BY without building your own sort
  • Scalar and aggregate functions (COALESCE, TRIM, SUM, COUNT)
  • Access to SQL views, CTEs, and table functions
  • Consistent NULL handling instead of RPG's "blank means null... maybe" convention

The performance story is straightforward: the SQL optimizer builds access plans at precompile time and caches them. For complex access patterns, SQL will outperform chained READ loops with runtime filtering.

How It Works

All examples use this customer master table:

-- DDL for CUSTMAST CREATE OR REPLACE TABLE CUSTDB.CUSTMAST ( CMCUSTID CHAR(10) NOT NULL, CMNAME VARCHAR(50) NOT NULL, CMCITY VARCHAR(30) DEFAULT 'UNKNOWN', CMSTATE CHAR(2) DEFAULT 'XX', CMBALANCE DECIMAL(11,2) DEFAULT 0, CMSTATUS CHAR(1) DEFAULT 'A', CMLASTORD DATE DEFAULT CURRENT_DATE, CONSTRAINT CUSTMAST_PK PRIMARY KEY (CMCUSTID) ); CREATE INDEX CUSTDB.CUSTMAST_X1 ON CUSTDB.CUSTMAST (CMSTATE, CMSTATUS);

A. Host variables — moving data between SQL and RPG

Host variables are RPG variables prefixed with : inside SQL statements. They're how data crosses the boundary.

**FREE CTL-OPT DFTACTGRP(*NO) // Must be *NO — DFTACTGRP(*YES) doesn't support SQL ACTGRP(*CALLER); // Run in caller's activation group DCL-PI GetCustName; pCustId CHAR(10) CONST; // :pCustId becomes the WHERE = value pCustName VARCHAR(50); // :pCustName receives the SELECT result END-PI; // SELECT INTO — single row only. If the query returns more than one row, you get a negative SQLCODE Exec SQL SELECT CMNAME INTO :pCustName FROM CUSTDB.CUSTMAST WHERE CMCUSTID = :pCustId; // :pCustId passed as a parameter — no concatenation IF SQLCODE = 0; // Row found — pCustName is populated with CMNAME ELSEIF SQLCODE = 100; pCustName = ''; // No row matched — not an error, just not found ELSE; pCustName = '*ERROR'; // Negative SQLCODE — authority, table not found, truncation, etc. ENDIF; RETURN;

Rules for host variables:

  • Prefix with : inside SQL, use bare name in RPG
  • RPG data types map automatically — CHAR(10) to CHAR(10), PACKED(11:2) to DECIMAL(11,2)
  • VARCHAR RPG variables map to SQL VARCHAR — length and data travel together
  • SELECT INTO is for single-row results only. If the query might return multiple rows, use a cursor (section C)

B. SQLCODE and SQLSTATE — knowing what happened

Every SQL statement sets two status fields. Check them.

FieldTypeKey ValuesSQLCODEInteger0 = success, 100 = not found / end of data, negative = errorSQLSTATECHAR(5)'00000' = success, '02000' = not found, anything starting with '01' = warning

SQLCODE is faster to check for the common cases. SQLSTATE is portable across databases and more precise for error diagnosis.

DCL-S sqlMsg VARCHAR(100); Exec SQL SELECT CMNAME INTO :pCustName FROM CUSTDB.CUSTMAST WHERE CMCUSTID = :pCustId; // SQLCODE and SQLSTATE are implicitly declared — the precompiler injects them SELECT; WHEN SQLCODE = 0; // Success — carry on WHEN SQLCODE = 100; // Row not found — SQLSTATE '02000'. Normal condition, not an error sqlMsg = 'Customer ' + %TRIM(pCustId) + ' not found'; WHEN SQLCODE < 0; // Real error — SQLSTATE gives you the specific cause sqlMsg = 'SQL error: SQLSTATE=' + SQLSTATE; // Log it, don't just ignore it — swallowing negative SQLCODEs is how silent corruption happens OTHER; // SQLCODE > 0 but not 100 — a positive warning. Rare but worth knowing about sqlMsg = 'Unexpected SQLCODE: ' + %CHAR(SQLCODE); ENDSL;

A common mistake: checking only SQLCODE = 0 and treating everything else as "not found." Negative SQLCODEs are real errors — authority failures, table not found, data truncation. Don't swallow them.

C. Cursors — processing multiple rows

When a query returns more than one row, you need a cursor. Declare it, open it, fetch in a loop, close it.

**FREE CTL-OPT DFTACTGRP(*NO) ACTGRP(*CALLER); DCL-S wCustId CHAR(10); DCL-S wName VARCHAR(50); DCL-S wBalance PACKED(11:2); DCL-S wCount INT(10) INZ(0); // DECLARE is processed at precompile time — it's a definition, not an executable statement. // The cursor name (C_ACTIVECUST) must be unique within the program. Exec SQL DECLARE C_ACTIVECUST CURSOR FOR SELECT CMCUSTID, CMNAME, CMBALANCE FROM CUSTDB.CUSTMAST WHERE CMSTATUS = 'A' AND CMBALANCE > 0 ORDER BY CMBALANCE DESC; // Highest balance first // OPEN executes the query and builds the result set Exec SQL OPEN C_ACTIVECUST; // DOW 1=1 is intentional — SQLCODE isn't set until the first FETCH, // so testing SQLCODE in the DOW condition requires a priming fetch. Don't do that. DOW 1 = 1; Exec SQL FETCH C_ACTIVECUST INTO :wCustId, :wName, :wBalance; IF SQLCODE = 100; // End of result set — normal exit LEAVE; ENDIF; IF SQLCODE < 0; // Error mid-cursor — log before leaving // Something broke — log and bail LEAVE; ENDIF; wCount += 1; // Row is ready — wCustId, wName, wBalance are populated // Process the row — write to a report, call a subprocedure, whatever ENDDO; // Always close the cursor — releases server-side resources and locks Exec SQL CLOSE C_ACTIVECUST; // wCount now has the number of rows processed RETURN;

The DOW 1 = 1 / LEAVE pattern is cleaner than DOW SQLCODE = 0 because SQLCODE isn't set until after the first FETCH. You'd need a priming fetch otherwise, which means duplicating the FETCH statement. Don't.

Scrollable cursors — if you need to move backward or jump to a specific row:

// SCROLL keyword enables bidirectional navigation — costs more because // Db2 for i materializes the full result set in a temp spool Exec SQL DECLARE C_SCROLL SCROLL CURSOR FOR SELECT CMCUSTID, CMNAME FROM CUSTDB.CUSTMAST WHERE CMSTATE = 'AZ' ORDER BY CMNAME; Exec SQL OPEN C_SCROLL; // ABSOLUTE n — jump directly to row n (1-based). SQLCODE 100 if n > row count Exec SQL FETCH ABSOLUTE 10 FROM C_SCROLL INTO :wCustId, :wName; // PRIOR — step backward one row. SQLCODE 100 if already at row 1 Exec SQL FETCH PRIOR FROM C_SCROLL INTO :wCustId, :wName; Exec SQL CLOSE C_SCROLL;

Scrollable cursors cost more — the database builds a temp result set to support random access. Use them only when you genuinely need to navigate, not as a default.

D. INSERT, UPDATE, DELETE — modifying data

DML statements use host variables the same way:

// INSERT — check SQLCODE for failures: duplicate primary key, authority, referential constraint, etc. Exec SQL INSERT INTO CUSTDB.CUSTMAST (CMCUSTID, CMNAME, CMCITY, CMSTATE, CMBALANCE, CMSTATUS) VALUES (:wCustId, :wName, :wCity, :wState, 0, 'A'); IF SQLCODE < 0; // Insert failed — duplicate key? Authority failure? Check SQLSTATE for the cause ENDIF; // UPDATE — SQLCODE = 0 does NOT mean a row was changed. // It only means the statement executed without error. Exec SQL UPDATE CUSTDB.CUSTMAST SET CMBALANCE = CMBALANCE + :wPayment, CMLASTORD = CURRENT_DATE WHERE CMCUSTID = :wCustId; // SQLERRD(3) is the actual row count — the only reliable way to know if anything changed IF SQLERRD(3) = 0; // No rows matched — customer doesn't exist or WHERE filtered everything out ENDIF; // DELETE with a date range — CURRENT_DATE is a special Db2 for i register, no host var needed Exec SQL DELETE FROM CUSTDB.CUSTMAST WHERE CMSTATUS = 'I' AND CMBALANCE = 0 AND CMLASTORD < CURRENT_DATE - 2 YEARS; // 2 years back from today // SQLERRD(3) holds the delete count here too — log it if auditing is a concern

SQLERRD(3) is the row-affected count for INSERT, UPDATE, and DELETE. Always check it — a successful SQLCODE of 0 doesn't mean any rows were actually changed. An UPDATE that matches zero rows still returns SQLCODE 0.

E. NULL handling with indicator variables

NULLs in SQL don't map to any RPG value — there's no concept of "absent" in RPG. Indicator variables bridge the gap.

DCL-S wCity VARCHAR(30); DCL-S wCityInd INT(5); // Indicator: 0 = not null, -1 = null, >0 = truncated (VARCHAR overflow) // Note the syntax: :wCity :wCityInd — no comma between host var and indicator var // If you add a comma by mistake, the precompiler interprets them as two separate SELECT targets Exec SQL SELECT CMCITY INTO :wCity :wCityInd FROM CUSTDB.CUSTMAST WHERE CMCUSTID = :pCustId; IF wCityInd < 0; // CMCITY is NULL in the database — wCity is undefined at this point, don't use it wCity = '(not on file)'; ENDIF;

The indicator variable sits right after the host variable — no comma between them. That syntax trips people up the first time.

You can also use indicators on INSERT to set a column to NULL:

wCityInd = -1; // -1 tells Db2 for i to store NULL — the value of wCity itself is ignored // The indicator variable goes right after its host variable — same no-comma rule as SELECT Exec SQL INSERT INTO CUSTDB.CUSTMAST (CMCUSTID, CMNAME, CMCITY, CMSTATE, CMBALANCE, CMSTATUS) VALUES (:wCustId, :wName, :wCity :wCityInd, :wState, 0, 'A'); // CMCITY will be stored as NULL regardless of what's in wCity

Indicator arrays for data structures — when you're fetching into a DS with multiple fields, you can declare a single indicator array instead of one indicator per field:

DCL-DS custRec; custId CHAR(10); custName VARCHAR(50); city VARCHAR(30); state CHAR(2); balance PACKED(11:2); END-DS; // DIM must match the number of fields in the DS exactly — one indicator per field, in order DCL-S custInd INT(5) DIM(5); Exec SQL SELECT CMCUSTID, CMNAME, CMCITY, CMSTATE, CMBALANCE INTO :custRec :custInd // DS and indicator array — one INTO clause covers all 5 fields FROM CUSTDB.CUSTMAST WHERE CMCUSTID = :pCustId; // custInd(1)=custId, custInd(2)=custName, custInd(3)=city, custInd(4)=state, custInd(5)=balance IF custInd(3) < 0; // city is NULL city = '(not on file)'; ENDIF;

F. Dynamic SQL with parameter markers

When you don't know the SQL at compile time — building WHERE clauses based on user input, for instance — use dynamic SQL with parameter markers. Never concatenate user input into SQL strings.

DCL-S wStmt VARCHAR(500); DCL-S wState CHAR(2); DCL-S wMinBal PACKED(11:2); DCL-S wCustId CHAR(10); DCL-S wName VARCHAR(50); wState = 'AZ'; wMinBal = 1000.00; // Build the SQL string with ? placeholders — never inject wState or wMinBal directly into the string // e.g., 'WHERE CMSTATE = ''' + wState + '''' opens the door to SQL injection wStmt = 'SELECT CMCUSTID, CMNAME FROM CUSTDB.CUSTMAST ' + 'WHERE CMSTATE = ? AND CMBALANCE >= ? ' + 'ORDER BY CMNAME'; // PREPARE parses the SQL and builds an access plan — S1 is the statement name Exec SQL PREPARE S1 FROM :wStmt; // DECLARE references the prepared statement by name — still a definition, not execution Exec SQL DECLARE C_DYN CURSOR FOR S1; // OPEN executes the plan, binding ? markers to host variables in left-to-right order // wState fills the first ?, wMinBal fills the second ? Exec SQL OPEN C_DYN USING :wState, :wMinBal; DOW 1 = 1; Exec SQL FETCH C_DYN INTO :wCustId, :wName; IF SQLCODE <> 0; // 100 = EOF, negative = error — either way, stop fetching LEAVE; ENDIF; // wCustId and wName populated — process the row ENDDO; Exec SQL CLOSE C_DYN;

Parameter markers (?) are placeholders filled at OPEN time via the USING clause. The database treats them as parameters, not as SQL text — so there's no SQL injection risk, and the optimizer can reuse access plans across different parameter values.

EXECUTE IMMEDIATE — for DDL or one-shot statements where you don't need a result set:

DCL-S wStmt VARCHAR(200); // EXECUTE IMMEDIATE is for one-and-done statements — DDL, SET commands, CALL // It parses, executes, and discards in one step — no PREPARE, no cursor wStmt = 'CREATE INDEX CUSTDB.CUSTMAST_X2 ' + 'ON CUSTDB.CUSTMAST (CMLASTORD)'; Exec SQL EXECUTE IMMEDIATE :wStmt; IF SQLCODE < 0; // Object already exists? Not authorized? Check SQLSTATE for the specific cause ENDIF;

Don't use EXECUTE IMMEDIATE for SELECT or DML with host variables — that's what PREPARE + cursor is for.

G. Transaction control — COMMIT and ROLLBACK

By default, RPG programs run under COMMIT(*NONE) — every SQL statement auto-commits. For multi-step operations that must succeed or fail together, you need explicit commit control.

**FREE // Named activation group gives us a commit scope boundary. // ACTGRP(*CALLER) would share the caller's boundary — any ROLLBACK issued by the caller // would undo our work too. Use a named group to own your own transaction. CTL-OPT DFTACTGRP(*NO) ACTGRP('ORDERAPP') COMMIT(*CS); // Cursor stability: reads don't lock, writes lock until COMMIT DCL-PI ProcessPayment; pCustId CHAR(10) CONST; pAmount PACKED(11:2) CONST; pResult CHAR(1); // 'S' = success, 'F' = failure END-PI; // MONITOR catches RPG exceptions (MCH, CPF). ROLLBACK here prevents partial commits // if something non-SQL blows up mid-transaction. MONITOR; // Step 1: Debit the customer balance — the WHERE includes CMBALANCE >= :pAmount // so the UPDATE itself refuses to go negative. If it matches 0 rows, we roll back. Exec SQL UPDATE CUSTDB.CUSTMAST SET CMBALANCE = CMBALANCE - :pAmount, CMLASTORD = CURRENT_DATE WHERE CMCUSTID = :pCustId AND CMBALANCE >= :pAmount; // Guard against overdraft at the SQL level IF SQLERRD(3) = 0; // 0 rows updated: customer not found OR balance too low — either way, abort Exec SQL ROLLBACK; pResult = 'F'; RETURN; ENDIF; // Step 2: Record the payment in the ledger — if this fails, undo step 1 too Exec SQL INSERT INTO CUSTDB.PAYMENT_LOG (PLCUSTID, PLAMOUNT, PLDATE, PLTYPE) VALUES (:pCustId, :pAmount, CURRENT_DATE, 'PMT'); IF SQLCODE < 0; Exec SQL ROLLBACK; // Undo the balance update from step 1 pResult = 'F'; RETURN; ENDIF; // Both steps succeeded — make it permanent Exec SQL COMMIT; pResult = 'S'; ON-ERROR; // RPG exception (not SQL) — still need to clean up Exec SQL ROLLBACK; pResult = 'F'; ENDMON; RETURN;

Key points on commit control:

  • COMMIT(*CS) on CTL-OPT enables cursor stability — reads don't lock, updates lock the row until commit
  • COMMIT(*CHG) locks only changed rows — lighter, but doesn't prevent non-repeatable reads
  • COMMIT(*NONE) means no transaction boundaries at all — fine for read-only programs, dangerous for multi-step writes
  • The journal (JRN) must be configured on the library/files — COMMIT without journaling gives you a negative SQLCODE at runtime
  • Named activation groups (ACTGRP('ORDERAPP')) give you a commit scope boundary. ACTGRP(*CALLER) shares the caller's commit scope, which can cause surprises when a calling program issues ROLLBACK

Key Takeaways

  • Use embedded SQL for anything beyond simple keyed access — joins, aggregates, computed columns, filtered queries all belong in SQL, not chained READ loops
  • Always check SQLCODE after every Exec SQL — 0 is success, 100 is not found, negative is an error. Don't just check for "not zero"
  • SQLERRD(3) is your row count — for INSERT, UPDATE, DELETE, check it. SQLCODE 0 doesn't mean rows were affected
  • Use parameter markers for dynamic SQL — never concatenate user values into SQL strings. ? placeholders prevent injection and improve plan reuse
  • NULL needs indicator variables — RPG has no NULL concept. The indicator variable (placed after the host variable, no comma) is the bridge
  • COMMIT/ROLLBACK for multi-step writes — if two operations must succeed together, wrap them in a transaction. Set COMMIT(*CS) on CTL-OPT and configure journaling on the files
  • DOW 1 = 1 / LEAVE for cursor loops — avoids the priming-fetch problem and keeps the FETCH in one place
  • Compile with CRTBNDRPG — it handles the SQL precompile automatically on 7.5 and 7.6

How do I use modern RPG data structures and arrays

Modern RPG makes extensive use of data structures and arrays to organize and manipulate business data efficiently. This section shows you how to define qualified data structures, create arrays of those structures, and—most importantly—populate them directly from your IBM i database using only embedded SQL statements. You will also see the newest array capabilities introduced in IBM i 7.4 and still available in 7.6: variable-dimension arrays. These techniques eliminate tedious looping and manual field-by-field assignment, reduce the chance of errors, and produce code that is dramatically easier to read and maintain.

The following topics are described in this section:

  • Defining qualified data structures for business records
  • The two main types of variable-dimension arrays: *VAR and *AUTO
  • Creating and populating array data structures with SQL
  • Using EVAL-CORR to move data between differently named or typed structures
  • Sorting and searching arrays after population
  • Best practices and performance considerations

Why modern data structures and arrays matter

A data structure in modern RPG is far more than a collection of fields. When you qualify it and combine it with arrays, you create a self-documenting container that mirrors the shape of your database tables or result sets. This approach brings three immediate benefits to any IBM i shop:

  1. Readability — Long, descriptive subfield names replace cryptic positions or numbered fields.
  2. Performance — SQL can fetch hundreds or thousands of rows into an array in a single operation, replacing slow RPG loops.
  3. Maintainability — Changes to a table column automatically flow through the data structure when you use EXTNAME or LIKEDS.

As discussed in 7.15, “How to use embedded SQL,” modern RPG treats the database as a first-class citizen. Data structures and arrays are the natural bridge between that database and your business logic.

Defining qualified data structures

Start every modern data structure with the QUALIFIED keyword. This forces you to use the dot notation (Cust.CUSTID) and eliminates the risk of field-name collisions.

You can define a data structure manually or base it directly on a database table or view with EXTNAME. The EXTNAME approach is preferred when the structure should stay in sync with the physical file.

Example - Data structure based directly on the CUSTOMER table

Dcl-Ds Cust ExtName('REDBOOKCDE/CUSTOMER') Qualified End-Ds;

Any future change to the NAME column length is automatically reflected the next time you recompile.

The two types of variable-dimension arrays

IBM i 7.4 introduced variable-dimension arrays that remove the need to guess a safe fixed size. There are two flavors:

  • *DIM(VAR : maximum) — The actual number of elements used is controlled by your program (you set it explicitly with %ELEM). This is ideal when you know exactly how many rows you want to fetch or when you want full control over the active size.
  • *DIM(AUTO : maximum) — The array grows automatically as you assign elements beyond the current size, up to the declared maximum. This is perfect for situations where the number of items is truly unknown until run time, such as processing JSON input or variable query results.

Both types are fully compatible with SQL multi-row FETCH and with EVAL-CORR.

Here are the two styles shown side-by-side using order data from the provided tables:

// Example 7-21 SQL fetch into a *VAR array (fixed number of rows expected) Dcl-Ds OrderVarDS Qualified Dim(*VAR : 20); OrderNo Char(7); CustId Char(10); CustName Varchar(50); OrderTotal Char(20); // Different type for illustration End-Ds; // Example 7-22 Host processing array using *AUTO (grows as needed) Dcl-Ds OrdersAutoDS Qualified Dim(*AUTO : 500); OrderNo Char(7); CustId Char(10); CustName Varchar(50); OrderTotal Zoned(11:2); End-Ds;

Notice the intentional differences in subfield names and data types. These differences are common when one structure is optimized for SQL fetch and another for host-program processing.

Populating array data structures with SQL

The real power appears when you combine these arrays with embedded SQL. You can load the entire array in one statement—no DO loops, no manual indexing.

Single-row fetch into a data structure

// Example 7-23 Single-row fetch into a qualified data structure Dcl-S InputCustId Char(10) Inz('CUST000123'); Exec Sql SELECT CUSTID, NAME INTO :Cust FROM REDBOOKCDE.CUSTOMER WHERE CUSTID = :InputCustId WITH NC;

Multi-row fetch into a variable-dimension array

SQL can deliver rows directly into either *VAR or *AUTO arrays. Use GET DIAGNOSTICS to learn exactly how many rows were returned.

Moving data between structures with EVAL-CORR

When subfield names or types differ, EVAL-CORR is the modern way to copy matching fields. It automatically handles name-based matching and skips fields that do not exist in the target or have incompatible types.

Complete working example

The following complete program demonstrates all the concepts in a realistic business scenario. It fetches aggregated order information from the ORDERS, CUSTOMER, and ORDERDTL tables into a *AUTO array, moves data with EVAL-CORR, and then sorts and searches the results—all using modern RPG techniques.

**FREE // Example 7-24 Complete program to retrieve orders using varying-dimension arrays // Demonstrates DIM(*AUTO), multi-row FETCH, GET DIAGNOSTICS, EVAL-CORR, // and sorting/searching with %SORTA and %LOOKUP Ctl-Opt DftActGrp(*No) Option(*NoDebugIO:*SrcStmt:*NoUnRef); // ================================================================ // Data Structures - RPG data types // ================================================================ // SQL fetch into a Fixed Structure Dcl-Ds OrderFixedDS Qualified Dim(20); OrderNo Char(7); CustId Char(10); Name Varchar(50); OrderTotal Zoned(11:2); End-Ds; // SQL fetch into a variable structure Dcl-Ds OrderVarDS Qualified Dim(*VAR: 20); OrderNo Char(7); CustId Char(10); CustName Varchar(50); OrderTotal Char(20); // Different type for illustration End-Ds; // Host processing array - varying dimension (*AUTO) Dcl-Ds OrdersAutoDS Qualified Dim(*AUTO : 500); OrderNo Char(7); CustId Char(10); CustName Varchar(50); OrderTotal Zoned(11:2); End-Ds; Dcl-S NumOrders Int(10) Inz(0); Dcl-S ArrayIDX Int(10); Dcl-S RowsToFetch Int(10) Inz(20); // =============================================================== // Main Procedure // ================================================================ Exec Sql DECLARE C1 CURSOR FOR SELECT O.ORDERNO, O.CUSTID, C.NAME, Coalesce(Sum(D.QTY * D.PRICE - D.DISCAMT), 0) As ORDERTOTAL FROM REDBOOKCDE.ORDERS O JOIN REDBOOKCDE.CUSTOMER C On O.CUSTID = C.CUSTID LEFT JOIN REDBOOKCDE.ORDERDTL D On O.ORDERNO = D.ORDERNO GROUP BY O.ORDERNO, O.CUSTID, C.NAME, O.ORDERDATE ORDER BY O.CUSTID Desc FETCH FIRST 20 ROWS ONLY; Exec Sql Open C1; // Fetch 20 rows into the *AUTO array Exec Sql FETCH C1 FOR :RowsToFetch ROWS INTO :OrdersAutoDS; // Great way to check results of SQL Statements Exec Sql Get Diagnostics :NumOrders = ROW_COUNT; Exec Sql Close C1; // Moves all field data with the exception of the CustName to Name fields // (inconsistent naming between structures) Eval-Corr OrderFixedDS = OrdersAutoDS; // Must manually set the dimension for the *VAR array before EVAL-CORR %Elem(OrderVarDS) = NumOrders; // Moves all field data with the exception of OrderTotal (wrong data type) Eval-Corr OrderVarDS = OrdersAutoDS; // Sorting and searching an array of orders after the SQL fetch // Sort by customer ID descending, then order total descending // (only the active portion of the *AUTO array - shown with 4 rows for brevity) SortA(D) %SubArr(OrdersAutoDS : 1 : 4) %Fields(OrderNo : OrderTotal); // Find the first order for a specific customer ArrayIDX = %Lookup('CUST000003' : OrdersAutoDS(*).CustId : 1 : NumOrders); If ArrayIDX > 0; Dsply ('Found order ' + OrdersAutoDS(ArrayIDX).OrderNo + ' for customer ' + OrdersAutoDS(ArrayIDX).CustId); EndIf; *InLr = *On; Return;

After the multi-row FETCH, the *AUTO array OrdersAutoDS contains exactly the number of rows returned (up to the requested 20). GET DIAGNOSTICS captures that count in NumOrders.

EVAL-CORR then copies matching subfields:

  • From OrdersAutoDS to OrderFixedDS — the CustName field is skipped because the target uses Name.
  • From OrdersAutoDS to OrderVarDS — OrderTotal is skipped because the target field is defined as Char(20) instead of Zoned(11:2).

For the *VAR array, you must explicitly set %ELEM(OrderVarDS) before the EVAL-CORR; otherwise only the first element would be affected. The *AUTO array needs no such step because its active size is managed automatically by the FETCH.

The program then demonstrates sorting with %SORTA(D) (descending on the listed fields) limited to the active rows via %SUBARR, and searching with %LOOKUP on the CustId subfield. These operations show how modern arrays become powerful in-memory structures after the initial SQL population.

Sorting and searching arrays

Once your array data structure is populated, you frequently need to reorder or locate specific entries without additional database calls. Modern RPG provides efficient built-in functions that operate directly on arrays of data structures.

%SORTA sorts the entire array (or a portion of it) on one or more subfields. For multiple keys, use the %FIELDS built-in function and list the subfields in priority order. You can sort only the active portion of a variable-dimension array with %SUBARR. The (D) extender on %SORTA requests descending sequence.

%LOOKUP quickly finds an element by value and returns its index. The (*) notation on the search argument tells RPG to search the active elements of the array.

These operations complete the modern array pattern: fetch once with SQL, move or transform with EVAL-CORR, sort or search in memory, then process or pass the array to a subprocedure. The combination is far faster and cleaner than legacy RPG looping with READ and manual swaps.

Best practices and performance considerations

  • Always qualify data structures and arrays.
  • Use EXTNAME or LIKEDS whenever possible so the structure stays synchronized with the database.
  • Prefer *AUTO when the number of rows is truly variable; use *VAR when you want explicit control over the dimension.
  • Keep the maximum dimension generous but realistic.
  • When you only need a subset of columns, declare the data structure with exactly those columns rather than using SELECT *.
  • Test with realistic data volumes. A 20-row fetch is instantaneous; larger result sets may require commitment control or a larger SQL cursor hold setting.
  • Combine these techniques with the fully free-format coding style covered in How do I write fully free format RPG?. The result is code that looks more like modern business applications written in any other language.

Shops that adopt array data structures populated by SQL routinely report 30–70% fewer lines of code in their data-retrieval routines and dramatically lower maintenance costs. New developers who know Java, Python, or JavaScript immediately recognize the pattern: a list of objects fetched from the database. The same business rules can now be reused from green-screen programs, REST services, or mobile front ends without duplication.

Modern RPG data structures and arrays are not cosmetic improvements. They are the foundation that lets you write concise, high-performance code that is easy to test, easy to hand off, and ready for whatever interface technology comes next.

How do I process JSONs within RPGLE programs?

What This Covers

This procedure covers the options available on IBM i to process JSON payloads.

What It Is

REST APIs, message queues, webhook callbacks — most integrations your RPG programs touch today exchange data as JSON. Your programs need to parse, extract from, and generate JSON.

Three options exist (this procedure focuses on SQL JSON functions — see the references at the end of this section for the other two):

SQL JSON FunctionsDATA-INTO with JSON parsersYAJL - External JSON libraryBest forSet-based processing, direct Db2 for i insertsMapping JSON directly into RPG data structure hierarchiesStreaming large docs, highly dynamic schemasExternal dependencyNoneYes - needs a parser program (e.g. YAJLINTO, QOAR/JSON_PARSE)Yes - YAJLLearning CurveLow - SQL devs pick it up quicklyMedium - need to understand DS mappingMedium - new API to learnPerformanceHigh — Db2 for i engine handles parsing natively, no RPG overheadMedium - Depends on the complexity of the JSON and the efficiency of the parserVariable - fast C parser, but RPG <-> C call overhead adds up on many small callsFlexibilityMedium - handles fixed/known JSON schemas well; struggles with highly variable or deeply nested structuresHigh - Can handle complex JSON structures with custom parsing logicHigh - Can handle any JSON structure as long as the library supports itUse CasesIngesting API payloads directly into Db2 for i tablesPopulating RPG data structures from JSON for in-program processing (e.g. config files, lookup data)Streaming or incrementally parsing very large JSON docs, or when you need to build JSON dynamically at runtimeStandardsISO SQL standard - syntax transfers to PostgreSQL, Oracle, SQL Server, etc.IBM i RPG-specific op-codePlatform-specific C library wrapperSupportYesNo - leverages an external non-support componentNo, Open Source

For more info on the other approaches, check out the following references:

Why It Matters

IBMI-INTF-PROCEDURE-097-001
IBMI-INTF-PROCEDURE-097-001

If your RPG programs can't consume or produce JSON, you'll need middleware just to translate data formats - adding latency, another moving part, and a dependency on a team that may not understand your business logic.

Native JSON handling on IBM i eliminates that translation layer. Your RPG program receives a JSON order, shreds it into Db2 for i rows, processes the business rules, and returns a JSON response - all without leaving the platform. That's fewer hops, fewer failure points, and business logic that stays where your team knows it best.

There's a portability angle, too. These SQL/JSON functions are part of the ISO SQL standard, so the skills transfer across PostgreSQL, Oracle, and SQL Server - unlike DATA-INTO and YAJL, which are IBM i-specific.

How It Works

Use Case:

  • An eCommerce application exchanges Orders with IBM i — all requests and responses are JSON (API payloads or event stream messages)
  • We will demonstrate how to:
    • A. Extract a single value with JSON_VALUE
    • B. Parse a JSON payload into rows with JSON_TABLE
    • C. Transform JSON arrays into relational tables
    • D. Return JSON responses
    • E. Read JSON from IFS files

Sample JSON payload for an Order

{ "orderNumber": "W7484738837", "customer": { "name": "John Doe", "email": "jdoe@aol.com", "phone": "480-746-7342" }, "products": [ { "productId": "P1001", "quantity": 2, "price": 19.45 }, { "productId": "P1022", "quantity": 1, "price": 24.50 } ], "orderTotal": 63.40 }

A. Extract Customer Email for validation purposes using JSON_VALUE

Dcl-Pr getCustomerEmail EXTPGM('GETCUSTEMAIL'); jsonData CLOB(500K) CCSID 1208 Const; email Varchar(256); End-Pr; Dcl-Pi getCustomerEmail ; jsonData CLOB(500K) CCSID 1208 Const; email Varchar(256); End-Pi; // Extract the customer's email from the incoming JSON payload Exec SQL VALUES JSON_VALUE(:jsonData, '$.customer.email' Returning Varchar(256) ) Into :email; If Sqlcode = 0; // Proceed with email validation logic Else; // Handle error - invalid JSON or missing email Endif; Return;

B. Parse the incoming JSON Payload and insert into Order Header and Order Detail tables

RPGLE Program to process the Order

Dcl-Pr processOrder EXTPGM('PROCESSORDER'); jsonData CLOB(500K) CCSID 1208 Const; End-Pr; Dcl-Pi processOrder ; jsonData CLOB(500K) CCSID 1208 Const; End-Pi; Dcl-DS orderData Qualified; orderNumber Varchar(20); customerName Varchar(100); customerEmail Varchar(256); customerPhone Varchar(20); orderTotal Packed(10:2); End-DS; Dcl-DS productData Qualified; productId Varchar(20); quantity Int(10); price Packed(10:2); End-DS; // Parse the JSON payload Exec SQL Declare ordersCursor Cursor For Select coalesce(orderNumber, '') as orderNumber, coalesce(customerName, '') as customerName, coalesce(customerEmail, '') as customerEmail, coalesce(customerPhone, '') as customerPhone, coalesce(orderTotal, 0) as orderTotal, coalesce(productId, '') as productId, coalesce(quantity, 0) as quantity, coalesce(price,0) as price From ( Select x.* from JSON_TABLE(:jsonData, '$' COLUMNS ( orderNumber Varchar(20) PATH '$.orderNumber', customerName Varchar(100) PATH '$.customer.name', customerEmail Varchar(256) PATH '$.customer.email', customerPhone Varchar(20) PATH '$.customer.phone', orderTotal Dec(10,2) PATH '$.orderTotal', NESTED '$.products[*]' COLUMNS ( productId Varchar(20) PATH '$.productId', quantity Int PATH '$.quantity', price Dec(10,2) PATH '$.price' ) )) as x ) as y; Exec SQL Open ordersCursor; DoU Sqlcode = 100; Exec SQL Fetch Next From ordersCursor Into :orderData.orderNumber, :orderData.customerName, :orderData.customerEmail, :orderData.customerPhone, :orderData.orderTotal, :productData.productId, :productData.quantity, :productData.price; If Sqlcode = 0; // Insert into Order Header and Order lines tables Endif; Enddo; Exec SQL Close ordersCursor;

Example B gives you row-by-row control when you need validation or transformation logic in RPG before inserting. If you just need a straight insert with no intermediate processing, Example C is simpler.

C. Extract the Product details and save in Order Details table using the JSON_TABLE function

// Transform the JSON array of products into rows in a relational table Exec SQL Insert Into OrderDetails (orderId, productId, quantity, price) Select orderNumber, productId, productQty, productPrice From JSON_TABLE(:jsonData, '$' COLUMNS ( orderNumber Varchar(20) PATH '$.orderNumber', NESTED '$.products[*]' COLUMNS ( productId Varchar(20) PATH '$.productId', productQty Int PATH '$.quantity', productPrice Dec(10,2) PATH '$.price' ) )) as products;

D. Return JSON responses from RPGLE programs

Dcl-C cComplete 'C'; // For an OrderID received as input, return the Product details as a JSON response Exec SQL Declare orderResponseJSON Cursor For Select JSON_ARRAYAGG( JSON_OBJECT( 'productId' : productId, 'quantity' : productQty, 'price' : productPrice ) ) as data From OrderDetails Where orderId = :orderId and orderStatus = :cComplete; Exec SQL Open orderResponseJSON; If Sqlcode <> 0; // Handle error EndIf; Exec SQL Set result sets Cursor orderResponseJSON;

Note: This RPG program would typically be wrapped in a stored procedure — the standard way to expose RPG business logic to external callers over SQL, HTTP, or ODBC/JDBC. The stored procedure provides a clean call interface, handles authorization, and supports returning result sets (for example, JSON responses) directly to the caller (in this case the eCommerce application).

E. Read JSON from IFS files

// Read a JSON document from an IFS file and parse it Dcl-S jsonData CLOB(500K) CCSID 1208; Dcl-S filePath Varchar(256) Value('/home/api/in/W7484738837.json'); // Read the JSON document from the IFS file into a CLOB variable Exec SQL Select LISTAGG(LINE, '') Into :jsonData From Table(QSYS2.IFS_READ_UTF8( PATH_NAME => :filePath )); // Now you can parse the jsonData variable using the techniques shown in the previous examples

The job profile needs *R on the file and *RX on every parent directory — missing authority surfaces as SQL7064.

Before passing jsonData to any of the JSON functions in the earlier examples, validate that it's well-formed:

// JSON_VALUE returns NULL and sets SQLCODE = 0 on a valid-but-missing path. // It sets SQLCODE = -171 when the first argument is not valid JSON at all. // A quick probe before the real parse catches malformed input early. Dcl-S isValid Varchar(5); Exec SQL VALUES JSON_VALUE(:jsonData, '$' Returning Varchar(5)) Into :isValid; If Sqlcode = -171; // SQL0171 - jsonData is not valid JSON // Handle error - log, signal caller, return early Endif;

This is worth adding to any RPG program that accepts JSON from an external caller. Malformed JSON passed straight to JSON_TABLE raises SQL0104 with a message that points at the SQL statement, not the bad input — which makes it hard to diagnose in a production job log.

Key Takeaways

  • Pick the right tool for the shape of your work — SQL JSON functions for Db2 for i-bound data, DATA-INTO for RPG variable processing
  • Always specify CCSID 1208 on CLOB parameters receiving external JSON
  • JSON_TABLE replaces pages of parsing logic with one SQL statement
  • Wrap your RPG in a stored procedure — it provides the standard call interface for external consumers and supports returning JSON result sets
  • Validate JSON before parsing — probe with JSON_VALUE first; SQLCODE -171 means the input isn't valid JSON; catching it there gives you a clean error path before JSON_TABLE or JSON_ARRAYAGG see the data
  • IFS reads require IFS authority — the job profile needs *R on the file and *RX on every parent directory; missing authority surfaces as SQL7064, not a permission message