[object Object]

A CMDB with circular relationships looks correct in any single record view and behaves badly everywhere else. Dependency views render incorrect blast radius, impact calculations diverge, change risk scoring spits out nonsense. The records all pass validation. The graph is broken. Here is how to find the loops and what to do about them.

How loops actually appear

Nobody draws a circle on purpose. Loops are emergent — they appear because two well-meaning automation paths each added a relationship that, when composed with a third manual edit, closed a cycle. The most common patterns:

  • An application is marked “Runs on” a server; the same server is marked “Hosts” the application; then a discovery pattern adds “Depends on” between them. The redundancy is mostly harmless. But add one more “Depends on” in the reverse direction and you have a cycle.
  • Service maps that span business services and technical services get cross-linked through tag-based discovery and pattern-based discovery, each unaware of what the other has done.
  • Cluster CIs (logical groupings) get a “Member of” relationship in one direction and a “Contains” relationship in the other; one or the other gets duplicated in the reverse direction during an import.

The point is not that any one of these is necessarily wrong. The point is that detecting cycles is something you have to do periodically and deliberately.

A working cycle-detection script

Below is a depth-bounded traversal that finds cycles starting from a seed CI. It uses GlideRecord, runs in a background script, and bounds depth to keep runtime predictable.

// Background script — detect cycles starting from a seed CI
function findCycles(seedSysId, maxDepth) {
    var found = [];
    var stack = [{ ci: seedSysId, path: [seedSysId] }];

    while (stack.length > 0) {
        var node = stack.pop();
        if (node.path.length > maxDepth) continue;

        var rel = new GlideRecord('cmdb_rel_ci');
        rel.addQuery('parent', node.ci);
        rel.query();
        while (rel.next()) {
            var child = rel.child.toString();
            if (node.path.indexOf(child) !== -1) {
                // Cycle detected — record the closed path
                found.push(node.path.concat([child]));
                continue;
            }
            stack.push({ ci: child, path: node.path.concat([child]) });
        }
    }
    return found;
}

var cycles = findCycles('1234567890abcdef1234567890abcdef', 8);
gs.info('Cycles found: ' + cycles.length);
for (var i = 0; i < cycles.length && i < 5; i++) {
    gs.info('  ' + cycles[i].join(' -> '));
}

A maxDepth of 8 catches almost every real-world cycle and keeps the traversal bounded. Be aware: this is a forward traversal on the parent field of cmdb_rel_ci. To catch cycles that traverse the relationship in the other direction, swap parent for child and run the second pass.

Scaling: GlideAggregate for the hot spots

You cannot run the per-seed traversal across the entire CMDB. You can scope it to the CIs most likely to participate in cycles. The likeliness signal is degree — CIs with very high in-degree and out-degree are over-represented in cycles.

// Find high-degree CIs as cycle-detection seeds
var hubs = [];
var agg = new GlideAggregate('cmdb_rel_ci');
agg.addAggregate('COUNT');
agg.groupBy('parent');
agg.orderByAggregate('COUNT');
agg.orderByDesc('COUNT');
agg.setLimit(200);
agg.query();
while (agg.next()) {
    var count = parseInt(agg.getAggregate('COUNT'));
    if (count >= 10) hubs.push({ ci: agg.parent.toString(), degree: count });
}
gs.info('High-degree hub CIs: ' + hubs.length);

Run cycle detection from each hub. In practice this finds 80 percent of cycles for 5 percent of the seeding effort. The remaining 20 percent — cycles between low-degree CIs — are usually less impactful and can be found in a slower, full-graph pass on a weekend.

What to do when you find a cycle

Do not auto-delete the offending relationship. A cycle of length 3 has three relationships, any of which might be the legitimate one. Auto-deletion will damage the graph more than the cycle did.

The disciplined cleanup workflow:

  1. The detection script writes findings to a u_cmdb_cycle table with the full path and the relationships involved.
  2. A human (or a service-mapping subject matter expert per domain) reviews each cycle and picks the relationship to break.
  3. The break is logged with reason: “stale discovery”, “manual error”, “duplicate from import”, “intentional but wrong direction”.
  4. The break is committed via update set so it can be tracked and rolled back if it breaks a dependency view.

The reason codes matter. After a quarter of cleanup data, the distribution tells you where to invest preventive effort. If 60 percent of cycles trace to one discovery pattern, fix the pattern, not the cycles.

Prevention: validation at write time

Better than periodic detection is prevention. A Business Rule on cmdb_rel_ci insert can refuse the relationship if it would close a short cycle:

// Business Rule: before insert on cmdb_rel_ci
(function executeRule(current, previous /*null when async*/) {
    var parentId = current.parent.toString();
    var childId = current.child.toString();
    if (wouldCreateShortCycle(parentId, childId, 4)) {
        gs.addErrorMessage('This relationship would create a cycle. Review before proceeding.');
        current.setAbortAction(true);
    }
})(current, previous);

function wouldCreateShortCycle(parent, child, maxLen) {
    var stack = [{ ci: child, depth: 1 }];
    while (stack.length > 0) {
        var node = stack.pop();
        if (node.depth >= maxLen) continue;
        var rel = new GlideRecord('cmdb_rel_ci');
        rel.addQuery('parent', node.ci);
        rel.query();
        while (rel.next()) {
            var next = rel.child.toString();
            if (next === parent) return true;
            stack.push({ ci: next, depth: node.depth + 1 });
        }
    }
    return false;
}

Cap the traversal at depth 4. Beyond that the runtime cost on every relationship insert becomes too high. Anything deeper gets caught by the periodic detection pass.

When the loop is intentional

Some cycles are not bugs. Mutual replication relationships, peered services, and a few specialized CI types legitimately have bidirectional dependencies. Mark these explicitly:

  • Add a u_cycle_approved field on cmdb_rel_ci.
  • The validation Business Rule skips records flagged approved.
  • Periodic detection ignores cycles where every edge is approved.

Without this escape hatch, the prevention rule becomes a nuisance and admins will turn it off. With it, the rule stays on and the approved cycles are documented.

Dependency view impact

Once you start cleaning cycles, expect dependency view shapes to change. A view that previously rendered as a tangle will resolve into a tree, and the visible blast radius of a CI will shrink. Stakeholders who got used to the wrong picture will read the smaller blast radius as “the tool is broken now.”

Communicate ahead. A short note on the change calendar: “CMDB cycle cleanup in progress; dependency views may shift for affected CIs. Contact platform team if a change looks wrong.” This costs nothing and prevents a week of escalations.

For the broader hygiene program of which cycle detection is one piece, see our CMDB hygiene quarterly playbook.

UI for the cleanup queue

Build a simple list view for the u_cmdb_cycle table with these columns: cycle path, hub CI, relationship types, suggested break, reviewer, status. Sort by impact (count of CIs whose dependency view is affected). Pin the list to the CMDB ops dashboard. Cleanup happens; cleanup gets tracked; cleanup gets credited at the quarterly review.

Cycle classes you will keep finding

After a few months of running detection, you will recognize repeat patterns. A short, mostly-complete list:

  • Storage-to-server-to-application triangles — discovery patterns each adding a “depends on” in a different direction
  • Cluster-member loops — member-of and contains relationships reversed in import data
  • Application-to-service-to-business-service loops — usually from service mapping running alongside manual relationship edits
  • Database-to-database-instance-to-database mirrors — replication relationships that someone modeled as bidirectional when one direction would have sufficed
  • Network-segment self-references — uncommon but distinctive; usually a discovery rule misconfiguration

Each class has a different remediation. Document them in a runbook so the next admin to encounter the pattern does not re-investigate from scratch.

When the source is import

A surprisingly large share of cycles trace to import sets that ran with relationship-creation logic but no cycle-check. The fix is upstream: add the cycle check to the import transform map, not to the production data after the fact.

// Transform map onBefore script — drop cycle-creating relationships
(function transformRow(source, target, map, log, isUpdate) {
    if (target.getTableName() !== 'cmdb_rel_ci') return;
    var p = target.parent.toString();
    var c = target.child.toString();
    if (wouldCreateShortCycle(p, c, 4)) {
        log.warn('Dropping cycle-creating relationship: ' + p + ' -> ' + c);
        target.ignore = true;
    }
})(source, target, map, log, isUpdate);

This costs nothing on imports that are clean. It saves hours of cleanup on imports that are not.

Tradeoffs to name out loud

Detection has a runtime cost. The hub-seeded pass takes minutes; the full pass takes hours. Run the full pass on a weekend, off-peak, against a clone if your tenant is sensitive. The validation rule on write has a per-insert latency cost that is small but non-zero — for bulk imports of relationships (a data center migration, for example), you may want to bypass the rule during the import and run a one-shot cycle detection at the end.

Do not treat the cycle count as a vanity zero. The goal is “no cycles I cannot explain,” not “no cycles at all.”

Key takeaways

  • Cycles emerge from composition, not malice. They are inevitable in an active CMDB and need a periodic detection pass.
  • Seed traversal from high-degree hubs to catch most cycles cheaply; reserve the full-graph pass for off-peak.
  • Never auto-delete cycle-forming relationships. Log, review, classify, commit through update set.
  • A short-cycle prevention rule at write time catches the new problems; periodic detection catches the legacy ones.
  • Cleanup will visibly change dependency views. Communicate before, not after.
[object Object]
Share