Tutorial 10: CL — The Glue Language
Every tutorial so far has been RPG. But RPG rarely runs in isolation. On IBM i, the thing that calls RPG programs — schedules them, sequences them, passes them parameters, overrides their files, logs their output — is CL (Control Language).
If RPG is where the business logic lives, CL is the connective tissue. Batch jobs are CL. Startup sequences are CL. Wrapper programs that accept string parameters and convert them before calling RPG are CL. You can’t be productive on IBM i without writing some.
This tutorial teaches enough CL to run your RPG programs from job schedulers and to write the wrapper code real production deployments need.
What you’ll build
A CL program called CL_REORD that orchestrates a reorder run. It:
- Logs the start
- Overrides the
QPRINToutput to a specific spool file name for easy retrieval - Calls REORDRPT (the batch program from Tutorial 7)
- Queries the row count from REORDCND via an RPG program you’ll write alongside
- Sends a completion message with the count to the operator’s message queue
- Cleans up overrides
- Logs the end
The RPG helper, COUNTROW, accepts a table name and returns the count back to CL through a parameter. It exists to demonstrate passing data back from RPG to CL.
What you’ll learn
- The anatomy of a CL program —
PGM,DCL,ENDPGM - CL variable declarations and the
&NAMEsyntax - Calling an RPG program from CL with
CALL PGM(...) PARM(...) - Converting between CL character variables and decimal (the most common type-mismatch bug)
SNDPGMMSGfor logging and operator notifications- The “CL wrapper around RPG” pattern that real shops use
- Why CL exists and when to reach for it
Before you start
- Part 1 complete
- REORDRPT from Tutorial 7 compiled and working
- Your practice library available to receive new programs
Step 1 — Write the RPG helper
Create a new member in QRPGLESRC called COUNTROW, type SQLRPGLE. Paste:
**free
ctl-opt dftactgrp(*no) actgrp(*caller)
option(*nodebugio:*srcstmt)
main(Main);
// ************************************************************
// * Name: COUNTROW
// * Type: ILE RPG Program (SQLRPGLE)
// * Desc: Returns the row count of a named table via
// * output parameter. Called from CL.
// * Tutorial: Part 3, Tutorial 10
// ************************************************************
dcl-proc Main;
dcl-pi *n;
wk_tableName char(10) const;
wk_rowCount packed(9:0);
end-pi;
dcl-s sql varchar(200);
exec sql
set option commit = *none,
datfmt = *iso,
closqlcsr = *endactgrp;
// Build dynamic SQL because the table name is a parameter.
// Note: parameterizing object names (like table names) isn't
// possible in standard parameterized SQL — it only works for
// values in WHERE clauses. So we construct the statement.
sql = 'select count(*) from ' + %trim(wk_tableName);
exec sql prepare countStmt from :sql;
if sqlstate <> '00000';
wk_rowCount = -1;
return;
endif;
exec sql execute countStmt into :wk_rowCount;
if sqlstate <> '00000';
wk_rowCount = -1;
endif;
return;
end-proc;
Compile with CRTSQLRPGI.
A note on the output parameter. The PI declares wk_rowCount packed(9:0) with no const — that’s the key. Without const, the parameter is passed by reference and the RPG program can write to it. The caller (in this case, CL) reads it back. This is how you return scalar data from an RPG program that doesn’t have a return value.
Step 2 — Write the CL wrapper
In your QCLLESRC source file (same pattern as QRPGLESRC — create it with CRTSRCPF FILE(YOUR_LIBRARY/QCLLESRC) RCDLEN(112) if it doesn’t exist), create a new member called CL_REORD, type CLLE. Paste:
/*******************************************************************/
/* */
/* Name: CL_REORD */
/* Type: CL Program */
/* Desc: Wrapper for REORDRPT batch run. Overrides output, */
/* calls the batch, reports the row count. */
/* Tutorial: Part 3, Tutorial 10 */
/* */
/*******************************************************************/
PGM
DCL VAR(&ROWCOUNTD) TYPE(*DEC) LEN(9 0)
DCL VAR(&ROWCOUNTC) TYPE(*CHAR) LEN(10)
DCL VAR(&TABLENAME) TYPE(*CHAR) LEN(10) VALUE('REORDCND')
/* Log start */
SNDPGMMSG MSG('CL_REORD: Starting reorder run') +
TOPGMQ(*EXT) MSGTYPE(*INFO)
/* Run the RPG batch program */
CALL PGM(REORDRPT)
/* Ask the helper for the row count */
CALL PGM(COUNTROW) PARM(&TABLENAME &ROWCOUNTD)
/* Convert decimal count to character for the message */
CHGVAR VAR(&ROWCOUNTC) VALUE(&ROWCOUNTD)
/* Report the result */
SNDPGMMSG MSG('CL_REORD: ' *CAT &ROWCOUNTC *CAT ' +
reorder candidates written') +
TOPGMQ(*EXT) MSGTYPE(*INFO)
/* Log end */
SNDPGMMSG MSG('CL_REORD: Reorder run complete') +
TOPGMQ(*EXT) MSGTYPE(*INFO)
FINAL:
ENDPGM
Compile with CRTBNDCL (the compile action labeled “CLLE json V7R3 CRTBNDCL from IFS source” in the VS Code actions config from Part 1).
Walkthrough
PGM and ENDPGM
Every CL program is bracketed by PGM at the top and ENDPGM at the bottom. Between them, the program runs top to bottom. CL has no free-format equivalent — it’s always this shape, always column-oriented-ish.
If the program takes parameters, the PGM line lists them: PGM PARM(&PROD &QTY). Our program takes no parameters.
CL variables
DCL VAR(&ROWCOUNTD) TYPE(*DEC) LEN(9 0)
DCL VAR(&ROWCOUNTC) TYPE(*CHAR) LEN(10)
DCL VAR(&TABLENAME) TYPE(*CHAR) LEN(10) VALUE('REORDCND')
DCL— declares a variableVAR(&NAME)— the variable name, always prefixed with&TYPE(*DEC)orTYPE(*CHAR)orTYPE(*LGL)(logical/boolean)LEN(n)for character (chars) orLEN(n d)for decimal (digits, decimal positions)VALUE(...)is optional — sets an initial value
The & prefix is always required. CL looks for &ROWCOUNTD as a variable name but ROWCOUNTD as a literal string. Forgetting the & is the most common CL bug for newcomers.
The RPG call
CALL PGM(REORDRPT)
Direct call, no parameters. The program runs, does its work (prints its own DSPLY messages), returns.
The parameterized call
CALL PGM(COUNTROW) PARM(&TABLENAME &ROWCOUNTD)
PARM(...) lists the parameters in order, space-separated. &TABLENAME is passed in (its value is ‘REORDCND’). &ROWCOUNTD is passed by reference — COUNTROW writes to it, and after the call, &ROWCOUNTD contains the count.
The type matching is critical. CL’s &ROWCOUNTD is *DEC LEN(9 0). COUNTROW’s PI declares wk_rowCount packed(9:0). Same type, same precision. If they mismatch — say, COUNTROW declared packed(11:0) while CL declared *DEC LEN(9 0) — the values received and stored will be silently wrong. The compiler can’t check cross-program, so consistency is on you.
This is the #1 source of production bugs in CL-to-RPG integration. Always match types exactly.
Decimal to character conversion
CHGVAR VAR(&ROWCOUNTC) VALUE(&ROWCOUNTD)
CHGVAR changes the value of a variable. When the source and target are different types, CL does the conversion automatically. Here it takes the decimal &ROWCOUNTD and converts it to the character &ROWCOUNTC.
Why two variables? Because SNDPGMMSG’s MSG parameter is a character string, not a decimal. You can’t directly concatenate a decimal into a message. So the pattern is: compute in decimal (that’s how RPG returned it), convert to character (for the message), send.
SNDPGMMSG
SNDPGMMSG MSG('CL_REORD: Starting reorder run') +
TOPGMQ(*EXT) MSGTYPE(*INFO)
SNDPGMMSG is “send program message.” It’s the main way CL programs log events.
MSG('...')— the message text (up to 512 chars)TOPGMQ(*EXT)— the external message queue, which is visible to the user running the job. You’ll also see*PRV(previous program in the call stack) and explicit queues likeTOPGMQ(*EXT) PGMQ(QSYS).MSGTYPE(*INFO)— informational. Others:*DIAG(diagnostic),*COMP(completion),*ESCAPE(escape, which halts callers).
The + at the end of a line is CL’s line-continuation marker. CL is fussy about line breaks — you can’t just continue a statement on the next line without it.
The *CAT operator
MSG('CL_REORD: ' *CAT &ROWCOUNTC *CAT ' reorder candidates written')
*CAT concatenates strings with trailing spaces preserved. *BCAT strips trailing blanks and adds one space. *TCAT preserves everything including trailing blanks with no space.
This tutorial uses *CAT because &ROWCOUNTC is fixed-length char(10) with leading zeros (e.g., '0000000013'), and we keep it simple by not trimming it. In real code you’d probably use *BCAT or a different conversion to get '13' without the leading zeros. (See “Try this” below.)
Run it
From the VS Code IBM i terminal or a 5250 session:
CALL CL_REORD
You should see:
- The REORDRPT DSPLYs fire (starting, scanning, summary)
- Messages in your job log:
CL_REORD: Starting reorder runCL_REORD: 0000000013 reorder candidates writtenCL_REORD: Reorder run complete
If you check the job log with DSPJOBLOG or DSPMSG, you’ll see the sequence.
Why CL exists
A reasonable question: why isn’t this RPG code? RPG has EXEC SQL, it can display messages, it can be the orchestrator.
Three real answers:
History. CL predates RPG’s modernization. For thirty years, CL was the only reasonable way to do job-level control. Shops have thousands of CL programs. Skipping CL means not being able to read the codebase.
Job-level operations. CL has native commands for things RPG can’t do easily: overriding files (OVRDBF), adding library entries (ADDLIBLE), allocating objects (ALCOBJ), monitoring for messages (MONMSG), managing spool files, setting job attributes. Doing these from RPG is possible but awkward. CL is the natural layer.
Composition with system services. Job schedulers, command menus, and operator tooling all speak CL. Your batch job runs under a SBMJOB that calls a CL program. Your nightly reporting runs through WRKJOBSCDE entries that call CL programs. You can call RPG directly, but the wrapper pattern keeps the ceremony separate from the business logic.
Practical rule: RPG for the computation, CL for the orchestration. A nightly run might be a CL program that sets library lists, overrides printer files, calls three RPG batch programs in sequence, and cleans up. Each RPG program focuses on its logic; the CL owns the plumbing.
Try this
Strip leading zeros. The message shows 0000000013 instead of 13. Modify CL_REORD to remove leading zeros. Hint: CL has no %trim — you’ll need to either convert differently or use a small RPG helper. Which feels cleaner?
Error handling with MONMSG. CL’s equivalent of monitor is MONMSG. Add MONMSG MSGID(CPF0000) EXEC(GOTO FINAL) right after the CALL REORDRPT line. Now if the RPG crashes, the CL jumps to the FINAL: label and exits cleanly instead of propagating the error to the caller.
OVRDBF for testing. Before the CALL, add OVRDBF FILE(PRODUCT) TOFILE(PRODUCT_TEST). After the CALL (in FINAL), add DLTOVR FILE(PRODUCT). Now REORDRPT would read from PRODUCT_TEST instead of PRODUCT when called through this CL — useful for running the batch against a test copy of the data without modifying the RPG source. This is override-based testing, a common IBM i pattern.
Parameterize the threshold. Modify STALEPR from Tutorial 9 and CL_REORD so that CL_REORD can run STALEPR with a specific threshold before the reorder runs. The CL accepts a threshold parameter and passes it to STALEPR. Practice accepting parameters and passing them through.
Schedule it. Use WRKJOBSCDE to add a job schedule entry that runs CL_REORD daily at 2 AM. This is how batch jobs actually get into production. Don’t forget to remove it when you’re done testing.
What’s next
Wave 3 is done — operational quality tools (error handling, date math, CL orchestration) are in place. Wave 4 closes out Part 3 with the last two tutorials:
- Tutorial 11 — the Roman numeral converter, a capstone that uses procedures for validation and conversion in an unexpectedly realistic way
- Tutorial 12 — RPGUnit, so you can write tests for all the programs in Part 3
Next: Tutorial 11: Roman Numeral Converter — A Procedures Capstone.