Job dependencies
See How workflows work.
Task walkthrough: Order jobs with dependencies. This page is the full configuration and troubleshooting reference.
A job runs only when all of its dependencies are satisfied. Dependencies are how a workflow is sequenced and how jobs coordinate shared state and capacity.
Dependency types
| Type | Satisfied when | Key settings |
|---|---|---|
| Job | A predecessor job behaves as the two dependency axes require (see below) | Predecessor Workflow + Job Name, Condition (success / failure / any), Type (requires / after / conflict), Days Offset |
| Threshold | A named threshold compares as required against a value | threshold, operator, thresholdValue — see Threshold dependencies |
| Resource | Enough of a named resource is available (the job reserves it) | resource, resourceRequired |
| Expression | A custom expression evaluates to True | expression — see Expression dependencies |
The two axes of a job dependency
A job dependency has two independent axes. Set both on the job editor's Dependencies tab, or on the workflow canvas when you draw the dependency:
- Condition — which result of the predecessor satisfies the dependency: Finished OK
(
success), Failed (failure), or Any (any, the ignore-error union). - Type — what happens when the predecessor is absent from the schedule for the resolved date:
| Type | Predecessor is present | Predecessor is absent for the date |
|---|---|---|
| Requires (default) | Waits for it to reach the Condition | Waits — the dependency is not met |
| After | Waits for it to reach the Condition | Proceeds — the dependency is treated as met |
Use After for "run after this if it runs" ordering, so an optional or conditionally scheduled predecessor doesn't stall the dependent on a day it isn't scheduled. Use Requires (the default) when the predecessor must actually run.
"Absent for the date" has three causes, and the dependency card names all three: the predecessor's frequency didn't fire for this date, the predecessor is Disabled, or it isn't defined in the workflow at all. A Requires dependency on an absent predecessor waits until an operator adds that predecessor to the instance.
A third Type, Conflict, inverts the relation entirely — it is about not running at the same time rather than about order. It is covered below.
A dependency with no explicit Type behaves as Requires and keeps its previous behavior. Editing the job requires you to set a Type — pre-filled Requires — before you can save.
Days Offset
Days Offset (−365 to +365) lets a job depend on a predecessor from a different day's run:
0 is the same day, −1 the previous day, +1 the next day. The offset resolves against that
day's instance of the predecessor, so a job can wait on — or run after — yesterday's or tomorrow's
run of another job.
A self-reference at a non-zero offset is allowed and points at the job's own prior or next dated
run. A job cannot depend on itself on the same day (same workflow, same job, offset 0); that
combination is now flagged on the dependency card as you author it and blocks the save, rather
than only being refused by the platform once you try to save.
Conflict — keeping two jobs off each other
Requires and After both say "run in this order." Conflict says something different: this job must not run while that job is running. It constrains concurrency, not order.
Use it for the classic mutual-exclusion cases — two jobs that must not hit the same core file at once, or a singleton job that must never overlap another copy of itself on a different schedule date.
A Conflict dependency behaves unlike the other two in several ways:
| Requires / After | Conflict | |
|---|---|---|
| Condition | Required (success / failure / any) | None — a conflict has no Condition, and setting one is rejected |
| Satisfied when | The predecessor reached the Condition | No matching job is currently running |
| Predecessor absent | Waits (Requires) or proceeds (After) | Proceeds — nothing to conflict with |
| Name matching | Exact job name | Exact, or a name prefix (jobNameLike) so one rule covers a family of jobs |
| Day scope | The date the Days Offset resolves to | That date, or every scheduled date (allDays), which ignores Days Offset |
Two more behaviors worth knowing:
- A job may hold both a Conflict and an ordering dependency on the same predecessor. "Wait for X, and also don't run alongside X" are two different statements, so they are kept as two records.
- A job may conflict with itself. A same-day self-conflict has nothing to collide with but
itself, and settles as satisfied. Combined with
allDays, a self-conflict is the standard way to express "only one copy of this job runs at a time, across every schedule date" — the ordinary singleton guard.
A job is held while a matching job is running, or has finished but not yet reached its final status. Once no match is running, the dependency is satisfied and the job proceeds.
A conflict that names its own workflow is recognised as a same-workflow conflict whatever the
capitalization and surrounding spaces — payroll and PAYROLL are the same workflow, and the
conflict holds the job as intended.
A conflict naming a different workflow is matched case-sensitively. If the name does not match a workflow that is built and resident, there is nothing to conflict with and the dependency is satisfied, so the job runs. Copy the predecessor's workflow name rather than typing it from memory: a name that differs in case names nothing, and the conflict will not hold the job.
When several jobs conflict with each other
If two or more jobs declare conflicts on one another and all of them are waiting on that conflict — none actually running — the platform releases them one at a time rather than releasing the whole set together. The rest stay waiting and are reconsidered on the next pass.
Either way, only one member of a conflicting set is ever dispatched at a time. That guarantee is enforced where jobs are dispatched, not by the order they leave the waiting state, so a conflict does its job even when several members become eligible in the same pass.
Authoring a conflict
Set Type to Conflict on a dependency, the same way you set Requires or After — on the job editor's Dependencies tab (from either the Workflows page or the Processes page) or in the New Dependency dialog. Because a conflict carries no Condition, that field disappears when you choose it.
When you turn on prefix matching, the Job Name field becomes free text: you are naming a pattern that jobs start with, not picking one existing job.
On the Workflows canvas a conflict is drawn as its own edge — dashed and without an arrowhead, since it expresses no order. A self-conflict draws as a loop on the job itself, and a prefix conflict draws against a node standing for the pattern rather than any one job. A conflict on a job in another workflow shows as a badge on the job.
When a job is held by both an unmet ordering dependency and a conflict, the ordering dependency is the one reported as the reason it is waiting. A conflict is a transient "something else is running right now", so surfacing it over a genuine unmet precondition would send you after the wrong thing.
Choosing a predecessor
A predecessor can be in the current workflow or another workflow — pick the workflow, then the job. When you edit a job dependency inside a running instance (from Processes), the predecessor's workflow is pinned to that instance's own workflow.
If a predecessor can't be resolved, the dependency shows a warning naming the cause — either the job is absent from the dated schedule, or it exists but under an instance name this job doesn't share.
A dependency records the predecessor's identity
A job dependency stores two things about the job it names: that job's persistent id and its name. The id is the primary key of the pair, and it is what lets a dependency survive a rename of the job it points at.
The editor fills both in for you, and the rules that follow only matter when you edit a configuration by hand or through the API.
How the pair is resolved when a workflow is saved:
- The id wins when it resolves to exactly one job. The name beside it is then refreshed from that job — so editing the name alone does not re-point the dependency. To aim a dependency at a different job, clear the id in the same edit, or just re-pick the predecessor in the editor.
- An id that resolves to nothing falls through to the name, and the name's own job then supplies a fresh id. A dangling id is repaired rather than trusted.
- An ambiguous id or name is declined. If two jobs somehow answer to the same id, or two share the name, the reference is left name-only and picks up an id on a later save, once the ambiguity is gone.
- A prefix reference never gets an id. A conflict authored against a name prefix matches a set of jobs whose membership changes as jobs are added and renamed, so there is no single identity to point at. Those references stay name-based permanently — see Authoring a conflict.
What that means for a rename. The dependent workflow's copy of the predecessor's name is brought up to date the next time that dependent workflow is saved. Until then the stored configuration — and therefore any version built from it — still carries the old name. So after renaming a job that other workflows depend on, save those workflows and create new versions of them.
Errors a hand-edited dependency can now hit at save time:
| Message | Meaning |
|---|---|
| Job 'X' not found in current workflow | A same-workflow dependency names a job this workflow doesn't have. |
| Job 'X' not found in schedule 'Y' | A cross-workflow dependency names the workflow by name, and that workflow has no such job. |
| A workflow reference that resolves to neither an id nor a name | The save fails. The workflow a dependency names is the dependency — a reference to a workflow that cannot be found is never stored. |
| Resolving this workflow's references rewrote a dependency to point at a renamed job or schedule… | Refreshing the stale names produced a duplicate dependency, or a dependency on the job's own self. Fix the named dependencies and save again. |
One deliberate exception: a cross-workflow dependency whose workflow id resolves but whose job is missing does not fail the save. That is exactly the reference a renamed predecessor strands, and refusing it would block the save that repairs it. It still fails at build.
A copy of a workflow drops the ids of dependencies pointing at the copied workflow's own jobs — those get fresh identities — and keeps the ids of dependencies pointing into other workflows, whose jobs are untouched. An import drops both, because a bundle's ids belong to the system it came from; the names carry over and the ids repopulate on the first save.
A deployment transformation rule may not change a dependency's job id, though the name beside it stays transformable — see Fields no rule may change.
A predecessor that fanned out into several instances
A wait-on-all dependency on a multi-instance predecessor is stored as one record per matched instance. In the Processes job instance editor those are shown as a single dependency card, named for the underlying job, rather than one card per instance — so a dependency on a job that expanded into three instances reads as one dependency, which is what it is.
Editing or deleting that card applies the change across every underlying instance when you save.
Cards are only combined when the underlying records agree on their Condition and Type. If they diverge — different conditions against different instances of the same predecessor — they stay as separate cards, because collapsing them would hide a real difference.
When the workflow builds as several schedule instances
A workflow with Allow Multi-Instance on builds once per named schedule instance for a date. Dependencies and conflicts resolve within one instance, because instances are independent runs over different property values:
| Reference | Resolves to |
|---|---|
| Same workflow, same date | This job's own instance. BRANCH1's job is not held by BRANCH2's. |
| Same workflow, a day offset | The same instance name on that date — every run of it. |
| Same workflow, conflict marked for every date | Only this instance's dated runs. A singleton job stays a singleton per branch. |
| Another workflow, with an instance named on the dependency | That instance, and every run of it. |
| Another workflow, no instance named | The predecessor's first instance — the lowest name, computed over the instances that workflow defines — and its first run. |
| Another workflow, conflict, no instance named | Every instance built for that date. |
A schedule instance can hold several runs for one date — see Repeat builds of one instance. Wherever a reference names an instance, it means the instance, not one run of it: a conflict sees every run, so live work on a later run is not overlooked and reported satisfied. A plain dependency that names no instance resolves to the first instance's first run, which is the run the workflow's own schedule produced.
Two consequences are worth planning around:
- A cross-workflow dependency with no instance named is not "whichever built first". The first instance is computed from the workflow's definition, not from the rows that exist at the moment the dependency is evaluated — so a fan-out part-way through cannot settle a dependent on the wrong branch. That matters because dependency resolution is sticky once settled.
- To make two branches contend, ask for it. Same-workflow conflicts are per-instance. Use a Resource dependency or a cross-workflow conflict when branches genuinely share something — both of those span instances.
Seeing a predecessor from outside the instance
A predecessor outside the waiting job's own workflow instance — in another workflow, or in this workflow on a different day — is drawn on the Processes page's workflow diagram as a dimmed stand-in card rather than being left off the canvas. It shows the workflow name, the job name, the schedule instance where one is named, and the Days Offset; it carries no status, because nothing on that canvas resolves it. See Predecessors outside this run.
An any-day conflict's stand-in is worth reading carefully: its arrow reports blocked whenever a match is live on any date, including dates the canvas is not showing. Over-reporting a block is the deliberate direction there.
A cross-workflow predecessor on the Design canvas
The Design module's workflow canvas draws the same idea for the job being authored. A dependency on a job in another workflow gets its own card — the job name on the first line, the workflow it lives in on the second — with an edge into the job that depends on it.
| Behaviour | Detail |
|---|---|
| One card per external job | Every dependency naming the same workflow.job pair shares one card; each dependency still draws its own edge. |
| Edge style follows the dependency | A Requires or After dependency draws the ordinary ordering edge, result colour and day-offset label included. A Conflict draws the conflict edge. |
| A prefix conflict is its own card | A conflict that names a job-name prefix rather than a job shows the prefix with a trailing *, and is a separate card from a real job of that name. |
| Display-only | There is no ⋮ menu and nothing to delete: the dependency is edited on the job editor's Dependencies tab. |
| Show or hide them | The Card Filters section has an External Jobs switch, beside Frequencies, Thresholds, Resources and Expressions. See The workflow editor tool panel. |
Selecting the card opens that workflow in a new tab. The whole card is the target — it takes keyboard focus, and Enter or Space opens it too. A double-selection opens one tab, not two.
The card carries only the workflow's name, so that is what is resolved. If it cannot be, a message says which case it was: the dependency no longer names a workflow, the workflow was not found, too many workflows have similar names (open it from the workflows list instead), or the browser blocked the tab.
A dependency authored against this workflow's old name — renamed but not yet saved — reads as external until the save, and appears as one of these cards. It is the same name-based comparison the rest of the canvas uses.
Dragging across the card still pans the canvas, so a selection that drifts more than a few pixels is read as a drag and does not open anything.
How job dependencies resolve
- Condition matters:
successwaits for the predecessor to finish OK,failurewaits for it to fail, andanyproceeds on either. A dependency onfailure/anyis valid design (e.g. a cleanup job that runs when its predecessor fails). - Type decides the absent-predecessor case only — see the two-axis table.
- Days Offset lets a job depend on a predecessor from a different day's run.
- Resource dependencies reserve capacity — a job waits if not enough of the resource is free.
Which predecessor statuses satisfy each Condition
A Condition is not a single status — each maps to the set of terminal statuses that release the dependent:
| Condition | Satisfied by | Notes |
|---|---|---|
success | FINISHED_OK, MARKED_FINISHED_OK, SKIPPED, FIXED | A skipped or marked-fixed predecessor counts as success. |
failure | FAILED, MARKED_FAILED, INITIALIZATION_ERROR, UNDER_REVIEW | UNDER_REVIEW is a deliberate non-terminal release point — the operator has acknowledged the failure, so recovery work starts without waiting for it to be marked Fixed or Failed. |
any | The union of the two lists above | any is the ignore-error union, not "any outcome." |
CANCELLEDandMISSED_START_TIMEsatisfy nothing under any condition — a predecessor that never produced a result cannot release its dependents. A job depending (even withany) on a predecessor that ends up cancelled or missing its start time will wait forever; that's the intended behavior, not a bug. Clear it with Force Start / Skip or fix the upstream job.*_PENDING_TERMstatuses do not satisfy dependents — the predecessor must reach its final terminal status first.
Threshold dependencies
A threshold dependency holds the job until a named threshold compares as required against the value on the dependency. You choose the comparison:
| Operator | Satisfied when the threshold is |
|---|---|
| Equal To | equal to the value |
| Not Equal To | anything but the value |
| Less Than | below the value |
| Less Than or Equal To | at or below the value |
| Greater Than | above the value |
| Greater Than or Equal To | at or above the value |
Greater Than or Equal To is what a dependency with no operator set means, so a dependency authored before the operator existed keeps the behavior it had.
The workflow canvas draws the operator on the dependency's edge — ≥ 5, < 10 — so the comparison
the job is waiting on is readable without opening the job.
If the threshold a dependency names has been deleted, the dependency is treated as satisfied and the job proceeds. That keeps a deleted threshold from silently parking every job that referenced it.
That is the only case that releases the job. Anything else that stops the value being read — the threshold is not found, nothing is configured, a refusal, a service error — leaves the dependency blocking, because the threshold may exist and the answer is unknown. Releasing a job cannot be undone, so an unreadable threshold fails closed.
Two consequences follow from that, and they are the ones worth knowing:
- A renamed threshold blocks. A dependency saved without the threshold's identifier is looked up by name instead, and a name that matches nothing is "not found" rather than "deleted" — so the job waits rather than being released. Every dependency authored in the current job editor carries the identifier, so this affects imported ones.
- A threshold identifier that never existed blocks too, for the same reason. An automation import keeps the identifiers from the bundle it came from, so a dependency can arrive naming one this environment has never had.
A dependency carrying neither an identifier nor a name blocks without looking anything up.
Expression dependencies
An Expression dependency holds the job until a custom
expression evaluates to True. The job waits at
WAIT_EXPRESSION_DEPENDENCY while it is being asked, and the platform re-asks it about once a
second until it answers True — an expression is never a once-and-done fact, so a job that
satisfied it and then restarted on a recurring cycle is asked again.
All of a job's expressions are evaluated as one expression
A job's expression dependencies are joined with && and evaluated as a single expression,
not evaluated separately and then ANDed. Line breaks and tabs are removed from the joined text
rather than replaced with a space, so an expression split across two lines is rejoined with nothing
between the halves.
||&& and || share one precedence level and associate left to right, so a half written
[[OI.A]] = "1" || [[OI.B]] = "1" joined with a second half reads
(A or B) && second — which is usually what you meant — but the same associativity makes
first || second && third read (first || second) && third. Parenthesize each half you author, and
prefer one expression dependency carrying the whole condition over several that have to be read
together.
The stored value is the expression body. A pasted [[= … ]] wrapper is stripped from each
expression before the join, so authoring it either way works.
Only True and False answer the gate
The rendered result is read with the same parse as the ToBool operator: true
or false, case-insensitively, with surrounding whitespace tolerated. Nothing else counts.
1 is not true, and 0 is not falseA number is refused outright, and so is a non-empty string — there is no truthiness here. An
expression that computes 1 + 0 does not satisfy the dependency; it puts the job
on hold. Write a comparison — [[= 1 + 0 > 0 ]] — so the
expression renders True.
What each outcome does to the job
| The expression | What happens to the job |
|---|---|
Renders True | Advances to the next waiting status, and on to dispatch when its other dependencies are met |
Renders False | Stays at WAIT_EXPRESSION_DEPENDENCY and is asked again on the next pass. This is the normal steady state of a gate that is not open yet |
Is malformed, renders something that is neither True nor False, or renders nothing | The job is put ON_HOLD, and the reason is recorded as its termination description |
| Could not be answered because something it reads was temporarily unavailable | Stays at WAIT_EXPRESSION_DEPENDENCY and is retried on the next pass — the same as False. A transient fault is not treated as a bad expression |
A job carrying no expression dependency is unaffected by any of this.
The failure text is the job's termination description, so it appears on the job detail panel, on the job's node on the workflow diagram, and as the marker beside its name in the jobs list. It leads with the cause and then names the authored expression, and it never contains a resolved value — so an expression comparing an encrypted property does not leak that property into an operator-visible message. Very long text is truncated.
Release the job once the expression is corrected. A job that was held, released and then ran successfully no longer carries the old failure text into its own result.
The reason is written only when the job changes status. If a held job is released and its expression
is then merely False, the job parks at WAIT_EXPRESSION_DEPENDENCY still showing the earlier
failure text. Read the status, not the text, as the current state.
An expression is resolved strictly, and against stored values
Two things about resolution differ from an expression in a job parameter:
- There is no degraded reading. A token the platform cannot resolve fails the expression instead
of rendering as its own source text, because source text that happens to read
Truewould open the gate. So a misspelled property name holds the job. - Operands are not masked. The comparison runs against the property's stored value, so an
encrypted property compares as itself rather than as a mask. The result is a
True/Falsethe platform acts on and never a value it stores or sends, which is what makes that safe.
Which run facts an expression can read
An expression dependency is evaluated before the job is matched to a machine, so the job's own facts read slightly differently here than they do in a job parameter at dispatch:
| Token | At the gate |
|---|---|
JI.$MACHINE NAME | Always available. Undetermined until the job has a machine |
JI.$ACTUAL RUN TIME | Empty — the job has not run |
JI.$MASTER JOB NAME | The job's base definition name, so a job expanded into instances reads the name it was expanded from. It is the stored name and does not follow a later rename |
JI.$EST RUN TIME | Available only if the job instance carries an estimated run time |
OI.$OPCONVER | Not available at the gate — a tenant property of the same name is read instead |
$EST RUN TIME is not seeded by the buildA built job carries no estimated run time, so an expression that reads JI.$EST RUN TIME cannot be
resolved and holds the job until someone sets Estimated Run Time on that job instance.
Clearing that field on the instance takes the value away again. Don't gate a job on this token.
A job marked to be skipped still resolves its expression
A deferred skip (JOB_TO_BE_SKIPPED) resolves its expression on the same gate rather than bypassing
it, so a skip lands in the same order relative to the expression as a run would have. An expression
that cannot be evaluated there does not put the job on hold — the skip you asked for is
preserved, and the job stays marked.
Troubleshooting
| Symptom | Likely cause | Resolution |
|---|---|---|
| Job sits waiting and never runs | A dependency isn't satisfied — identify which one | Check each dependency: predecessor result, threshold value, resource availability, expression (Operator → Builder if the dependency is mis-set). |
| A job with an expression dependency is On Hold, with a reason naming the expression | The expression could not be evaluated, or it rendered something that is neither True nor False | Fix the expression on the job and release the job. See What each outcome does to the job. |
| A job with an expression dependency is held, and the reason names an estimated run time | The expression reads JI.$EST RUN TIME, which the build does not populate | Set Estimated Run Time on the job instance, or gate the job on something else (Builder). |
| A job with an expression dependency waits with no reason recorded | The expression is rendering False — the gate is simply not open yet | Expected. Check what the expression reads; a held job would have named a cause instead (Operator). |
| Job waits on a predecessor that already finished OK | The dependency expects failure or a different Condition/day offset | Confirm the Condition and Days Offset match intent (Builder). |
| Job waits even though the predecessor isn't scheduled today | The dependency is Requires, which waits when the predecessor is absent | Switch the Type to After if the predecessor is optional; leave it Requires if it must run (Builder). |
| Job waits regardless of Type, with a warning on the predecessor | The predecessor exists but only under an instance name this job doesn't share; cross-instance dependencies aren't allowed | Compare the two jobs' instance names and re-point the dependency (Builder). |
| A job in one branch waits on a predecessor that ran in another branch | The reference names no instance, so it resolved to the predecessor workflow's first defined instance | Name the instance on the dependency, or keep the branches in the same workflow so same-workflow scoping applies (Builder). |
| Job waits only while another named job is running, then proceeds | A Conflict dependency is holding it — by design, to stop the two overlapping | Expected. If the two should be allowed to overlap, remove the conflict (Builder). |
| Job never gets a resource | Not enough of the named resource is available; another job holds it | Check resource counts and which jobs hold it (Builder / Operator). |
| Job runs earlier/later than expected relative to another | Missing or extra job dependency | Review the dependency wiring (Builder). |
| A job sits in Waiting on start time forever, with nothing logged and no status explaining it | A start-time window was set through the API or an automation import with a bound that isn't a time. An unreadable bound never opens the window, and an unreadable end reads as permanently past | Such a request is now rejected, and the error names the offending bound. A job already stuck this way needs the schedule date rebuilt (Administrator). |
Contact support when
- All dependencies are demonstrably satisfied but the job still does not become eligible.
Include the workflow instance, the job, and the state of each of the job's dependencies.