windmill-labs/windmill · warning

Skipping ${cyclic.length} script(s) on a dependency cycle: $

Error message

Skipping ${cyclic.length} script(s) on a dependency cycle: ${cyclic.sort().join(", ")}

What it means

During `wmill pipeline run`, topoOrder orders the selected scripts; any scripts that form a dependency cycle cannot be ordered and are excluded from the run, with this warning listing them. Running a cyclic subset is impossible, so the CLI drops those nodes rather than deadlocking.

Source

Thrown at cli/src/commands/pipeline/pipeline.ts:873

      if (reachable.has(e)) {
        reachableEnds.push(e);
      } else {
        droppedEnds.push(e);
        log.warn(
          `end '${idLabel(e)}' is only reachable through a skipped input/event handler — not run (bind it with --upload to include it).`,
        );
      }
    }
  }

  // Selections can still pull a non-runnable node in via graph reachability (e.g.
  // whole-pipeline mode collects every root's closure) — drop macro libraries and
  // local-only display nodes before ordering so they never run.
  for (const p of notRunnablePaths) selectedScripts.delete(p);

  const { order, cyclic } = topoOrder(graph, selectedScripts);
  if (cyclic.length > 0) {
    log.warn(`Skipping ${cyclic.length} script(s) on a dependency cycle: ${cyclic.sort().join(", ")}`);
  }

  // `// partitioned` scripts need a resolved `partition` arg. Deployed runs get
  // it from backend run-start resolution, but previews (`--local`) never do —
  // so resolve it client-side there: `--partition` wins; otherwise time kinds
  // default to the current UTC period (mirroring the backend defaults; custom
  // `tz=`/`format=`/`start=` opts are a backend concern — pass --partition
  // explicitly to match them). `dynamic` has no default. For deployed runs the
  // arg is only injected when `--partition` is given (an explicit backfill).
  const partitionKindByPath = new Map(
    graph.runnables
      .filter((r) => r.usage_kind === "script" && r.partition_kind)
      .map((r) => [r.path, r.partition_kind!]),
  );
  const partitionValueFor = (nodePath: string): string | undefined => {
    const kind = partitionKindByPath.get(nodePath);
    if (!kind) return undefined;
    if (opts.partition) return opts.partition;

View on GitHub (pinned to e474e8803c)

Solutions

  1. Open the pipeline in the UI and remove/reverse the dependency edge that closes the cycle.
  2. Check the listed paths (the warning sorts them) and trace their mutual dependencies.
  3. Re-run once the DAG is acyclic — the skipped scripts will then be included.
  4. If the cycle is intentional display-only wiring, keep it out of the runnable selection (macro libraries/local display nodes are normally filtered).

Example fix

// before: a depends on b, b depends on a -> both skipped
// after (edit pipeline definition): remove a's dependency on b
// a: depends: [b]  ->  a: depends: []
Defensive patterns

Strategy: validation

Validate before calling

// detect cycles in the dependency graph before running
function hasCycle(nodes) {
  const state = {};
  function visit(n) {
    if (state[n] === 1) return true;
    if (state[n] === 2) return false;
    state[n] = 1;
    for (const d of depsOf(n)) if (visit(d)) return true;
    state[n] = 2;
    return false;
  }
  return Object.keys(nodes).some(visit);
}

Prevention

When it happens

Trigger: The selected window of the pipeline DAG contains a cycle (script A depends on B which depends on A), e.g. after a misconfigured dependency edge or a hand-edited pipeline definition.

Common situations: Accidentally adding a dependency edge backwards while editing the flow; importing a pipeline whose generated dependencies form a loop; macro/library scripts referencing each other.

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/954baa6c1a5090bc. Report an issue: GitHub.