LearnAI ToolsCareerPractice BuildsPlayContact
Mainframe SystemsBeginner~1.5 hours

Write & Submit a JCL Job

Write a complete JCL job that runs a utility program against a sequential dataset.

JCLDatasetsTSO/ISPF

Overview

Every unit of work on z/OS — whether it is a payroll run, a report, or a one-off file copy — starts life as a JCL job: a small, rigidly-formatted text file that tells the operating system what program to run, what datasets it needs, and what to do with the output. Unlike a shell script, JCL does not execute anything itself; it is a set of instructions that the JES (Job Entry Subsystem) reads, schedules, and hands off to a program one step at a time. Getting comfortable with JCL means getting comfortable with three statement types — `JOB`, `EXEC`, and `DD` — because nearly every job you will ever write, no matter how complex, is built from just those three.

This project builds the simplest complete job that still does real work: one step that runs IBM's IEBGENER utility to copy a sequential dataset from one name to another. IEBGENER is deliberately chosen as the first program to run under JCL because it does exactly one thing — read a sequential file and write it back out — which keeps the focus on the JCL itself rather than on what the program does. By the end you will understand JOB-statement accounting information, `DISP` parameters, DD-to-program name matching, and how the same job is submitted and monitored through TSO/ISPF.

What You'll Build
  • A `//JOBNAME JOB` statement with accounting information, job class, and message class.
  • An `//EXEC PGM=IEBGENER` step that copies a sequential dataset.
  • Input and output `DD` statements with correctly matched `DISP` parameters.
  • A submittable job, ready to hand to JES from the ISPF editor.
  • A walkthrough of monitoring a submitted job's status and reading its output through SDSF.

Prerequisites

  • What a batch job is — a unit of work submitted to run without further interaction, as opposed to an interactive TSO session.
  • Basic dataset naming — a fully-qualified dataset name (DSN) like `USER1.STUDENT.INPUT`, made of period-separated qualifiers.
  • What a sequential dataset is — a file whose records are read and written in physical order, the mainframe equivalent of a flat file.
  • Familiarity with navigating ISPF panels (option 2 = Edit, option 6 = Command entry).
  • The idea of a return code — a small integer a program sets on completion, where 0 means success.

Project Structure

A JCL job is a flat sequence of statements, not a tree of files, so "structure" here means the roles each statement plays and how they connect to each other. The `JOB` statement names and identifies the whole job to JES. The `EXEC` statement names the program the step runs. Every `DD` statement underneath an `EXEC` supplies one dataset (or other resource) that program expects, and the `ddname` on the left has to match a name the program itself looks for — for IEBGENER, that means `SYSUT1` (input), `SYSUT2` (output), `SYSPRINT` (messages), and `SYSIN` (control statements).

StatementRoleKey Parameters
//COPYJOB JOBIdentifies the job to JESaccounting info, CLASS, MSGCLASS, NOTIFY
//STEP1 EXECNames the program this step runsPGM=IEBGENER
//SYSUT1 DDIEBGENER's input datasetDSN=..., DISP=SHR
//SYSUT2 DDIEBGENER's output datasetDSN=..., DISP=(NEW,CATLG,DELETE), SPACE, DCB
//SYSPRINT DDWhere IEBGENER writes its messagesSYSOUT=*
//SYSIN DDControl statements IEBGENER reads (none needed here)DUMMY

Step 1: The JOB Statement

The JOB statement is always the first line of a job, and it is the only statement that names the job itself — everything after it belongs to a step. The positional parameters right after `JOB` are site-defined accounting information (here, a placeholder account number and programmer name); most shops require *something* in that slot even if it is never billed against. `CLASS` tells JES which initiator queue to run the job in — different classes can mean different resource limits or priority. `MSGCLASS` controls where the job's system messages and SYSOUT get routed, and `NOTIFY=&SYSUID` tells JES to send a TSO message back to whoever submitted it once the job finishes, which is how you find out a job has completed without watching a screen.

//COPYJOB JOB (ACCT123),'J SMITH',CLASS=A,MSGCLASS=X,
// NOTIFY=&SYSUID,REGION=0M
//*
//* Job name COPYJOB (max 8 chars), account ACCT123, programmer J SMITH
//* CLASS=A - run in initiator class A (site-defined, ask your admin)
//* MSGCLASS=X - route job log / SYSOUT to class X (usually held for viewing)
//* NOTIFY=&SYSUID - TSO message to the submitting user ID when the job ends
//* REGION=0M - let the job use as much region storage as the system allows
//*
Why does column position matter?

Classic JCL is column-sensitive: the `//` must start in column 1, the name field follows immediately with no space before it, and continuation lines must begin in columns 4-16. A misplaced space is one of the most common reasons a beginner's JCL fails with a JCL error before the job ever runs a program.

Step 2: The EXEC Statement

The EXEC statement is where a job actually does something: it names one program (or, as Project 3 in this course covers, a cataloged PROC) to run as this step. `STEP1` is the step name — arbitrary, but referenced later by other steps that need to check this step's return code (`COND=`) or reuse its datasets. `PGM=IEBGENER` tells JES which program to load and give control to; IEBGENER ships as part of z/OS and its whole job is "copy SYSUT1 to SYSUT2," record for record.

//STEP1 EXEC PGM=IEBGENER
//*
//* STEP1 is this step's name, referenced later by COND= on other steps
//* PGM=IEBGENER - the utility program this step runs; it just copies
//* whatever dataset is on SYSUT1 to whatever dataset is on SYSUT2
//*

Step 3: DD Statements for Input and Output

Every DD statement underneath `STEP1` supplies one resource IEBGENER expects by name. `SYSUT1` is the input — it already exists, so `DISP=SHR` (shared) is correct and lets other jobs read it at the same time. `SYSUT2` is the output — it does not exist yet, so `DISP=(NEW,CATLG,DELETE)` says: create it new, catalog it (register the name so later jobs can find it without knowing which volume it lives on) if the step ends normally, and delete it if the step abends. `SPACE=(TRK,(5,5))` requests 5 tracks with 5 more as a secondary extent if the primary fills up, and the `DCB` (Data Control Block) parameters describe the record format: `RECFM=FB` (fixed-length, blocked), `LRECL=80` (80-byte records, the classic card-image length), `BLKSIZE=8000` (100 records per physical block). `SYSPRINT` just needs somewhere to send messages, so `SYSOUT=*` routes it to the job's own output class. `SYSIN DD DUMMY` tells IEBGENER there are no control statements — copy everything, unchanged.

//SYSUT1 DD DSN=USER1.STUDENT.INPUT,DISP=SHR
//*
//* SYSUT1 = IEBGENER's input dataset. It already exists, so DISP=SHR
//* (shared read access) is correct - do NOT use SHR on something you
//* are about to create or overwrite.
//*
//SYSUT2 DD DSN=USER1.STUDENT.OUTPUT,
// DISP=(NEW,CATLG,DELETE),
// SPACE=(TRK,(5,5),RLSE),
// UNIT=SYSDA,
// DCB=(RECFM=FB,LRECL=80,BLKSIZE=8000)
//*
//* SYSUT2 = IEBGENER's output dataset, created fresh by this step.
//* DISP=(NEW,CATLG,DELETE):
//* NEW - this step creates the dataset
//* CATLG - keep and catalog it if the step ends with condition code 0
//* DELETE - remove it if the step abends, so no half-written file survives
//* SPACE=(TRK,(5,5),RLSE) - 5 tracks primary, 5 secondary, release unused space
//* UNIT=SYSDA - allocate on any available disk (direct access storage)
//* DCB describes the record layout: fixed 80-byte records, blocked 100/block
//*
//SYSPRINT DD SYSOUT=*
//*
//* SYSPRINT = where IEBGENER writes its own run messages (record counts, etc.)
//* SYSOUT=* routes it to this job's normal output class
//*
//SYSIN DD DUMMY
//*
//* SYSIN = IEBGENER's control-statement input. DUMMY means "no control
//* statements" - with none supplied, IEBGENER copies every record as-is.
//*

Step 4: Submit the Job via TSO/ISPF

JCL is just text, so the first step is getting it into a dataset: open the ISPF Edit panel (option 2), type a new or existing PDS member name to hold the job, and enter the statements from Steps 1-3 exactly as written. From inside the editor, typing `SUBMIT` (or the equivalent `SUB` primary command) on the command line hands the whole member to JES as a job. JES immediately assigns it a job number (e.g. `JOB01234`) and queues it against an initiator in the class you specified — it does not run inline in your TSO session, so control returns to the editor right away while the job runs asynchronously somewhere in the background.

Command ===> SUBMIT
****** ***************************** Top of Data ******************************
000001 //COPYJOB JOB (ACCT123),'J SMITH',CLASS=A,MSGCLASS=X,
000002 // NOTIFY=&SYSUID,REGION=0M
000003 //STEP1 EXEC PGM=IEBGENER
...
000012 //SYSIN DD DUMMY
****** **************************** Bottom of Data ****************************
IKJ56250I JOB COPYJOB(JOB01234) SUBMITTED
-- SUBMIT hands the edited member to JES, which assigns it job number JOB01234
-- and queues it against an initiator in CLASS=A; the job now runs asynchronously

Step 5: Read the Job Output

SDSF (System Display and Search Facility) is the standard panel for watching a job after it is submitted — most shops put it on an ISPF menu option or a `TSO SDSF` command. The `ST` (status) panel lists your jobs with their current phase and, once finished, the final condition code. Drilling into a completed job shows its job log and every DD's SYSOUT, including SYSPRINT, where IEBGENER reports exactly how many records it copied. A condition code of `0000` on `STEP1` means the step completed with no errors; anything nonzero (or an actual system abend code like `S0C7`) means something needs fixing before the output dataset can be trusted.

SDSF ST Panel

Click Run to see what this code prints.

Complete JCL

Here is the complete job assembled from the three JCL statements above, ready to paste into an ISPF Edit session and submit.

//COPYJOB JOB (ACCT123),'J SMITH',CLASS=A,MSGCLASS=X,
// NOTIFY=&SYSUID,REGION=0M
//STEP1 EXEC PGM=IEBGENER
//SYSUT1 DD DSN=USER1.STUDENT.INPUT,DISP=SHR
//SYSUT2 DD DSN=USER1.STUDENT.OUTPUT,
// DISP=(NEW,CATLG,DELETE),
// SPACE=(TRK,(5,5),RLSE),
// UNIT=SYSDA,
// DCB=(RECFM=FB,LRECL=80,BLKSIZE=8000)
//SYSPRINT DD SYSOUT=*
//SYSIN DD DUMMY

Sample Run

Submitting this job against an existing 240-record `USER1.STUDENT.INPUT` dataset produces the following end-to-end result: a new cataloged dataset, a zero condition code, and a NOTIFY message confirming completion.

Job Completion

Click Run to see what this code prints.

Extend This Project

  • Add a second step that runs `IEFBR14` (a do-nothing program used purely for allocation) to pre-allocate `SYSUT2` with different space parameters before STEP1 runs.
  • Change `SYSUT1` to read from two datasets by concatenating two `DD` statements under the same `SYSUT1` ddname.
  • Add `SYSIN DD *` control cards to IEBGENER (`GENERATE MAXFLDS=1` plus a `RECORD` statement) to copy only part of each record instead of the whole thing.
  • Change `DISP=(NEW,CATLG,DELETE)` on `SYSUT2` to `DISP=(MOD,CATLG,DELETE)` and rerun the job to see how MOD appends instead of failing on a duplicate name.
  • Add a `COND=(0,NE)` parameter to a hypothetical STEP2 so it is bypassed if STEP1 does not end with condition code 0.

Summary

You wrote a complete, submittable JCL job from the three statement types that make up nearly all JCL: a `JOB` statement identifying the work to JES, an `EXEC` statement naming the program to run, and `DD` statements supplying that program's input, output, and messages by the exact ddnames it expects. You also walked the full lifecycle a mainframe operator lives in every day — editing the JCL in ISPF, submitting it, and reading its result back through SDSF — which is the same lifecycle every job in the rest of this course, no matter how many steps it grows to, ultimately reduces to.