pentaho/pentaho-kettle · error · RuntimeException

Endless loop detected for substitution of variable:

Error message

Endless loop detected for substitution of variable: 

What it means

StringUtil.substitute (variable substitution engine) throws a RuntimeException when the recursion depth of variable replacement exceeds 50, meaning a variable value references itself directly or through a cycle (e.g. A=${B}, B=${A}). The guard prevents infinite recursion / stack overflow.

Solutions

  1. Find the self-referencing or circular variable (the name is appended to the message) and break the cycle.
  2. Rename the intermediate variables so each value references only other, non-circular variables.
  3. Pre-resolve the variable value and store the literal result instead of a placeholder containing itself.
  4. Audit kettle.properties, system properties and transformation/job parameters for repeated variable names.

Example fix

// before
props.setProperty("JAVA_HOME", "${JAVA_HOME}/jdk11");
// after
props.setProperty("JAVA_HOME_PARENT", "/opt/java");
props.setProperty("JAVA_HOME", "${JAVA_HOME_PARENT}/jdk11");
Defensive patterns

Strategy: validation

Validate before calling

// detect self-reference before substitution
String v = variables.getVariable("VAR_A");
if (v != null && v.contains("${VAR_A}")) throw new KettleException("VAR_A references itself");

Try / catch

try {
  result = StringUtil.environmentSubstitute(raw);
} catch (RuntimeException e) {
  if (e.getMessage().startsWith("Endless loop detected")) {
    throw new KettleException("Circular variable definition: " + e.getMessage(), e);
  }
  throw e;
}

Prevention

When it happens

Trigger: A kettle.properties / environment variable whose value contains a reference to itself, or a circular chain of variable references like VAR_A=${VAR_B} and VAR_B=${VAR_A}, expanded via StringUtil.environmentSubstitute / substitute with ${...} (or custom open/close delimiters).

Common situations: Setting VAR_HOME=${VAR_HOME}/sub in kettle.properties or a transformation-level parameter referencing itself; copying variables between environments where one was previously resolved and re-registered including its own placeholder.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of pentaho/pentaho-kettle@f3058517a1 (2026-09-13). Data as JSON: /api/errors/0dfeef6110de6002. Report an issue: GitHub.

Appendix: source

Thrown at core/src/main/java/org/pentaho/di/core/util/StringUtil.java:152

      if ( j > -1 ) {
        String varName = rest.substring( i + open.length(), j );
        Object value = variablesValues.get( varName );
        if ( value == null ) {
          value = open + varName + close;
        } else if ( aString.equals( value ) ) {
          //Needed to avoid an endless loop if left to continue to next else statement
          return (String) value;
        } else {
          // check for another variable inside this value
          int another = ( (String) value ).indexOf( open ); // check
          // here
          // first for
          // speed
          if ( another > -1 ) {
            // for safety: avoid recursive
            if ( recursion > 50 ) {
              // endless loops with stack overflow
              throw new RuntimeException( "Endless loop detected for substitution of variable: " + (String) value );
            }
            value = substitute( (String) value, variablesValues, open, close, ++recursion );
          }
        }
        buffer.append( rest.substring( 0, i ) );
        buffer.append( value );
        rest = rest.substring( j + close.length() );
      } else {
        // no closing tag found; end the search
        buffer.append( rest );
        rest = "";
      }
      // keep searching
      i = rest.indexOf( open );
    }
    buffer.append( rest );
    return buffer.toString();
  }

View on GitHub (pinned to f3058517a1)