JCL job dependencies exist beyond the JCL
Map JCL job dependencies by reconciling procedures, scheduler rules and production traces, then prove which nightly job produced each input.

A nightly batch dependency map is usually wrong in a very specific way: it records the order people intended and misses the data relationships the system actually uses. The job that reads a record at 02:00 may have no explicit predecessor link to the job that wrote it. The connection can hide in a cataloged procedure, a symbolic parameter, a scheduler resource, a generation data group, a database commit, or a file renamed by an entirely different job.
I have seen teams spend days reading JCL and still identify the wrong producer. JCL tells you what a submitted job can open. The scheduler tells you why it was eligible to run that night. Production evidence tells you what it did, with which resolved names, and when. You need all three views, joined by stable identities and time. Nobody in the building knows the full graph because no single person or control plane ever had to represent it.
A dependency is a data fact, not a schedule arrow
The useful definition is strict: job B depends on job A when B consumes state that A produced, or when A changes a condition that controls whether B can run correctly. A predecessor arrow may enforce that relationship, but the arrow itself is only an ordering rule. Schedulers also contain operational ordering that has nothing to do with data, such as holding a noisy report until a backup window closes. Conversely, two jobs can exchange data without any direct scheduler link.
Keep four edge types separate in the graph. A data edge connects a writer to a reader through a dataset, GDG generation, database table, message, or control record. A control edge connects a job to a return-code condition, event, resource, or trigger. An order edge records a scheduler predecessor. An inferred edge records a plausible connection that still needs evidence. If you collapse these into one arrow, every investigation turns into an argument about what an arrow means.
Record the object on every data edge. PAYM020 does not merely depend on PAYM010; it reads PROD.PAYMENTS.CLEARED.G0123V00, which PAYM010 closed successfully at 01:47:12. That statement can be checked. A bare job-to-job arrow cannot explain whether the relationship came from the current generation, yesterday's generation, a shared table, or a manual override.
This distinction matters during failure. If A ends with condition code 0 but writes an empty file, the order edge is satisfied while the data contract is broken. If an operator reruns A after B, the scheduler history may show a valid sequence even though B consumed the earlier generation. Treat schedule success and data correctness as separate questions.
Use an edge record that can survive scrutiny:
consumer_job: PAYM020
producer_job: PAYM010
object: PROD.PAYMENTS.CLEARED.G0123V00
consumer_access: read
producer_access: create-and-close
producer_close_utc: 01:47:12
consumer_open_utc: 02:00:08
schedule_edge: none
evidence: expanded-jcl, catalog, smf
confidence: observed
The exact storage format is unimportant. The separation of object, time, edge type, evidence, and confidence is not.
Start from the 02:00 reader and resolve what ran
Begin with the consuming job instance, not a job name copied from a diagram. You need the job name, JES job ID, scheduler occurrence or run number, actual start time, system, and the business date the application believed it was processing. Batch dates often differ from calendar dates around midnight, holidays, and reruns. Without an occurrence identity, records from two runs of the same job will contaminate each other.
Retrieve the submitted or expanded JCL for that occurrence if your retained output provides it. Source JCL in a library is weaker evidence because a procedure or scheduler variable may have changed since the run. Expand cataloged and in-stream procedures, apply SET values, resolve symbolic parameters, and capture overrides on the EXEC and DD statements. Include dynamically allocated datasets reported by the program or trace tooling; they will not appear as DD statements in the source.
A small example shows why reading only the member fails:
//PAYM020 JOB ...
// SET BDATE=20260813
//READ EXEC PROC=PAYREAD,ENV=P,DAY=&BDATE
//INFILE DD DSN=PROD.PAYMENTS.CLEARED(+0),DISP=SHR
//CTL DD DSN=PROD.CTL.PAY.&BDATE,DISP=SHR
The catalog resolves (+0) at allocation time, not when someone later opens the JCL member. After a new generation is created, today's (+0) may name a different physical generation from the one read at 02:00. Store both the relative expression and the absolute GnnnnVnn name observed for that occurrence. Do the same for date symbols: the text &BDATE is not lineage until you record its resolved value.
Then classify each input. Permanent sequential datasets, GDGs, VSAM clusters, temporary datasets passed within a job, UNIX files, database tables, and application control files need different tracing methods. A DD with DISP=SHR suggests input but does not prove the program read it, and programs sometimes open a nominal output DD for input. Access evidence outranks naming convention.
For the record read at 02:00, trace outward from the file or database object that contains it. Do not search the whole estate for the record value first. Values repeat, formats change, and personally identifiable values create handling problems. Establish the containing object, member, partition, or table and the consumer's access time, then search backward for writers.
Expanded JCL reveals candidates, not producers
Static analysis should produce a candidate graph quickly, but it cannot award final authorship. Parse each expanded job into steps and DD statements. Normalize dataset names only after preserving the original expression. Record program names, procedure origins, disposition, generation references, concatenation position, and symbol values. A concatenation can make lineage conditional: a program may find a control member in the first library tonight and the third library after a deployment.
Disposition provides hints. DISP=NEW with cataloging on normal completion points toward creation. DISP=MOD may append or create. DISP=OLD grants exclusive use but says nothing about whether the program reads, replaces, or updates the content. DISP=SHR permits shared access and can appear on both readers and writers. Treat these as candidate labels, never as proof of the open mode.
Temporary datasets create step-level edges inside one job. A &&WORK dataset passed from one step to the next may explain the record before a permanent output appears. Capture it with an occurrence-scoped identity such as job ID plus DD allocation identifier. Do not merge every &&TEMP in the estate into one object.
Also inspect utilities and called programs. A SORT step can create the file while its step name says COPY. IDCAMS can alter a VSAM cluster named in control statements rather than in the most obvious DD. A COBOL program can construct a dataset name and request dynamic allocation. A database update may happen behind a generic plan or stored procedure. Static JCL analysis should mark these as unresolved effects and send them to the right evidence source. Guessing from step names is how false lineage becomes documentation.
A useful parser emits candidates such as:
PAYM010/SORTCLR -> may_write -> PROD.PAYMENTS.CLEARED(+1)
PAYM020/READ -> may_read -> PROD.PAYMENTS.CLEARED(+0)
PAYM025/ARCHIVE -> may_read -> PROD.PAYMENTS.CLEARED(-1)
Now resolve relative generations per occurrence. If PAYM010 creates G0123V00 and catalogs it before PAYM020 allocates (+0), both expressions point to the same object. If PAYM020 allocated before the catalog update, they do not. Sequence alone cannot settle this because allocation and open times matter.
The scheduler explains eligibility and hidden gates
Export the scheduler definitions and occurrence history for the same business date. Definitions show intended predecessors, calendars, cyclic rules, resources, events, input-arrival conditions, return-code tests, and variable tables. History shows which of those rules applied, which jobs were suppressed, added, held, forced complete, rerun, or manually released. You need both. A clean definition export can describe a night that never happened.
Scheduler resources often conceal the missing connection. A producer may set FILE.CLEARED.READY, while the consumer waits for that resource instead of naming the producer. Another job may also set the same resource during recovery. Model the resource as a node: producer sets resource, resource releases consumer. Flattening it into a producer-consumer edge loses the alternative writer and the operator action that can satisfy it.
Calendars create conditional graphs. The month-end producer may run only on the last banking day, while the consumer runs daily and falls back to the previous file on other days. One universal diagram cannot represent that honestly. Attach predicates such as business calendar, application date, and run type to the edge. Generate a graph for an occurrence or for a named scenario, rather than pretending every possible edge is active every night.
Return-code logic needs the same care. A consumer might run after a warning code but read a fallback dataset. Another step may execute only when a prior step produced a specific code. Preserve conditions at job and step level. after PAYM010 is less informative than eligible when PAYM010 completes with RC <= 4 and FILE.CLEARED.READY exists.
Manual actions belong in the graph because they change causality. Capture the operator identity if policy allows, action time, old state, new state, and reason text. A forced-complete predecessor did not produce data. It only made the scheduler behave as if the prerequisite had finished. When the downstream job later reads stale data, that distinction ends the argument quickly.
Do not treat the scheduler database as a data catalog. It answers why work ran, not necessarily what bytes it consumed. Its strongest contribution is a time-stamped account of eligibility, exceptions, and human intervention.
Production traces decide which candidate actually wrote it
Production evidence turns possible edges into observed ones. On z/OS, join job and step timing with dataset access records, JES output, catalog activity, utility messages, application logs, and database audit or log data that your environment already retains. IBM's SMF documentation separates job accounting from dataset activity for a reason: no single record type is a complete lineage feed. SMF type 30 can anchor job and step execution, while type 14 and 15 records can provide close activity for non-VSAM datasets when the relevant recording is enabled. VSAM and database work require their own records.
That qualification matters. Absence of an SMF dataset record does not prove absence of access. Recording options, access method, buffering, subsystem behavior, and retention can remove the evidence you hoped to find. Label the edge not observed rather than did not happen unless your collection controls justify the stronger claim.
For a sequential dataset investigation, work backward from the consumer open or close window:
- Resolve the consumer occurrence and its absolute dataset name.
- Find observed reads for that dataset by the consumer's job and step.
- Search earlier write or create activity for the exact absolute name.
- Join candidate writers to type 30 timing and JES step output.
- Check catalog and scheduler history around reruns or manual releases.
Suppose PAYM020 reads G0123V00 at 02:00. The scheduler says PAYM010 ended at 01:48, but SMF shows PAYM010 closed G0123V00 at 01:47. A recovery job also opened it for output at 01:55 and closed it at 01:58. The scheduler predecessor is not the last writer. Your graph needs both write events, and the consumer edge should point to the version after the recovery close. Dataset identity alone is insufficient when writers update an existing object; time separates the states.
Database records require a different join. Identify the table and business key read by the consumer, then use subsystem logs, audit data, commit timestamps, correlation IDs, plan or package names, and job context where available. A row's writer is the transaction that committed the visible version, not merely the batch job that started first. Isolation level also affects which committed version the reader could see. If existing telemetry cannot connect a transaction to a job occurrence, say so and retain a bounded candidate set.
Time and identity prevent plausible but false joins
Most incorrect graphs come from joining on names without intervals. Job names repeat. Dataset names can be reused. A GDG relative reference changes meaning. Scheduler occurrences can be rebuilt. Database rows have successive versions. Model events first, then derive edges between states observed at particular times.
Use a common timeline, preferably UTC plus the original local timestamp and zone. Mainframe, scheduler, database, and distributed log clocks may differ. Measure clock offsets if you can, and retain an uncertainty window if you cannot. Do not invent an exact ordering between events whose timestamps fall inside that window. A writer close reported at 01:59:59.8 and a reader open reported at 02:00:00.1 look ordered until you learn that the systems differ by two seconds.
Identity should include the system and occurrence. A practical composite for a batch execution is scheduler application and occurrence, job name, JES job ID, system ID, and start time. For a step, add step name and step sequence because names can repeat after procedure expansion. For a dataset state, use the fully qualified absolute name, volume or catalog context when needed, and a write interval.
Assign confidence from evidence, not intuition. Observed means telemetry directly records the access. Corroborated means independent sources agree, such as expanded JCL plus utility output plus timing. Declared means only a scheduler or configuration rule says the edge exists. Inferred means conventions or proximity suggest it. Conflicted means sources disagree. These labels let operations use the map without pretending all edges deserve the same trust.
Keep negative findings equally precise. No writer found in retained SMF between 00:00 and 02:00; dataset records enabled for the relevant systems; earlier state not examined is useful. Unknown producer throws away the search boundary. Evidence has scope, and the graph should carry that scope.
Reruns expose the graph nobody documented
Reruns are where nominal dependency maps fail. A scheduler can create a new occurrence, restart at a later step, or rerun under the same job name with a new JES ID. The job may reuse an existing GDG, create another generation, append to a fixed dataset, or update only rows that failed before. Model every execution and every write as an event. Never overwrite the first run with the recovery run in your lineage store.
Consider a common failure. The regular producer creates G0123V00 and ends with RC 8 after the data step but before it sets the scheduler resource. An operator checks the file, forces the occurrence complete, and releases the consumer. A recovery job later corrects several records in place. The consumer begins between those actions. The definition graph says the producer failed and the recovery followed it. The actual data graph says the consumer read the original file before correction. Both statements can be true.
File existence checks are weak gates. A stale fixed-name file can satisfy them. A GDG (+0) can resolve to the last cataloged generation even when tonight's producer never ran. A zero return code can accompany a logically empty extract. Prefer conditions that bind readiness to the business date and object version, then verify the content contract. A small control record containing application date, producer occurrence, row count, and completion status can make the binding explicit, provided the producer writes it only after the data commit or close succeeds.
Do not recommend adding scheduler arrows until every observed data edge has one. Some data is intentionally shared across applications, and forcing a direct predecessor can serialize unrelated work or create cycles. Add scheduling controls where correctness needs ordering. For other edges, monitoring and freshness checks may express the contract better. The popular advice to make the diagram match production by adding predecessors confuses documentation with execution policy.
Rerun tests should cover the awkward states: producer failure after partial output, restart after the writing step, duplicate generation creation, manual force-complete, late recovery writer, and consumer start during correction. If the dependency graph cannot describe those states, it is a happy-path picture.
Build an evidence ledger before drawing the graph
A graph is a view over evidence, not the primary record. Store an append-only ledger of observations and declarations, then derive the graph for a business date or incident window. This makes corrections possible without erasing what an earlier tool concluded. It also lets reviewers follow an edge back to source records.
Each ledger entry needs a source, collection time, event time, system, occurrence identity, object identity, action, resolved attributes, and retention pointer. Keep raw evidence under the access controls it already requires; the lineage store can hold a reference and selected non-sensitive fields. Dataset names and job metadata can themselves reveal business functions, so do not assume the map is harmless.
Use a reconciliation query that reports disagreements rather than silently choosing a source. For example, flag a declared scheduler predecessor with no observed shared object, an observed writer-reader pair with no control edge, a relative GDG reference that could not be resolved, or a consumer open that precedes the chosen producer close inside the clock uncertainty window. Those are work queues for an engineer, not automatic proof of defects.
Ownership becomes manageable when it attaches to evidence domains. Scheduler administrators own definition extraction. Storage or platform teams own catalog and SMF collection. Application teams explain dynamic allocation and record semantics. Database teams trace committed row versions. One person does not need to know the whole graph; the ledger needs stable interfaces between their evidence.
Set retention from the investigation horizon. If scheduler history lasts longer than dataset activity, an old incident will appear to have only order edges. If expanded JCL disappears before the next month-end run, symbol resolution becomes guesswork. Record the earliest available timestamp per source so users can see when confidence drops.
CodeHero reads the whole legacy tree, including COBOL and JCL, in parallel and can use recorded production traffic in a parity harness when rewriting the system. That combination is useful because the static candidate graph and observed behavior remain separate evidence until they agree.
A dependable map changes how the batch is replaced
Once the ledger can answer who wrote the state consumed at 02:00, use it to define migration boundaries. Group jobs by transactional and data contracts, not by scheduler folder or naming prefix. A producer and consumer may belong in one replacement slice even if different teams own them. Two adjacent scheduler jobs may remain separate because their only relationship is an operational window.
Turn every observed edge into a parity assertion. Given the same recorded input state, the replacement should produce the same externally relevant dataset records, database changes, return conditions, and readiness signals. Compare content after normalizing fields that are intentionally nondeterministic, such as run IDs or timestamps, but document each normalization. A parity harness that ignores ordering, empty-output behavior, and rerun semantics will approve the easy night and fail during recovery.
Keep the legacy observation running during cutover rehearsals. A new service may publish a Postgres transaction where the old batch cataloged a dataset and set a resource. The implementation changes, but the contract still has a producer state, a visibility point, a consumer, and a business date. Map old evidence to the new contract explicitly rather than copying job arrows into a service diagram.
The first concrete deliverable should be narrow: one consumer occurrence, every input it actually opened, and the last successful writer of each visible state. Include unresolved candidates and evidence gaps. Review it with operations during a night that includes a rerun, not only against a clean schedule. Then expand upstream and downstream by observed objects.
The review process needs its own acceptance criteria. For every observed data edge, a reviewer should be able to open the retained evidence, identify both occurrences, resolve the object name, and reproduce the time ordering without relying on a team convention. For every declared edge that lacks an observation, the record should state whether telemetry was missing, the path was inactive on that business date, or the declaration appears stale. A graph can contain uncertainty; it cannot hide the reason for it.
Recompute the view whenever definitions, procedures, program allocation logic, or collection settings change. Do not rebuild every edge from scratch on a timer. Instead, invalidate the candidate relationships touched by the changed artifact and wait for the next qualifying production occurrence to confirm them. This keeps a change to one procedure from making the entire graph look newly discovered, and it preserves the history needed to explain why yesterday and today differ.
Add checks at the boundary of the evidence pipeline. Reject an access event with no system clock source. Quarantine an absolute GDG name that conflicts with the catalog snapshot. Warn when two scheduler occurrences claim the same JES job ID on the same system and overlapping time. Report when an object marked as newly created has no close or commit event before its consumer starts. These checks do not decide business correctness, but they stop malformed evidence from becoming a confident dependency.
Measure the map by questions it can answer, not by its node count. Pick past incidents and ask which exact state each consumer saw, why the scheduler released it, what manual action changed the route, and which source proves every answer. Include a clean night, a delayed input, a restart from a later step, and a recovery write after the nominal producer. If engineers still need to search spool output by hand for one of those cases, the ledger has a named gap to close. A large graph with vague edges is less useful than a smaller graph whose claims can be reproduced.
The useful graph will never be a single timeless poster. It is a query over versioned evidence: show the declared plan, show what happened on a chosen night, and show where the two disagree. When someone asks who wrote the record read at 02:00, the answer should name an occurrence, an object version, a visibility time, and the records that prove it. Anything less is still a hunch.
FAQ
Can JCL alone reveal every nightly batch dependency?
No. JCL can identify declared and possible dataset access, but procedures, dynamic allocation, databases and runtime generation resolution leave gaps. Use expanded JCL to find candidates, then confirm them with scheduler history and production evidence.
How do I find which job created a GDG generation?
Resolve the consumer's relative reference to the absolute GnnnnVnn name for that exact occurrence. Search catalog and dataset activity for the create and close event, then join it to the producer's JES job ID and step timing.
Does a scheduler predecessor prove a data dependency?
No. A predecessor proves an ordering rule was declared or applied. Confirm a data dependency by identifying the shared object and showing that the consumer saw the state produced by that occurrence.
What should I do when SMF shows no dataset access?
Check whether the relevant record types, systems, access methods and retention window were covered. Record the result as not observed within that scope; do not turn missing telemetry into proof that access never happened.
How should rerun jobs appear in a dependency graph?
Give every rerun its own occurrence and JES job ID, and retain each write as a separate event. Connect the consumer to the object state visible when it opened the data, even if that state came from a recovery job.
Can a zero return code prove the input was ready?
No. A job can end normally after writing an empty, stale or logically incomplete result. Bind readiness to the business date and object version, and check a content contract where the risk justifies it.
How do I trace a database row back to a batch job?
Use the row's business key and visible committed version, then correlate database log or audit data with plan, package, transaction and job context. If telemetry cannot complete that join, preserve the bounded candidate writers instead of choosing by timing alone.
Why do dependency maps fail around midnight?
Calendar date, business date and scheduler occurrence date can differ, while clocks across systems can drift. Store the business date explicitly and compare events on a common timeline with a stated uncertainty window.
Should every observed data dependency become a scheduler link?
No. Add ordering only when correctness requires it. Shared reference data may need freshness monitoring or version checks rather than a predecessor that serializes unrelated work.
What is the smallest useful batch lineage deliverable?
Document one consumer occurrence, the exact inputs it opened, and the last writer of each visible state. Include source records, timestamps, confidence labels and unresolved candidates so another engineer can reproduce the conclusion.