conductor-oss/conductor · error · IllegalArgumentException
External tool data contains a cyclic structure
Error message
External tool data contains a cyclic structure
What it means
Thrown by ExternalDataLimits.validateStructure when an object already present in the per-validation IdentityHashMap is encountered again — i.e. the object graph is cyclic (a self-referential or mutually-referential structure). This guards against infinite recursion and unbounded serialization of tool data into workflow state. IllegalArgumentException. Note the validator removes each object from the seen-set in a finally block as it unwinds, so only true cycles (a node reachable from itself along the active path) trip this, not DAGs/diamonds.
Source
Thrown at ai/src/main/java/org/conductoross/conductor/ai/http/ExternalDataLimits.java:46
/** Rejects object graphs that would make workflow state expensive or unsafe to process. */
public static void validateStructure(Object value) {
validateStructure(value, 0, new IdentityHashMap<>());
}
private static void validateStructure(
Object value, int depth, IdentityHashMap<Object, Boolean> seen) {
if (value == null
|| value instanceof String
|| value instanceof Number
|| value instanceof Boolean) {
return;
}
if (depth > MAX_NESTING_DEPTH) {
throw new IllegalArgumentException(
"External tool data exceeds the maximum nesting depth of " + MAX_NESTING_DEPTH);
}
if (seen.put(value, Boolean.TRUE) != null) {
throw new IllegalArgumentException("External tool data contains a cyclic structure");
}
try {
if (value instanceof Map<?, ?> map) {
for (Map.Entry<?, ?> entry : map.entrySet()) {
validateStructure(entry.getKey(), depth + 1, seen);
validateStructure(entry.getValue(), depth + 1, seen);
}
} else if (value instanceof Collection<?> collection) {
for (Object item : collection) {
validateStructure(item, depth + 1, seen);
}
} else if (value.getClass().isArray()) {
for (int i = 0; i < Array.getLength(value); i++) {
validateStructure(Array.get(value, i), depth + 1, seen);
}
}
} finally {
seen.remove(value);View on GitHub (pinned to cf7c3e4a8a)
Solutions
- Break the cycle before validation: convert the cyclic structure to a tree (JSON-like) by copying into plain Maps/Lists with no back-references, or null-out parent pointers.
- Serialize to JSON and re-parse into a fresh structure — JSON has no cycles, so the round-trip removes them.
- If the data is legitimately a graph, project it into an acyclic form (e.g. a flat node list + edge list) before handing it to the workflow.
Example fix
// before — parent references child which references parent
parent.setChild(child); child.setParent(parent); // cycle
// after — copy to DTOs without back-references
Map<String,Object> out = Map.of("name", parent.getName(),
"children", parent.getChildren().stream().map(c -> Map.of("name", c.getName())).toList()); Defensive patterns
Strategy: validation
Validate before calling
// Remove cycles before validation: copy into plain JSON-shaped structures // (Map/List with no back-references) or round-trip through JSON Object acyclic = objectMapper.readTree(objectMapper.writeValueAsString(graph)); ExternalDataLimits.validateStructure(acyclic);
Try / catch
try {
ExternalDataLimits.validateStructure(value);
} catch (IllegalArgumentException e) {
// cyclic structure — break references at the source, then re-validate
throw new IllegalArgumentException("Tool data has a cycle; convert to a tree first", e);
} Prevention
- Break parent/child back-references when serializing domain objects to tool output.
- Round-trip through JSON to guarantee an acyclic tree before validation.
- For graph data, emit a flat node/edge list instead of nested references.
When it happens
Trigger: An external tool result contains a cyclic object graph — e.g. a Map that contains itself as a value, or two objects referencing each other — passed to validateStructure. Identity-based (==) detection, so it fires on the exact same object instance recurring.
Common situations: Serializing a domain object that has bidirectional parent/child links into tool output without breaking cycles; reusing the same mutable Map/List instance in multiple places and letting it reference itself; a serializer that preserves references.
Related errors
- External tool data exceeds the maximum nesting depth of {MAX
- Execution not found: ${executionId}
- inspectPlan: agentConfig is required
- inspectPlan: plan is required
- inspectPlan: agentConfig.strategy must be 'plan_execute', go
AI-assisted analysis of conductor-oss/conductor@cf7c3e4a8a (2026-08-14).
Data as JSON: /api/errors/7bdb72b2d6cb01b9.
Report an issue: GitHub.