The previous post covered connecting IBM i to cloud platforms. This post steps back to something closer to the core of how IBM i works: Control Language.
CL is the scripting language of IBM i. It is not a general-purpose language — it does not compete with RPG for business logic or with SQL for data access. It does something neither of those does well: it controls the environment in which other programs run. Job setup, library lists, file overrides, error monitoring, subsystem management, and batch job orchestration — these are CL’s domain. Understanding CL is understanding how IBM i operations actually work.
This post covers CL from a modern perspective: ILE CL structure, practical patterns, error handling, SQL integration, and where CL fits in a 2026 DevOps pipeline.
What CL is and what it is for
Control Language is IBM i’s system command language. Every command you type at a 5250 command line — STRSBS, CHGCURLIB, OVRDBF, SBMJOB — is a CL command. A CL program is a compiled sequence of those commands with added logic: variables, branching, loops, and error handling.
CL programs are compiled objects on IBM i, not scripts. They run under a job with a user profile, a library list, and all the security controls that apply to any other IBM i program. This is different from shell scripts on Linux: a CL program is a first-class IBM i object.
What CL is typically used for:
- Job setup — building library lists, setting job attributes before calling application programs
- Batch job orchestration — chaining programs, checking return codes, branching on failure
- File overrides — redirecting file references to different physical files or members for testing and multi-tenant processing
- System administration — automating repetitive operator tasks
- Error handling wrappers — catching exceptions from RPG or SQL programs and taking appropriate action
- Scheduled job streams — the control layer for nightly batch processes
What CL is not used for: Business logic, data transformation, string manipulation at scale, or anything requiring complex data structures. RPG or SQL is the right tool for those needs.
ILE CL versus OPM CL
IBM i has two CL programming models:
OPM CL (Original Program Model) — the older model. A CL program is a single compilation unit. No subprocedures, no service programs, no binding. Still functional, still common in legacy systems. Compiled with CRTCLPGM.
ILE CL (Integrated Language Environment) — the current model, available since V3R6. Supports subprocedures, prototyped calls, binding to service programs, and integration with other ILE languages. Compiled with CRTBNDCL (bound program) or CRTCLMOD + CRTPGM (module + bind).
For any new CL development, use ILE CL. It supports all modern IBM i features and integrates cleanly with ILE RPG programs.
Identifying which model a source file uses: ILE CL source uses PGM/ENDPGM or PROCEDURE/ENDPROC. OPM CL uses PGM/ENDPGM without subprocedures. The file extension convention is .clle for ILE CL source in the IFS.
ILE CL program structure
A minimal ILE CL program:
PGM
DCL VAR(&ORDLIB) TYPE(*CHAR) LEN(10) VALUE('ORDLIB')
DCL VAR(&RETCODE) TYPE(*INT) LEN(4)
/* Add application library to library list */
ADDLIBLE LIB(&ORDLIB) POSITION(*FIRST)
/* Call RPG order processing program */
CALL PGM(CRTORD) PARM('ORD00143' &RETCODE)
IF COND(&RETCODE *NE 0) THEN(DO)
SNDPGMMSG MSG('Order creation failed, RC=' *CAT &RETCODE) +
MSGTYPE(*ESCAPE)
ENDDO
ENDPGMVariable types in ILE CL:
*CHAR— fixed-length character string (up to 9999 characters in ILE CL)*INT— integer, 2 or 4 bytes*UINT— unsigned integer*DEC— packed decimal (length and decimals required)*LGL— logical (boolean), single character'1'(true) or'0'(false)*PTR— pointer (for system API calls)
DCL VAR(&CUSTNO) TYPE(*CHAR) LEN(10) DCL VAR(&ORDAMT) TYPE(*DEC) LEN(11 2) DCL VAR(&ACTIVE) TYPE(*LGL) DCL VAR(&COUNTER) TYPE(*INT) LEN(4)
Error handling with MONMSG
MONMSG (Monitor Message) is CL’s primary error handling mechanism. It intercepts escape messages — the IBM i equivalent of exceptions — before they terminate the job.
Program-level MONMSG (catch-all):
PGM
MONMSG MSGID(CPF0000) EXEC(GOTO CMDLBL(ERROR))
/* ... program logic ... */
RETURN
ERROR:
SNDPGMMSG MSG('Batch process failed — check joblog') +
TOMSGQ(QSYSOPR) MSGTYPE(*INFO)
/* Log or alert, then end */
ENDPGMThe program-level MONMSG at the top catches any CPF message (CPF0000 is the generic CPF message class) and branches to the ERROR label. This is a safety net, not a substitute for specific error handling.
Command-level MONMSG (specific error handling):
PGM
DCL VAR(&LIBEXISTS) TYPE(*LGL) VALUE('0')
/* Check if library exists before adding */
ADDLIBLE LIB(APPLIB)
MONMSG MSGID(CPF2103) EXEC(CHGVAR VAR(&LIBEXISTS) VALUE('1'))
IF COND(&LIBEXISTS *EQ '0') THEN(DO)
CRTLIB LIB(APPLIB) TYPE(*TEST)
ADDLIBLE LIB(APPLIB)
ENDDO
ENDPGMCPF2103 is the specific message ID for “library already in library list”. Monitoring for that specific ID and continuing gracefully is the right pattern — do not catch all errors when you know which one to expect.
Common CPF message IDs worth knowing:
CPF2103— Library already exists in library list (ADDLIBLE)CPF2110— Library not found (ADDLIBLE, RMVLIBLE)CPF3203— Cannot allocate object (file in use)CPF4131— Override already exists for fileCPF9801— Object not foundCPF9802— Not authorised to objectMCH0000— Machine check (catch-all for machine-level exceptions)
Retrieving the failing message for logging:
PGM
DCL VAR(&MSGID) TYPE(*CHAR) LEN(7)
DCL VAR(&MSGDTA) TYPE(*CHAR) LEN(100)
DCL VAR(&MSGF) TYPE(*CHAR) LEN(10)
DCL VAR(&MSGFLIB)TYPE(*CHAR) LEN(10)
MONMSG MSGID(CPF0000) EXEC(GOTO CMDLBL(ERROR))
/* ... program logic ... */
RETURN
ERROR:
/* Receive the last escape message */
RCVMSG MSGTYPE(*EXCP) MSGID(&MSGID) MSG(&MSGDTA) +
MSGF(&MSGF) MSGFLIB(&MSGFLIB)
SNDPGMMSG MSG('Program failed: ' *CAT &MSGID *BCAT &MSGDTA) +
TOMSGQ(QSYSOPR)
ENDPGMLibrary list management
The library list is how IBM i resolves unqualified object names. A CL program that sets up the library list correctly before calling application programs is the foundation of every IBM i job stream.
Building a controlled library list at job start:
PGM PARM(&ENV)
DCL VAR(&ENV) TYPE(*CHAR) LEN(10) /* 'PROD', 'TEST', 'DEV' */
DCL VAR(&APPLIB) TYPE(*CHAR) LEN(10)
/* Clear user portion of library list */
CHGLIBL LIBL(QTEMP)
/* Set application library based on environment */
SELECT
WHEN COND(&ENV *EQ 'PROD') THEN(DO)
CHGVAR VAR(&APPLIB) VALUE('ORDLIB')
ENDDO
WHEN COND(&ENV *EQ 'TEST') THEN(DO)
CHGVAR VAR(&APPLIB) VALUE('ORDLIBTEST')
ENDDO
OTHERWISE DO
CHGVAR VAR(&APPLIB) VALUE('ORDLIBDEV')
ENDDO
ENDSELECT
ADDLIBLE LIB(&APPLIB) POSITION(*FIRST)
ADDLIBLE LIB(ORDDATA) POSITION(*AFTER &APPLIB)
ADDLIBLE LIB(COMMLIB) POSITION(*LAST)
/* Confirm list */
DSPLIBL /* Optional — remove in production batch */
ENDPGMThis pattern — clear the user library list, then build it explicitly — guarantees the job runs against the intended environment regardless of who submitted it or how their profile’s initial library list is configured.
File overrides
OVRDBF (Override Database File) redirects a file reference — when program A opens file ORDHDR, it actually opens ORDHDR in a different library, or a specific member, or with different record selection.
Override to a specific library:
OVRDBF FILE(ORDHDR) TOFILE(ORDLIBTEST/ORDHDR)
Now any program in the job that opens ORDHDR will open the test version, not the production file.
Override to a specific member (for multi-tenant or date-based partitioning):
DCL VAR(&MEMBER) TYPE(*CHAR) LEN(10)
CHGVAR VAR(&MEMBER) VALUE('JAN2026')
OVRDBF FILE(ORDARCH) MBR(&MEMBER) OVRSCOPE(*JOB)Override scope:
*ACTGRPDFN— default; override applies to the current activation group*JOB— override applies to all programs in the job, regardless of call depth*CALLLVL— override applies at the current call level and programs it calls
Delete overrides when done:
DLTOVR FILE(ORDHDR) /* Delete specific file override */ DLTOVR FILE(*ALL) /* Delete all overrides at current call level */
Failing to clean up overrides is a common source of subtle bugs in CL programs — an override set for one step in a job stream remains active for subsequent steps unless explicitly deleted.
Submitting batch jobs from CL
SBMJOB submits a program to run in a batch subsystem. It is how most IBM i processing moves off the interactive subsystem.
PGM PARM(&DATE)
DCL VAR(&DATE) TYPE(*CHAR) LEN(7)
DCL VAR(&JOBNAME) TYPE(*CHAR) LEN(10)
CHGVAR VAR(&JOBNAME) VALUE('NGTBATCH')
SBMJOB CMD(CALL PGM(NIGHTBATCH) PARM(&DATE)) +
JOB(&JOBNAME) +
JOBD(BATCHJOBD) +
JOBQ(BATCHQ) +
USER(BATCHUSR) +
HOLD(*NO) +
LOG(4 00 *SECLVL)
SNDPGMMSG MSG('Batch job ' *CAT &JOBNAME *BCAT 'submitted')
ENDPGMKey SBMJOB parameters:
JOBD— job description controlling initial library list, output queue, and message loggingJOBQ— which job queue to submit to; controls priority and which subsystem picks it upUSER— profile the batch job runs under; should be a limited service profile, not an interactive userLOG— job log detail level;4 00 *SECLVLlogs all messages including those below severity 00 with second-level text
Running SQL from CL
RUNSQL and RUNSQLSTM allow SQL statements to execute directly from CL, without an RPG program or interactive session.
Single SQL statement:
RUNSQL SQL('UPDATE ORDLIB/ORDHDR SET ORDSTS = ''CL'' +
WHERE ORDDAT < CURRENT_DATE - 365 DAYS +
AND ORDSTS = ''OP''') +
COMMIT(*NONE)Note the doubled single quotes inside the SQL string — CL string literals use doubled quotes for embedded quotes.
Running a SQL script file from the IFS:
RUNSQLSTM SRCSTMF('/home/admin/scripts/monthly-archive.sql') +
COMMIT(*NONE) +
NAMING(*SQL)This is powerful for batch maintenance scripts — write the SQL in a proper editor, store it in the IFS under Git control, run it from CL as part of a scheduled job stream.
Capturing a scalar SQL result into a CL variable:
CL cannot directly receive a SQL SELECT result, but a single-row result can be retrieved via QCMDEXC calling a stored procedure that sets an output parameter:
DCL VAR(&OPENCOUNT) TYPE(*INT) LEN(4)
/* Stored procedure sets &OPENCOUNT via OUT parameter */
CALL PGM(GETORDCNT) PARM(&OPENCOUNT)
IF COND(&OPENCOUNT *GT 1000) THEN(DO)
SNDPGMMSG MSG('Open order count exceeds threshold: ' +
*CAT &OPENCOUNT) TOMSGQ(QSYSOPR) MSGTYPE(*INFO)
ENDDOCL subprocedures (ILE CL)
ILE CL supports subprocedures — named, callable sections within a CL module. This allows CL logic to be organised without separate compiled programs for every small task.
PGM
DCL VAR(&RESULT) TYPE(*CHAR) LEN(200)
/* Call local subprocedure */
CALLSUBR SUBR(GETENVNAME) RTNVAL(&RESULT)
SNDPGMMSG MSG('Environment: ' *CAT &RESULT)
RETURN
/* ─── Subprocedure ─────────────────────────── */
SUBR SUBR(GETENVNAME)
DCL VAR(&RTNVAL) TYPE(*CHAR) LEN(200)
DCL VAR(&SYSNAME) TYPE(*CHAR) LEN(8)
RTVNETA SYSNAME(&SYSNAME)
IF COND(&SYSNAME *EQ 'PRODSYS') THEN(DO)
CHGVAR VAR(&RTNVAL) VALUE('PRODUCTION')
ENDDO
ELSE DO
CHGVAR VAR(&RTNVAL) VALUE('NON-PRODUCTION')
ENDDO
RTNSUBR RTNVAL(&RTNVAL)
ENDSUBR
ENDPGMSubprocedures keep related logic together and make CL programs easier to read and maintain. For complex job streams, organising setup, processing, and cleanup into subprocedures is significantly cleaner than a single flat program with GOTO labels.
Retrieving system and job information
CL has dedicated commands for retrieving runtime information about the job and system — these are used constantly in operational and batch CL programs.
PGM
DCL VAR(&JOBNAME) TYPE(*CHAR) LEN(10)
DCL VAR(&USER) TYPE(*CHAR) LEN(10)
DCL VAR(&JOBNBR) TYPE(*CHAR) LEN(6)
DCL VAR(&SYSNAME) TYPE(*CHAR) LEN(8)
DCL VAR(&CURDATE) TYPE(*CHAR) LEN(7) /* CYYMMDD */
DCL VAR(&CURTIME) TYPE(*CHAR) LEN(6) /* HHMMSS */
RTVJOBA JOB(&JOBNAME) USER(&USER) NBR(&JOBNBR)
RTVNETA SYSNAME(&SYSNAME)
RTVSYSVAL SYSVAL(QDATE) RTNVAR(&CURDATE)
RTVSYSVAL SYSVAL(QTIME) RTNVAR(&CURTIME)
SNDPGMMSG MSG('Job ' *CAT &JOBNAME *BCAT '/' *CAT +
&USER *BCAT 'started on' *BCAT &SYSNAME *BCAT +
'at' *BCAT &CURTIME)
ENDPGMCommon retrieval commands:
RTVJOBA— retrieve current job attributes (name, user, number, status, type)RTVNETA— retrieve network attributes (system name, local network ID)RTVSYSVAL— retrieve a system value (QDATE, QTIME, QSECURITY, etc.)RTVUSRPRF— retrieve user profile attributesRTVDTAARA— retrieve a data area value (useful for job-to-job communication)
CL in a DevOps pipeline
As covered in Post 17, IBM i build pipelines use Bob or GNU Make to compile source. CL programs fit into this pipeline like any other compiled object — source lives in the IFS under Git, Bob compiles from there.
CL source in the IFS repository:
project-repo/
└── src/
└── clle/
├── JOBSETUP.clle ← Library list setup
├── NIGHTBATCH.clle ← Nightly batch orchestration
└── DPLYCHECK.clle ← Deployment verification CLCompiling ILE CL from IFS source:
CRTBNDCL PGM(BLDLIB/JOBSETUP) +
SRCSTMF('/home/build/project-repo/src/clle/JOBSETUP.clle') +
DBGVIEW(*SOURCE)CL as a deployment verification step:
A CL program that verifies a deployment completed correctly is a practical CI/CD gate:
PGM PARM(&TARGETLIB)
DCL VAR(&TARGETLIB) TYPE(*CHAR) LEN(10)
DCL VAR(&OBJCOUNT) TYPE(*INT) LEN(4)
DCL VAR(&ERRFOUND) TYPE(*LGL) VALUE('0')
MONMSG MSGID(CPF0000) EXEC(GOTO CMDLBL(ERROR))
/* Verify expected programs exist */
CHKOBJ OBJ(&TARGETLIB/CRTORD) OBJTYPE(*PGM)
MONMSG MSGID(CPF9801) EXEC(DO)
SNDPGMMSG MSG('MISSING: ' *CAT &TARGETLIB *CAT '/CRTORD') +
MSGTYPE(*ESCAPE)
ENDDO
CHKOBJ OBJ(&TARGETLIB/ORDHDR) OBJTYPE(*FILE)
MONMSG MSGID(CPF9801) EXEC(DO)
SNDPGMMSG MSG('MISSING: ' *CAT &TARGETLIB *CAT '/ORDHDR') +
MSGTYPE(*ESCAPE)
ENDDO
SNDPGMMSG MSG('Deployment verification passed for ' *CAT &TARGETLIB) +
MSGTYPE(*INFO)
RETURN
ERROR:
SNDPGMMSG MSG('Deployment verification FAILED for ' *CAT &TARGETLIB) +
MSGTYPE(*ESCAPE)
ENDPGMThe CI pipeline calls this CL program after deployment. If any expected object is missing, the CL sends an escape message, the program ends abnormally, and the pipeline step fails. This is a CL program doing what CL is actually good at: controlling and verifying the environment.
Common operational patterns
Data area for inter-job communication:
/* Writer job: signal completion */
CHGDTAARA DTAARA(QTEMP/BATCHSTS) VALUE('DONE')
/* Reader job: poll until done */
TOP:
RTVDTAARA DTAARA(QTEMP/BATCHSTS) RTNVAR(&STATUS)
IF COND(&STATUS *NE 'DONE') THEN(DO)
DLYJOB DLY(30)
GOTO CMDLBL(TOP)
ENDDOSave and restore for deployment:
/* Save current production objects before deploying */
SAVOBJ OBJ(*ALL) LIB(ORDLIB) DEV(*SAVF) +
SAVF(ORDLIB/PRODBACKUP) TGTRLS(*CURRENT)
/* Restore from save file if rollback needed */
RSTOBJ OBJ(*ALL) SAVLIB(ORDLIB) DEV(*SAVF) +
SAVF(ORDLIB/PRODBACKUP) MBROPT(*ALL) RSTLIB(ORDLIB)Clearing and initialising a work library:
CLRLIB LIB(WORKLIB) /* Clear all objects */ /* Or selective clear */ DLTOBJ OBJ(WORKLIB/*ALL) OBJTYPE(*FILE) DLTOBJ OBJ(WORKLIB/*ALL) OBJTYPE(*PGM)
CL versus Python and shell scripts for automation
With PASE available, IBM i administrators can write automation in Python or bash instead of CL. When should you use which?
Use CL when:
- The task manipulates IBM i objects, library lists, or subsystems — CL has native commands for this; Python does not
- The program runs as a compiled IBM i object with full object authority and auditing
- Error handling needs to integrate with IBM i message handling (escape messages, job logs)
- The program calls RPG or other ILE programs directly
- The existing team knows CL and the task does not require anything CL cannot do
Use Python or bash when:
- The task involves REST API calls, JSON manipulation, or file processing better served by a scripting language
- The automation runs in a CI/CD pipeline where Python is the natural tool
- The task involves complex string manipulation or data transformation
- The audience includes developers who do not know CL
CL and PASE scripts are not in competition — they are complementary. A Python script that sets up a test environment might call CL programs that set library lists and compile objects. A CL job stream might call a Node.js script that sends a Teams notification on completion.
CL is not a legacy tool that needs replacing. It is the correct tool for a specific class of IBM i operational tasks. Understanding where that boundary lies is what separates effective IBM i developers from those who try to do everything in RPG or everything in Python.
Next post: RPG in 2026 — modern free-format RPG IV, prototyped procedure calls, data structures, SQL integration, and what current RPG actually looks like compared to the fixed-format code that gave the language its reputation.