JCL anti-pattern
JCL condition code chains
COND parameters bypass a step when the condition is true, the reverse of how most people read them, and chains of COND, EVEN and ONLY encode the batch job's real error handling.
- FAMILY
- JCL
- CUTOVER RISK
- High
- Hides business logic or coupling that a rewrite will drop unless it is extracted explicitly.
- MODERN MAPPING
- Explicit step DAG with run conditions
- GENERATED ARTIFACT
- LNDLY01.steps.json
Before and after
legacy source → modern mapping
Legacy: exhibits the pattern
1//LNDLY01 JOB (LN01),'LOAN DAILY',CLASS=A,MSGCLASS=X2//STEP010 EXEC PGM=LNEXTR013//LNIN DD DSN=PROD.LOAN.MASTER,DISP=SHR4//EXTRACT DD DSN=PROD.LOAN.EXTRACT,DISP=(NEW,CATLG,DELETE)5//STEP020 EXEC PGM=LNACCR02,COND=(4,LT)6//EXTRACT DD DSN=PROD.LOAN.EXTRACT,DISP=SHR7//STEP030 EXEC PGM=LNPOST03,8// COND=((4,LT),(4,EQ,STEP020))9//STEP040 EXEC PGM=LNRECN04,COND=EVEN10//STEP050 EXEC PGM=LNRECOV,COND=ONLY
Modern: Explicit step DAG with run conditions
1# Run-when conditions translated from COND2# (COND says when to BYPASS; these say when to RUN)3with DAG("ln_daily", schedule=None) as dag:4 extract = batch_step("LNEXTR01", ok_max_rc=4)5 accrue = batch_step("LNACCR02", ok_max_rc=4)6 # run only if no prior RC > 4 and accrue RC != 47 post = batch_step("LNPOST03",8 skip_when={"LNACCR02": [4]})9 recon = batch_step("LNRECN04",10 trigger_rule="all_done") # EVEN11 recover = batch_step("LNRECOV",12 trigger_rule="one_failed") # ONLY13 extract >> accrue >> post >> [recon, recover]
Why it is dangerous in modernization
COND=(4,LT) on a step means: bypass this step if 4 is less than the return code of any previous step. In plain terms, run it only if every earlier step ended with RC 4 or lower. The test describes when not to run, and it reads backwards to most engineers who did not grow up with JCL. Chains of these tests, plus COND=EVEN (run even after an abend) and COND=ONLY (run only after an abend), are the job's error handling and restart logic. Migrations that rebuild batch flows in a scheduler from step names and program names, and translate COND by intuition, invert conditions, run recovery steps on success, or skip reconciliation steps that the legacy job always ran.
How PYD ENTERPRISES detects it
The JCL parser resolves cataloged and in-stream procedures, symbolic parameters and overrides, then evaluates each COND parameter into a normalised predicate over prior steps' return codes and abend status. COND on the JOB statement is merged into every step. IF/THEN/ELSE/ENDIF constructs are parsed into the same predicate form. The output is a directed graph of steps with explicit run-when conditions, and it is joined with dataset lineage (DD statements with DISP) so you can see which datasets each conditional path creates, keeps or deletes.
Maps to: Explicit step DAG with run conditions
The job becomes a workflow in your orchestrator of choice, whether that is Airflow, AWS Step Functions, Argo Workflows or Spring Batch, with each COND translated into a positive run-when condition and each return code threshold made an explicit success criterion. Recovery steps under COND=ONLY become failure handlers; COND=EVEN steps become always-run steps.
Generated specification fragment
1{2 "job": "LNDLY01",3 "steps": [4 { "step": "STEP010", "pgm": "LNEXTR01", "runWhen": "always" },5 { "step": "STEP020", "pgm": "LNACCR02",6 "runWhen": "no abend AND max(RC prior) <= 4" },7 { "step": "STEP030", "pgm": "LNPOST03",8 "runWhen": "no abend AND max(RC prior) <= 4 AND (STEP020 bypassed OR RC(STEP020) != 4)" },9 { "step": "STEP040", "pgm": "LNRECN04",10 "runWhen": "always, including after abend" },11 { "step": "STEP050", "pgm": "LNRECOV",12 "runWhen": "only if a prior step abended" }13 ],14 "source": "LNDLY01.jcl:1-10"15}
Every generated element links back to the source file and line range shown in the legacy pane. Review and sign off before building against it.
Reading COND correctly
A COND test has the form (code,operator[,stepname]). The system compares the code you wrote with a step's return code, in that order, and if the comparison is true the step is bypassed. So (4,LT) asks whether 4 is less than a previous RC. If any previous step ended with RC 8, then 4 < 8 is true and the step does not run. (4,EQ,STEP020) bypasses the step when STEP020 ended with exactly RC 4. With several tests in one COND, the step is bypassed if any test is true.
Two further rules are easy to miss. If a previous step abended, every following step is bypassed unless it says COND=EVEN or COND=ONLY. And a test that names a step which was itself bypassed is ignored, which is why the generated predicate for STEP030 includes the case where STEP020 did not run.
IF/THEN/ELSE is not the same mental model
Newer jobs use // IF (STEP020.RC <= 4) THEN constructs, which state when to run. Estates usually contain both styles, sometimes in the same job, and a JOB-level COND can still bypass steps inside an IF block. Converting both into a single run-when form, as PYD ENTERPRISES does, removes the need for every engineer on the migration team to become fluent in both.
Return codes carry business meaning
In the example, LNACCR02 sets RC 4 when there were no accruals to post, so posting is skipped on purpose. That is a business rule, set by a MOVE 4 TO RETURN-CODE deep in a COBOL program, and enforced by JCL. PYD ENTERPRISES links each return code assignment in the program to the COND tests that consume it, so the rule is extracted once with both ends attached, instead of being lost when the scheduler is rebuilt.
Related patterns
Usually found in the same codebase.
Find every JCL condition code chains in your codebase.
Run PYD ENTERPRISES in your private VPC and get each occurrence with file, line range, modern mapping and equivalence fixtures, ready for your architects to review.