You want to run a COBOL program on a mainframe. You know which program to run. You know which files it needs to read. You know where you want the output to go. But the operating system does not know any of this until you tell it explicitly, in a specific language, before execution begins.
That language is JCL: Job Control Language. It is the script you write to tell z/OS what to run, what data to use, where to put the results, and what to do if something goes wrong. Every batch job on a mainframe, every overnight processing run, every utility execution is driven by a JCL stream. You cannot submit work to JES2 without JCL.
This post assumes you have read Architecture: JES and Batch Processing. It covers the three statement types, the parameters that matter most in practice, cataloged procedures, conditional execution, and the dataset lifecycle model that makes JCL work the way it does.
The Basic Shape of a JCL Job
Every JCL job has the same skeleton. A single JOB statement at the top identifies the job. Below it, one or more EXEC statements each define one step: one program to run. Under each EXEC, one or more DD statements connect the program's logical file references to physical datasets on disk, tape, or the spool.
All JCL statements begin with `//` in columns 1 and 2. A comment line begins with `//*`. A continuation line begins with `//` followed by at least one space, then more parameters. The fixed-column layout is a direct inheritance from 80-column punched cards: JCL was designed for cards, and the column discipline has never fully left.
//PAYROLL JOB (ACCT01),'PAYROLL RUN',CLASS=A,
// MSGCLASS=X,NOTIFY=&SYSUID
//*
//* Monthly payroll processing job
//*
//STEP1 EXEC PGM=PAYCALC
//EMPFILE DD DSN=HR.EMPLOYEES.MASTER,DISP=SHR
//PAYFILE DD DSN=HR.PAYROLL.CURRENT,DISP=SHR
//OUTFILE DD DSN=HR.PAYROLL.OUTPUT,
// DISP=(NEW,CATLG,DELETE),
// UNIT=SYSDA,
// SPACE=(CYL,(10,5)),
// DCB=(RECFM=FB,LRECL=200,BLKSIZE=27800)
//SYSPRINT DD SYSOUT=*
//
//STEP2 EXEC PGM=PAYPRINT,COND=(4,LT,STEP1)
//INFILE DD DSN=HR.PAYROLL.OUTPUT,DISP=SHR
//REPORT DD SYSOUT=A
This job has two steps. Step 1 runs `PAYCALC`, reading two existing files and creating a new output file. Step 2 runs `PAYPRINT`, but only if Step 1 returned a code less than 4. Each element of this structure has specific rules, and none of it is guesswork.
The JOB Statement
The JOB statement is the first line of every job. Its name field (the first word after `//`) is the job name: up to eight characters, starting with a letter. The job name is how operators and tools identify the job in the spool.
The most important JOB parameters:
- `CLASS=`: the job class, a single character that routes the job to the right initiators. Covered in the JES post: class A might be production, class T might be test.
- `MSGCLASS=`: the output class for the job log and system messages. A common value is `X`, which holds the output on spool for the submitter to browse before it is processed by an output writer.
- `NOTIFY=&SYSUID`: sends a TSO message to the submitting user when the job completes. The `&SYSUID` is a JES symbolic that substitutes the submitter's TSO user ID automatically.
- `PRTY=`: job priority, 0 to 15. Higher wins.
- `TIME=`: a CPU time limit for the entire job, in minutes and seconds. If the job exceeds this, JES issues a `S322` abend. Protects against runaway programs.
- `REGION=`: maximum virtual storage for each step, e.g. `REGION=0M` lets z/OS determine the limit dynamically. Undersizing REGION causes `S80A` abends when a program cannot get the memory it needs.
- `TYPRUN=HOLD`: submits the job but holds it in the queue until an operator releases it.
The EXEC Statement
Each EXEC statement defines one step. The step name (the word between `//` and `EXEC`) is up to eight characters and must be unique within the job. The step name matters: other statements reference it when passing datasets between steps or setting conditional logic.
There are two forms of EXEC:
- `EXEC PGM=programname`: runs a specific load module directly. The program must be in a dataset accessible via the system's load library concatenation or a `STEPLIB` DD statement in the same step.
- `EXEC PROC=procname` (or just `EXEC procname`): invokes a cataloged procedure, a reusable set of JCL stored in a partitioned dataset. Covered below.
Key EXEC parameters:
- `PARM=`: passes a character string directly to the program at startup. In COBOL, the program receives this in its `PARM` field. Useful for passing runtime flags, date overrides, or environment indicators without modifying the program itself.
- `COND=`: controls whether the step runs based on return codes from earlier steps. Discussed in detail in the conditional execution section below.
- `TIME=`: a CPU time limit for this step specifically, overriding the job-level limit.
- `STEPLIB DD`: not an EXEC parameter itself, but a special DD statement that adds a private load library to the search path for this step. If `STEPLIB` is coded, z/OS looks there for the program before searching the system libraries.
The DD Statement
The DD statement is where most of JCL's complexity lives. Every file a program reads or writes must have a DD statement. The DD name (the word after `//`) is the logical name the program uses in its file definitions. In a COBOL batch program, the `SELECT` statement maps a logical file name to a `DDNAME`, and JCL's DD name must match exactly.
A DD statement can describe a permanent dataset on disk, a temporary dataset that exists only for the duration of the job, inline data embedded in the JCL stream, or a spool output destination.
DSN: The Dataset Name
`DSN=` (or `DSNAME=`) specifies the physical name of the dataset. Dataset names follow a dot-separated hierarchical convention: `HR.EMPLOYEES.MASTER` has three qualifiers. The first qualifier is typically an organization or application prefix, controlled by RACF security. The full name can be up to 44 characters.
Special DSN forms:
- `DSN=&&TEMP`: a temporary dataset. The double ampersand prefix tells z/OS to generate a unique system name. Temporary datasets are automatically deleted at job end regardless of the DISP setting.
- `DSN=*.STEP1.DDNAME`: a backward reference. Refers to the dataset allocated to `DDNAME` in `STEP1`, without repeating its full definition. Used to pass a dataset created in one step to a later step.
- `DSN=MY.PDS(MEMBER)`: accesses a specific member of a partitioned dataset (PDS), the mainframe equivalent of a file within a directory.
- `DSN=MY.GDG(0)`: accesses the current generation of a Generation Data Group. Covered below.
DISP: The Disposition
`DISP=` is the most critical DD parameter. It has three positional values:
DISP=(status, normal-disposition, abnormal-disposition)
Status tells z/OS the current state of the dataset:
- `NEW`: create a new dataset. Space allocation parameters are required.
- `OLD`: the dataset exists; allocate it with exclusive access. No other job can open it simultaneously.
- `SHR`: the dataset exists; allocate it in shared mode. Multiple jobs can read it concurrently. Use this for any read-only access.
- `MOD`: position the write pointer at the end of the dataset, for appending records. If the dataset does not exist, behaves like `NEW`.
Normal disposition tells z/OS what to do with the dataset when the step completes successfully:
- `CATLG`: add the dataset to the catalog so it can be found by name in future jobs. Always use this for new permanent datasets.
- `KEEP`: retain the dataset on disk but do not catalog it. Rarely used in modern practice.
- `DELETE`: delete the dataset. Use this for temporary work files you want cleaned up.
- `PASS`: make the dataset available to subsequent steps in the same job without closing it. Use this for datasets passed between steps.
Abnormal disposition tells z/OS what to do if the step abends. Separating normal and abnormal dispositions is what makes `DISP=(NEW,CATLG,DELETE)` the standard idiom for creating output files: on success, catalog it; on failure, delete the partial output rather than leaving garbage in the catalog.
Space Allocation
New datasets require a `SPACE=` parameter that tells z/OS how much disk space to allocate. The syntax is:
SPACE=(unit,(primary,secondary,directory))
Unit is the allocation unit: `TRK` (tracks), `CYL` (cylinders), or a block size in bytes. Primary is the initial allocation. Secondary is the amount added each time the dataset needs to extend beyond its current allocation, up to 15 times. Directory is only for partitioned datasets and specifies the number of 256-byte directory blocks (each holds roughly 6 to 8 member entries).
The most common mistake with SPACE is undersizing. A dataset that runs out of primary and all 15 secondary extents will abend the step with `B37` (out of space on the volume) or `E37` (out of extents). Getting SPACE right requires knowing roughly how much data the program will produce.
DCB: Dataset Characteristics
The `DCB=` (Data Control Block) parameter describes the record format of the dataset:
- `RECFM=FB`: fixed-length, blocked records. The most common format for COBOL batch files. All records are the same length; multiple records are packed into a physical block for I/O efficiency.
- `RECFM=VB`: variable-length, blocked. Records have different lengths; each record has a 4-byte header indicating its length.
- `LRECL=`: the logical record length in bytes.
- `BLKSIZE=`: the physical block size. For FB records, `BLKSIZE` must be a multiple of `LRECL`. Larger block sizes mean fewer physical I/O operations and better throughput. If omitted, z/OS determines an optimal block size automatically.
Conditional Execution
A batch job rarely runs all steps unconditionally. If step 1 fails, steps 2 and 3 should not run. If the extract finds no records, the sort and load steps are pointless. JCL provides two mechanisms for conditional execution.
The COND Parameter (Legacy)
The `COND=` parameter on an EXEC statement tells z/OS to skip the step if a condition is true. The syntax is `COND=(code,operator,stepname)`, meaning: skip this step if the return code from `stepname` satisfies `operator code`.
//STEP2 EXEC PGM=PAYPRINT,COND=(4,LT,STEP1)
This means: skip STEP2 if 4 is less than STEP1's return code. In other words, run STEP2 only if STEP1 returned 4 or less. The double-negative logic (`skip if`) is a persistent source of confusion for developers new to JCL. Reading it as "bypass this step if the condition is met" is the correct mental model.
The IF/THEN/ELSE Construct (Modern)
The modern alternative, introduced in MVS/ESA, is far more readable:
// IF (STEP1.RC = 0) THEN
//STEP2 EXEC PGM=PAYPRINT
//INFILE DD DSN=HR.PAYROLL.OUTPUT,DISP=SHR
// ELSE
//STEP2ERR EXEC PGM=ERRNOTIFY
// ENDIF
The IF/THEN/ELSE construct supports standard comparison operators (`=`, `<`, `>`, `<=`, `>=`, `<>`) and boolean logic (`AND`, `OR`, `NOT`). It can test `ABEND` conditions as well as return codes: `IF (STEP1.ABEND) THEN` runs the ELSE branch if step 1 abended rather than returning normally. Most modern JCL uses IF/THEN/ELSE; COND remains widespread in older production JCL that has not been touched in decades.
Cataloged Procedures
In practice, most production jobs do not contain raw JCL from top to bottom. They invoke cataloged procedures (procs): pre-written, parameterized JCL stored as members of a partitioned dataset (typically `SYS1.PROCLIB` or a site-specific proc library).
A proc encapsulates the standard steps for a common operation. The compile-and-link proc for COBOL programs contains the steps to invoke the compiler, the linkage editor, and any standard pre-processing. Rather than repeating those steps in every compile job, developers write a one-line EXEC that calls the proc:
//COMPILE EXEC COBUCL,
// PARM.COB='RENT,APOST',
// PARM.LKED='LIST,MAP'
A proc can define symbolic parameters: placeholders prefixed with `&` that the calling job can override. The proc might define `&DSN` as the input dataset name. The calling job passes `DSN=MY.SOURCE.COBOL` on the EXEC statement, which substitutes throughout the proc. This makes a single proc reusable across any number of programs.
The calling job can also override individual DD statements within a proc by coding a DD with the name `stepname.ddname`. This allows the standard proc to be used while substituting a different input file for a specific run, without modifying the proc itself.
Generation Data Groups
Batch processing often needs to maintain a rolling history of datasets: last month's payroll, the previous week's extract, the last seven daily reports. VSAM and sequential files do not have a built-in versioning concept, but JCL's Generation Data Group (GDG) mechanism provides one.
A GDG is a catalog entry that groups multiple dataset generations under a single base name. Each generation is a separate physical dataset, named with a generation suffix like `G0001V00`, `G0002V00`. The catalog maintains the current generation and a configurable limit on how many generations to keep.
In JCL, generations are referenced with relative notation:
- `DSN=MY.PAYROLL.GDG(0)`: the current (most recent) generation.
- `DSN=MY.PAYROLL.GDG(-1)`: the previous generation.
- `DSN=MY.PAYROLL.GDG(+1)`: a new generation to be created.
A monthly payroll job typically reads `(0)` (last month's base) and writes to `(+1)` (this month's new generation). When the job succeeds, the `(+1)` generation is cataloged and becomes the new `(0)`. If the limit is reached, the oldest generation is either deleted or uncataloged depending on the GDG's `SCRATCH` or `NOEMPTY` parameter. This rotation happens automatically without any JCL changes from month to month.
Special DD Statements
Several DD names have fixed meanings that z/OS and JES2 expect to find in every job:
- `SYSPRINT`: the primary message output DD for most utility programs. Almost every IBM utility writes its processing summary and error messages to `SYSPRINT`. Coding `//SYSPRINT DD SYSOUT=*` is near-universal.
- `SYSIN`: the control statement input for utility programs. IDCAMS, SORT, IEBGENER, and most other system utilities read their instructions from `SYSIN`. Coded as `DD *` for inline data or `DD DSN=` to read from a file.
- `SYSOUT`: program output destined for the spool. `SYSOUT=*` uses the job's `MSGCLASS`. `SYSOUT=A` routes to output class A.
- `STEPLIB`: a private load library searched before the system libraries for this step's program. Essential when running a new version of a program without affecting other jobs on the system.
- `SYSUDUMP` / `SYSABEND`: dump datasets written when a program abends. Coding `DD SYSOUT=*` captures the dump to spool for later analysis. Without this, some dump information may be lost.
What JCL Actually Does at Runtime
When JES2 reads a submitted JCL stream, it does not execute it immediately. It stores the raw JCL on the spool, then runs the converter: a JES2 component that interprets the JCL, expands any called procedures, substitutes symbolic parameters, and builds an internal representation called internal text. This internal text is what the initiator actually uses when the job runs.
The converter catches many JCL errors before execution begins: references to non-existent procedures, missing required parameters, syntactically invalid statements. A job that fails converter processing gets a JCL error status and never executes. The error messages appear in the job's spool output and must be corrected before resubmission.
At execution time, the initiator reads the internal text step by step. Before each step runs, z/OS performs allocation: it opens every dataset referenced in the step's DD statements, allocates space for new datasets, and locks datasets opened with `DISP=OLD`. Only after all allocations succeed does the step program begin running. If any allocation fails (dataset not found, space unavailable, enqueue conflict), the step receives an allocation error before the program ever starts.
Summary
JCL is a contract between the programmer and the operating system. The JOB statement identifies the work and sets its scheduling attributes. The EXEC statements define the sequence of programs to run. The DD statements map logical file names to physical datasets, specifying how each dataset should be created, opened, and disposed of before, during, and after execution.
The DISP parameter is the most consequential single piece of JCL. Getting it right means datasets are created, cataloged, passed, and cleaned up correctly. Getting it wrong means partial output left in the catalog, temporary files never deleted, or production datasets opened exclusively when they should have been shared.
Cataloged procedures reduce repetition and enforce consistency across jobs. Symbolic parameters make them reusable. Conditional execution via IF/THEN/ELSE gives batch jobs the control flow needed to handle the real world: steps that run only when predecessors succeed, error notification steps that run only when they fail.
JCL is not elegant. Its column-based syntax and punched-card heritage make it unlike any other language. But it is explicit, deterministic, and precise: exactly what is needed when you are telling a mainframe what to do with production data in the middle of the night, with no one watching.
Part of the Mainframe Decoded series — IBM Z and z/OS, clearly explained for engineers.