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.

text
/* 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.

text
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.

text
/* 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.

text
/* 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.

text
/* 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.

text
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.

text
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.

FlavourSource
PARSE VAR nameDecompose an existing variable
PARSE PULLRead from the queue (or terminal if empty)
PARSE ARGDecompose the argument string passed to the EXEC or routine
PARSE VALUE expr WITHDecompose an arbitrary expression
PARSE SOURCESystem info — invocation type, command type, name, address, calltype
PARSE VERSIONREXX interpreter version, language, date
PARSE EXTERNALRead direct from terminal, bypassing the queue
PARSE NUMERICCurrent 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.

text
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.

text
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.

text
/* 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.

text
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.

VerbAction
QUEUE 'text'Append to the back of the queue (FIFO order)
PUSH 'text'Prepend to the front of the queue (LIFO order)
PULL varRemove and read from the front (uppercased)
PARSE PULL varRemove and read from the front (case preserved)
QUEUED()Current depth of the queue
NEWSTACKCreate a fresh stack; subsequent puts/pulls operate on it
DELSTACKDiscard the current stack and return to the previous
text
/* 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

text
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

text
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

text
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.

text
/* 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.

text
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).

text
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.

text
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.

text
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.

ConditionFires on
ERRORHost command returned non-zero (>0) and SIGNAL ON ERROR is active
FAILUREHost command returned negative RC (catastrophic failure)
HALTAttention key / break interrupt
SYNTAXREXX syntax or semantic error
NOVALUEReference to an uninitialised variable
LOSTDIGITSNumeric result lost precision
NOTREADYI/O stream signalled not ready
text
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.

text
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.

FormMeaning
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 (FINISRead N and close the DD
EXECIO n DISKW ddname (STEM x.Write x.1..x.N to ddname
EXECIO * DISKW ddname (STEM x. FINISWrite all and close
EXECIO n DISKRU ddname (STEM x.Read for update — needed to write back to same record
text
/* 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.

text
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.

EnvironmentUse for
TSOTSO/E commands — LISTC, ALLOC, SUBMIT, DELETE, …
ISPEXECISPF dialog services — DISPLAY, VGET/VPUT, BROWSE, EDIT
ISREDITISPF edit macros — only valid inside an Edit session
MVSA small set of MVS commands — IDENTIFY, ATTACH, LINK
LINKMVSCall a load module by name, pass arguments by reference
LINKPGMCall a load module with R1 parameter list (no MVS conventions)
ATTCHMVSAttach a subtask (asynchronous)
SYSCALLUNIX System Services callable services
(default)Whatever environment was active when the EXEC was loaded
text
/* 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.

text
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.

text
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

text
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.

text
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.

text
/* 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.

text
//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.

text
/* 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.

text
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.

text
/* 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.

text
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.

text
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

  1. /* REXX */ comment missing or wrong column — the very first line must start with a REXX comment containing REXX somewhere in it. Otherwise z/OS treats the member as a CLIST and you get cryptic IKJ messages.
  2. PROCEDURE vs no PROCEDURE — without PROCEDURE, every label-routine sees every caller variable. One stray assignment can clobber a caller's state. Use PROCEDURE EXPOSE deliberately.
  3. NUMERIC DIGITS 9 is the default — any calculation that exceeds 9 significant digits silently loses precision unless you raise it (NUMERIC DIGITS 30 is a safe default for arbitrary integers).
  4. Stem .0 is just a convention — REXX doesn't auto-maintain it. If you grow a stem by assignment, update .0 yourself or use EXECIO which sets it for you.
  5. PULL uppercases; PARSE PULL does notPULL var translates the queue head to uppercase. Always use PARSE PULL when case matters.
  6. EXECIO FINIS is 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.
  7. SIGL vs RCSIGL is the source line number where the last SIGNAL or CALL happened; RC is the return code of the last host command. They are independent — confusing them is a classic debugging time-sink.
  8. ADDRESS ISPEXEC outside ISPF — calling ISPF services from plain TSO READY returns RC=20. Detect with if sysvar(sysispf) <> 'ACTIVE' then ... before doing dialog work.
  9. 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.
  10. Hidden REXX environmentADDRESS sets 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 right ADDRESS block.
  11. REXX queue leaks — uncleared queue entries from a child EXEC bleed into the caller. Always NEWSTACK/DELSTACK around any sub-EXEC that produces output, or drain explicitly.
  12. DROP vs = (empty string)DROP var removes the variable so SYMBOL('VAR') = 'LIT'; assigning '' keeps it defined as a zero-length string. They behave the same in arithmetic but differ in SYMBOL() and NOVALUE.
  13. PARSE ARG vs ARGARG alone uppercases; PARSE ARG preserves case. Same rule as PULL/PARSE PULL.
  14. REXX vs CLIST — both run under TSO, but the languages are unrelated. A .CLIST file is processed by the CLIST interpreter (proc-like, distinct syntax) and a .REXX or .EXEC file starts with /* REXX */. Don't mix.
  15. Forgetting DYNAMNBR= on IKJEFT01 — the default DYNAMNBR is small (about 20). Any EXEC that ALLOCs many DDs gets IKJ56228I "DATA SET NOT ALLOCATED, TOO MANY DATA SETS". Set DYNAMNBR=100 or higher in the EXEC card.

See tso-ispf for the broader TSO and ISPF environment.

Sources