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
- Open and re-save the transformation from a working copy so steps and hops are serialized together.
- Inspect NODE_HOPS entries in the stored data for hops referencing missing/renamed steps.
- Restore the transformation from a previous repository version.
- 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
- Re-save transformations after renaming steps to keep hops consistent
- Avoid out-of-band edits to repository content
- Use version history to recover from corrupted loads
- Migrate content with proper import/export tools, not raw node copies
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
- The number of notes read
- TransLoadProgressDialog.Exception.ErrorLoadingTransformation
- TransSaveProgressDialog.Exception.ErrorSavingTransformation
- Unable to load transformation from path [" + absPath + "]
- Unable to load transformation with id [" + idTransformation…
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)