IBM i ILE Binding, Activation Groups, and Service Programs in 2026: BNDDIR, CRTPGM, *NEW vs *CALLER, and Designing for Reuse

The previous post covered IBM i message files and exception handling in CL — message types (*ESCAPE, *STATUS, *NOTIFY), SNDPGMMSG, MONMSG structured exception handling, RCVMSG for retrieving message data, and building a complete CL error handling pattern with audit logging. This post covers the ILE binding and activation group model in depth: how modules, programs, service programs, and binding directories relate to each other, how CRTPGM and CRTSRVPGM work, what the difference between *NEW, *CALLER, and named activation groups means for your runtime behaviour, and how to design reusable service programs that survive upgrades without recompiling callers.

The ILE Object Model Recap

ILE (Integrated Language Environment) introduces a layered object model that is quite different from the older OPM (Original Program Model). Understanding the four building blocks is essential before touching any compile commands.

  • Module (*MODULE) — the compiled output of a single source member. Created with CRTRPGMOD (RPG IV), CRTCMOD (C), or CRTCLMOD (CL). A module is not executable on its own; it contains compiled procedures and their object code, plus a table of unresolved external references.
  • Program (*PGM) — an executable object created by binding one or more modules together with CRTPGM. The linker resolves all external references at bind time. Only one module may be the entry module (containing the program entry procedure, PEP).
  • Service Program (*SRVPGM) — a shared library of exported procedures. Created with CRTSRVPGM. A service program has no entry point; instead, callers bind to the procedures it exports. Multiple programs can bind to the same service program, sharing its code but each getting their own activation.
  • Binding Directory (*BNDDIR) — an ordered list of service programs and modules that the system searches when resolving unresolved references during CRTPGM or CRTBNDRPG. It does not contain the objects themselves — it is merely a name list pointing to where they live.

The relationship is: source → module (CRTRPGMOD) → program (CRTPGM binding modules) or service program (CRTSRVPGM). Binding directories make it convenient to tell CRTPGM where to search without listing every service program explicitly.

CRTPGM and Module Binding

CRTPGM is the linker for IBM i ILE. It takes one or more modules, resolves all unresolved procedure calls between them, and produces a single executable *PGM object. Here is a realistic example binding an order processing program from three modules:

CRTPGM PGM(ORDLIB/ORDPRC)
       MODULE(ORDLIB/ORDPRC ORDLIB/ORDVAL ORDLIB/ORDCALC)
       BNDDIR(ORDLIB/ORDBD)
       ACTGRP(*NEW)
       DETAIL(*EXTENDED)
       TEXT('Order processing - main entry')

In this command:

  • MODULE lists the modules to bind, in order. The first module listed is the entry module unless you specify ENTMOD explicitly.
  • BNDDIR lists binding directories to search for any references not satisfied by the explicitly listed modules. ORDLIB/ORDBD might contain ORDSVC and DBSVC service programs.
  • ACTGRP(*NEW) assigns the activation group for the program at runtime (discussed below).
  • DETAIL(*EXTENDED) writes a detailed binding listing to the job log, showing every module considered, every procedure reference resolved, and which service program satisfied each reference — invaluable for diagnosing unresolved reference errors.

To create the individual modules first:

CRTRPGMOD MODULE(ORDLIB/ORDPRC)  SRCFILE(ORDLIB/QRPGLESRC) SRCMBR(ORDPRC)
CRTRPGMOD MODULE(ORDLIB/ORDVAL)  SRCFILE(ORDLIB/QRPGLESRC) SRCMBR(ORDVAL)
CRTRPGMOD MODULE(ORDLIB/ORDCALC) SRCFILE(ORDLIB/QRPGLESRC) SRCMBR(ORDCALC)

Alternatively, CRTBNDRPG compiles a single-source RPG program in one step (source → module → program), which is convenient for simple programs but bypasses the opportunity to share modules across programs. For production service-oriented design, prefer CRTRPGMOD followed by CRTPGM.

Use DSPPGM to inspect a bound program after creation:

DSPPGM PGM(ORDLIB/ORDPRC) DETAIL(*MODULE)

This shows every module bound into the program, plus which service programs are referenced and their current signature. Keep this output when debugging signature mismatch errors after updating a service program.

Service Programs (CRTSRVPGM)

A *SRVPGM is the IBM i equivalent of a shared library (DLL on Windows, .so on Linux). It exposes a set of procedures that calling programs bind to at compile time. The key distinction from a *PGM: there is no entry point, so it cannot be called directly; callers use CALLPRC (in CL) or a procedure call (in RPG free-format) to invoke its exported procedures.

Creating a service program requires an export list — a source file member that lists the exact procedure names to make public. By convention this file has extension .exp or is a member in QSRVSRC:

/* ORDLIB/QSRVSRC(ORDSVC) - export list for ORDSVC service program */
STRPGMEXP PGMLVL(*CURRENT) SIGNATURE('ORDSVC_V1')
  EXPORT SYMBOL('GETORDER')
  EXPORT SYMBOL('VALIDATEORDER')
  EXPORT SYMBOL('SAVEORDER')
  EXPORT SYMBOL('CANCELORDER')
ENDPGMEXP

Then create the service program:

CRTSRVPGM SRVPGM(ORDLIB/ORDSVC)
          MODULE(ORDLIB/ORDSVC)
          SRCFILE(ORDLIB/QSRVSRC)
          SRCMBR(ORDSVC)
          ACTGRP(*CALLER)
          TEXT('Order service program - V1')

Signature management is the most important concept for long-term service program maintenance. When you create a service program, IBM i computes a signature from the export list. Every program that binds to the service program records that signature. If you later recreate the service program with a changed or new export list, the signature changes, and all bound callers fail at activation time with a signature mismatch error.

The solution is to use *CURRENT and *COMPATIBLE in the export source:

/* Adding a new procedure - backward compatible upgrade */
STRPGMEXP PGMLVL(*CURRENT) SIGNATURE('ORDSVC_V2')
  EXPORT SYMBOL('GETORDER')
  EXPORT SYMBOL('VALIDATEORDER')
  EXPORT SYMBOL('SAVEORDER')
  EXPORT SYMBOL('CANCELORDER')
  EXPORT SYMBOL('GETORDERLINES')   /* new in V2 */
ENDPGMEXP

STRPGMEXP PGMLVL(*PRV) SIGNATURE('ORDSVC_V1')
  EXPORT SYMBOL('GETORDER')
  EXPORT SYMBOL('VALIDATEORDER')
  EXPORT SYMBOL('SAVEORDER')
  EXPORT SYMBOL('CANCELORDER')
ENDPGMEXP

With this two-block export source, CRTSRVPGM creates a service program with two signatures. Programs bound to ORDSVC_V1 continue to work without recompilation; new programs can bind to ORDSVC_V2 and call GETORDERLINES as well. Use UPDSRVPGM to update an existing service program in place, preserving its object identity:

UPDSRVPGM SRVPGM(ORDLIB/ORDSVC)
          MODULE(ORDLIB/ORDSVC)
          SRCFILE(ORDLIB/QSRVSRC)
          SRCMBR(ORDSVC)

Inspect a service program’s exports and signature with:

DSPSRVPGM SRVPGM(ORDLIB/ORDSVC) DETAIL(*PROCEXP)

Binding Directories (CRTBNDDIR / ADDBNDIRE)

A binding directory is a lightweight catalogue: it maps service program and module names to their libraries so that CRTPGM and CRTBNDRPG can find them automatically without requiring you to list every dependency on the command line.

/* Create the binding directory */
CRTBNDDIR BNDDIR(ORDLIB/ORDBD) TEXT('Order system binding directory')

/* Add service programs to it */
ADDBNDIRE BNDDIR(ORDLIB/ORDBD) OBJ((ORDLIB/ORDSVC *SRVPGM))
ADDBNDIRE BNDDIR(ORDLIB/ORDBD) OBJ((ORDLIB/DBSVC  *SRVPGM))
ADDBNDIRE BNDDIR(ORDLIB/ORDBD) OBJ((ORDLIB/LOGSVC *SRVPGM))

/* Optionally add a utility module */
ADDBNDIRE BNDDIR(ORDLIB/ORDBD) OBJ((ORDLIB/STRUTIL *MODULE))

Now any CRTPGM or CRTBNDRPG command that specifies BNDDIR(ORDLIB/ORDBD) will automatically search for unresolved references in those three service programs and the utility module, in the listed order. You can specify multiple binding directories:

CRTPGM PGM(ORDLIB/ORDPRC)
       MODULE(ORDLIB/ORDPRC)
       BNDDIR(ORDLIB/ORDBD QSYS/QC2LE)

QC2LE is the IBM-supplied binding directory for C runtime functions; including it is standard practice for any program that indirectly uses C library procedures through utility service programs.

Display the contents of a binding directory:

DSPBNDDIR BNDDIR(ORDLIB/ORDBD)

Activation Groups

An activation group is a runtime container within a job. It holds the storage, open file handles, and commitment control state for the ILE programs and service programs running within it. Understanding activation groups is critical because they control resource sharing and isolation between programs.

*NEW Activation Group

When a program is compiled with ACTGRP(*NEW), each call to that program creates a brand-new activation group. When the program returns or ends normally, the activation group is destroyed — open files are closed, storage is released, and any uncommitted transactions in that group are rolled back.

CRTPGM PGM(ORDLIB/ORPUTIL)
       MODULE(ORDLIB/ORPUTIL)
       ACTGRP(*NEW)

Use *NEW for:

  • Utility programs that must not share state with the caller
  • Programs that manage their own files and must ensure clean-up on exit
  • Interactive programs submitted to batch where isolation is required

The drawback of *NEW is overhead: creating and destroying an activation group for every call is expensive. If a utility is called thousands of times in a batch loop, this overhead accumulates.

*CALLER Activation Group

With ACTGRP(*CALLER), the called program or service program runs inside whatever activation group the caller is already using. It shares the caller’s open files, storage, and — most importantly — the caller’s commitment control scope.

CRTSRVPGM SRVPGM(ORDLIB/ORDSVC)
          MODULE(ORDLIB/ORDSVC)
          SRCFILE(ORDLIB/QSRVSRC)
          SRCMBR(ORDSVC)
          ACTGRP(*CALLER)

Use *CALLER for service programs that:

  • Participate in the caller’s database transaction (so their INSERT/UPDATE is part of the same COMMIT)
  • Are called very frequently and must avoid activation group overhead
  • Need to share open database files with the caller for performance

Note: *CALLER is the recommended activation group for most service programs in a transaction-processing environment. If a service program uses *NEW, its database changes are in a separate commitment control scope and will not be rolled back if the caller issues a ROLLBACK — a common source of partial-commit bugs.

Named Activation Groups

A named activation group is created the first time a program with that activation group name is activated in a job. It persists across calls — the activation group is not destroyed when the program returns. This makes it ideal for stateful resources: database connection pools, cached data, or service programs that maintain internal state between calls.

CRTPGM PGM(ORDLIB/CONPOOL)
       MODULE(ORDLIB/CONPOOL)
       ACTGRP(ORDGRP)
CRTSRVPGM SRVPGM(ORDLIB/CACHESVC)
          MODULE(ORDLIB/CACHESVC)
          SRCFILE(ORDLIB/QSRVSRC)
          SRCMBR(CACHESVC)
          ACTGRP(ORDGRP)

Both objects run in the named activation group ORDGRP. Data written to static storage in CACHESVC on one call is still there on the next call. To explicitly end a named activation group from RPG, use the DEALLOC(E) built-in or from CL use RCLACTGRP:

RCLACTGRP ACTGRP(ORDGRP)

Commitment Control and Activation Groups

Commitment control scope is tied directly to the activation group. When you issue STRCMTCTL, you start commitment control for the current activation group. Every database change made by programs running in that activation group is part of the same transaction until you issue COMMIT or ROLLBACK.

STRCMTCTL LCKLVL(*CHG) CMTSCOPE(*ACTGRP)

The CMTSCOPE(*ACTGRP) parameter specifies that the commitment control scope applies to the current activation group only. This is the default and the recommended setting for ILE programs.

The critical danger is mixing *NEW programs with commitment control when you expect unified transaction semantics. Consider this scenario:

  • Program A (ACTGRP named ORDGRP) starts a transaction and calls Program B
  • Program B was compiled with ACTGRP(*NEW) and also does database writes
  • Program A then issues a ROLLBACK

Result: Program B’s writes are in a separate activation group, so they are not rolled back by Program A’s ROLLBACK. This partial-commit bug is silent and very hard to detect in production. The fix is to compile Program B with ACTGRP(*CALLER) so it participates in Program A’s transaction.

/* RPG IV example showing commitment control in a service program */
**FREE
ctl-opt actgrp(*caller) dftactgrp(*no);

dcl-proc SaveOrder export;
  dcl-pi *n ind;
    pOrderNum varchar(10) const;
    pCustNum  varchar(10) const;
    pAmt      packed(11:2) const;
  end-pi;

  exec sql
    INSERT INTO ORDLIB.ORDHDR
      (ORDNUM, CUSTNUM, ORDAMT, ORDDTE)
    VALUES
      (:pOrderNum, :pCustNum, :pAmt, CURRENT_DATE);

  if sqlcode < 0;
    return *off;  /* caller will rollback its activation group */
  endif;

  return *on;
end-proc;

Because this service program uses ACTGRP(*CALLER), the INSERT is part of whatever transaction the calling program started. If the caller rolls back, this INSERT is rolled back too.

Practical Service Program Design — A Complete Example

This section walks through the complete lifecycle of the ORDLIB/ORDSVC service program: source, export list, compile, binding directory, and a calling program.

Step 1: Write the service program source (ORDLIB/QRPGLESRC/ORDSVC)

**FREE
ctl-opt nomain actgrp(*caller) dftactgrp(*no);

/copy ORDLIB/QCPYSRC,ORDSPECS        /* shared data structure prototypes */

dcl-proc GetOrder export;
  dcl-pi *n ind;
    pOrderNum varchar(10)  const;
    pOrder    likeDs(orderDs) options(*exact);
  end-pi;

  exec sql
    SELECT ORDNUM, CUSTNUM, ORDAMT, ORDSTS, ORDDTE
      INTO :pOrder
      FROM ORDLIB.ORDHDR
     WHERE ORDNUM = :pOrderNum
       AND ORDSTS  'X';

  return (sqlcode = 0);
end-proc;

dcl-proc ValidateOrder export;
  dcl-pi *n ind;
    pOrder likeDs(orderDs) const;
  end-pi;

  dcl-s custExists ind inz(*off);

  exec sql
    SELECT 1 INTO :custExists
      FROM ORDLIB.CUSTMST
     WHERE CUSTNUM = :pOrder.custNum
       AND CUSSTS  = 'A';

  if not custExists;
    return *off;
  endif;

  if pOrder.ordAmt <= 0;
    return *off;
  endif;

  return *on;
end-proc;

dcl-proc SaveOrder export;
  dcl-pi *n ind;
    pOrder likeDs(orderDs) const;
  end-pi;

  exec sql
    INSERT INTO ORDLIB.ORDHDR
      (ORDNUM, CUSTNUM, ORDAMT, ORDSTS, ORDDTE)
    VALUES
      (:pOrder.ordNum, :pOrder.custNum,
       :pOrder.ordAmt, 'N', CURRENT_DATE);

  return (sqlcode = 0);
end-proc;

Step 2: Create the export list (ORDLIB/QSRVSRC/ORDSVC)

STRPGMEXP PGMLVL(*CURRENT) SIGNATURE('ORDSVC_V1')
  EXPORT SYMBOL('GETORDER')
  EXPORT SYMBOL('VALIDATEORDER')
  EXPORT SYMBOL('SAVEORDER')
ENDPGMEXP

Step 3: Compile module and create service program

CRTRPGMOD MODULE(ORDLIB/ORDSVC)
          SRCFILE(ORDLIB/QRPGLESRC)
          SRCMBR(ORDSVC)
          DBGVIEW(*LIST)

CRTSRVPGM SRVPGM(ORDLIB/ORDSVC)
          MODULE(ORDLIB/ORDSVC)
          SRCFILE(ORDLIB/QSRVSRC)
          SRCMBR(ORDSVC)
          ACTGRP(*CALLER)
          TEXT('Order service program V1')

Step 4: Create the binding directory and add ORDSVC

CRTBNDDIR BNDDIR(ORDLIB/ORDBD)
          TEXT('Order system binding directory')

ADDBNDIRE BNDDIR(ORDLIB/ORDBD)
          OBJ((ORDLIB/ORDSVC *SRVPGM))

Step 5: Write a calling RPG program that binds via BNDDIR

**FREE
ctl-opt actgrp(*new) dftactgrp(*no) bnddir('ORDLIB/ORDBD');

/copy ORDLIB/QCPYSRC,ORDSPECS

dcl-s orderNum varchar(10);
dcl-ds order   likeDs(orderDs);
dcl-s ok       ind;

orderNum = 'ORD001234';

ok = GetOrder(orderNum : order);
if not ok;
  dsply ('Order not found: ' + orderNum);
  *inlr = *on;
  return;
endif;

ok = ValidateOrder(order);
if not ok;
  dsply ('Order validation failed for: ' + orderNum);
  *inlr = *on;
  return;
endif;

dsply ('Order loaded: cust=' + order.custNum);
*inlr = *on;

Compile this calling program:

CRTBNDRPG PGM(ORDLIB/ORDMAIN)
          SRCFILE(ORDLIB/QRPGLESRC)
          SRCMBR(ORDMAIN)
          BNDDIR(ORDLIB/ORDBD)
          ACTGRP(*NEW)

Debugging ILE Binding Problems

Binding and activation group problems surface in a handful of recurring patterns. Here are the most common errors and their diagnostic approaches.

RNQ0202 — Unresolved external reference

This compile-time error means the linker could not find a procedure that one of your modules calls. Diagnose with DETAIL(*EXTENDED) on CRTPGM, which lists every unresolved symbol. Check that:

  • The procedure name in the CALLPRC or RPG prototype exactly matches the EXPORT SYMBOL in the service program’s export list (case-sensitive on some platform configurations)
  • The service program is listed in the BNDDIR or MODULE parameter
  • The service program exists in the library specified in the BNDDIR entry
CRTPGM PGM(ORDLIB/ORDPRC)
       MODULE(ORDLIB/ORDPRC)
       BNDDIR(ORDLIB/ORDBD)
       DETAIL(*EXTENDED)    /* write full binding map to job log */

Signature mismatch at activation time

If a service program is recreated (CRTSRVPGM without a *PRV export block), its signature changes. Any bound caller then fails at activation with message MCH4431 or a service program not found error. Diagnose with DSPPGM on the calling program to see what signature it recorded at bind time, then DSPSRVPGM to see the current signatures on the service program:

DSPPGM    PGM(ORDLIB/ORDPRC)     DETAIL(*SRVPGM)
DSPSRVPGM SRVPGM(ORDLIB/ORDSVC) DETAIL(*SIGNATURE)

If the signatures do not match, you must either recompile the calling program (to pick up the new signature) or use the *PRV export block technique described above so both old and new signatures coexist in the service program.

*CALLER activation group used with commitment control — partial commit

As described earlier, mixing *NEW and *CALLER programs in a transaction causes partial commits. Use the following SQL to audit which programs in a library use *NEW versus *CALLER activation groups:

SELECT PROGRAM_NAME, ACTIVATION_GROUP_ATTRIBUTE
  FROM QSYS2.PROGRAM_INFO
 WHERE PROGRAM_LIBRARY = 'ORDLIB'
   AND PROGRAM_TYPE    = '*PGM'
 ORDER BY PROGRAM_NAME;

For service programs:

SELECT SERVICE_PROGRAM_NAME, ACTIVATION_GROUP_ATTRIBUTE
  FROM QSYS2.SERVICE_PROGRAM_INFO
 WHERE SERVICE_PROGRAM_LIBRARY = 'ORDLIB'
 ORDER BY SERVICE_PROGRAM_NAME;

Any service program that does database writes and shows *NEW in the activation group attribute is a candidate for a partial-commit bug if it is called from within a transaction managed by the caller.

Summary of activation group guidance:

  • Service programs that participate in caller transactions: use *CALLER
  • Standalone utility programs that manage their own resources: use *NEW
  • Stateful or connection-pool service programs: use a named activation group
  • Interactive programs (menus, displays): typically *NEW for isolation
  • Never mix *NEW with commitment control where you expect unified rollback semantics

Next post: DB2 for i JSON and XML Support — JSON_VALUE, JSON_QUERY, JSON_TABLE for shredding JSON into rows, FOR JSON to produce JSON output from SQL, XMLTABLE for XML parsing, and building JSON REST responses directly in DB2 for i SQL.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top