pentaho/pentaho-kettle · error · KettleException

The number of hops read

Error message

The number of hops read [${hops}] was not the number we expected [${expected}]

What it means

TransDelegate.dataNodeToElement reads hops (step connections) from the stored DataNode and verifies that transMeta.nrTransHops() equals the number of hop nodes stored (nrHops). A mismatch throws this KettleException. It usually means some hops were skipped during load — typically because their from/to steps were not found in the transformation.

Solutions

  1. Open and re-save the transformation from a working copy so steps and hops are serialized together.
  2. Inspect NODE_HOPS entries in the stored data for hops referencing missing/renamed steps.
  3. Restore the transformation from a previous repository version.
  4. Verify the transformation loads in the same Kettle version it was saved with; version mismatches can change hop serialization.

Example fix

// before
if ( stepFrom != null && stepTo != null ) {
  transMeta.addTransHop( new TransHopMeta( stepFrom, stepTo, enabled ) );
} // later throws when count mismatches
// after
if ( stepFrom != null && stepTo != null ) {
  transMeta.addTransHop( new TransHopMeta( stepFrom, stepTo, enabled ) );
} else {
  log.logError( "Hop references missing step; from=" + fromName + " to=" + toName + " in " + transMeta.getName() );
  nrHops--; // align expected count with skippable hops, or fail with a clear message
}
Defensive patterns

Strategy: try-catch

Validate before calling

// verify each hop's steps exist before load-time consistency check
for ( String stepName : hopStepNames ) {
  if ( transMeta.findStep( stepName ) == null ) throw new KettleException( "Hop references missing step: " + stepName );
}

Try / catch

try {
  delegate.dataNodeToElement( node, transMeta );
} catch ( KettleException e ) {
  if ( e.getMessage().contains( "number of hops read" ) ) {
    logError( "Hop/step mismatch; check for renamed or missing steps in " + transMeta.getName() );
  } else { throw e; }
}

Prevention

When it happens

Trigger: Loading a transformation where a hop references a step that failed to load or is absent, so addTransHop is skipped and nrTransHops < nrHops; or stored data contains hops whose step names don't resolve.

Common situations: Transformations edited or migrated between repositories where step names changed, partially failed imports, manually edited repository nodes, or loading content saved by a different Kettle version.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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

Appendix: source

Thrown at plugins/pur/core/src/main/java/org/pentaho/di/repository/pur/TransDelegate.java:443

      String stepFromName = getString( hopNode, TRANS_HOP_FROM );
      String stepToName = getString( hopNode, TRANS_HOP_TO );
      boolean enabled = true;
      if ( hopNode.hasProperty( TRANS_HOP_ENABLED ) ) {
        enabled = hopNode.getProperty( TRANS_HOP_ENABLED ).getBoolean();
      }

      StepMeta stepFrom = StepMeta.findStep( transMeta.getSteps(), stepFromName );
      StepMeta stepTo = StepMeta.findStep( transMeta.getSteps(), stepToName );

      // Make sure to only accept valid hops PDI-5519
      //
      if ( stepFrom != null && stepTo != null ) {
        transMeta.addTransHop( new TransHopMeta( stepFrom, stepTo, enabled ) );
      }

    }
    if ( transMeta.nrTransHops() != nrHops ) {
      throw new KettleException( "The number of hops read [" + transMeta.nrTransHops()
          + "] was not the number we expected [" + nrHops + "]" );
    }

    // Load the details at the end, to make sure we reference the databases correctly, etc.
    //
    loadTransformationDetails( rootNode, transMeta );
    loadDependencies( rootNode, transMeta );

    transMeta.eraseParameters();

    DataNode paramsNode = rootNode.getNode( NODE_PARAMETERS );

    int count = (int) paramsNode.getProperty( PROP_NR_PARAMETERS ).getLong();
    for ( int idx = 0; idx < count; idx++ ) {
      DataNode paramNode = paramsNode.getNode( TRANS_PARAM_PREFIX + idx );
      String key = getString( paramNode, PARAM_KEY );
      String def = getString( paramNode, PARAM_DEFAULT );
      String desc = getString( paramNode, PARAM_DESC );

View on GitHub (pinned to f3058517a1)