cheat sheet
REXX Snippets
Practical REXX patterns for z/OS automation, TSO commands, and ISPF services.
REXX Snippets
What it is
REXX (Restructured Extended Executor) is IBM's procedural scripting language built into z/OS, z/VM, and CMS, originally designed by Mike Cowlishaw at IBM in the 1980s and still actively maintained as part of the z/OS base. It integrates tightly with TSO and ISPF, supports the ADDRESS instruction to route commands to different host environments, and can call any TSO command, ISPF service, or program directly from a script. Reach for REXX when you need to automate TSO/ISPF operations, process datasets, submit JCL dynamically, or write utility EXECs that run interactively under TSO — it is the most practical scripting language for mainframe automation.
The TSO/E REXX interpreter shipped with z/OS 3.2 (GA September 2025) remains binary-compatible with prior releases; the everyday snippets below run unchanged on z/OS 2.5 through 3.2. For workflows that need REXX outside an interactive TSO session, IRXJCL and IKJEFT01 still drive batch; the modern off-platform complement is the Zowe CLI (zowe zos-jobs submit, zowe zos-files ...) when you want the same automation from VS Code, a CI runner, or any non-3270 environment. Where REXX historically called ISPEXEC for tables and panels, the newer trend is to keep core logic in REXX and surface UI through a Zowe-driven VS Code extension instead of a 3270 panel.
Script skeleton
Every REXX EXEC must begin with a comment (/* REXX */) in the very first line — this is how z/OS identifies the file as REXX rather than JCL or CLIST. arg reads positional parameters passed from TSO or a calling EXEC; say writes to the terminal; exit <rc> terminates with a return code.
/* REXX - My script description */
address TSO
arg parm1 parm2
if parm1 = '' then do
say 'Usage: EX MYSCRIPT parm1 parm2'
exit 8
end
say 'Processing:' parm1
exit 0
Variables and strings
REXX is typeless — every variable is a string, and arithmetic is performed by interpreting string values as numbers when needed. String concatenation is done by juxtaposition (adjacent tokens) or the explicit || operator; built-in functions like substr, left, right, pos, and translate cover most text manipulation needs without external libraries.
name = 'Alice Dev'
count = 42
pi = 3.14159
/* Concatenation */
msg = 'Hello,' name'!' /* Hello, Alice Dev! */
msg = 'Hello,' || name || '!' /* explicit concat */
/* Substring */
s = 'ABCDEFGH'
say substr(s, 3, 4) /* CDEF */
say left(s, 3) /* ABC */
say right(s, 3) /* FGH */
/* Length, pos, upper */
say length(s) /* 8 */
say pos('DEF', s) /* 4 */
say translate(s) /* uppercase */
Conditionals & loops
REXX IF/THEN/ELSE uses do...end blocks for multi-statement branches; unlike many languages, the THEN keyword is required even for single-line bodies. DO WHILE tests the condition before each iteration, DO UNTIL tests after, and DO FOREVER with LEAVE is the standard idiom for event-driven loops.
/* IF/THEN/ELSE */
if rc = 0 then
say 'OK'
else if rc = 4 then
say 'Warning'
else do
say 'Error RC='rc
exit rc
end
/* DO loop */
do i = 1 to 10
say i
end
/* DO WHILE */
do while condition
...
end
/* DO UNTIL */
do until rc <> 0
...
end
/* DO FOREVER */
do forever
if done then leave
end
/* SELECT (case) */
select
when rc = 0 then say 'OK'
when rc = 4 then say 'Warning'
otherwise say 'Error' rc
end
TSO commands from REXX
ADDRESS TSO sets the default host environment so that quoted strings on their own are routed to TSO as commands rather than evaluated as REXX expressions. Check rc immediately after each TSO command — TSO commands do not raise REXX exceptions on failure, they just set a non-zero return code.
/* Issue TSO command */
address TSO "LISTCAT ENT('MY.*.DATA')"
/* Allocate a file */
address TSO "ALLOC DA('MY.FILE') SHR"
/* Submit job */
address TSO "SUBMIT 'MY.JCL(MYJOB)'"
/* Check return code */
address TSO "LISTCAT ENT('MY.DATA') ALL"
if rc > 0 then say 'Dataset not found, RC='rc
Read a dataset line by line
The standard z/OS REXX pattern is to allocate the dataset to a file handle with ALLOC, then use linein('//DD:ddname') inside a do while lines(...) loop to read records one at a time, and finally FREE the allocation when done. This works for any sequential dataset or PDS member.
/* REXX - Read and process a sequential dataset */
address TSO "ALLOC F(INFILE) DA('MY.INPUT.FILE') SHR REUSE"
do while lines('//DD:INFILE') > 0
line = linein('//DD:INFILE')
if left(line,1) = '*' then iterate /* skip comments */
say 'Processing:' line
end
address TSO "FREE F(INFILE)"
Write a dataset
lineout('//DD:ddname', text) appends a record to the allocated DD; calling it with no second argument flushes and closes the file. Always pair ALLOC with FREE to release the DD name, especially in long-running EXECs that open multiple datasets.
address TSO "ALLOC F(OUTFILE) DA('MY.OUTPUT') SHR REUSE"
call lineout '//DD:OUTFILE', 'Header line'
call lineout '//DD:OUTFILE', 'Data line 1'
call lineout '//DD:OUTFILE', 'Data line 2'
call lineout '//DD:OUTFILE' /* flush/close */
address TSO "FREE F(OUTFILE)"
Call ISPF services
ADDRESS ISPEXEC routes subsequent commands to the ISPF dialog manager, enabling REXX to display panels, read and write dialog variables (VGET/VPUT), and invoke ISPF Edit or Browse programmatically. This only works when the EXEC runs under an ISPF session; calling ISPEXEC services from a plain TSO READY prompt will return an error.
address ISPEXEC
/* Display a panel */
"DISPLAY PANEL(MYPANEL)"
/* Set/get dialog variables */
"VPUT (VAR1 VAR2) SHARED"
"VGET (VAR1 VAR2) SHARED"
/* Edit a dataset */
"EDIT DATASET('MY.SOURCE.LIB(MYMEMBER)')"
/* Browse */
"BROWSE DATASET('MY.OUTPUT.FILE')"
PARSE — the workhorse
PARSE is REXX's signature instruction: a pattern-driven string-decomposition statement that handles input from many different sources. Each PARSE flavour names a source (VAR, PULL, ARG, VALUE, SOURCE, VERSION, EXTERNAL, NUMERIC) and a template made of variable names, literal patterns, positional anchors, and skip tokens.
| Flavour | Source |
|---|---|
PARSE VAR name | Decompose an existing variable |
PARSE PULL | Read from the queue (or terminal if empty) |
PARSE ARG | Decompose the argument string passed to the EXEC or routine |
PARSE VALUE expr WITH | Decompose an arbitrary expression |
PARSE SOURCE | System info — invocation type, command type, name, address, calltype |
PARSE VERSION | REXX interpreter version, language, date |
PARSE EXTERNAL | Read direct from terminal, bypassing the queue |
PARSE NUMERIC | Current NUMERIC settings (DIGITS, FUZZ, FORM) |
PARSE UPPER ... | Same as any of the above but folds input to uppercase first |
PARSE templates
A template is a sequence of variable names interspersed with delimiters or column markers. Variables take whatever lies between delimiters; the period . is a skip placeholder.
line = 'ALICE,42,DEVELOPER,ACTIVE'
parse var line name ',' age ',' role ',' status
say name age role status /* ALICE 42 DEVELOPER ACTIVE */
/* Skip a field with . */
parse var line name ',' . ',' role .
say name role /* ALICE DEVELOPER */
/* Column positions (1-based) */
record = 'ALICE 00042NEW YORK'
parse var record name 1 . 9 age 14 city
say '['name']' '['age']' '['city']'
/* PARSE ARG - command-line style */
parse arg first second rest
say 'first='first 'second='second 'rest='rest
/* PARSE SOURCE - run-time context */
parse source system_env command_type exec_name dd_name ds_name . . addr_env .
say 'Running' exec_name 'from' addr_env
/* PARSE VERSION */
parse version lang ver date
say lang 'version' ver 'released' date
Greedy and word-by-word
PARSE VAR var w1 w2 w3 splits on blanks into the first three words plus the remainder into the last variable. This is the most common shape for command parsing.
parse var 'COMMAND ARG1 ARG2 EXTRA1 EXTRA2' cmd a1 a2 rest
say cmd /* COMMAND */
say a1 /* ARG1 */
say a2 /* ARG2 */
say rest /* EXTRA1 EXTRA2 */
Stems and compound symbols
A stem is a variable whose name ends in a dot — list. — and any expression after the dot creates a compound symbol. Stems are REXX's only data structure; they behave like sparse associative arrays where each "tail" is independent and the bare stem acts as the default value for any unassigned tail.
/* Numeric stem (array-style) */
list.0 = 3 /* convention: .0 holds the count */
list.1 = 'alpha'
list.2 = 'beta'
list.3 = 'gamma'
do i = 1 to list.0
say i list.i
end
/* String tails (associative) */
age.alice = 30
age.bob = 42
say age.alice /* 30 */
/* Default value with bare-stem assign */
status. = 'UNKNOWN' /* every status.x defaults to UNKNOWN */
status.alice = 'ACTIVE'
say status.alice /* ACTIVE */
say status.charlie /* UNKNOWN — never assigned */
/* Clearing a stem */
drop list. /* removes the default and every tail */
Indirect tails
The tail can itself be a variable, allowing computed indexing — the foundation of most stem-as-table patterns.
keys = 'alice bob charlie'
do i = 1 to words(keys)
k = word(keys, i)
parse value '0' with score.k
end
score.alice = 95
say score.alice /* 95 */
say score.bob /* 0 */
The REXX queue
The REXX queue is a system-wide FIFO/LIFO stack maintained by TSO that can be read by PARSE PULL and PULL and written by QUEUE (add to back, FIFO) and PUSH (add to front, LIFO). The queue is the standard inter-EXEC communication channel and the way TSO commands return text to your script.
| Verb | Action |
|---|---|
QUEUE 'text' | Append to the back of the queue (FIFO order) |
PUSH 'text' | Prepend to the front of the queue (LIFO order) |
PULL var | Remove and read from the front (uppercased) |
PARSE PULL var | Remove and read from the front (case preserved) |
QUEUED() | Current depth of the queue |
NEWSTACK | Create a fresh stack; subsequent puts/pulls operate on it |
DELSTACK | Discard the current stack and return to the previous |
/* Build the queue */
queue 'first'
queue 'second'
push 'priority' /* goes to the front */
say queued() /* 3 */
/* Drain the queue */
do queued()
parse pull line
say 'got:' line
end
/* got: priority got: first got: second */
/* Isolate this EXEC's queue from the caller's */
newstack
'LISTC LVL('"ALICE"') OFILE(*)' /* TSO command queues output here */
do queued()
parse pull line
/* process */
end
delstack
Functions library
REXX ships with a deep set of built-in functions; the string and numeric groups cover most everyday needs. Below are the ones that show up in nearly every production EXEC.
String functions
say length('alice') /* 5 */
say left('alice', 8) /* 'alice ' */
say right('alice', 8, '0') /* '000alice' */
say center('hi', 6, '*') /* '**hi**' */
say substr('abcdef', 2, 3) /* 'bcd' */
say pos('cd', 'abcdef') /* 3 */
say lastpos(',', 'a,b,c,d') /* 7 */
say word('one two three', 2) /* 'two' */
say words('one two three') /* 3 */
say wordpos('two', 'one two three') /* 2 */
say subword('a b c d', 2, 2) /* 'b c' */
say strip(' hi ') /* 'hi' */
say strip(' hi ', 'L') /* 'hi ' — leading only */
say strip('***hi***', 'B', '*') /* 'hi' */
say translate('Hello') /* 'HELLO' (default upper) */
say translate('hello', 'abcdef', 'ABCDEF') /* a-f re-mapped */
say copies('-', 10) /* '----------' */
say reverse('alice') /* 'ecila' */
say space('a b c', 1) /* 'a b c' */
say abbrev('ALICE', 'AL', 2) /* 1 — 'AL' is a 2+ char prefix */
say verify('abc123', 'abcdefg') /* 4 — first byte not in 2nd arg */
say insert('NEW', 'abc', 2) /* 'abNEWc' */
say delstr('abcdef', 3, 2) /* 'abef' */
say overlay('XX', 'abcdef', 3) /* 'abXXef' */
Numeric functions
say format(3.14159, , 2) /* '3.14' — 2 decimal places */
say format(1234.5, 8, 2) /* ' 1234.50' */
say trunc(3.97) /* 3 */
say trunc(3.97, 1) /* '4.0' */
say abs(-42) /* 42 */
say sign(-5) /* -1 */
say max(7, 12, 3) /* 12 */
say min(7, 12, 3) /* 3 */
say random(1, 100) /* random integer 1..100 */
say random(, , 12345) /* seed the generator */
/* NUMERIC DIGITS affects everything above */
numeric digits 30
say 2 ** 64 /* full precision big integer */
Conversion functions
say d2x(255) /* 'FF' — decimal -> hex */
say x2d('FF') /* 255 — hex -> decimal */
say d2c(65) /* 'A' — decimal -> char (EBCDIC) */
say c2d('A') /* 65 */
say c2x('A') /* 'C1' — EBCDIC code for A */
say x2c('C1') /* 'A' */
say b2x('11110000') /* 'F0' */
say x2b('F0') /* '11110000' */
say date() /* dd Mon yyyy */
say date('S') /* yyyymmdd — sortable */
say date('U') /* mm/dd/yy */
say date('J') /* yyddd Julian */
say time() /* hh:mm:ss */
say time('L') /* hh:mm:ss.ffffff (long) */
say time('R') /* elapsed since timer reset */
DO loop forms
The DO instruction is REXX's loop construct, and it accepts several mutually exclusive control clauses. They can also be combined with WHILE/UNTIL and OVER/FOR for more nuanced iteration.
/* Counted */
do i = 1 to 10
say i
end
/* Counted with step */
do i = 100 to 1 by -10
say i
end
/* Counted with FOR limit */
do i = 1 to 1000 by 1 for 5
say i /* prints 1..5, stops at FOR */
end
/* WHILE - test before */
do while queued() > 0
parse pull line
end
/* UNTIL - test after */
do until rc <> 0
address TSO "LISTDS '"dsn"'"
end
/* FOREVER + LEAVE */
do forever
call check_status
if status = 'DONE' then leave
end
/* OVER - iterate the tails of a stem */
do k over names.
say k '=>' names.k
end
/* Nested - LEAVE / ITERATE target the nearest loop, or name it */
do outer = 1 to 5
do inner = 1 to 5
if condition then leave outer
if other then iterate outer
end
end
SELECT — multi-way branch
SELECT...WHEN...OTHERWISE...END is the REXX equivalent of switch/case. Each WHEN takes a boolean expression (not a value to match), and the first true branch runs. OTHERWISE is required if no WHEN would match — leaving it out and falling through produces SYNTAX RC=42 at run time, not at parse time.
select
when rc = 0 then say 'OK'
when rc < 4 then say 'Warning'
when rc < 8 then say 'Error'
when rc < 12 then say 'Severe'
otherwise say 'Fatal RC='rc
end
/* Multi-statement branches need DO...END */
select
when status = 'NEW' then do
say 'creating'
call create_record
end
when status = 'UPDATE' then do
say 'updating'
call update_record
end
otherwise nop /* no-op — legal, but OTHERWISE still required */
end
Functions and subroutines
REXX routines are defined with a label (name:) followed by code that returns with RETURN [expr]. The difference between a function and a subroutine is how it's called: name(args) returns a value (function); call name args returns control with the result available in RESULT (subroutine).
say add(3, 5) /* 8 */
call greet 'Alice' /* call form */
say result /* 'Hello Alice' */
exit
/* Function */
add: procedure
parse arg a, b
return a + b
/* Subroutine */
greet:
parse arg name
return 'Hello' name
PROCEDURE EXPOSE
By default a routine sees all caller variables. PROCEDURE isolates the routine's variable scope; PROCEDURE EXPOSE var1 var2 stem. selectively re-exposes named variables and stems. Without it, every routine can accidentally trample any variable in the caller.
count = 0
call increment
say count /* 1 — caller's count was changed */
increment:
count = count + 1
return
/* Better: isolate, expose explicitly */
count = 0
call increment2
say count /* 1 — still updated, but isolated */
increment2: procedure expose count
count = count + 1
return
Recursion
REXX routines are naturally recursive; each call gets its own stack frame.
say factorial(5) /* 120 */
exit
factorial: procedure
parse arg n
if n <= 1 then return 1
return n * factorial(n - 1)
Error handling
REXX's exception model uses SIGNAL ON/SIGNAL OFF to install handlers for specific conditions. When a condition fires, control jumps to a labeled handler; CONDITION() returns details about why.
| Condition | Fires on |
|---|---|
ERROR | Host command returned non-zero (>0) and SIGNAL ON ERROR is active |
FAILURE | Host command returned negative RC (catastrophic failure) |
HALT | Attention key / break interrupt |
SYNTAX | REXX syntax or semantic error |
NOVALUE | Reference to an uninitialised variable |
LOSTDIGITS | Numeric result lost precision |
NOTREADY | I/O stream signalled not ready |
signal on error
signal on syntax
signal on novalue
address TSO "DELETE 'ALICE.MISSING.DS'" /* triggers ERROR if RC>0 */
say 'this line still runs - SIGNAL is one-shot per type'
x = undef + 1 /* triggers NOVALUE */
exit
error:
say 'Host command failed: RC='rc 'at line' sigl
say 'Source:' condition('D')
exit 8
syntax:
say 'Syntax error on line' sigl ':' errortext(rc)
exit 12
novalue:
say 'Uninitialised variable at line' sigl
exit 16
SIGL holds the source line number where the condition fired; RC holds the host command's return code for ERROR/FAILURE; CONDITION('D') returns extra description text.
CALL ON instead of SIGNAL ON
CALL ON ERROR NAME handler is a less disruptive alternative — instead of an unconditional jump, it calls the handler as a subroutine. The handler can RETURN and execution resumes at the next instruction.
call on error name retry_handler
address TSO "ALLOC F(SYSUT1) DA('ALICE.MAYBE.MISSING') SHR"
say 'continuing' /* runs even if handler was invoked */
exit
retry_handler:
say 'allocation failed RC='rc '- retrying with NEW'
address TSO "ALLOC F(SYSUT1) DA('ALICE.MAYBE.MISSING') NEW",
"SPACE(1,1) TRACKS RECFM(F B) LRECL(80)"
return
EXECIO — file I/O
EXECIO is the TSO command for batch-level file I/O — read N records at once into a stem, or write a stem out to a file. EXECIO * means "all remaining". Use EXECIO when you need bulk transfers; use LINEIN/LINEOUT for record-at-a-time streaming.
| Form | Meaning |
|---|---|
EXECIO n DISKR ddname (STEM x. | Read N records from ddname into x.1 .. x.N |
EXECIO * DISKR ddname (STEM x. | Read all records into x.; x.0 holds count |
EXECIO n DISKR ddname (FINIS | Read N and close the DD |
EXECIO n DISKW ddname (STEM x. | Write x.1..x.N to ddname |
EXECIO * DISKW ddname (STEM x. FINIS | Write all and close |
EXECIO n DISKRU ddname (STEM x. | Read for update — needed to write back to same record |
/* Read entire dataset into a stem */
address TSO "ALLOC F(IN) DA('ALICE.INPUT') SHR REUSE"
"EXECIO * DISKR IN (STEM rec. FINIS"
say 'Read' rec.0 'records'
do i = 1 to rec.0
say rec.i
end
address TSO "FREE F(IN)"
/* Build a stem and write it out */
out.1 = 'HEADER'
out.2 = 'RECORD 1'
out.3 = 'RECORD 2'
out.4 = 'TRAILER'
out.0 = 4
address TSO "ALLOC F(OUT) DA('ALICE.OUTPUT') OLD REUSE"
"EXECIO * DISKW OUT (STEM out. FINIS"
address TSO "FREE F(OUT)"
Reading and writing the same dataset
To rewrite a record in place, use DISKRU (read for update). After reading record N, the next DISKW rewrites the same record.
address TSO "ALLOC F(DAT) DA('ALICE.MASTER') OLD REUSE"
do i = 1 to 100
"EXECIO 1 DISKRU DAT"
parse pull rec
if substr(rec, 1, 5) = 'OLD ' then
rec = overlay('NEW ', rec, 1, 5)
push rec
"EXECIO 1 DISKW DAT"
end
"EXECIO 0 DISKW DAT (FINIS"
address TSO "FREE F(DAT)"
ADDRESS environments
The ADDRESS instruction selects the host command environment for unquoted command strings. Different environments accept different syntax: TSO for TSO commands, ISPEXEC for ISPF services, ISREDIT for edit macros, MVS for restricted MVS commands, LINKMVS/LINKPGM/ATTCHMVS for linking to non-REXX programs.
| Environment | Use for |
|---|---|
TSO | TSO/E commands — LISTC, ALLOC, SUBMIT, DELETE, … |
ISPEXEC | ISPF dialog services — DISPLAY, VGET/VPUT, BROWSE, EDIT |
ISREDIT | ISPF edit macros — only valid inside an Edit session |
MVS | A small set of MVS commands — IDENTIFY, ATTACH, LINK |
LINKMVS | Call a load module by name, pass arguments by reference |
LINKPGM | Call a load module with R1 parameter list (no MVS conventions) |
ATTCHMVS | Attach a subtask (asynchronous) |
SYSCALL | UNIX System Services callable services |
(default) | Whatever environment was active when the EXEC was loaded |
/* Switch environments mid-script */
address TSO
"ALLOC F(IN) DA('ALICE.INPUT') SHR"
address ISPEXEC
"VGET (ZUSER ZAPPLID)"
say 'User' ZUSER 'in application' ZAPPLID
/* One-shot - only this command runs in the other env */
address TSO "LISTDS '"ds"'"
address ISPEXEC "DISPLAY PANEL(ALICE01)" /* back to TSO after */
Calling external programs with LINKMVS
LINKMVS pgmname argname1 argname2 ... invokes a load module and passes REXX variables by reference; the program can modify them in place. The program must be in STEPLIB, JOBLIB, LINKLST, or TASKLIB.
input = 'hello'
output = copies(' ', 80)
address LINKMVS "ALICEPGM input output"
say 'program returned:' strip(output)
ISPF services
ADDRESS ISPEXEC opens the full ISPF dialog services API — panels, variables, tables, file tailoring, edit recovery. Below are the most-used services; see ispf-edit for editor-specific macros.
Dialog variables — VGET/VPUT
ISPF maintains three variable pools: the function pool (private to the current dialog), the shared pool (shared with parent dialogs), and the profile pool (persisted across sessions per user/application). VGET copies from a pool into the REXX function pool; VPUT copies REXX variables into a pool.
address ISPEXEC
/* Read profile variables */
"VGET (ZUSER ZSCREEN ZAPPLID) SHARED"
say 'User='ZUSER 'Screen='ZSCREEN
/* Write to the shared pool so child dialogs can read them */
JOBNAME = 'ALICEJ01'
JOBOWNER = 'ALICE'
"VPUT (JOBNAME JOBOWNER) SHARED"
/* Profile pool - persists across logon */
"VPUT (LASTRUN) PROFILE"
Display a panel
address ISPEXEC
"DISPLAY PANEL(ALICEP01)"
if rc = 8 then say 'User pressed END'
/* DISPLAY with message */
zedsmsg = 'Saved.'
zedlmsg = 'Job submitted successfully as ALICEJ01.'
"SETMSG MSG(ISRZ001)"
"DISPLAY PANEL(ALICEP01)"
Tables — quick example
ISPF tables are RAM-resident keyed tables, optionally saved to disk. Use them for any per-row data your dialog needs to manage interactively.
address ISPEXEC
"TBCREATE MYTAB KEYS(USERID) NAMES(JOBNAME STATUS) NOWRITE"
USERID = 'ALICE'; JOBNAME = 'ALICEJ01'; STATUS = 'ACTIVE'
"TBADD MYTAB"
USERID = 'BOB'; JOBNAME = 'BOBJ01'; STATUS = 'HELD'
"TBADD MYTAB"
"TBTOP MYTAB"
do forever
"TBSKIP MYTAB"
if rc <> 0 then leave
say USERID JOBNAME STATUS
end
"TBCLOSE MYTAB"
Edit macros — ADDRESS ISREDIT
Inside an Edit macro (a REXX EXEC invoked from Edit), use ADDRESS ISREDIT to drive the editor.
/* REXX edit macro - uppercase all lines */
address ISREDIT
"MACRO"
"(LASTLN) = LINENUM .ZLAST"
do i = 1 to LASTLN
"(LINE) = LINE" i
"LINE" i "= '"translate(LINE)"'"
end
REXX in batch — IKJEFT01
To run a REXX EXEC from JCL, use TSO in batch via PGM=IKJEFT01 (or IKJEFT1B which never abends on non-zero RC). SYSPROC and SYSEXEC are the search libraries for EXECs; SYSTSIN provides the TSO commands; SYSTSPRT captures the output.
//ALICEJ11 JOB (ACCT),'RUN REXX',CLASS=A,MSGCLASS=X,NOTIFY=&SYSUID
//RUN EXEC PGM=IKJEFT01,DYNAMNBR=100
//SYSPROC DD DSN=ALICE.REXX.LIB,DISP=SHR
//SYSEXEC DD DSN=ALICE.REXX.LIB,DISP=SHR
//SYSTSPRT DD SYSOUT=*
//SYSTSIN DD *
%ALICEX01 PARM1 PARM2
/*
The leading % tells TSO to search SYSEXEC first (REXX); without it, ALICEX01 would resolve to a CLIST or a load module if those exist. DYNAMNBR=100 raises the dynamic-allocation limit so the EXEC can ALLOC many DDs.
Real-world recipes
Parse a LISTCAT and submit one job per dataset
A common automation: run LISTC LVL('ALICE') to find all datasets under a qualifier, parse the output, and submit a per-dataset job.
/* REXX - fan out a job for each dataset under ALICE */
address TSO
newstack
"LISTC LVL('ALICE') OFILE(*)"
do queued()
parse pull line
if word(line,1) = 'NONVSAM' & word(line,2) = '-------' then do
dsn = word(line, 3)
say 'Submitting for' dsn
queue "//ALICEJ"right(j+100,3,0) "JOB (ACCT),'PROC',CLASS=A,MSGCLASS=X"
queue "//STEP01 EXEC PGM=ALICEPGM"
queue "//INFILE DD DSN="dsn",DISP=SHR"
queue "//SYSOUT DD SYSOUT=*"
"SUBMIT * END(@)"
j = j + 1
end
end
delstack
Error-handling wrapper
A reusable wrapper around any TSO command that captures and classifies the result.
call tso "LISTDS 'ALICE.MAYBE'"
if result = 'NOTFOUND' then say 'Allocate it first'
exit
tso: procedure
parse arg cmd
signal on syntax name tso_syntax
address TSO
newstack
cmd "OUTPUT(LINES.)"
saverc = rc
delstack
select
when saverc = 0 then return 'OK'
when saverc = 12 then return 'NOTFOUND'
when saverc >= 16 then return 'FATAL'
otherwise return 'WARN'
end
tso_syntax:
return 'SYNTAX'
Batch-friendly EXEC with arg validation
A pattern for production EXECs that may be invoked interactively or from a JCL SYSTSIN. Validate arguments, set up environment, and exit with a clear RC.
/* REXX - daily ETL driver - %ALICEETL hlq date */
parse upper arg hlq date .
if hlq = '' | date = '' then do
say 'Usage: %ALICEETL hlq date(yyyymmdd)'
exit 4
end
if length(date) <> 8 | datatype(date,'W') = 0 then do
say 'Invalid date:' date
exit 8
end
address TSO
"ALLOC F(IN) DA('"hlq".DAILY."date"') SHR REUSE"
if rc <> 0 then do
say 'Cannot allocate input - RC='rc
exit 12
end
"ALLOC F(OUT) DA('"hlq".PROC."date"') NEW CATALOG",
"SPACE(50,10) CYL RECFM(F B) LRECL(200)"
call do_work
"FREE F(IN OUT)"
exit 0
do_work:
"EXECIO * DISKR IN (STEM line. FINIS"
do i = 1 to line.0
/* transform... */
out.i = translate(line.i)
end
out.0 = line.0
"EXECIO * DISKW OUT (STEM out. FINIS"
return
Build dynamic JCL and submit via internal reader
SUBMIT * END(@) reads JCL from the queue, terminated by @ in column 1, and pushes it onto the JES internal reader.
address TSO
queue "//ALICEJ12 JOB (ACCT),'GEN JOB',CLASS=A,MSGCLASS=X,NOTIFY=ALICE"
queue "//STEP01 EXEC PGM=IEFBR14"
queue "//DD1 DD DSN=ALICE.SCRATCH."date('S')","
queue "// DISP=(NEW,CATLG,DELETE),"
queue "// SPACE=(CYL,(1,1)),"
queue "// DCB=(RECFM=FB,LRECL=80)"
queue "@"
"SUBMIT * END(@)"
Read SDSF spool from REXX (REXX/SDSF)
ISFEXEC is the SDSF REXX interface — same actions as the SDSF panel but scripted. Add ISFRC checking to detect errors.
rc = isfcalls('ON')
isfprefix = 'ALICEJ*'
address SDSF "ISFEXEC ST"
do i = 1 to JNAME.0
say JNAME.i JOBID.i STATUS.i RETCODE.i
end
call isfcalls 'OFF'
Common pitfalls
/* REXX */comment missing or wrong column — the very first line must start with a REXX comment containingREXXsomewhere in it. Otherwise z/OS treats the member as a CLIST and you get cryptic IKJ messages.PROCEDUREvs noPROCEDURE— withoutPROCEDURE, every label-routine sees every caller variable. One stray assignment can clobber a caller's state. UsePROCEDURE EXPOSEdeliberately.NUMERIC DIGITS 9is the default — any calculation that exceeds 9 significant digits silently loses precision unless you raise it (NUMERIC DIGITS 30is a safe default for arbitrary integers).- Stem
.0is just a convention — REXX doesn't auto-maintain it. If you grow a stem by assignment, update.0yourself or useEXECIOwhich sets it for you. PULLuppercases;PARSE PULLdoes not —PULL vartranslates the queue head to uppercase. Always usePARSE PULLwhen case matters.- EXECIO
FINISis mandatory — without it the DD remains open between EXECIOs in the same step. The next EXECIO continues where the last one left off; reopen by FREEing and ALLOCing again. SIGLvsRC—SIGLis the source line number where the lastSIGNALorCALLhappened;RCis the return code of the last host command. They are independent — confusing them is a classic debugging time-sink.ADDRESS ISPEXECoutside ISPF — calling ISPF services from plain TSO READY returnsRC=20. Detect withif sysvar(sysispf) <> 'ACTIVE' then ...before doing dialog work.- Quoting traps inside
ADDRESS TSO— REXX evaluates the string before passing it. To embed a single quote in a TSO command, double it ('') or break the string and re-concatenate."LISTC ENT('"dsn"')"is the canonical pattern. - Hidden REXX environment —
ADDRESSsets the default environment, but a quoted command on its own line uses that default. If you mix interpreters (TSO and ISPEXEC) make sure each command is in the rightADDRESSblock. - REXX queue leaks — uncleared queue entries from a child EXEC bleed into the caller. Always
NEWSTACK/DELSTACKaround any sub-EXEC that produces output, or drain explicitly. DROPvs=(empty string) —DROP varremoves the variable soSYMBOL('VAR') = 'LIT'; assigning''keeps it defined as a zero-length string. They behave the same in arithmetic but differ inSYMBOL()andNOVALUE.PARSE ARGvsARG—ARGalone uppercases;PARSE ARGpreserves case. Same rule asPULL/PARSE PULL.- REXX vs CLIST — both run under TSO, but the languages are unrelated. A
.CLISTfile is processed by the CLIST interpreter (proc-like, distinct syntax) and a.REXXor.EXECfile starts with/* REXX */. Don't mix. - Forgetting
DYNAMNBR=on IKJEFT01 — the default DYNAMNBR is small (about 20). Any EXEC that ALLOCs many DDs getsIKJ56228I"DATA SET NOT ALLOCATED, TOO MANY DATA SETS". SetDYNAMNBR=100or higher in the EXEC card.
See tso-ispf for the broader TSO and ISPF environment.