CL job scheduling on IBM i: SBMJOB, WRKJOBSCDE, and automating batch work

Every IBM i shop runs batch jobs. End-of-day processing, report generation, file transfers, data archiving — these are the jobs nobody watches, the ones that are supposed to run overnight and finish quietly before the morning shift arrives. When they work, nobody notices. When they fail, everyone does.

CL is the language you use to build, submit, schedule, and monitor those jobs. This post covers the commands that matter most: SBMJOB for submitting work to batch, WRKJOBSCDE for scheduling recurring jobs, and the monitoring patterns that tell you when something went wrong before your manager does.

How IBM i batch processing works

On IBM i, every running program runs inside a job. Interactive jobs are the ones users log into. Batch jobs run in the background, in subsystems designed for that purpose — typically QBATCH or a custom batch subsystem your shop has defined.

When you submit a job, you are asking the system to run a program (or CL command) as a separate, independent job — completely detached from your own session. The submitting job does not wait. It fires the request and moves on.

The batch job runs under its own user profile, its own job description, its own library list. Getting those right is most of what job submission is actually about.

SBMJOB — the command you will use every day

The basic form is simple:

SBMJOB CMD(CALL PGM(MYLIB/NIGHTLY))

That submits a job to QBATCH using your current job description. In practice you will almost always specify more:

SBMJOB CMD(CALL PGM(MYLIB/NIGHTLY) PARM('FULL' '20260518')) +
       JOB(NIGHTLYRPT)         +
       JOBD(MYLIB/BATCHJD)     +
       JOBQ(MYLIB/BATCHQ)      +
       OUTQ(MYLIB/BATCHOUTQ)   +
       USER(BATCHUSR)          +
       LOGCLPGM(*YES)          +
       MSGQ(MYLIB/BATCHMSGQ)

What each parameter does:

  • CMD — the program or command to run. You can pass parameters here exactly like a direct CALL.
  • JOB — the name that appears in WRKACTJOB and the job log. Use something meaningful.
  • JOBD — job description, which controls library list, message logging, and dozens of other attributes. Always specify this explicitly in production.
  • JOBQ — the queue the job waits in until a subsystem picks it up. Different queues can have different priorities and run under different subsystems.
  • OUTQ — where spooled output (reports, printed output) goes. Separate from your interactive output queue.
  • USER — the user profile the batch job runs under. In production this is almost always a dedicated batch user, not a person’s profile.
  • LOGCLPGM(*YES) — logs every CL command that executes into the job log. Invaluable for debugging. Turn it on for any job that does real work.
  • MSGQ — where completion and error messages are sent. If you specify a message queue here you can monitor it programmatically.

Passing parameters to batch jobs

Parameters in SBMJOB are passed as part of the CMD parameter. CL passes everything as character strings:

DCL VAR(&RUNDATE) TYPE(*CHAR) LEN(8)
DCL VAR(&RUNTYPE) TYPE(*CHAR) LEN(4)

RTVSYSVAL SYSVAL(QDATE) RTNVAR(&RUNDATE)
CHGVAR VAR(&RUNTYPE) VALUE('FULL')

SBMJOB CMD(CALL PGM(MYLIB/NIGHTLY) PARM(&RUNTYPE &RUNDATE)) +
       JOB(NIGHTLYRPT) +
       JOBD(MYLIB/BATCHJD)

Your RPG program receives these as its *ENTRY parameters. Make sure the lengths match what the program expects — CL does not enforce types at the boundary.

Holding and releasing jobs

Sometimes you want to submit a job but not let it run yet — useful when you need to set something up first, or when you want jobs queued but held until you are ready to release them at a specific time.

/* Submit held — job sits in queue but will not start */
SBMJOB CMD(CALL PGM(MYLIB/MONTHEND)) +
       JOB(MONTHEND) +
       JOBD(MYLIB/BATCHJD) +
       HOLD(*YES)

/* Later — release it to run */
RLSJOB JOB(MONTHEND)

You can also hold an entire job queue and release it when the time is right:

HLDJOBQ JOBQ(MYLIB/BATCHQ)
/* ... set up, load files, verify data ... */
RLSJOBQ JOBQ(MYLIB/BATCHQ)

WRKJOBSCDE — scheduling recurring jobs

IBM i has a built-in job scheduler that most shops underuse. WRKJOBSCDE (Work with Job Schedule Entries) gives you a screen to view and manage scheduled jobs. To add an entry from CL:

ADDJOBSCDE JOB(DAILYRPT)              +
           CMD(CALL PGM(MYLIB/DAILYRPT) PARM('DAILY')) +
           FRQ(*WEEKLY)               +
           SCDDAY(*MON *TUE *WED *THU *FRI) +
           SCDTIME(220000)            +
           JOBD(MYLIB/BATCHJD)        +
           USER(BATCHUSR)             +
           TEXT('Daily report — weeknights at 22:00')

Key parameters:

  • FRQ — frequency: *ONCE, *WEEKLY, *MONTHLY. For daily use *WEEKLY with all days listed.
  • SCDDAY — which days to run. *ALL for every day, or list specific days.
  • SCDTIME — time in HHMMSS format. 220000 means 22:00:00.
  • OMITDAY — days to skip, useful for excluding weekends from a *WEEKLY job.

To modify an existing entry:

CHGJOBSCDE JOB(DAILYRPT) SCDTIME(210000)

To remove one:

RMVJOBSCDE JOB(DAILYRPT)

The scheduler is reliable and survives IPL (system restart). For most shops it is the right place for recurring batch work rather than third-party schedulers — unless you need cross-system dependencies or advanced calendar logic.

Checking job status from CL

After submitting a job you often need to know whether it finished — especially when one job depends on the output of another. The CHKJOB approach uses RTVJOBA and a loop:

DCL VAR(&JOBNAME)   TYPE(*CHAR) LEN(10) VALUE('NIGHTLYRPT')
DCL VAR(&JOBSTS)    TYPE(*CHAR) LEN(10)
DCL VAR(&WAITCOUNT) TYPE(*DEC)  LEN(3 0) VALUE(0)
DCL VAR(&MAXWAIT)   TYPE(*DEC)  LEN(3 0) VALUE(60)

CHKJOB: CHKOBJ OBJ(QSYS/&JOBNAME) OBJTYPE(*JOBD)
MONMSG MSGID(CPF9801) EXEC(GOTO CMDLBL(JOBDONE))

/* Job still active — wait 60 seconds and check again */
DLYJOB DLY(60)
CHGVAR VAR(&WAITCOUNT) VALUE(&WAITCOUNT + 1)

IF COND(&WAITCOUNT >= &MAXWAIT) THEN(DO)
  SNDPGMMSG MSG('Timeout waiting for NIGHTLYRPT') MSGTYPE(*ESCAPE)
ENDDO

GOTO CMDLBL(CHKJOB)

JOBDONE:
SNDPGMMSG MSG('NIGHTLYRPT completed — proceeding') MSGTYPE(*INFO)

DLYJOB DLY(60) pauses the CL program for 60 seconds before checking again. The timeout guard prevents an infinite loop if the job hangs.

Reading completion messages from a batch job

A more robust pattern is to have the batch job send a message to a known message queue when it finishes — success or failure — and have the monitoring job wait on that queue:

/* In the batch job — send completion status */
SNDMSG MSG('NIGHTLYRPT completed successfully') TOUSR(BATCHUSR) +
       MSGTYPE(*INFO)

/* Or on failure */
SNDMSG MSG('NIGHTLYRPT FAILED — see job log') TOUSR(BATCHUSR) +
       MSGTYPE(*INFO)

A better version uses a dedicated message queue and RCVMSG in the monitoring job:

/* Monitoring job waits on the message queue */
DCL VAR(&MSGTEXT) TYPE(*CHAR) LEN(256)

RCVMSG MSGQ(MYLIB/BATCHMSGQ) WAIT(3600) MSGDTA(&MSGTEXT)
MONMSG MSGID(CPF2410) EXEC(DO)
  /* CPF2410 = timeout, no message received */
  SNDPGMMSG MSG('Timed out waiting for batch completion') +
             MSGTYPE(*ESCAPE)
ENDDO

IF COND(%SST(&MSGTEXT 1 6) = 'FAILED') THEN(DO)
  SNDPGMMSG MSG('Batch job reported failure') MSGTYPE(*ESCAPE)
ENDDO

WAIT(3600) tells RCVMSG to wait up to 3600 seconds (one hour) for a message. If nothing arrives, it raises CPF2410 which you can monitor.

A complete job submission wrapper

Putting the patterns together, here is a CL program that submits a batch job, waits for confirmation, and handles failure:

PGM PARM(&RUNDATE)

DCL VAR(&RUNDATE)  TYPE(*CHAR) LEN(8)
DCL VAR(&MSGTEXT)  TYPE(*CHAR) LEN(256)
DCL VAR(&JOBLOG)   TYPE(*CHAR) LEN(100)

/* Clear the reply queue before submitting */
CLRMSGQ MSGQ(MYLIB/BATCHMSGQ)
MONMSG MSGID(CPF0000)

/* Submit the batch job */
SBMJOB CMD(CALL PGM(MYLIB/NIGHTLY) PARM('FULL' &RUNDATE)) +
       JOB(NIGHTLYRPT)       +
       JOBD(MYLIB/BATCHJD)   +
       JOBQ(MYLIB/BATCHQ)    +
       USER(BATCHUSR)        +
       LOGCLPGM(*YES)        +
       MSGQ(MYLIB/BATCHMSGQ)
MONMSG MSGID(CPF0000) EXEC(DO)
  SNDPGMMSG MSG('Failed to submit NIGHTLY job') MSGTYPE(*ESCAPE)
ENDDO

SNDPGMMSG MSG('NIGHTLYRPT submitted — waiting for completion') +
           MSGTYPE(*INFO)

/* Wait up to 2 hours for the batch job to reply */
RCVMSG MSGQ(MYLIB/BATCHMSGQ) WAIT(7200) MSGDTA(&MSGTEXT)
MONMSG MSGID(CPF2410) EXEC(DO)
  SNDPGMMSG MSG('Timeout: NIGHTLYRPT did not complete in 2 hours') +
             MSGTYPE(*ESCAPE)
ENDDO

/* Check the reply */
IF COND(%SST(&MSGTEXT 1 2) *NE 'OK') THEN(DO)
  CHGVAR VAR(&JOBLOG) VALUE('Job failed. Check BATCHMSGQ or job log.')
  SNDPGMMSG MSG(&JOBLOG) MSGTYPE(*ESCAPE)
ENDDO

SNDPGMMSG MSG('Nightly run completed successfully') MSGTYPE(*INFO)
RETURN

ENDPGM

Common mistakes and how to avoid them

Using your own user profile for batch jobs. When the person who set up the job leaves the company and their profile is disabled, every batch job that ran under their profile stops working. Always use a dedicated batch user profile — one that belongs to the application, not a person.

Not specifying JOBD. If you omit JOBD, the job inherits your current job description. That is almost never what you want in production. Specify the job description explicitly so the library list, logging level, and message routing are always predictable.

Forgetting LOGCLPGM(*YES). When a batch job fails and nobody turned on CL logging, the job log contains almost nothing useful. It takes seconds to add and saves hours when things go wrong.

Submitting to QBATCH directly in production. Default QBATCH mixes all batch work together. A long-running job can block a short urgent one. Separate job queues for different work types give you control over priority and throughput.

No timeout on RCVMSG. If a batch job hangs and never sends a completion message, RCVMSG without a timeout waits forever — tying up the monitoring job indefinitely. Always specify WAIT.

Commands worth knowing next

Once SBMJOB and WRKJOBSCDE are comfortable, these are the natural next steps:

  • WRKACTJOB — see all active jobs on the system, their status, CPU usage, and which lock they might be waiting on.
  • ENDJOB — end a stuck or runaway batch job cleanly (OPTION(*CNTRLD)) or forcibly (OPTION(*IMMED)).
  • WRKJOB — drill into a specific job to see its job log, open files, locks, and call stack.
  • DSPJOBLOG — display the full job log for a completed job, which lives in QHST after the job ends.
  • STRJOBD / CHGJOBSCDE — manage job descriptions and scheduler entries programmatically from automated deployment scripts.

Batch job management is one of those areas where a small investment in understanding pays back every single day. The jobs run overnight, unattended, on a schedule nobody thinks about — until one fails. The patterns in this post are what make the difference between a failure that wakes someone up at 2am and one that sends a clean error message to a queue, waits for the morning team, and tells them exactly what to fix.

Next: Writing CL programs that manage the IFS — reading directories, moving files, and handling the integrated file system from your batch routines.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top