You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hop.apache.org by ha...@apache.org on 2026/08/15 08:24:36 UTC

(hop) branch main updated: issue #7953 : Add Multi Mapping transform (#7958)

This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 9b8227aa18 issue #7953 : Add Multi Mapping transform (#7958)
9b8227aa18 is described below

commit 9b8227aa18a02dd6e1041455a5c153683b14e071
Author: Matt Casters <ma...@gmail.com>
AuthorDate: Sat Aug 15 10:24:30 2026 +0200

    issue #7953 : Add Multi Mapping transform (#7958)
    
    * issue #7953 : Add Multi Mapping transform
    
    Restore the PDI Mapping contract as a new Multi Mapping transform with
    0..N inputs and outputs, info and target hops, and row producers/listeners.
    Simple Mapping is unchanged.
    
    * issue #7953 : apache-rat
---
 .../ROOT/assets/images/transforms/icons/MMAP.svg   |  42 +
 docs/hop-user-manual/modules/ROOT/nav.adoc         |   1 +
 .../modules/ROOT/pages/pipeline/transforms.adoc    |   1 +
 .../pages/pipeline/transforms/mapping-input.adoc   |   3 +
 .../pages/pipeline/transforms/mapping-output.adoc  |   3 +
 .../pages/pipeline/transforms/multi-mapping.adoc   |  79 ++
 .../pages/pipeline/transforms/simple-mapping.adoc  |   2 +
 .../pages/snippets/best-practices/mappings.adoc    |   2 +
 .../transforms/0108-multi-mapping-child-01.hpl     | 155 ++++
 .../transforms/0108-multi-mapping-parent-01.hpl    | 188 +++++
 .../datasets/golden-multi-mapping-01.csv           |   5 +
 .../metadata/dataset/golden-multi-mapping-01.json  |  48 ++
 .../0108-multi-mapping-parent-01 UNIT.json         |  48 ++
 .../transforms/mapping/MappingTransforms.java      | 121 +++
 .../pipeline/transforms/mapping/SimpleMapping.java |  24 +-
 .../transforms/multimapping/MultiIOMappings.java   |  93 +++
 .../transforms/multimapping/MultiMapping.java      | 470 +++++++++++
 .../transforms/multimapping/MultiMappingData.java  |  54 ++
 .../multimapping/MultiMappingDialog.java           | 901 +++++++++++++++++++++
 .../multimapping/MultiMappingInputDefinition.java  | 139 ++++
 .../multimapping/MultiMappingInputRename.java      |  68 ++
 .../transforms/multimapping/MultiMappingMeta.java  | 610 ++++++++++++++
 .../multimapping/MultiMappingOutputDefinition.java | 132 +++
 .../multimapping/MultiMappingOutputRename.java     |  68 ++
 .../transforms/mapping/src/main/resources/MMAP.svg |  42 +
 .../messages/messages_en_US.properties             | 112 +++
 .../samples/transforms/multi-mapping-child.hpl     | 165 ++++
 .../samples/transforms/multi-mapping-parent.hpl    | 218 +++++
 .../transforms/mapping/MappingTransformsTest.java  |  50 ++
 .../MultiMappingMetaInjectionTest.java             |  68 ++
 .../multimapping/MultiMappingMetaTest.java         | 239 ++++++
 .../transforms/multimapping/MultiMappingTest.java  | 114 +++
 .../src/test/resources/multi-mapping-transform.xml |  86 ++
 33 files changed, 4329 insertions(+), 22 deletions(-)

diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/MMAP.svg b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/MMAP.svg
new file mode 100644
index 0000000000..a986b31a31
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/MMAP.svg
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
+     width="42px" height="42px" viewBox="0 0 42 42">
+    <g>
+        <rect x="14.753" y="8.232" fill="#0E3A5A" width="2.523" height="1.699"/>
+        <rect x="18.276" y="8.232" fill="#0E3A5A" width="2.524" height="1.699"/>
+        <rect x="28.85" y="8.232" fill="#0E3A5A" width="2.525" height="1.699"/>
+        <rect x="32.41" y="24.557" fill="#0E3A5A" width="1.699" height="2.396"/>
+        <rect x="25.325" y="8.232" fill="#0E3A5A" width="2.524" height="1.699"/>
+        <rect x="21.801" y="8.232" fill="#0E3A5A" width="2.524" height="1.699"/>
+        <rect x="32.41" y="17.767" fill="#0E3A5A" width="1.699" height="2.396"/>
+        <rect x="32.41" y="21.162" fill="#0E3A5A" width="1.699" height="2.395"/>
+        <rect x="32.41" y="10.977" fill="#0E3A5A" width="1.699" height="2.395"/>
+        <rect x="32.41" y="14.371" fill="#0E3A5A" width="1.699" height="2.396"/>
+        <polygon fill="#0E3A5A"
+                 points="32.41,9.977 34.109,9.977 34.109,8.232 32.375,8.232 32.375,9.932 32.41,9.932"/>
+        <rect x="7.167" y="7.296" fill="#FFFFFF" width="3.666" height="3.666"/>
+        <rect x="7.167" y="31.037" fill="#FFFFFF" width="3.666" height="3.667"/>
+        <rect x="31.167" y="31.037" fill="#FFFFFF" width="3.666" height="3.667"/>
+        <rect x="31.167" y="7.296" fill="#FFFFFF" width="3.666" height="3.666"/>
+        <path fill="#0E3A5A" d="M34.109,29.338v-1.386H32.41v1.386h-2.942v3.172H12.532v-3.172h-2.7V12.661h2.7V9.932h1.221V8.232h-1.221
+		V5.597H5.468v7.064h2.665v16.677H5.468v7.065h7.064v-2.194h16.936v2.194h7.064v-7.065H34.109z M7.167,10.962V7.296h3.666v3.666
+		H7.167z M10.833,34.704H7.167v-3.667h3.666V34.704z M34.833,34.704h-3.666v-3.667h3.666V34.704z M34.833,10.962h-3.666V7.296h3.666
+		V10.962z"/>
+    </g>
+</svg>
diff --git a/docs/hop-user-manual/modules/ROOT/nav.adoc b/docs/hop-user-manual/modules/ROOT/nav.adoc
index a118fd3206..333fe3e94d 100644
--- a/docs/hop-user-manual/modules/ROOT/nav.adoc
+++ b/docs/hop-user-manual/modules/ROOT/nav.adoc
@@ -210,6 +210,7 @@ under the License.
 *** xref:pipeline/transforms/mongodbdelete.adoc[MongoDB delete]
 *** xref:pipeline/transforms/mongodbinput.adoc[MongoDB Input]
 *** xref:pipeline/transforms/mongodboutput.adoc[MongoDB Output]
+*** xref:pipeline/transforms/multi-mapping.adoc[Multi Mapping]
 *** xref:pipeline/transforms/multimerge.adoc[Multiway Merge Join]
 *** xref:pipeline/transforms/mysqlbulkloader.adoc[MySQL bulk loader]
 *** xref:pipeline/transforms/neo4j-cypher.adoc[Neo4j Cypher]
diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc
index 693269fb0d..52a2351ecf 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc
@@ -161,6 +161,7 @@ Those are explained once in xref:pipeline/formatting-values.adoc[Formatting numb
 * xref:pipeline/transforms/mongodbdelete.adoc[MongoDB delete]
 * xref:pipeline/transforms/mongodbinput.adoc[MongoDB Input]
 * xref:pipeline/transforms/mongodboutput.adoc[MongoDB Output]
+* xref:pipeline/transforms/multi-mapping.adoc[Multi Mapping]
 * xref:pipeline/transforms/multimerge.adoc[Multiway Merge Join]
 * xref:pipeline/transforms/mysqlbulkloader.adoc[MySQL bulk loader]
 * xref:pipeline/transforms/neo4j-cypher.adoc[Neo4j Cypher]
diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-input.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-input.adoc
index 364300d60b..9227792935 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-input.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-input.adoc
@@ -29,6 +29,9 @@ The Mapping input transform represents and describes the input for a Mapping pip
 
 The data of this input will be provided by the parent pipeline (the pipeline that calls the mapping).
 
+A child used from xref:pipeline/transforms/simple-mapping.adoc[Simple Mapping] must contain exactly one Mapping input.
+A child used from xref:pipeline/transforms/multi-mapping.adoc[Multi Mapping] may contain zero or more Mapping input transforms.
+
 |
 == Supported Engines
 [%noheader,cols="2,1a",frame=none, role="table-supported-engines"]
diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-output.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-output.adoc
index 731f27c9ee..77e38032b4 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-output.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-output.adoc
@@ -29,6 +29,9 @@ The Mapping output transform describes the output of the mapping pipeline to the
 
 The data entering this transform will be streamed to the parent pipeline.
 
+A child used from xref:pipeline/transforms/simple-mapping.adoc[Simple Mapping] must contain exactly one Mapping output.
+A child used from xref:pipeline/transforms/multi-mapping.adoc[Multi Mapping] may contain zero or more Mapping output transforms.
+
 |
 == Supported Engines
 [%noheader,cols="2,1a",frame=none, role="table-supported-engines"]
diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/multi-mapping.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/multi-mapping.adoc
new file mode 100644
index 0000000000..31ac2e84f9
--- /dev/null
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/multi-mapping.adoc
@@ -0,0 +1,79 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+  http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+////
+:documentationPath: /pipeline/transforms/
+:language: en_US
+:description: The Multi Mapping transform re-uses a child pipeline with zero or more inputs and outputs, including info and target streams.
+
+= image:transforms/icons/MMAP.svg[Multi Mapping transform Icon, role="image-doc-icon"] Multi Mapping
+
+[%noheader,cols="3a,1a", role="table-no-borders" ]
+|===
+|
+== Description
+
+The Multi Mapping transform re-uses a child pipeline (a mapping) with *zero or more* inputs and outputs.
+
+Unlike xref:pipeline/transforms/simple-mapping.adoc[Simple Mapping], which always has one xref:pipeline/transforms/mapping-input.adoc[Mapping input] and one xref:pipeline/transforms/mapping-output.adoc[Mapping output], Multi Mapping can:
+
+* start a child that generates its own data (0 inputs)
+* run a child that only writes data (0 outputs)
+* join or look up using several Mapping Input transforms
+* split rows to several Mapping Output transforms
+
+Non-main input mappings become *info hops* (the same idea as xref:pipeline/transforms/streamlookup.adoc[Stream Lookup]).
+Non-main output mappings become *target hops* (the same idea as xref:pipeline/transforms/switchcase.adoc[Switch / Case]).
+
+That lets you wrap a reusable Stream Lookup or Switch / Case style pipeline.
+
+The child pipeline is always executed with a local Hop pipeline run configuration so rows can be streamed in and out of the mapping.
+
+|
+== Supported Engines
+[%noheader,cols="2,1a",frame=none, role="table-supported-engines"]
+!===
+!Hop Engine! image:check_mark.svg[Supported, 24]
+!Single Threaded! image:cross.svg[Supported, 24]
+!Native Spark! image:cross.svg[Supported, 24]
+!Beam Spark! image:cross.svg[Supported, 24]
+!Beam Flink! image:cross.svg[Supported, 24]
+!Beam Dataflow! image:cross.svg[Supported, 24]
+!===
+|===
+
+== Options
+
+* Transform name: a unique name in your pipeline
+* Pipeline: the mapping (sub-) pipeline to run
+* Run configuration: local Hop run configuration only
+* Parameters tab: variables to pass into the child, plus inherit-all-variables
+* Input tabs (Add Input): one tab per input mapping
+** Input source transform: parent transform that sends rows (empty = auto-detect remaining hops)
+** Mapping Input transform: child Mapping Input that receives those rows
+** Main data path: checked for the primary stream; unchecked for an info hop
+** Update mapped field names downstream: reverse input renames on the way out
+** Field mapping table
+* Output tabs (Add Output): one tab per output mapping
+** Mapping Output transform: child Mapping Output that produces rows
+** Output target transform: parent transform that receives those rows (empty = auto-detect remaining hops)
+** Main data path: checked for the primary stream; unchecked for a target hop
+** Field mapping table
+
+You can close every input or output tab. A mapping with no inputs is a generator. A mapping with no outputs is a sink.
+
+== Sample
+
+The samples project contains parent/child examples under `transforms/` for a single stream, a generator, a join, an info lookup, and multiple target outputs.
diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/simple-mapping.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/simple-mapping.adoc
index 6d6b6ff4f5..d497c70fe3 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/simple-mapping.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/simple-mapping.adoc
@@ -44,6 +44,8 @@ In the Simple Mapping transform you can specify one xref:pipeline/transforms/map
 
 Use this transform if you find yourself repeating the same logic multiple times over several different pipelines.
 
+If you need more than one input or output, or info / target hops (for example a reusable Stream Lookup or Switch / Case), use xref:pipeline/transforms/multi-mapping.adoc[Multi Mapping] instead.
+
 == Options
 
 The options are fairly self-explanatory:
diff --git a/docs/hop-user-manual/modules/ROOT/pages/snippets/best-practices/mappings.adoc b/docs/hop-user-manual/modules/ROOT/pages/snippets/best-practices/mappings.adoc
index f1389a0234..8667fae483 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/snippets/best-practices/mappings.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/snippets/best-practices/mappings.adoc
@@ -26,4 +26,6 @@ The Simple Mapping is a pipeline reading from a xref:pipeline/transforms/mapping
 
 You can re-use the work in other pipelines using the xref:pipeline/transforms/simple-mapping.adoc[Simple Mapping] transform.
 
+When the reusable logic needs more than one input or output, or info / target hops, use xref:pipeline/transforms/multi-mapping.adoc[Multi Mapping] instead.
+
 
diff --git a/integration-tests/transforms/0108-multi-mapping-child-01.hpl b/integration-tests/transforms/0108-multi-mapping-child-01.hpl
new file mode 100644
index 0000000000..a74521e35d
--- /dev/null
+++ b/integration-tests/transforms/0108-multi-mapping-child-01.hpl
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+  <info>
+    <pipeline_version/>
+    <capture_transform_performance>N</capture_transform_performance>
+    <transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    <transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <pipeline_type>Normal</pipeline_type>
+    <pipeline_status>0</pipeline_status>
+    <parameters/>
+    <name>0101-multi-mapping-child-01</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <created_user>-</created_user>
+    <modified_user>-</modified_user>
+    <created_date>2022/11/08 21:16:00.171</created_date>
+    <modified_date>2022/11/08 21:16:00.171</modified_date>
+  </info>
+  <transform>
+    <type>MappingInput</type>
+    <name>fieldA, fieldB</name>
+    <fields>
+      <field>
+        <name>fieldA</name>
+        <type>Integer</type>
+        <length>-1</length>
+        <precision>-1</precision>
+      </field>
+      <field>
+        <name>fieldB</name>
+        <type>Integer</type>
+        <length>-1</length>
+        <precision>-1</precision>
+      </field>
+    </fields>
+    <distribute>Y</distribute>
+    <copies>1</copies>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>80</yloc>
+    </GUI>
+    <description/>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+  </transform>
+  <transform>
+    <type>MappingOutput</type>
+    <name>fieldSum</name>
+    <distribute>Y</distribute>
+    <copies>1</copies>
+    <GUI>
+      <xloc>512</xloc>
+      <yloc>80</yloc>
+    </GUI>
+    <description/>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+  </transform>
+  <transform>
+    <type>ScriptValueMod</type>
+    <name>fieldSum = fieldA+fieldB</name>
+    <jsScripts>
+      <jsScript>
+        <jsScript_type>0</jsScript_type>
+        <jsScript_name>Script 1</jsScript_name>
+        <jsScript_script>
+var fieldSum = fieldA + fieldB;
+
+var par1 = getVariable("PAR1", "");
+var par2 = getVariable("PAR2", "");
+</jsScript_script>
+      </jsScript>
+    </jsScripts>
+    <fields>
+      <field>
+        <name>fieldSum</name>
+        <rename>fieldSum</rename>
+        <type>Integer</type>
+        <length>-1</length>
+        <precision>-1</precision>
+        <replace>N</replace>
+      </field>
+      <field>
+        <name>par1</name>
+        <rename>par1</rename>
+        <type>String</type>
+        <length>-1</length>
+        <precision>-1</precision>
+        <replace>N</replace>
+      </field>
+      <field>
+        <name>par2</name>
+        <rename>par2</rename>
+        <type>String</type>
+        <length>-1</length>
+        <precision>-1</precision>
+        <replace>N</replace>
+      </field>
+    </fields>
+    <optimizationLevel>9</optimizationLevel>
+    <languageVersion>ES6</languageVersion>
+    <distribute>Y</distribute>
+    <copies>1</copies>
+    <GUI>
+      <xloc>320</xloc>
+      <yloc>80</yloc>
+    </GUI>
+    <description/>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+  </transform>
+  <order>
+    <hop>
+      <from>fieldSum = fieldA+fieldB</from>
+      <to>fieldSum</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>fieldA, fieldB</from>
+      <to>fieldSum = fieldA+fieldB</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <notepads/>
+  <attributes/>
+  <transform_error_handling/>
+</pipeline>
diff --git a/integration-tests/transforms/0108-multi-mapping-parent-01.hpl b/integration-tests/transforms/0108-multi-mapping-parent-01.hpl
new file mode 100644
index 0000000000..0ea29ba101
--- /dev/null
+++ b/integration-tests/transforms/0108-multi-mapping-parent-01.hpl
@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+  <info>
+    <pipeline_version/>
+    <capture_transform_performance>N</capture_transform_performance>
+    <transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    <transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <pipeline_type>Normal</pipeline_type>
+    <pipeline_status>0</pipeline_status>
+    <parameters/>
+    <name>0101-multi-mapping-parent-01</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <created_user>-</created_user>
+    <modified_user>-</modified_user>
+    <created_date>2022/11/08 21:15:20.215</created_date>
+    <modified_date>2022/11/08 21:15:20.215</modified_date>
+  </info>
+  <transform>
+    <type>MultiMapping</type>
+    <name>0108-multi-mapping-child-01.hpl</name>
+    <runConfiguration>local</runConfiguration>
+    <mappings>
+      <input>
+        <mapping>
+          <input_transform/>
+          <output_transform/>
+          <description/>
+          <connector>
+            <parent>a</parent>
+            <child>fieldA</child>
+          </connector>
+          <connector>
+            <parent>b</parent>
+            <child>fieldB</child>
+          </connector>
+          <main_path>Y</main_path>
+          <rename_on_output>Y</rename_on_output>
+        </mapping>
+      </input>
+      <output>
+        <mapping>
+          <input_transform/>
+          <output_transform/>
+          <description/>
+          <connector>
+            <parent>fieldSum</parent>
+            <child>sum</child>
+          </connector>
+          <connector>
+            <parent>par1</parent>
+            <child>parameter1</child>
+          </connector>
+          <connector>
+            <parent>par2</parent>
+            <child>parameter2</child>
+          </connector>
+          <main_path>Y</main_path>
+          <rename_on_output>N</rename_on_output>
+        </mapping>
+      </output>
+      <parameters>
+        <variablemapping>
+          <variable>PAR1</variable>
+          <input>val1</input>
+        </variablemapping>
+        <variablemapping>
+          <variable>PAR2</variable>
+          <input>val2</input>
+        </variablemapping>
+        <inherit_all_vars>Y</inherit_all_vars>
+      </parameters>
+    </mappings>
+    <filename>${PROJECT_HOME}/0108-multi-mapping-child-01.hpl</filename>
+    <distribute>Y</distribute>
+    <copies>1</copies>
+    <GUI>
+      <xloc>304</xloc>
+      <yloc>80</yloc>
+    </GUI>
+    <description/>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+  </transform>
+  <transform>
+    <type>Dummy</type>
+    <name>Output</name>
+    <distribute>Y</distribute>
+    <copies>1</copies>
+    <GUI>
+      <xloc>512</xloc>
+      <yloc>80</yloc>
+    </GUI>
+    <description/>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+  </transform>
+  <transform>
+    <type>DataGrid</type>
+    <name>sample data</name>
+    <fields>
+      <field>
+        <name>a</name>
+        <type>Integer</type>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+      <field>
+        <name>b</name>
+        <type>Integer</type>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+    </fields>
+    <data>
+      <line>
+        <item>1</item>
+        <item>8</item>
+      </line>
+      <line>
+        <item>2</item>
+        <item>7</item>
+      </line>
+      <line>
+        <item>3</item>
+        <item>6</item>
+      </line>
+      <line>
+        <item>4</item>
+        <item>5</item>
+      </line>
+    </data>
+    <distribute>Y</distribute>
+    <copies>1</copies>
+    <GUI>
+      <xloc>96</xloc>
+      <yloc>80</yloc>
+    </GUI>
+    <description/>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+  </transform>
+  <order>
+    <hop>
+      <from>0108-multi-mapping-child-01.hpl</from>
+      <to>Output</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>sample data</from>
+      <to>0108-multi-mapping-child-01.hpl</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <notepads/>
+  <attributes/>
+  <transform_error_handling/>
+</pipeline>
diff --git a/integration-tests/transforms/datasets/golden-multi-mapping-01.csv b/integration-tests/transforms/datasets/golden-multi-mapping-01.csv
new file mode 100644
index 0000000000..b998ac6687
--- /dev/null
+++ b/integration-tests/transforms/datasets/golden-multi-mapping-01.csv
@@ -0,0 +1,5 @@
+a,b,sum,parameter1,parameter2
+1,8,9,val1,val2
+2,7,9,val1,val2
+3,6,9,val1,val2
+4,5,9,val1,val2
diff --git a/integration-tests/transforms/metadata/dataset/golden-multi-mapping-01.json b/integration-tests/transforms/metadata/dataset/golden-multi-mapping-01.json
new file mode 100644
index 0000000000..0013203dc9
--- /dev/null
+++ b/integration-tests/transforms/metadata/dataset/golden-multi-mapping-01.json
@@ -0,0 +1,48 @@
+{
+  "base_filename": "golden-multi-mapping-01.csv",
+  "name": "golden-multi-mapping-01",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 5,
+      "field_precision": 0,
+      "field_name": "a",
+      "field_format": "####0;-####0"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 5,
+      "field_precision": 0,
+      "field_name": "b",
+      "field_format": "####0;-####0"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 5,
+      "field_precision": 0,
+      "field_name": "sum",
+      "field_format": "####0;-####0"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "parameter1",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "parameter2",
+      "field_format": ""
+    }
+  ],
+  "folder_name": ""
+}
\ No newline at end of file
diff --git a/integration-tests/transforms/metadata/unit-test/0108-multi-mapping-parent-01 UNIT.json b/integration-tests/transforms/metadata/unit-test/0108-multi-mapping-parent-01 UNIT.json
new file mode 100644
index 0000000000..85e0aec256
--- /dev/null
+++ b/integration-tests/transforms/metadata/unit-test/0108-multi-mapping-parent-01 UNIT.json	
@@ -0,0 +1,48 @@
+{
+  "database_replacements": [],
+  "autoOpening": true,
+  "description": "Multi Mapping 1-in-1-out parity with Simple Mapping",
+  "persist_filename": "",
+  "test_type": "UNIT_TEST",
+  "variableValues": [],
+  "basePath": "",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "a",
+          "data_set_field": "a"
+        },
+        {
+          "transform_field": "b",
+          "data_set_field": "b"
+        },
+        {
+          "transform_field": "sum",
+          "data_set_field": "sum"
+        },
+        {
+          "transform_field": "parameter1",
+          "data_set_field": "parameter1"
+        },
+        {
+          "transform_field": "parameter2",
+          "data_set_field": "parameter2"
+        }
+      ],
+      "field_order": [
+        "a",
+        "b",
+        "sum",
+        "parameter1",
+        "parameter2"
+      ],
+      "data_set_name": "golden-multi-mapping-01",
+      "transform_name": "Output"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0108-multi-mapping-parent-01 UNIT",
+  "trans_test_tweaks": [],
+  "pipeline_filename": "./0108-multi-mapping-parent-01.hpl"
+}
\ No newline at end of file
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/MappingTransforms.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/MappingTransforms.java
new file mode 100644
index 0000000000..0e3e8ff245
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/MappingTransforms.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.mapping;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.pipeline.Pipeline;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.engine.IEngineComponent;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transforms.input.MappingInput;
+import org.apache.hop.pipeline.transforms.input.MappingInputMeta;
+import org.apache.hop.pipeline.transforms.output.MappingOutput;
+import org.apache.hop.pipeline.transforms.output.MappingOutputMeta;
+
+/** Shared helpers for locating Mapping Input / Output transforms in a child pipeline. */
+public final class MappingTransforms {
+
+  private MappingTransforms() {
+    // Utility class
+  }
+
+  public static boolean isInfoMapping(MappingIODefinition definition) {
+    return definition != null
+        && !definition.isMainDataPath()
+        && StringUtils.isNotEmpty(definition.getInputTransformName());
+  }
+
+  public static boolean isTargetMapping(MappingIODefinition definition) {
+    return definition != null
+        && !definition.isMainDataPath()
+        && StringUtils.isNotEmpty(definition.getOutputTransformName());
+  }
+
+  public static List<MappingInput> findMappingInputs(Pipeline mappingPipeline) {
+    List<MappingInput> list = new ArrayList<>();
+    if (mappingPipeline == null) {
+      return list;
+    }
+    for (IEngineComponent component : mappingPipeline.getComponents()) {
+      if (component instanceof MappingInput mappingInput) {
+        list.add(mappingInput);
+      }
+    }
+    return list;
+  }
+
+  public static List<MappingOutput> findMappingOutputs(Pipeline mappingPipeline) {
+    List<MappingOutput> list = new ArrayList<>();
+    if (mappingPipeline == null) {
+      return list;
+    }
+    for (IEngineComponent component : mappingPipeline.getComponents()) {
+      if (component instanceof MappingOutput mappingOutput) {
+        list.add(mappingOutput);
+      }
+    }
+    return list;
+  }
+
+  public static List<TransformMeta> findMappingInputMetas(PipelineMeta pipelineMeta) {
+    List<TransformMeta> list = new ArrayList<>();
+    if (pipelineMeta == null) {
+      return list;
+    }
+    for (TransformMeta transformMeta : pipelineMeta.getTransforms()) {
+      if (transformMeta.getTransform() instanceof MappingInputMeta) {
+        list.add(transformMeta);
+      }
+    }
+    return list;
+  }
+
+  public static List<TransformMeta> findMappingOutputMetas(PipelineMeta pipelineMeta) {
+    List<TransformMeta> list = new ArrayList<>();
+    if (pipelineMeta == null) {
+      return list;
+    }
+    for (TransformMeta transformMeta : pipelineMeta.getTransforms()) {
+      if (transformMeta.getTransform() instanceof MappingOutputMeta) {
+        list.add(transformMeta);
+      }
+    }
+    return list;
+  }
+
+  public static MappingInput findMappingInput(Pipeline mappingPipeline, String transformName) {
+    for (MappingInput mappingInput : findMappingInputs(mappingPipeline)) {
+      if (Utils.isEmpty(transformName) || transformName.equals(mappingInput.getTransformName())) {
+        return mappingInput;
+      }
+    }
+    return null;
+  }
+
+  public static MappingOutput findMappingOutput(Pipeline mappingPipeline, String transformName) {
+    for (MappingOutput mappingOutput : findMappingOutputs(mappingPipeline)) {
+      if (Utils.isEmpty(transformName) || transformName.equals(mappingOutput.getTransformName())) {
+        return mappingOutput;
+      }
+    }
+    return null;
+  }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMapping.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMapping.java
index 53fcd4f548..97036a3a52 100644
--- a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMapping.java
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMapping.java
@@ -17,7 +17,6 @@
 
 package org.apache.hop.pipeline.transforms.mapping;
 
-import java.util.ArrayList;
 import java.util.List;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hop.core.Const;
@@ -29,7 +28,6 @@ import org.apache.hop.pipeline.RowProducer;
 import org.apache.hop.pipeline.SingleThreadedPipelineExecutor;
 import org.apache.hop.pipeline.TransformWithMappingMeta;
 import org.apache.hop.pipeline.config.PipelineRunConfiguration;
-import org.apache.hop.pipeline.engine.IEngineComponent;
 import org.apache.hop.pipeline.engine.PipelineEngineFactory;
 import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
 import org.apache.hop.pipeline.engines.local.LocalPipelineRunConfiguration;
@@ -245,29 +243,11 @@ public class SimpleMapping extends BaseTransform<SimpleMappingMeta, SimpleMappin
   }
 
   public static List<MappingInput> findMappingInputs(Pipeline mappingPipeline) {
-    List<MappingInput> list = new ArrayList<>();
-
-    List<IEngineComponent> components = mappingPipeline.getComponents();
-    for (IEngineComponent component : components) {
-      if (component instanceof MappingInput mappingInput) {
-        list.add(mappingInput);
-      }
-    }
-
-    return list;
+    return MappingTransforms.findMappingInputs(mappingPipeline);
   }
 
   private List<MappingOutput> findMappingOutputs(Pipeline mappingPipeline) {
-    List<MappingOutput> list = new ArrayList<>();
-
-    List<IEngineComponent> components = mappingPipeline.getComponents();
-    for (IEngineComponent component : components) {
-      if (component instanceof MappingOutput mappingOutput) {
-        list.add(mappingOutput);
-      }
-    }
-
-    return list;
+    return MappingTransforms.findMappingOutputs(mappingPipeline);
   }
 
   @Override
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiIOMappings.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiIOMappings.java
new file mode 100644
index 0000000000..76ab4cad8d
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiIOMappings.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.multimapping;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.pipeline.transforms.mapping.MappingIODefinition;
+import org.apache.hop.pipeline.transforms.mapping.MappingParameters;
+
+@Getter
+@Setter
+public class MultiIOMappings implements Cloneable {
+
+  @HopMetadataProperty(
+      key = "mapping",
+      groupKey = "input",
+      injectionGroupKey = "INPUT_MAPPINGS",
+      injectionGroupDescription = "MultiMappingMeta.Injection.INPUT_MAPPINGS")
+  private List<MultiMappingInputDefinition> inputMappings;
+
+  @HopMetadataProperty(
+      key = "mapping",
+      groupKey = "output",
+      injectionGroupKey = "OUTPUT_MAPPINGS",
+      injectionGroupDescription = "MultiMappingMeta.Injection.OUTPUT_MAPPINGS")
+  private List<MultiMappingOutputDefinition> outputMappings;
+
+  @HopMetadataProperty(key = "parameters")
+  private MappingParameters mappingParameters;
+
+  public MultiIOMappings() {
+    this.inputMappings = new ArrayList<>();
+    this.outputMappings = new ArrayList<>();
+    this.mappingParameters = new MappingParameters();
+  }
+
+  public MultiIOMappings(MultiIOMappings m) {
+    this();
+    if (m.inputMappings != null) {
+      for (MultiMappingInputDefinition definition : m.inputMappings) {
+        this.inputMappings.add(new MultiMappingInputDefinition(definition));
+      }
+    }
+    if (m.outputMappings != null) {
+      for (MultiMappingOutputDefinition definition : m.outputMappings) {
+        this.outputMappings.add(new MultiMappingOutputDefinition(definition));
+      }
+    }
+    this.mappingParameters =
+        m.mappingParameters != null
+            ? new MappingParameters(m.mappingParameters)
+            : new MappingParameters();
+  }
+
+  @Override
+  public MultiIOMappings clone() {
+    return new MultiIOMappings(this);
+  }
+
+  public List<MappingIODefinition> getInputIoDefinitions() {
+    List<MappingIODefinition> list = new ArrayList<>();
+    for (MultiMappingInputDefinition definition : inputMappings) {
+      list.add(definition.toIoDefinition());
+    }
+    return list;
+  }
+
+  public List<MappingIODefinition> getOutputIoDefinitions() {
+    List<MappingIODefinition> list = new ArrayList<>();
+    for (MultiMappingOutputDefinition definition : outputMappings) {
+      list.add(definition.toIoDefinition());
+    }
+    return list;
+  }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMapping.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMapping.java
new file mode 100644
index 0000000000..aa271c94d1
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMapping.java
@@ -0,0 +1,470 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.multimapping;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.IRowSet;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.Pipeline;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.RowProducer;
+import org.apache.hop.pipeline.TransformWithMappingMeta;
+import org.apache.hop.pipeline.config.PipelineRunConfiguration;
+import org.apache.hop.pipeline.engine.PipelineEngineFactory;
+import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
+import org.apache.hop.pipeline.engines.local.LocalPipelineRunConfiguration;
+import org.apache.hop.pipeline.transform.BaseTransform;
+import org.apache.hop.pipeline.transform.IRowListener;
+import org.apache.hop.pipeline.transform.ITransform;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transforms.input.MappingInput;
+import org.apache.hop.pipeline.transforms.mapping.MappingIODefinition;
+import org.apache.hop.pipeline.transforms.mapping.MappingTransforms;
+import org.apache.hop.pipeline.transforms.mapping.MappingValueRename;
+import org.apache.hop.pipeline.transforms.mapping.RowDataInputMapper;
+import org.apache.hop.pipeline.transforms.mapping.RowOutputDataMapper;
+import org.apache.hop.pipeline.transforms.output.MappingOutput;
+
+/** Execute a mapping with 0..N inputs and outputs using row producers and row listeners. */
+public class MultiMapping extends BaseTransform<MultiMappingMeta, MultiMappingData> {
+
+  private static final Class<?> PKG = MultiMappingMeta.class;
+
+  public MultiMapping(
+      TransformMeta transformMeta,
+      MultiMappingMeta meta,
+      MultiMappingData data,
+      int copyNr,
+      PipelineMeta pipelineMeta,
+      Pipeline pipeline) {
+    super(transformMeta, meta, data, copyNr, pipelineMeta, pipeline);
+  }
+
+  @Override
+  public boolean processRow() throws HopException {
+    try {
+      if (first) {
+        first = false;
+        data.wasStarted = true;
+        wireInputProducers();
+        wireOutputListeners();
+        data.mappingPipeline.startThreads();
+        drainInfoStreams();
+        data.infoDrained = true;
+      }
+
+      if (!data.mainRowSets.isEmpty()) {
+        Object[] row = nextMainRow();
+        if (row != null) {
+          return true;
+        }
+      }
+
+      finishProducers();
+      data.mappingPipeline.waitUntilFinished();
+      setOutputDone();
+      return false;
+    } catch (Exception e) {
+      if (data.mappingPipeline != null) {
+        data.mappingPipeline.stopAll();
+      }
+      throw new HopException(e);
+    }
+  }
+
+  private void wireInputProducers() throws HopException {
+    List<MappingInput> mappingInputs = MappingTransforms.findMappingInputs(data.mappingPipeline);
+    Set<String> referencedInputs = new HashSet<>();
+
+    List<MappingIODefinition> inputDefinitions = new ArrayList<>();
+    for (MultiMappingInputDefinition definition : meta.getInputMappings()) {
+      inputDefinitions.add(definition.toIoDefinition());
+    }
+
+    if (inputDefinitions.isEmpty() && mappingInputs.size() == 1) {
+      MappingIODefinition auto =
+          new MappingIODefinition(null, mappingInputs.get(0).getTransformName());
+      auto.setMainDataPath(true);
+      inputDefinitions.add(auto);
+    }
+
+    for (MappingIODefinition definition : inputDefinitions) {
+      MappingInput mappingInput =
+          MappingTransforms.findMappingInput(
+              data.mappingPipeline, definition.getOutputTransformName());
+      if (mappingInput == null) {
+        throw new HopException(
+            BaseMessages.getString(
+                PKG,
+                "MultiMapping.Exception.MappingInputNotFound",
+                Const.NVL(definition.getOutputTransformName(), "")));
+      }
+      referencedInputs.add(mappingInput.getTransformName());
+
+      RowProducer rowProducer =
+          data.mappingPipeline.addRowProducer(mappingInput.getTransformName(), 0);
+      RowDataInputMapper mapper = new RowDataInputMapper(definition, rowProducer);
+      data.allInputMappers.add(mapper);
+
+      if (MappingTransforms.isInfoMapping(definition)) {
+        IRowSet rowSet = findInputRowSet(definition.getInputTransformName());
+        if (rowSet == null) {
+          throw new HopException(
+              BaseMessages.getString(
+                  PKG,
+                  "MultiMapping.Exception.InfoRowSetNotFound",
+                  definition.getInputTransformName()));
+        }
+        data.infoRowSets.add(rowSet);
+        data.rowSetMappers.put(rowSet, mapper);
+      } else if (StringUtils.isNotEmpty(definition.getInputTransformName())) {
+        IRowSet rowSet = findInputRowSet(definition.getInputTransformName());
+        if (rowSet == null) {
+          throw new HopException(
+              BaseMessages.getString(
+                  PKG,
+                  "MultiMapping.Exception.InputRowSetNotFound",
+                  definition.getInputTransformName()));
+        }
+        data.mainRowSets.add(rowSet);
+        data.rowSetMappers.put(rowSet, mapper);
+      } else {
+        for (IRowSet rowSet : getInputRowSets()) {
+          if (!isInfoRowSet(rowSet) && !data.rowSetMappers.containsKey(rowSet)) {
+            data.mainRowSets.add(rowSet);
+            data.rowSetMappers.put(rowSet, mapper);
+          }
+        }
+      }
+    }
+
+    for (MappingInput mappingInput : mappingInputs) {
+      if (!referencedInputs.contains(mappingInput.getTransformName())) {
+        RowProducer rowProducer =
+            data.mappingPipeline.addRowProducer(mappingInput.getTransformName(), 0);
+        rowProducer.finished();
+      }
+    }
+  }
+
+  private boolean isInfoRowSet(IRowSet rowSet) {
+    for (IRowSet infoRowSet : data.infoRowSets) {
+      if (infoRowSet == rowSet) {
+        return true;
+      }
+    }
+    String origin = rowSet.getOriginTransformName();
+    for (MultiMappingInputDefinition definition : meta.getInputMappings()) {
+      if (MappingTransforms.isInfoMapping(definition.toIoDefinition())
+          && origin != null
+          && origin.equalsIgnoreCase(definition.getInputTransformName())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private void wireOutputListeners() throws HopException {
+    List<MappingOutput> mappingOutputs = MappingTransforms.findMappingOutputs(data.mappingPipeline);
+    List<MappingValueRename> inputRenames = new ArrayList<>();
+    for (MultiMappingInputDefinition inputDefinition : meta.getInputMappings()) {
+      if (inputDefinition.isRenamingOnOutput()) {
+        MultiMappingMeta.addInputRenames(
+            inputRenames, inputDefinition.toIoDefinition().getValueRenames());
+      }
+    }
+    MappingIODefinition combinedInputRenames = new MappingIODefinition();
+    combinedInputRenames.setRenamingOnOutput(!inputRenames.isEmpty());
+    combinedInputRenames.setValueRenames(inputRenames);
+
+    Set<String> claimedTargets = new HashSet<>();
+    for (MultiMappingOutputDefinition definition : meta.getOutputMappings()) {
+      if (MappingTransforms.isTargetMapping(definition.toIoDefinition())) {
+        claimedTargets.add(definition.getOutputTransformName());
+      }
+    }
+
+    List<MappingIODefinition> outputDefinitions = new ArrayList<>();
+    for (MultiMappingOutputDefinition definition : meta.getOutputMappings()) {
+      outputDefinitions.add(definition.toIoDefinition());
+    }
+    if (outputDefinitions.isEmpty() && mappingOutputs.size() == 1) {
+      MappingIODefinition auto =
+          new MappingIODefinition(mappingOutputs.get(0).getTransformName(), null);
+      auto.setMainDataPath(true);
+      outputDefinitions.add(auto);
+    }
+
+    Set<String> referencedOutputs = new HashSet<>();
+    for (MappingIODefinition definition : outputDefinitions) {
+      MappingOutput mappingOutput =
+          MappingTransforms.findMappingOutput(
+              data.mappingPipeline, definition.getInputTransformName());
+      if (mappingOutput == null) {
+        throw new HopException(
+            BaseMessages.getString(
+                PKG,
+                "MultiMapping.Exception.MappingOutputNotFound",
+                Const.NVL(definition.getInputTransformName(), "")));
+      }
+      referencedOutputs.add(mappingOutput.getTransformName());
+
+      List<IRowSet> targetRowSets = resolveOutputRowSets(definition, claimedTargets);
+      RowOutputDataMapper outputDataMapper =
+          new RowOutputDataMapper(
+              combinedInputRenames,
+              definition,
+              (rowMeta, row) -> putRowsTo(rowMeta, row, targetRowSets));
+      mappingOutput.addRowListener(outputDataMapper);
+    }
+  }
+
+  private List<IRowSet> resolveOutputRowSets(
+      MappingIODefinition definition, Set<String> claimedTargets) throws HopTransformException {
+    List<IRowSet> rowSets = new ArrayList<>();
+    if (StringUtils.isNotEmpty(definition.getOutputTransformName())) {
+      IRowSet rowSet = findOutputRowSet(definition.getOutputTransformName());
+      if (rowSet != null) {
+        rowSets.add(rowSet);
+      }
+      return rowSets;
+    }
+    for (IRowSet rowSet : getOutputRowSets()) {
+      String destination = rowSet.getDestinationTransformName();
+      if (destination == null || !claimedTargets.contains(destination)) {
+        rowSets.add(rowSet);
+      }
+    }
+    return rowSets;
+  }
+
+  private void putRowsTo(IRowMeta rowMeta, Object[] row, List<IRowSet> rowSets)
+      throws HopTransformException {
+    if (rowSets.isEmpty()) {
+      return;
+    }
+    for (IRowSet rowSet : rowSets) {
+      putRowTo(rowMeta, row, rowSet);
+    }
+  }
+
+  private void drainInfoStreams() throws HopException {
+    for (IRowSet rowSet : data.infoRowSets) {
+      RowDataInputMapper mapper = data.rowSetMappers.get(rowSet);
+      Object[] row = getRowFrom(rowSet);
+      while (row != null && !isStopped() && !data.mappingPipeline.isFinishedOrStopped()) {
+        IRowMeta rowMeta = rowSet.getRowMeta();
+        boolean put = false;
+        while (!(data.mappingPipeline.isFinishedOrStopped() || put)) {
+          put = mapper.putRow(rowMeta, row);
+        }
+        if (!put) {
+          break;
+        }
+        row = getRowFrom(rowSet);
+      }
+      mapper.finished();
+      data.finishedRowSets.add(rowSet);
+    }
+  }
+
+  private Object[] nextMainRow() throws HopException {
+    int attempts = data.mainRowSets.size();
+    for (int i = 0; i < attempts; i++) {
+      if (data.mainRowSetIndex >= data.mainRowSets.size()) {
+        data.mainRowSetIndex = 0;
+      }
+      IRowSet rowSet = data.mainRowSets.get(data.mainRowSetIndex);
+      data.mainRowSetIndex++;
+      if (data.finishedRowSets.contains(rowSet)) {
+        continue;
+      }
+      Object[] row = getRowFrom(rowSet);
+      if (row == null) {
+        RowDataInputMapper mapper = data.rowSetMappers.get(rowSet);
+        if (mapper != null) {
+          mapper.finished();
+        }
+        data.finishedRowSets.add(rowSet);
+        continue;
+      }
+      RowDataInputMapper mapper = data.rowSetMappers.get(rowSet);
+      IRowMeta rowMeta = rowSet.getRowMeta();
+      boolean put = false;
+      while (!(data.mappingPipeline.isFinishedOrStopped() || put)) {
+        put = mapper.putRow(rowMeta, row);
+      }
+      if (!put) {
+        return null;
+      }
+      return row;
+    }
+    return null;
+  }
+
+  private void finishProducers() {
+    if (data.producersFinished) {
+      return;
+    }
+    data.producersFinished = true;
+    for (RowDataInputMapper mapper : data.allInputMappers) {
+      mapper.finished();
+    }
+  }
+
+  public void prepareMappingExecution() throws HopException {
+    if (data.mappingPipelineMeta == null) {
+      data.mappingPipelineMeta =
+          new PipelineMeta(variables.resolve(meta.getFilename()), metadataProvider, variables);
+      data.mappingPipelineMeta.clearChanged();
+    }
+
+    String runConfigName = resolve(meta.getRunConfigurationName());
+    if (StringUtils.isEmpty(runConfigName)) {
+      data.mappingPipeline = new LocalPipelineEngine(data.mappingPipelineMeta, this, this);
+    } else {
+      PipelineRunConfiguration runConfig =
+          metadataProvider.getSerializer(PipelineRunConfiguration.class).load(runConfigName);
+      if (runConfig == null) {
+        throw new HopException("Unable to find run configuration with name " + runConfigName);
+      }
+      if (!(runConfig.getEngineRunConfiguration() instanceof LocalPipelineRunConfiguration)) {
+        throw new HopException(
+            "Apache Hop can only run multi mappings with a local pipeline engine, not with run configuration "
+                + runConfigName);
+      }
+      data.mappingPipeline =
+          (LocalPipelineEngine)
+              PipelineEngineFactory.createPipelineEngine(
+                  getPipeline(), runConfigName, metadataProvider, data.mappingPipelineMeta);
+    }
+
+    data.mappingPipeline.copyParametersFromDefinitions(data.mappingPipelineMeta);
+    TransformWithMappingMeta.activateParams(
+        data.mappingPipeline,
+        data.mappingPipeline,
+        this,
+        data.mappingPipelineMeta.listParameters(),
+        meta.getMappingParameters().getVariables(),
+        meta.getMappingParameters().getInputs(),
+        meta.getMappingParameters().isInheritingAllVariables());
+
+    data.mappingPipeline.setParentPipeline(getPipeline());
+    data.mappingPipeline.setParent(this);
+    data.mappingPipeline.setSafeModeEnabled(getPipeline().isSafeModeEnabled());
+    data.mappingPipeline.setGatheringMetrics(getPipeline().isGatheringMetrics());
+
+    try {
+      data.mappingPipeline.prepareExecution();
+    } catch (HopException e) {
+      throw new HopException(
+          BaseMessages.getString(PKG, "MultiMapping.Exception.UnableToPrepareExecutionOfMapping"),
+          e);
+    }
+
+    getPipeline().addActiveSubPipeline(getTransformName(), data.mappingPipeline);
+  }
+
+  @Override
+  public boolean init() {
+    if (super.init()) {
+      try {
+        data.mappingPipelineMeta =
+            TransformWithMappingMeta.loadMappingMeta(meta, getMetadataProvider(), this);
+        if (data.mappingPipelineMeta != null) {
+          prepareMappingExecution();
+          return true;
+        } else {
+          logError("No valid mapping was specified!");
+          return false;
+        }
+      } catch (Exception e) {
+        logError(
+            "Unable to load the mapping pipeline '"
+                + resolve(meta.getFilename())
+                + "' (PROJECT_HOME="
+                + getVariable("PROJECT_HOME")
+                + "): "
+                + e);
+        logError(Const.getStackTracker(e));
+        setErrors(1);
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public void dispose() {
+    try {
+      if (data.mappingPipeline != null) {
+        if (data.wasStarted && !data.mappingPipeline.isFinished()) {
+          data.mappingPipeline.waitUntilFinished();
+        }
+        if (data.mappingPipeline.getErrors() > 0) {
+          logError(BaseMessages.getString(PKG, "MultiMapping.Log.ErrorOccurredInSubPipeline"));
+          setErrors(1);
+        }
+      }
+    } finally {
+      super.dispose();
+    }
+  }
+
+  @Override
+  public void stopRunning() {
+    if (data.mappingPipeline != null) {
+      data.mappingPipeline.stopAll();
+    }
+  }
+
+  @Override
+  public void stopAll() {
+    if (data.mappingPipeline != null) {
+      data.mappingPipeline.stopAll();
+    }
+    super.stopAll();
+  }
+
+  @Override
+  public void addRowListener(IRowListener rowListener) {
+    super.addRowListener(rowListener);
+    if (data.mappingPipeline == null) {
+      return;
+    }
+    for (MappingOutput mappingOutput : MappingTransforms.findMappingOutputs(data.mappingPipeline)) {
+      mappingOutput.addRowListener(rowListener);
+    }
+  }
+
+  public Pipeline getMappingPipeline() {
+    return data.mappingPipeline;
+  }
+
+  ITransform getMappingInputTransform() {
+    List<MappingInput> inputs = MappingTransforms.findMappingInputs(data.mappingPipeline);
+    return inputs.isEmpty() ? null : inputs.get(0);
+  }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingData.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingData.java
new file mode 100644
index 0000000000..f15350e684
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingData.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.multimapping;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.hop.core.IRowSet;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
+import org.apache.hop.pipeline.transform.BaseTransformData;
+import org.apache.hop.pipeline.transform.ITransformData;
+import org.apache.hop.pipeline.transforms.mapping.RowDataInputMapper;
+
+@SuppressWarnings("java:S1104")
+public class MultiMappingData extends BaseTransformData implements ITransformData {
+
+  public LocalPipelineEngine mappingPipeline;
+  public PipelineMeta mappingPipelineMeta;
+  public boolean wasStarted;
+  public boolean infoDrained;
+  public boolean producersFinished;
+
+  public List<RowDataInputMapper> allInputMappers = new ArrayList<>();
+  public List<IRowSet> infoRowSets = new ArrayList<>();
+  public List<IRowSet> mainRowSets = new ArrayList<>();
+  public Map<IRowSet, RowDataInputMapper> rowSetMappers = new HashMap<>();
+  public Set<IRowSet> finishedRowSets = new HashSet<>();
+  public int mainRowSetIndex;
+
+  public MultiMappingData() {
+    super();
+    mappingPipeline = null;
+    wasStarted = false;
+  }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingDialog.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingDialog.java
new file mode 100644
index 0000000000..fdd81867ce
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingDialog.java
@@ -0,0 +1,901 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.multimapping;
+
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.Props;
+import org.apache.hop.core.SourceToTargetMapping;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.vfs.HopVfs;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.config.PipelineRunConfiguration;
+import org.apache.hop.pipeline.engines.local.LocalPipelineRunConfiguration;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transforms.mapping.MappingParameters;
+import org.apache.hop.pipeline.transforms.mapping.MappingTransforms;
+import org.apache.hop.pipeline.transforms.mapping.MappingVariableMapping;
+import org.apache.hop.pipeline.transforms.mapping.SimpleMappingMeta;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.BaseDialog;
+import org.apache.hop.ui.core.dialog.EnterMappingDialog;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
+import org.apache.hop.ui.core.dialog.MessageBox;
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.widget.ColumnInfo;
+import org.apache.hop.ui.core.widget.ColumnsResizer;
+import org.apache.hop.ui.core.widget.ComboVar;
+import org.apache.hop.ui.core.widget.TableView;
+import org.apache.hop.ui.core.widget.TextVar;
+import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.pipeline.HopPipelineFileType;
+import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CCombo;
+import org.eclipse.swt.custom.CTabFolder;
+import org.eclipse.swt.custom.CTabFolder2Adapter;
+import org.eclipse.swt.custom.CTabFolderEvent;
+import org.eclipse.swt.custom.CTabItem;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.TableItem;
+
+public class MultiMappingDialog extends BaseTransformDialog {
+  private static final Class<?> PKG = MultiMappingMeta.class;
+
+  private final MultiMappingMeta mappingMeta;
+  private final MultiIOMappings workingMappings;
+
+  private TextVar wPath;
+  private ComboVar wRunConfig;
+  private CTabFolder wTabFolder;
+  private Button wbAddInput;
+  private Button wbAddOutput;
+
+  private PipelineMeta mappingPipelineMeta;
+  private ModifyListener lsMod;
+  private final List<Runnable> changeList = new ArrayList<>();
+
+  public MultiMappingDialog(
+      Shell parent,
+      IVariables variables,
+      MultiMappingMeta transformMeta,
+      PipelineMeta pipelineMeta) {
+    super(parent, variables, transformMeta, pipelineMeta);
+    this.mappingMeta = transformMeta;
+    this.workingMappings = new MultiIOMappings(transformMeta.getIoMappings());
+  }
+
+  @Override
+  public String open() {
+    createShell(BaseMessages.getString(PKG, "MultiMappingDialog.Shell.Title"));
+    buildButtonBar().ok(e -> ok()).cancel(e -> cancel()).build();
+
+    lsMod = e -> mappingMeta.setChanged();
+    changed = mappingMeta.hasChanged();
+
+    Label wlPath = new Label(shell, SWT.RIGHT);
+    PropsUi.setLook(wlPath);
+    wlPath.setText(BaseMessages.getString(PKG, "MultiMappingDialog.Pipeline.Label"));
+    FormData fdlPath = new FormData();
+    fdlPath.left = new FormAttachment(0, 0);
+    fdlPath.top = new FormAttachment(wSpacer, margin);
+    fdlPath.right = new FormAttachment(middle, -margin);
+    wlPath.setLayoutData(fdlPath);
+
+    Button wbEdit = new Button(shell, SWT.PUSH);
+    PropsUi.setLook(wbEdit);
+    wbEdit.setText(BaseMessages.getString(PKG, "MultiMappingDialog.Edit.Label"));
+    FormData fdEdit = new FormData();
+    fdEdit.right = new FormAttachment(100, 0);
+    fdEdit.top = new FormAttachment(wlPath, 0, SWT.CENTER);
+    wbEdit.setLayoutData(fdEdit);
+    wbEdit.addListener(SWT.Selection, e -> editPipeline());
+
+    Button wbNew = new Button(shell, SWT.PUSH);
+    PropsUi.setLook(wbNew);
+    wbNew.setText(BaseMessages.getString(PKG, "MultiMappingDialog.New.Label"));
+    FormData fdNew = new FormData();
+    fdNew.right = new FormAttachment(wbEdit, -margin);
+    fdNew.top = new FormAttachment(wlPath, 0, SWT.CENTER);
+    wbNew.setLayoutData(fdNew);
+    wbNew.addListener(SWT.Selection, e -> newPipeline());
+
+    Button wbBrowse = new Button(shell, SWT.PUSH);
+    PropsUi.setLook(wbBrowse);
+    wbBrowse.setText(BaseMessages.getString(PKG, "MultiMappingDialog.Browse.Label"));
+    FormData fdBrowse = new FormData();
+    fdBrowse.right = new FormAttachment(wbNew, -margin);
+    fdBrowse.top = new FormAttachment(wlPath, 0, SWT.CENTER);
+    wbBrowse.setLayoutData(fdBrowse);
+    wbBrowse.addListener(SWT.Selection, e -> selectFilePipeline());
+
+    wPath = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+    PropsUi.setLook(wPath);
+    FormData fdPath = new FormData();
+    fdPath.left = new FormAttachment(wlPath, margin);
+    fdPath.top = new FormAttachment(wlPath, 0, SWT.CENTER);
+    fdPath.right = new FormAttachment(wbBrowse, -margin);
+    wPath.setLayoutData(fdPath);
+
+    Label wlRunConfig = new Label(shell, SWT.RIGHT);
+    PropsUi.setLook(wlRunConfig);
+    wlRunConfig.setText(BaseMessages.getString(PKG, "MultiMappingDialog.RunConfig.Label"));
+    FormData fdlRunConfig = new FormData();
+    fdlRunConfig.left = new FormAttachment(0, 0);
+    fdlRunConfig.top = new FormAttachment(wPath, margin);
+    fdlRunConfig.right = new FormAttachment(middle, -margin);
+    wlRunConfig.setLayoutData(fdlRunConfig);
+    wRunConfig = new ComboVar(variables, shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+    PropsUi.setLook(wRunConfig);
+    FormData fdRunConfig = new FormData();
+    fdRunConfig.left = new FormAttachment(wlRunConfig, margin);
+    fdRunConfig.top = new FormAttachment(wlRunConfig, 0, SWT.CENTER);
+    fdRunConfig.right = new FormAttachment(wbBrowse, -margin);
+    wRunConfig.setLayoutData(fdRunConfig);
+
+    wbAddOutput = new Button(shell, SWT.PUSH);
+    PropsUi.setLook(wbAddOutput);
+    wbAddOutput.setText(BaseMessages.getString(PKG, "MultiMappingDialog.button.AddOutput"));
+    FormData fdAddOutput = new FormData();
+    fdAddOutput.right = new FormAttachment(100, 0);
+    fdAddOutput.top = new FormAttachment(wRunConfig, margin);
+    wbAddOutput.setLayoutData(fdAddOutput);
+    wbAddOutput.addListener(
+        SWT.Selection, e -> addOutputTab(new MultiMappingOutputDefinition(), true));
+
+    wbAddInput = new Button(shell, SWT.PUSH);
+    PropsUi.setLook(wbAddInput);
+    wbAddInput.setText(BaseMessages.getString(PKG, "MultiMappingDialog.button.AddInput"));
+    FormData fdAddInput = new FormData();
+    fdAddInput.right = new FormAttachment(wbAddOutput, -margin);
+    fdAddInput.top = new FormAttachment(wRunConfig, margin);
+    wbAddInput.setLayoutData(fdAddInput);
+    wbAddInput.addListener(
+        SWT.Selection, e -> addInputTab(new MultiMappingInputDefinition(), true));
+
+    wTabFolder = new CTabFolder(shell, SWT.BORDER);
+    PropsUi.setLook(wTabFolder, Props.WIDGET_STYLE_TAB);
+    wTabFolder.setUnselectedCloseVisible(true);
+    FormData fdTabFolder = new FormData();
+    fdTabFolder.left = new FormAttachment(0, 0);
+    fdTabFolder.top = new FormAttachment(wbAddInput, margin);
+    fdTabFolder.right = new FormAttachment(100, 0);
+    fdTabFolder.bottom = new FormAttachment(wOk, -margin);
+    wTabFolder.setLayoutData(fdTabFolder);
+    wTabFolder.addCTabFolder2Listener(
+        new CTabFolder2Adapter() {
+          @Override
+          public void close(CTabFolderEvent event) {
+            CTabItem item = (CTabItem) event.item;
+            if (item.getData() == null) {
+              event.doit = false;
+              return;
+            }
+            MessageBox box = new MessageBox(shell, SWT.YES | SWT.NO | SWT.ICON_QUESTION);
+            box.setText(
+                BaseMessages.getString(
+                    PKG, "MultiMappingDialog.CloseDefinitionTabAreYouSure.Title"));
+            box.setMessage(
+                BaseMessages.getString(
+                    PKG, "MultiMappingDialog.CloseDefinitionTabAreYouSure.Message"));
+            if (box.open() != SWT.YES) {
+              event.doit = false;
+              return;
+            }
+            Object data = item.getData();
+            if (data instanceof MultiMappingInputDefinition definition) {
+              workingMappings.getInputMappings().remove(definition);
+            } else if (data instanceof MultiMappingOutputDefinition definition) {
+              workingMappings.getOutputMappings().remove(definition);
+            }
+            mappingMeta.setChanged();
+          }
+        });
+
+    getData();
+    mappingMeta.setChanged(changed);
+    wTabFolder.setSelection(0);
+    focusTransformName();
+    BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
+    return transformName;
+  }
+
+  private void selectFilePipeline() {
+    try {
+      HopPipelineFileType fileType = new HopPipelineFileType();
+      String filename =
+          BaseDialog.presentFileDialog(
+              false, shell, fileType.getFilterExtensions(), fileType.getFilterNames(), true);
+      if (filename != null) {
+        loadPipelineFile(filename);
+        wPath.setText(filename);
+      }
+    } catch (HopException ex) {
+      new ErrorDialog(
+          shell,
+          BaseMessages.getString(PKG, "MultiMappingDialog.ErrorLoadingPipeline.DialogTitle"),
+          BaseMessages.getString(PKG, "MultiMappingDialog.ErrorLoadingPipeline.DialogMessage"),
+          ex);
+    }
+  }
+
+  private void newPipeline() {
+    try {
+      HopPipelineFileType fileType = new HopPipelineFileType();
+      String filename =
+          BaseDialog.presentFileDialog(
+              false, shell, fileType.getFilterExtensions(), fileType.getFilterNames(), true);
+      if (Utils.isEmpty(filename)) {
+        return;
+      }
+      if (!filename.endsWith(".hpl")) {
+        filename = filename + ".hpl";
+      }
+      PipelineMeta newMeta = new PipelineMeta();
+      newMeta.setFilename(filename);
+      try (OutputStream outputStream = HopVfs.getOutputStream(filename, false)) {
+        outputStream.write(newMeta.getXml(variables).getBytes(StandardCharsets.UTF_8));
+      }
+      loadPipelineFile(filename);
+      wPath.setText(filename);
+      mappingMeta.setChanged();
+    } catch (Exception ex) {
+      new ErrorDialog(
+          shell,
+          BaseMessages.getString(PKG, "MultiMappingDialog.ErrorLoadingPipeline.DialogTitle"),
+          BaseMessages.getString(PKG, "MultiMappingDialog.ErrorLoadingPipeline.DialogMessage"),
+          ex);
+    }
+  }
+
+  private void editPipeline() {
+    try {
+      if (Utils.isEmpty(wPath.getText())) {
+        return;
+      }
+      HopGui.getInstance().fileDelegate.fileOpen(variables.resolve(wPath.getText()));
+    } catch (Exception ex) {
+      new ErrorDialog(
+          shell,
+          BaseMessages.getString(PKG, "MultiMappingDialog.ErrorLoadingPipeline.DialogTitle"),
+          BaseMessages.getString(PKG, "MultiMappingDialog.ErrorLoadingPipeline.DialogMessage"),
+          ex);
+    }
+  }
+
+  private void loadPipelineFile(String fname) throws HopException {
+    mappingPipelineMeta = new PipelineMeta(variables.resolve(fname), metadataProvider, variables);
+    mappingPipelineMeta.clearChanged();
+  }
+
+  void loadPipeline() throws HopException {
+    String filename = wPath.getText();
+    if (Utils.isEmpty(filename)) {
+      return;
+    }
+    if (!filename.endsWith(".hpl")) {
+      filename = filename + ".hpl";
+      wPath.setText(filename);
+    }
+    loadPipelineFile(filename);
+  }
+
+  public void getData() {
+    wPath.setText(Const.NVL(mappingMeta.getFilename(), ""));
+    addParametersTab(workingMappings.getMappingParameters());
+    for (MultiMappingInputDefinition definition : workingMappings.getInputMappings()) {
+      addInputTab(definition, false);
+    }
+    for (MultiMappingOutputDefinition definition : workingMappings.getOutputMappings()) {
+      addOutputTab(definition, false);
+    }
+    wTabFolder.setSelection(0);
+
+    Pattern p = Pattern.compile("^[/\\w]*(\\$\\{\\w+})[/.\\w]*");
+    Matcher m = p.matcher(Const.NVL(mappingMeta.getFilename(), ""));
+    if (!m.lookingAt()) {
+      try {
+        loadPipeline();
+      } catch (Exception e) {
+        // Ignore errors while populating the dialog
+      }
+    }
+    try {
+      List<PipelineRunConfiguration> runConfigs =
+          metadataProvider.getSerializer(PipelineRunConfiguration.class).loadAll();
+      for (PipelineRunConfiguration runConfig : runConfigs) {
+        if (runConfig.getEngineRunConfiguration() instanceof LocalPipelineRunConfiguration) {
+          wRunConfig.add(runConfig.getName());
+        }
+      }
+      wRunConfig.setText(Const.NVL(mappingMeta.getRunConfigurationName(), ""));
+    } catch (Exception e) {
+      LogChannel.UI.logError("Error loading pipeline run configurations", e);
+    }
+  }
+
+  private void addParametersTab(MappingParameters parameters) {
+    CTabItem wParametersTab = new CTabItem(wTabFolder, SWT.NONE);
+    wParametersTab.setFont(GuiResource.getInstance().getFontDefault());
+    wParametersTab.setText(BaseMessages.getString(PKG, "MultiMappingDialog.Parameters.Title"));
+    wParametersTab.setToolTipText(
+        BaseMessages.getString(PKG, "MultiMappingDialog.Parameters.Tooltip"));
+
+    Composite composite = new Composite(wTabFolder, SWT.NONE);
+    PropsUi.setLook(composite);
+    FormLayout layout = new FormLayout();
+    layout.marginWidth = 15;
+    layout.marginHeight = 15;
+    composite.setLayout(layout);
+
+    Button wInheritAll = new Button(composite, SWT.CHECK);
+    PropsUi.setLook(wInheritAll);
+    wInheritAll.setText(BaseMessages.getString(PKG, "MultiMappingDialog.Parameters.InheritAll"));
+    FormData fdInherit = new FormData();
+    fdInherit.left = new FormAttachment(0, 0);
+    fdInherit.bottom = new FormAttachment(100, 0);
+    wInheritAll.setLayoutData(fdInherit);
+    wInheritAll.setSelection(parameters.isInheritingAllVariables());
+
+    ColumnInfo[] colinfo =
+        new ColumnInfo[] {
+          new ColumnInfo(
+              BaseMessages.getString(PKG, "MultiMappingDialog.Parameters.column.Variable"),
+              ColumnInfo.COLUMN_TYPE_TEXT,
+              false,
+              false),
+          new ColumnInfo(
+              BaseMessages.getString(PKG, "MultiMappingDialog.Parameters.column.ValueOrField"),
+              ColumnInfo.COLUMN_TYPE_TEXT,
+              false,
+              false),
+        };
+    colinfo[1].setUsingVariables(true);
+    TableView wMappingParameters =
+        new TableView(
+            variables,
+            composite,
+            SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER,
+            colinfo,
+            Math.max(parameters.getVariableMappings().size(), 1),
+            false,
+            lsMod,
+            props,
+            false);
+    PropsUi.setLook(wMappingParameters);
+    FormData fdMappings = new FormData();
+    fdMappings.left = new FormAttachment(0, 0);
+    fdMappings.right = new FormAttachment(100, 0);
+    fdMappings.top = new FormAttachment(0, 0);
+    fdMappings.bottom = new FormAttachment(wInheritAll, -10);
+    wMappingParameters.setLayoutData(fdMappings);
+    wMappingParameters.getTable().addListener(SWT.Resize, new ColumnsResizer(0, 50, 50));
+
+    for (int i = 0; i < parameters.getVariableMappings().size(); i++) {
+      MappingVariableMapping mapping = parameters.getVariableMappings().get(i);
+      TableItem tableItem = wMappingParameters.table.getItem(i);
+      tableItem.setText(1, Const.NVL(mapping.getName(), ""));
+      tableItem.setText(2, Const.NVL(mapping.getValue(), ""));
+    }
+    wMappingParameters.setRowNums();
+    wMappingParameters.optWidth(true);
+
+    wParametersTab.setControl(composite);
+    changeList.add(
+        () -> {
+          parameters.getVariableMappings().clear();
+          for (TableItem item : wMappingParameters.getNonEmptyItems()) {
+            parameters
+                .getVariableMappings()
+                .add(new MappingVariableMapping(item.getText(1), item.getText(2)));
+          }
+          parameters.setInheritingAllVariables(wInheritAll.getSelection());
+        });
+  }
+
+  private void addInputTab(MultiMappingInputDefinition definition, boolean addToModel) {
+    if (addToModel) {
+      workingMappings.getInputMappings().add(definition);
+      mappingMeta.setChanged();
+    }
+    CTabItem tab = new CTabItem(wTabFolder, SWT.CLOSE);
+    tab.setFont(GuiResource.getInstance().getFontDefault());
+    tab.setData(definition);
+    tab.setText(
+        tabTitle(
+            BaseMessages.getString(PKG, "MultiMappingDialog.InputTab.Title"),
+            definition.getInputTransformName()));
+
+    Composite composite = new Composite(wTabFolder, SWT.NONE);
+    PropsUi.setLook(composite);
+    FormLayout layout = new FormLayout();
+    layout.marginWidth = 15;
+    layout.marginHeight = 15;
+    composite.setLayout(layout);
+
+    Label wlSource = new Label(composite, SWT.RIGHT);
+    PropsUi.setLook(wlSource);
+    wlSource.setText(
+        BaseMessages.getString(PKG, "MultiMappingDialog.InputTab.label.InputSourceTransformName"));
+    FormData fdlSource = new FormData();
+    fdlSource.left = new FormAttachment(0, 0);
+    fdlSource.top = new FormAttachment(0, 0);
+    fdlSource.right = new FormAttachment(middle, -margin);
+    wlSource.setLayoutData(fdlSource);
+    CCombo wSource = new CCombo(composite, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+    PropsUi.setLook(wSource);
+    wSource.setItems(pipelineMeta.getPrevTransformNames(transformMeta));
+    wSource.setText(Const.NVL(definition.getInputTransformName(), ""));
+    FormData fdSource = new FormData();
+    fdSource.left = new FormAttachment(wlSource, margin);
+    fdSource.top = new FormAttachment(wlSource, 0, SWT.CENTER);
+    fdSource.right = new FormAttachment(100, 0);
+    wSource.setLayoutData(fdSource);
+
+    Label wlTarget = new Label(composite, SWT.RIGHT);
+    PropsUi.setLook(wlTarget);
+    wlTarget.setText(
+        BaseMessages.getString(PKG, "MultiMappingDialog.InputTab.label.OutputTargetTransformName"));
+    FormData fdlTarget = new FormData();
+    fdlTarget.left = new FormAttachment(0, 0);
+    fdlTarget.top = new FormAttachment(wSource, margin);
+    fdlTarget.right = new FormAttachment(middle, -margin);
+    wlTarget.setLayoutData(fdlTarget);
+    CCombo wTarget = new CCombo(composite, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+    PropsUi.setLook(wTarget);
+    wTarget.setItems(childTransformNames(true));
+    wTarget.setText(Const.NVL(definition.getOutputTransformName(), ""));
+    FormData fdTarget = new FormData();
+    fdTarget.left = new FormAttachment(wlTarget, margin);
+    fdTarget.top = new FormAttachment(wlTarget, 0, SWT.CENTER);
+    fdTarget.right = new FormAttachment(100, 0);
+    wTarget.setLayoutData(fdTarget);
+
+    Label wlDesc = new Label(composite, SWT.RIGHT);
+    PropsUi.setLook(wlDesc);
+    wlDesc.setText(BaseMessages.getString(PKG, "MultiMappingDialog.InputTab.label.Description"));
+    FormData fdlDesc = new FormData();
+    fdlDesc.left = new FormAttachment(0, 0);
+    fdlDesc.top = new FormAttachment(wTarget, margin);
+    fdlDesc.right = new FormAttachment(middle, -margin);
+    wlDesc.setLayoutData(fdlDesc);
+    org.eclipse.swt.widgets.Text wDesc =
+        new org.eclipse.swt.widgets.Text(composite, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+    PropsUi.setLook(wDesc);
+    wDesc.setText(Const.NVL(definition.getDescription(), ""));
+    FormData fdDesc = new FormData();
+    fdDesc.left = new FormAttachment(wlDesc, margin);
+    fdDesc.top = new FormAttachment(wlDesc, 0, SWT.CENTER);
+    fdDesc.right = new FormAttachment(100, 0);
+    wDesc.setLayoutData(fdDesc);
+
+    Button wMain = new Button(composite, SWT.CHECK);
+    PropsUi.setLook(wMain);
+    wMain.setText(BaseMessages.getString(PKG, "MultiMappingDialog.input.MainDataPath"));
+    wMain.setSelection(definition.isMainDataPath());
+    FormData fdMain = new FormData();
+    fdMain.left = new FormAttachment(middle, margin);
+    fdMain.top = new FormAttachment(wDesc, margin);
+    wMain.setLayoutData(fdMain);
+
+    Button wRename = new Button(composite, SWT.CHECK);
+    PropsUi.setLook(wRename);
+    wRename.setText(BaseMessages.getString(PKG, "MultiMappingDialog.input.RenamingOnOutput"));
+    wRename.setSelection(definition.isRenamingOnOutput());
+    FormData fdRename = new FormData();
+    fdRename.left = new FormAttachment(middle, margin);
+    fdRename.top = new FormAttachment(wMain, margin);
+    wRename.setLayoutData(fdRename);
+
+    Button wbEnterMapping = new Button(composite, SWT.PUSH);
+    PropsUi.setLook(wbEnterMapping);
+    wbEnterMapping.setText(BaseMessages.getString(PKG, "MultiMappingDialog.button.EnterMapping"));
+    FormData fdbEnter = new FormData();
+    fdbEnter.bottom = new FormAttachment(100);
+    fdbEnter.right = new FormAttachment(100);
+    wbEnterMapping.setLayoutData(fdbEnter);
+
+    ColumnInfo[] colinfo =
+        new ColumnInfo[] {
+          new ColumnInfo(
+              BaseMessages.getString(PKG, "MultiMappingDialog.InputTab.column.SourceField"),
+              ColumnInfo.COLUMN_TYPE_TEXT,
+              false,
+              false),
+          new ColumnInfo(
+              BaseMessages.getString(PKG, "MultiMappingDialog.InputTab.column.TargetField"),
+              ColumnInfo.COLUMN_TYPE_TEXT,
+              false,
+              false),
+        };
+    TableView wFieldMappings =
+        new TableView(
+            variables,
+            composite,
+            SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER,
+            colinfo,
+            1,
+            false,
+            lsMod,
+            props,
+            false);
+    FormData fdFields = new FormData();
+    fdFields.left = new FormAttachment(0, 0);
+    fdFields.right = new FormAttachment(100, 0);
+    fdFields.top = new FormAttachment(wRename, margin);
+    fdFields.bottom = new FormAttachment(wbEnterMapping, -10);
+    wFieldMappings.setLayoutData(fdFields);
+    populateRenames(wFieldMappings, definition.getValueRenames());
+
+    wbEnterMapping.addListener(
+        SWT.Selection,
+        e -> {
+          try {
+            loadPipeline();
+            IRowMeta sourceRowMeta =
+                Utils.isEmpty(wSource.getText())
+                    ? pipelineMeta.getPrevTransformFields(variables, transformMeta)
+                    : pipelineMeta.getTransformFields(
+                        variables, pipelineMeta.findTransform(wSource.getText()));
+            TransformMeta mappingInput =
+                SimpleMappingMeta.findMappingInputTransform(mappingPipelineMeta, wTarget.getText());
+            IRowMeta targetRowMeta =
+                mappingPipelineMeta.getTransformFields(variables, mappingInput);
+            EnterMappingDialog dialog =
+                new EnterMappingDialog(
+                    shell, sourceRowMeta.getFieldNames(), targetRowMeta.getFieldNames());
+            List<SourceToTargetMapping> mappings = dialog.open();
+            if (mappings != null) {
+              wFieldMappings.clearAll(false);
+              for (SourceToTargetMapping mapping : mappings) {
+                TableItem item = new TableItem(wFieldMappings.table, SWT.NONE);
+                item.setText(1, mapping.getSourceString(sourceRowMeta.getFieldNames()));
+                item.setText(2, mapping.getTargetString(targetRowMeta.getFieldNames()));
+              }
+              wFieldMappings.removeEmptyRows();
+              wFieldMappings.setRowNums();
+              wFieldMappings.optWidth(true);
+            }
+          } catch (Exception ex) {
+            new ErrorDialog(
+                shell,
+                BaseMessages.getString(PKG, "System.Dialog.Error.Title"),
+                BaseMessages.getString(
+                    PKG,
+                    "MultiMappingDialog.Exception.ErrorGettingMappingSourceAndTargetFields",
+                    ex.toString()),
+                ex);
+          }
+        });
+
+    tab.setControl(composite);
+    wTabFolder.setSelection(tab);
+    changeList.add(
+        () -> {
+          definition.setInputTransformName(wSource.getText());
+          definition.setOutputTransformName(wTarget.getText());
+          definition.setDescription(wDesc.getText());
+          definition.setMainDataPath(wMain.getSelection());
+          definition.setRenamingOnOutput(wRename.getSelection());
+          definition.getValueRenames().clear();
+          for (TableItem item : wFieldMappings.getNonEmptyItems()) {
+            definition
+                .getValueRenames()
+                .add(new MultiMappingInputRename(item.getText(1), item.getText(2)));
+          }
+        });
+  }
+
+  private void addOutputTab(MultiMappingOutputDefinition definition, boolean addToModel) {
+    if (addToModel) {
+      workingMappings.getOutputMappings().add(definition);
+      mappingMeta.setChanged();
+    }
+    CTabItem tab = new CTabItem(wTabFolder, SWT.CLOSE);
+    tab.setFont(GuiResource.getInstance().getFontDefault());
+    tab.setData(definition);
+    tab.setText(
+        tabTitle(
+            BaseMessages.getString(PKG, "MultiMappingDialog.OutputTab.Title"),
+            definition.getOutputTransformName()));
+
+    Composite composite = new Composite(wTabFolder, SWT.NONE);
+    PropsUi.setLook(composite);
+    FormLayout layout = new FormLayout();
+    layout.marginWidth = 15;
+    layout.marginHeight = 15;
+    composite.setLayout(layout);
+
+    Label wlSource = new Label(composite, SWT.RIGHT);
+    PropsUi.setLook(wlSource);
+    wlSource.setText(
+        BaseMessages.getString(PKG, "MultiMappingDialog.OutputTab.label.InputSourceTransformName"));
+    FormData fdlSource = new FormData();
+    fdlSource.left = new FormAttachment(0, 0);
+    fdlSource.top = new FormAttachment(0, 0);
+    fdlSource.right = new FormAttachment(middle, -margin);
+    wlSource.setLayoutData(fdlSource);
+    CCombo wSource = new CCombo(composite, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+    PropsUi.setLook(wSource);
+    wSource.setItems(childTransformNames(false));
+    wSource.setText(Const.NVL(definition.getInputTransformName(), ""));
+    FormData fdSource = new FormData();
+    fdSource.left = new FormAttachment(wlSource, margin);
+    fdSource.top = new FormAttachment(wlSource, 0, SWT.CENTER);
+    fdSource.right = new FormAttachment(100, 0);
+    wSource.setLayoutData(fdSource);
+
+    Label wlTarget = new Label(composite, SWT.RIGHT);
+    PropsUi.setLook(wlTarget);
+    wlTarget.setText(
+        BaseMessages.getString(
+            PKG, "MultiMappingDialog.OutputTab.label.OutputTargetTransformName"));
+    FormData fdlTarget = new FormData();
+    fdlTarget.left = new FormAttachment(0, 0);
+    fdlTarget.top = new FormAttachment(wSource, margin);
+    fdlTarget.right = new FormAttachment(middle, -margin);
+    wlTarget.setLayoutData(fdlTarget);
+    CCombo wTarget = new CCombo(composite, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+    PropsUi.setLook(wTarget);
+    wTarget.setItems(pipelineMeta.getNextTransformNames(transformMeta));
+    wTarget.setText(Const.NVL(definition.getOutputTransformName(), ""));
+    FormData fdTarget = new FormData();
+    fdTarget.left = new FormAttachment(wlTarget, margin);
+    fdTarget.top = new FormAttachment(wlTarget, 0, SWT.CENTER);
+    fdTarget.right = new FormAttachment(100, 0);
+    wTarget.setLayoutData(fdTarget);
+
+    Label wlDesc = new Label(composite, SWT.RIGHT);
+    PropsUi.setLook(wlDesc);
+    wlDesc.setText(BaseMessages.getString(PKG, "MultiMappingDialog.OutputTab.label.Description"));
+    FormData fdlDesc = new FormData();
+    fdlDesc.left = new FormAttachment(0, 0);
+    fdlDesc.top = new FormAttachment(wTarget, margin);
+    fdlDesc.right = new FormAttachment(middle, -margin);
+    wlDesc.setLayoutData(fdlDesc);
+    org.eclipse.swt.widgets.Text wDesc =
+        new org.eclipse.swt.widgets.Text(composite, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+    PropsUi.setLook(wDesc);
+    wDesc.setText(Const.NVL(definition.getDescription(), ""));
+    FormData fdDesc = new FormData();
+    fdDesc.left = new FormAttachment(wlDesc, margin);
+    fdDesc.top = new FormAttachment(wlDesc, 0, SWT.CENTER);
+    fdDesc.right = new FormAttachment(100, 0);
+    wDesc.setLayoutData(fdDesc);
+
+    Button wMain = new Button(composite, SWT.CHECK);
+    PropsUi.setLook(wMain);
+    wMain.setText(BaseMessages.getString(PKG, "MultiMappingDialog.input.MainDataPath"));
+    wMain.setSelection(definition.isMainDataPath());
+    FormData fdMain = new FormData();
+    fdMain.left = new FormAttachment(middle, margin);
+    fdMain.top = new FormAttachment(wDesc, margin);
+    wMain.setLayoutData(fdMain);
+
+    Button wbGetFields = new Button(composite, SWT.PUSH);
+    PropsUi.setLook(wbGetFields);
+    wbGetFields.setText(BaseMessages.getString(PKG, "MultiMappingDialog.button.GetFields"));
+    FormData fdbGet = new FormData();
+    fdbGet.bottom = new FormAttachment(100);
+    fdbGet.right = new FormAttachment(100);
+    wbGetFields.setLayoutData(fdbGet);
+
+    ColumnInfo[] colinfo =
+        new ColumnInfo[] {
+          new ColumnInfo(
+              BaseMessages.getString(PKG, "MultiMappingDialog.OutputTab.column.SourceField"),
+              ColumnInfo.COLUMN_TYPE_TEXT,
+              false,
+              false),
+          new ColumnInfo(
+              BaseMessages.getString(PKG, "MultiMappingDialog.OutputTab.column.TargetField"),
+              ColumnInfo.COLUMN_TYPE_TEXT,
+              false,
+              false),
+        };
+    TableView wFieldMappings =
+        new TableView(
+            variables,
+            composite,
+            SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER,
+            colinfo,
+            1,
+            false,
+            lsMod,
+            props,
+            false);
+    FormData fdFields = new FormData();
+    fdFields.left = new FormAttachment(0, 0);
+    fdFields.right = new FormAttachment(100, 0);
+    fdFields.top = new FormAttachment(wMain, margin);
+    fdFields.bottom = new FormAttachment(wbGetFields, -10);
+    wFieldMappings.setLayoutData(fdFields);
+    populateOutputRenames(wFieldMappings, definition.getValueRenames());
+
+    wbGetFields.addListener(
+        SWT.Selection,
+        e -> {
+          try {
+            loadPipeline();
+            TransformMeta mappingOutput =
+                SimpleMappingMeta.findMappingOutputTransform(
+                    mappingPipelineMeta, wSource.getText());
+            IRowMeta sourceRowMeta =
+                mappingPipelineMeta.getTransformFields(variables, mappingOutput);
+            BaseTransformDialog.getFieldsFromPrevious(
+                sourceRowMeta,
+                wFieldMappings,
+                1,
+                new int[] {1},
+                new int[] {},
+                -1,
+                -1,
+                (tableItem, v) -> {
+                  tableItem.setText(2, tableItem.getText(1));
+                  return true;
+                });
+          } catch (Exception ex) {
+            new ErrorDialog(
+                shell,
+                BaseMessages.getString(PKG, "System.Dialog.Error.Title"),
+                BaseMessages.getString(
+                    PKG,
+                    "MultiMappingDialog.Exception.ErrorGettingMappingSourceAndTargetFields",
+                    ex.toString()),
+                ex);
+          }
+        });
+
+    tab.setControl(composite);
+    wTabFolder.setSelection(tab);
+    changeList.add(
+        () -> {
+          definition.setInputTransformName(wSource.getText());
+          definition.setOutputTransformName(wTarget.getText());
+          definition.setDescription(wDesc.getText());
+          definition.setMainDataPath(wMain.getSelection());
+          definition.getValueRenames().clear();
+          for (TableItem item : wFieldMappings.getNonEmptyItems()) {
+            definition
+                .getValueRenames()
+                .add(new MultiMappingOutputRename(item.getText(1), item.getText(2)));
+          }
+        });
+  }
+
+  private void populateRenames(TableView view, List<MultiMappingInputRename> renames) {
+    for (MultiMappingInputRename rename : renames) {
+      TableItem item = new TableItem(view.table, SWT.NONE);
+      item.setText(1, Const.NVL(rename.getSourceValueName(), ""));
+      item.setText(2, Const.NVL(rename.getTargetValueName(), ""));
+    }
+    view.removeEmptyRows();
+    view.setRowNums();
+    view.optWidth(true);
+  }
+
+  private void populateOutputRenames(TableView view, List<MultiMappingOutputRename> renames) {
+    for (MultiMappingOutputRename rename : renames) {
+      TableItem item = new TableItem(view.table, SWT.NONE);
+      item.setText(1, Const.NVL(rename.getSourceValueName(), ""));
+      item.setText(2, Const.NVL(rename.getTargetValueName(), ""));
+    }
+    view.removeEmptyRows();
+    view.setRowNums();
+    view.optWidth(true);
+  }
+
+  private String[] childTransformNames(boolean mappingInput) {
+    if (mappingPipelineMeta == null) {
+      try {
+        loadPipeline();
+      } catch (Exception e) {
+        return new String[0];
+      }
+    }
+    if (mappingPipelineMeta == null) {
+      return new String[0];
+    }
+    List<TransformMeta> transforms =
+        mappingInput
+            ? MappingTransforms.findMappingInputMetas(mappingPipelineMeta)
+            : MappingTransforms.findMappingOutputMetas(mappingPipelineMeta);
+    return transforms.stream().map(TransformMeta::getName).toArray(String[]::new);
+  }
+
+  private String tabTitle(String base, String transformName) {
+    if (Utils.isEmpty(transformName)) {
+      return base;
+    }
+    return base + " : " + transformName;
+  }
+
+  private void cancel() {
+    transformName = null;
+    mappingMeta.setChanged(changed);
+    dispose();
+  }
+
+  private void ok() {
+    if (Utils.isEmpty(wTransformName.getText())) {
+      return;
+    }
+    if (Utils.isEmpty(wPath.getText())) {
+      MessageBox mb = new MessageBox(shell, SWT.OK | SWT.ICON_ERROR);
+      mb.setText(BaseMessages.getString(PKG, "MultiMappingDialog.FilenameMissing.Header"));
+      mb.setMessage(BaseMessages.getString(PKG, "MultiMappingDialog.FilenameMissing.Message"));
+      mb.open();
+      return;
+    }
+    if (isSelfReferencing()) {
+      MessageBox mb = new MessageBox(shell, SWT.OK | SWT.ICON_ERROR);
+      mb.setText(BaseMessages.getString(PKG, "MultiMappingDialog.SelfReference.Header"));
+      mb.setMessage(BaseMessages.getString(PKG, "MultiMappingDialog.SelfReference.Message"));
+      mb.open();
+      return;
+    }
+
+    transformName = wTransformName.getText();
+    mappingMeta.setFilename(wPath.getText());
+    mappingMeta.setRunConfigurationName(wRunConfig.getText());
+
+    Pattern p = Pattern.compile("^[/\\w]*(\\$\\{\\w+})[/.\\w]*");
+    Matcher m = p.matcher(mappingMeta.getFilename());
+    if (!m.lookingAt()) {
+      try {
+        loadPipeline();
+      } catch (HopException e) {
+        new ErrorDialog(
+            shell,
+            BaseMessages.getString(PKG, "MultiMappingDialog.ErrorLoadingSpecifiedPipeline.Title"),
+            BaseMessages.getString(PKG, "MultiMappingDialog.ErrorLoadingSpecifiedPipeline.Message"),
+            e);
+        return;
+      }
+    }
+
+    for (Runnable applyChanges : changeList) {
+      applyChanges.run();
+    }
+    mappingMeta.setIoMappings(workingMappings);
+    mappingMeta.resetTransformIoMeta();
+    mappingMeta.setChanged(true);
+    dispose();
+  }
+
+  private boolean isSelfReferencing() {
+    return variables.resolve(wPath.getText()).equals(variables.resolve(pipelineMeta.getFilename()));
+  }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingInputDefinition.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingInputDefinition.java
new file mode 100644
index 0000000000..2ebf6269c9
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingInputDefinition.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.multimapping;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transforms.mapping.MappingIODefinition;
+import org.apache.hop.pipeline.transforms.mapping.MappingValueRename;
+
+/** Parent source → child Mapping Input. */
+@Getter
+@Setter
+public class MultiMappingInputDefinition implements Cloneable {
+
+  @SuppressWarnings("java:S2065")
+  private transient TransformMeta inputTransform;
+
+  @HopMetadataProperty(
+      key = "input_transform",
+      injectionKey = "INPUT_SOURCE_TRANSFORM",
+      injectionKeyDescription = "MultiMappingMeta.Injection.INPUT_SOURCE_TRANSFORM")
+  private String inputTransformName;
+
+  @HopMetadataProperty(
+      key = "output_transform",
+      injectionKey = "INPUT_MAPPING_TRANSFORM",
+      injectionKeyDescription = "MultiMappingMeta.Injection.INPUT_MAPPING_TRANSFORM")
+  private String outputTransformName;
+
+  @HopMetadataProperty(
+      key = "description",
+      injectionKey = "INPUT_DESCRIPTION",
+      injectionKeyDescription = "MultiMappingMeta.Injection.INPUT_DESCRIPTION")
+  private String description;
+
+  @HopMetadataProperty(
+      key = "connector",
+      injectionGroupKey = "INPUT_RENAMES",
+      injectionGroupDescription = "MultiMappingMeta.Injection.INPUT_RENAMES")
+  private List<MultiMappingInputRename> valueRenames;
+
+  @HopMetadataProperty(
+      key = "main_path",
+      injectionKey = "INPUT_MAIN_PATH",
+      injectionKeyDescription = "MultiMappingMeta.Injection.INPUT_MAIN_PATH")
+  private boolean mainDataPath;
+
+  @HopMetadataProperty(
+      key = "rename_on_output",
+      injectionKey = "INPUT_RENAME_ON_OUTPUT",
+      injectionKeyDescription = "MultiMappingMeta.Injection.INPUT_RENAME_ON_OUTPUT")
+  private boolean renamingOnOutput;
+
+  public MultiMappingInputDefinition() {
+    this.valueRenames = new ArrayList<>();
+  }
+
+  public MultiMappingInputDefinition(String inputTransformName, String outputTransformName) {
+    this();
+    this.inputTransformName = inputTransformName;
+    this.outputTransformName = outputTransformName;
+  }
+
+  public MultiMappingInputDefinition(MultiMappingInputDefinition d) {
+    this();
+    copyFrom(d);
+  }
+
+  public void copyFrom(MultiMappingInputDefinition d) {
+    this.inputTransformName = d.inputTransformName;
+    this.outputTransformName = d.outputTransformName;
+    this.description = d.description;
+    this.mainDataPath = d.mainDataPath;
+    this.renamingOnOutput = d.renamingOnOutput;
+    this.inputTransform = d.inputTransform;
+    this.valueRenames.clear();
+    for (MultiMappingInputRename rename : d.valueRenames) {
+      this.valueRenames.add(new MultiMappingInputRename(rename));
+    }
+  }
+
+  @Override
+  public MultiMappingInputDefinition clone() {
+    return new MultiMappingInputDefinition(this);
+  }
+
+  public MappingIODefinition toIoDefinition() {
+    MappingIODefinition definition =
+        new MappingIODefinition(inputTransformName, outputTransformName);
+    definition.setDescription(description);
+    definition.setMainDataPath(mainDataPath);
+    definition.setRenamingOnOutput(renamingOnOutput);
+    definition.setInputTransform(inputTransform);
+    List<MappingValueRename> renames = new ArrayList<>();
+    for (MultiMappingInputRename rename : valueRenames) {
+      renames.add(rename.toValueRename());
+    }
+    definition.setValueRenames(renames);
+    return definition;
+  }
+
+  public static MultiMappingInputDefinition fromIoDefinition(MappingIODefinition d) {
+    MultiMappingInputDefinition definition = new MultiMappingInputDefinition();
+    if (d == null) {
+      return definition;
+    }
+    definition.inputTransformName = d.getInputTransformName();
+    definition.outputTransformName = d.getOutputTransformName();
+    definition.description = d.getDescription();
+    definition.mainDataPath = d.isMainDataPath();
+    definition.renamingOnOutput = d.isRenamingOnOutput();
+    definition.inputTransform = d.getInputTransform();
+    if (d.getValueRenames() != null) {
+      for (MappingValueRename rename : d.getValueRenames()) {
+        definition.valueRenames.add(MultiMappingInputRename.fromValueRename(rename));
+      }
+    }
+    return definition;
+  }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingInputRename.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingInputRename.java
new file mode 100644
index 0000000000..9156a8443c
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingInputRename.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.multimapping;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.pipeline.transforms.mapping.MappingValueRename;
+
+@Getter
+@Setter
+public class MultiMappingInputRename implements Cloneable {
+
+  @HopMetadataProperty(
+      key = "parent",
+      injectionKey = "INPUT_RENAME_SOURCE",
+      injectionKeyDescription = "MultiMappingMeta.Injection.INPUT_RENAME_SOURCE")
+  private String sourceValueName;
+
+  @HopMetadataProperty(
+      key = "child",
+      injectionKey = "INPUT_RENAME_TARGET",
+      injectionKeyDescription = "MultiMappingMeta.Injection.INPUT_RENAME_TARGET")
+  private String targetValueName;
+
+  public MultiMappingInputRename() {}
+
+  public MultiMappingInputRename(String sourceValueName, String targetValueName) {
+    this.sourceValueName = sourceValueName == null ? "" : sourceValueName;
+    this.targetValueName = targetValueName == null ? "" : targetValueName;
+  }
+
+  public MultiMappingInputRename(MultiMappingInputRename rename) {
+    this.sourceValueName = rename.sourceValueName;
+    this.targetValueName = rename.targetValueName;
+  }
+
+  @Override
+  public MultiMappingInputRename clone() {
+    return new MultiMappingInputRename(this);
+  }
+
+  public MappingValueRename toValueRename() {
+    return new MappingValueRename(sourceValueName, targetValueName);
+  }
+
+  public static MultiMappingInputRename fromValueRename(MappingValueRename rename) {
+    if (rename == null) {
+      return new MultiMappingInputRename();
+    }
+    return new MultiMappingInputRename(rename.getSourceValueName(), rename.getTargetValueName());
+  }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMeta.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMeta.java
new file mode 100644
index 0000000000..a2b921f584
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMeta.java
@@ -0,0 +1,610 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.multimapping;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.CheckResult;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.annotations.ActionTransformType;
+import org.apache.hop.core.annotations.Transform;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopTransformException;
+import org.apache.hop.core.file.IHasFilename;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.HopMetadataPropertyType;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.pipeline.ISubPipelineAwareMeta;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.PipelineMeta.PipelineType;
+import org.apache.hop.pipeline.TransformWithMappingMeta;
+import org.apache.hop.pipeline.transform.ITransformIOMeta;
+import org.apache.hop.pipeline.transform.TransformIOMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transform.stream.IStream;
+import org.apache.hop.pipeline.transform.stream.IStream.StreamType;
+import org.apache.hop.pipeline.transform.stream.Stream;
+import org.apache.hop.pipeline.transform.stream.StreamIcon;
+import org.apache.hop.pipeline.transforms.input.MappingInputMeta;
+import org.apache.hop.pipeline.transforms.mapping.MappingIODefinition;
+import org.apache.hop.pipeline.transforms.mapping.MappingParameters;
+import org.apache.hop.pipeline.transforms.mapping.MappingTransforms;
+import org.apache.hop.pipeline.transforms.mapping.MappingValueRename;
+import org.apache.hop.pipeline.transforms.mapping.SimpleMappingMeta;
+import org.apache.hop.resource.ResourceEntry;
+import org.apache.hop.resource.ResourceEntry.ResourceType;
+import org.apache.hop.resource.ResourceReference;
+
+/** Meta-data for the Multi Mapping transform: 0..N inputs and outputs to a child pipeline. */
+@Transform(
+    id = "MultiMapping",
+    name = "i18n::MultiMapping.Name",
+    description = "i18n::MultiMapping.Description",
+    image = "MMAP.svg",
+    categoryDescription = "i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.Mapping",
+    keywords = "i18n::MultiMappingMeta.keyword",
+    documentationUrl = "/pipeline/transforms/multi-mapping.html",
+    actionTransformTypes = {ActionTransformType.HOP_FILE, ActionTransformType.HOP_PIPELINE})
+@Getter
+@Setter
+public class MultiMappingMeta extends TransformWithMappingMeta<MultiMapping, MultiMappingData>
+    implements ISubPipelineAwareMeta {
+
+  private static final Class<?> PKG = MultiMappingMeta.class;
+
+  private static final IStream NEW_INFO_STREAM =
+      new Stream(
+          StreamType.INFO,
+          null,
+          BaseMessages.getString(PKG, "MultiMappingMeta.InfoStream.New.Description"),
+          StreamIcon.INFO,
+          null);
+
+  private static final IStream NEW_TARGET_STREAM =
+      new Stream(
+          StreamType.TARGET,
+          null,
+          BaseMessages.getString(PKG, "MultiMappingMeta.TargetStream.New.Description"),
+          StreamIcon.TARGET,
+          null);
+
+  @HopMetadataProperty(
+      key = "runConfiguration",
+      hopMetadataPropertyType = HopMetadataPropertyType.PIPELINE_RUN_CONFIG,
+      injectionKey = "RUN_CONFIGURATION",
+      injectionKeyDescription = "MultiMappingMeta.Injection.RUN_CONFIGURATION")
+  private String runConfigurationName;
+
+  @HopMetadataProperty(key = "mappings")
+  private MultiIOMappings ioMappings;
+
+  public MultiMappingMeta() {
+    super();
+    ioMappings = new MultiIOMappings();
+  }
+
+  public MultiMappingMeta(MultiMappingMeta m) {
+    super(m);
+    this.runConfigurationName = m.runConfigurationName;
+    this.ioMappings =
+        m.ioMappings != null ? new MultiIOMappings(m.ioMappings) : new MultiIOMappings();
+  }
+
+  @Override
+  public MultiMappingMeta clone() {
+    return new MultiMappingMeta(this);
+  }
+
+  @Override
+  public void setDefault() {
+    MultiMappingInputDefinition inputDefinition = new MultiMappingInputDefinition(null, null);
+    inputDefinition.setMainDataPath(true);
+    inputDefinition.setRenamingOnOutput(true);
+    ioMappings.getInputMappings().add(inputDefinition);
+
+    MultiMappingOutputDefinition outputDefinition = new MultiMappingOutputDefinition(null, null);
+    outputDefinition.setMainDataPath(true);
+    ioMappings.getOutputMappings().add(outputDefinition);
+  }
+
+  public MappingParameters getMappingParameters() {
+    return ioMappings.getMappingParameters();
+  }
+
+  public void setMappingParameters(MappingParameters mappingParameters) {
+    ioMappings.setMappingParameters(mappingParameters);
+  }
+
+  public List<MultiMappingInputDefinition> getInputMappings() {
+    return ioMappings.getInputMappings();
+  }
+
+  public List<MultiMappingOutputDefinition> getOutputMappings() {
+    return ioMappings.getOutputMappings();
+  }
+
+  @Override
+  public void getFields(
+      IRowMeta row,
+      String origin,
+      IRowMeta[] info,
+      TransformMeta nextTransform,
+      IVariables variables,
+      IHopMetadataProvider metadataProvider)
+      throws HopTransformException {
+    PipelineMeta mappingPipelineMeta;
+    try {
+      mappingPipelineMeta = loadMappingMeta(this, metadataProvider, variables);
+    } catch (HopException e) {
+      throw new HopTransformException(
+          BaseMessages.getString(PKG, "MultiMappingMeta.Exception.UnableToLoadMappingPipeline"), e);
+    }
+
+    List<MappingValueRename> inputRenameList = new ArrayList<>();
+    String[] infoTransforms = getInfoTransforms();
+
+    for (MultiMappingInputDefinition inputDefinition : ioMappings.getInputMappings()) {
+      MappingIODefinition definition = inputDefinition.toIoDefinition();
+      IRowMeta inputRowMeta = resolveInputRowMeta(row, info, infoTransforms, definition);
+
+      TransformMeta mappingInputTransform =
+          SimpleMappingMeta.findMappingInputTransform(
+              mappingPipelineMeta, definition.getOutputTransformName());
+      MappingInputMeta mappingInputMeta = (MappingInputMeta) mappingInputTransform.getTransform();
+      mappingInputMeta.setInputRowMeta(inputRowMeta);
+
+      if (definition.isRenamingOnOutput()) {
+        addInputRenames(inputRenameList, definition.getValueRenames());
+      }
+    }
+
+    MultiMappingOutputDefinition outputDefinition = findOutputDefinition(nextTransform);
+    if (outputDefinition == null) {
+      // No output mappings: this transform produces no fields.
+      row.clear();
+      return;
+    }
+
+    TransformMeta mappingOutputTransform =
+        SimpleMappingMeta.findMappingOutputTransform(
+            mappingPipelineMeta, outputDefinition.getInputTransformName());
+    IRowMeta mappingOutputRowMeta =
+        mappingPipelineMeta.getTransformFields(variables, mappingOutputTransform);
+
+    if (!inputRenameList.isEmpty()) {
+      for (MappingValueRename rename : inputRenameList) {
+        IValueMeta valueMeta = mappingOutputRowMeta.searchValueMeta(rename.getTargetValueName());
+        if (valueMeta != null) {
+          valueMeta.setName(rename.getSourceValueName());
+        }
+      }
+    }
+    for (MappingValueRename rename : outputDefinition.toIoDefinition().getValueRenames()) {
+      IValueMeta valueMeta = mappingOutputRowMeta.searchValueMeta(rename.getSourceValueName());
+      if (valueMeta != null) {
+        valueMeta.setName(rename.getTargetValueName());
+      }
+    }
+
+    row.clear();
+    row.addRowMeta(mappingOutputRowMeta);
+  }
+
+  private IRowMeta resolveInputRowMeta(
+      IRowMeta row, IRowMeta[] info, String[] infoTransforms, MappingIODefinition definition)
+      throws HopTransformException {
+    if (definition.isMainDataPath() || Utils.isEmpty(definition.getInputTransformName())) {
+      IRowMeta inputRowMeta = row.clone();
+      applyRenames(inputRowMeta, definition.getValueRenames());
+      return inputRowMeta;
+    }
+
+    int infoIndex =
+        infoTransforms == null
+            ? -1
+            : Const.indexOfString(definition.getInputTransformName(), infoTransforms);
+    if (infoIndex < 0) {
+      throw new HopTransformException(
+          BaseMessages.getString(
+              PKG,
+              "MultiMappingMeta.Exception.UnableToFindMetadataInfo",
+              definition.getInputTransformName()));
+    }
+    if (info != null && infoIndex < info.length && info[infoIndex] != null) {
+      IRowMeta inputRowMeta = info[infoIndex].clone();
+      applyRenames(inputRowMeta, definition.getValueRenames());
+      return inputRowMeta;
+    }
+    return null;
+  }
+
+  private static void applyRenames(IRowMeta inputRowMeta, List<MappingValueRename> valueRenames)
+      throws HopTransformException {
+    if (inputRowMeta == null || inputRowMeta.isEmpty() || valueRenames == null) {
+      return;
+    }
+    for (MappingValueRename valueRename : valueRenames) {
+      IValueMeta valueMeta = inputRowMeta.searchValueMeta(valueRename.getSourceValueName());
+      if (valueMeta == null) {
+        throw new HopTransformException(
+            BaseMessages.getString(
+                PKG,
+                "MultiMappingMeta.Exception.UnableToFindField",
+                valueRename.getSourceValueName()));
+      }
+      valueMeta.setName(valueRename.getTargetValueName());
+    }
+  }
+
+  MultiMappingOutputDefinition findOutputDefinition(TransformMeta nextTransform) {
+    if (nextTransform != null) {
+      for (MultiMappingOutputDefinition definition : ioMappings.getOutputMappings()) {
+        if (nextTransform.getName().equals(definition.getOutputTransformName())) {
+          return definition;
+        }
+      }
+    }
+    MultiMappingOutputDefinition main = null;
+    for (MultiMappingOutputDefinition definition : ioMappings.getOutputMappings()) {
+      if (definition.isMainDataPath() || Utils.isEmpty(definition.getOutputTransformName())) {
+        main = definition;
+      }
+    }
+    return main;
+  }
+
+  static void addInputRenames(
+      List<MappingValueRename> renameList, List<MappingValueRename> addRenameList) {
+    if (addRenameList == null) {
+      return;
+    }
+    for (MappingValueRename rename : addRenameList) {
+      if (renameList.indexOf(rename) < 0) {
+        renameList.add(rename);
+      }
+    }
+  }
+
+  public String[] getInfoTransforms() {
+    List<String> names = new ArrayList<>();
+    for (MultiMappingInputDefinition definition : ioMappings.getInputMappings()) {
+      if (MappingTransforms.isInfoMapping(definition.toIoDefinition())) {
+        names.add(definition.getInputTransformName());
+      }
+    }
+    return names.isEmpty() ? null : names.toArray(String[]::new);
+  }
+
+  public String[] getTargetTransforms() {
+    List<String> names = new ArrayList<>();
+    for (MultiMappingOutputDefinition definition : ioMappings.getOutputMappings()) {
+      if (MappingTransforms.isTargetMapping(definition.toIoDefinition())) {
+        names.add(definition.getOutputTransformName());
+      }
+    }
+    return names.isEmpty() ? null : names.toArray(String[]::new);
+  }
+
+  @Override
+  public void check(
+      List<ICheckResult> remarks,
+      PipelineMeta pipelineMeta,
+      TransformMeta transformMeta,
+      IRowMeta prev,
+      String[] input,
+      String[] output,
+      IRowMeta info,
+      IVariables variables,
+      IHopMetadataProvider metadataProvider) {
+    if (StringUtils.isEmpty(filename)) {
+      remarks.add(
+          new CheckResult(
+              ICheckResult.TYPE_RESULT_ERROR,
+              BaseMessages.getString(PKG, "MultiMappingMeta.CheckResult.NoMappingSpecified"),
+              transformMeta));
+      return;
+    }
+
+    remarks.add(
+        new CheckResult(
+            ICheckResult.TYPE_RESULT_OK,
+            BaseMessages.getString(PKG, "MultiMappingMeta.CheckResult.MappingPipelineSpecified"),
+            transformMeta));
+
+    if (prev == null || prev.isEmpty()) {
+      remarks.add(
+          new CheckResult(
+              ICheckResult.TYPE_RESULT_WARNING,
+              BaseMessages.getString(PKG, "MultiMappingMeta.CheckResult.NotReceivingAnyFields"),
+              transformMeta));
+    } else {
+      remarks.add(
+          new CheckResult(
+              ICheckResult.TYPE_RESULT_OK,
+              BaseMessages.getString(
+                  PKG, "MultiMappingMeta.CheckResult.TransformReceivingFields", prev.size() + ""),
+              transformMeta));
+    }
+
+    if (input.length > 0) {
+      remarks.add(
+          new CheckResult(
+              ICheckResult.TYPE_RESULT_OK,
+              BaseMessages.getString(
+                  PKG, "MultiMappingMeta.CheckResult.TransformReceivingFieldsFromOtherTransforms"),
+              transformMeta));
+    } else {
+      remarks.add(
+          new CheckResult(
+              ICheckResult.TYPE_RESULT_WARNING,
+              BaseMessages.getString(PKG, "MultiMappingMeta.CheckResult.NoInputReceived"),
+              transformMeta));
+    }
+
+    try {
+      PipelineMeta mappingPipelineMeta = loadMappingMeta(this, metadataProvider, variables);
+      for (MultiMappingInputDefinition definition : ioMappings.getInputMappings()) {
+        if (StringUtils.isNotEmpty(definition.getInputTransformName())
+            && pipelineMeta.findTransform(definition.getInputTransformName()) == null) {
+          remarks.add(
+              new CheckResult(
+                  ICheckResult.TYPE_RESULT_ERROR,
+                  BaseMessages.getString(
+                      PKG,
+                      "MultiMappingMeta.CheckResult.ParentTransformNotFound",
+                      definition.getInputTransformName()),
+                  transformMeta));
+        }
+        if (MappingTransforms.isInfoMapping(definition.toIoDefinition())
+            && Utils.isEmpty(definition.getInputTransformName())) {
+          remarks.add(
+              new CheckResult(
+                  ICheckResult.TYPE_RESULT_WARNING,
+                  BaseMessages.getString(
+                      PKG, "MultiMappingMeta.CheckResult.InfoMappingMissingSource"),
+                  transformMeta));
+        }
+        SimpleMappingMeta.findMappingInputTransform(
+            mappingPipelineMeta, definition.getOutputTransformName());
+      }
+      for (MultiMappingOutputDefinition definition : ioMappings.getOutputMappings()) {
+        if (StringUtils.isNotEmpty(definition.getOutputTransformName())
+            && pipelineMeta.findTransform(definition.getOutputTransformName()) == null) {
+          remarks.add(
+              new CheckResult(
+                  ICheckResult.TYPE_RESULT_ERROR,
+                  BaseMessages.getString(
+                      PKG,
+                      "MultiMappingMeta.CheckResult.ParentTransformNotFound",
+                      definition.getOutputTransformName()),
+                  transformMeta));
+        }
+        SimpleMappingMeta.findMappingOutputTransform(
+            mappingPipelineMeta, definition.getInputTransformName());
+      }
+    } catch (HopException e) {
+      remarks.add(
+          new CheckResult(
+              ICheckResult.TYPE_RESULT_ERROR,
+              BaseMessages.getString(
+                      PKG, "MultiMappingMeta.CheckResult.UnableToLoadMappingPipeline")
+                  + " : "
+                  + e.getMessage(),
+              transformMeta));
+    }
+  }
+
+  @Override
+  public List<ResourceReference> getResourceDependencies(
+      IVariables variables, TransformMeta transformMeta) {
+    List<ResourceReference> references = new ArrayList<>(5);
+    String realFilename = variables.resolve(filename);
+    ResourceReference reference = new ResourceReference(transformMeta);
+    references.add(reference);
+    if (StringUtils.isNotEmpty(realFilename)) {
+      reference.getEntries().add(new ResourceEntry(realFilename, ResourceType.ACTIONFILE));
+    }
+    return references;
+  }
+
+  @Override
+  public ITransformIOMeta getTransformIOMeta() {
+    ITransformIOMeta ioMeta = super.getTransformIOMeta(false);
+    if (ioMeta == null) {
+      ioMeta = new TransformIOMeta(true, true, true, false, true, true);
+      for (MultiMappingInputDefinition definition : ioMappings.getInputMappings()) {
+        MappingIODefinition ioDefinition = definition.toIoDefinition();
+        if (MappingTransforms.isInfoMapping(ioDefinition)) {
+          ioMeta.addStream(
+              new Stream(
+                  StreamType.INFO,
+                  definition.getInputTransform(),
+                  BaseMessages.getString(PKG, "MultiMappingMeta.InfoStream.Description"),
+                  StreamIcon.INFO,
+                  definition.getInputTransformName()));
+        }
+      }
+      for (MultiMappingOutputDefinition definition : ioMappings.getOutputMappings()) {
+        MappingIODefinition ioDefinition = definition.toIoDefinition();
+        if (MappingTransforms.isTargetMapping(ioDefinition)) {
+          ioMeta.addStream(
+              new Stream(
+                  StreamType.TARGET,
+                  null,
+                  BaseMessages.getString(
+                      PKG,
+                      "MultiMappingMeta.TargetStream.Description",
+                      Const.NVL(definition.getOutputTransformName(), "")),
+                  StreamIcon.TARGET,
+                  definition.getOutputTransformName()));
+        }
+      }
+      setTransformIOMeta(ioMeta);
+    }
+    return ioMeta;
+  }
+
+  @Override
+  public void searchInfoAndTargetTransforms(List<TransformMeta> transforms) {
+    for (MultiMappingInputDefinition definition : ioMappings.getInputMappings()) {
+      if (MappingTransforms.isInfoMapping(definition.toIoDefinition())) {
+        definition.setInputTransform(
+            TransformMeta.findTransform(transforms, definition.getInputTransformName()));
+      }
+    }
+    List<IStream> infoStreams = getTransformIOMeta().getInfoStreams();
+    int infoIndex = 0;
+    for (MultiMappingInputDefinition definition : ioMappings.getInputMappings()) {
+      if (MappingTransforms.isInfoMapping(definition.toIoDefinition())
+          && infoIndex < infoStreams.size()) {
+        infoStreams.get(infoIndex++).setTransformMeta(definition.getInputTransform());
+      }
+    }
+    List<IStream> targetStreams = getTransformIOMeta().getTargetStreams();
+    int targetIndex = 0;
+    for (MultiMappingOutputDefinition definition : ioMappings.getOutputMappings()) {
+      if (MappingTransforms.isTargetMapping(definition.toIoDefinition())
+          && targetIndex < targetStreams.size()) {
+        targetStreams
+            .get(targetIndex++)
+            .setTransformMeta(
+                TransformMeta.findTransform(transforms, definition.getOutputTransformName()));
+      }
+    }
+  }
+
+  @Override
+  public List<IStream> getOptionalStreams() {
+    List<IStream> list = new ArrayList<>();
+    list.add(NEW_INFO_STREAM);
+    list.add(NEW_TARGET_STREAM);
+    return list;
+  }
+
+  @Override
+  public void handleStreamSelection(IStream stream) {
+    if (stream == NEW_INFO_STREAM) {
+      MultiMappingInputDefinition definition = new MultiMappingInputDefinition();
+      definition.setMainDataPath(false);
+      if (stream.getTransformMeta() != null) {
+        definition.setInputTransformName(stream.getTransformMeta().getName());
+        definition.setInputTransform(stream.getTransformMeta());
+      }
+      ioMappings.getInputMappings().add(definition);
+      resetTransformIoMeta();
+    } else if (stream == NEW_TARGET_STREAM) {
+      MultiMappingOutputDefinition definition = new MultiMappingOutputDefinition();
+      definition.setMainDataPath(false);
+      if (stream.getTransformMeta() != null) {
+        definition.setOutputTransformName(stream.getTransformMeta().getName());
+      }
+      ioMappings.getOutputMappings().add(definition);
+      resetTransformIoMeta();
+    }
+  }
+
+  @Override
+  public boolean cleanAfterHopFromRemove(TransformMeta toTransform) {
+    if (toTransform == null) {
+      return false;
+    }
+    boolean changed = false;
+    String toName = toTransform.getName();
+    for (MultiMappingOutputDefinition definition : ioMappings.getOutputMappings()) {
+      if (toName.equals(definition.getOutputTransformName())) {
+        definition.setOutputTransformName(null);
+        changed = true;
+      }
+    }
+    if (changed) {
+      resetTransformIoMeta();
+    }
+    return changed;
+  }
+
+  @Override
+  public boolean cleanAfterHopToRemove(TransformMeta fromTransform) {
+    if (fromTransform == null) {
+      return false;
+    }
+    boolean changed = false;
+    String fromName = fromTransform.getName();
+    for (MultiMappingInputDefinition definition : ioMappings.getInputMappings()) {
+      if (fromName.equals(definition.getInputTransformName())) {
+        definition.setInputTransformName(null);
+        definition.setInputTransform(null);
+        changed = true;
+      }
+    }
+    if (changed) {
+      resetTransformIoMeta();
+    }
+    return changed;
+  }
+
+  @Override
+  public boolean excludeFromRowLayoutVerification() {
+    return true;
+  }
+
+  @Override
+  public boolean excludeFromCopyDistributeVerification() {
+    return true;
+  }
+
+  @Override
+  public PipelineType[] getSupportedPipelineTypes() {
+    return new PipelineType[] {PipelineType.Normal};
+  }
+
+  @Override
+  public String[] getReferencedObjectDescriptions() {
+    return new String[] {
+      BaseMessages.getString(PKG, "MultiMappingMeta.ReferencedObject.Description"),
+    };
+  }
+
+  private boolean isMappingDefined() {
+    return StringUtils.isNotEmpty(filename);
+  }
+
+  @Override
+  public boolean[] isReferencedObjectEnabled() {
+    return new boolean[] {isMappingDefined()};
+  }
+
+  @Override
+  public IHasFilename loadReferencedObject(
+      int index, IHopMetadataProvider metadataProvider, IVariables variables) throws HopException {
+    return loadMappingMeta(this, metadataProvider, variables);
+  }
+
+  @Override
+  public boolean supportsDrillDown() {
+    return true;
+  }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingOutputDefinition.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingOutputDefinition.java
new file mode 100644
index 0000000000..21e6a53d6b
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingOutputDefinition.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.multimapping;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.pipeline.transforms.mapping.MappingIODefinition;
+import org.apache.hop.pipeline.transforms.mapping.MappingValueRename;
+
+/** Child Mapping Output → parent target. */
+@Getter
+@Setter
+public class MultiMappingOutputDefinition implements Cloneable {
+
+  @HopMetadataProperty(
+      key = "input_transform",
+      injectionKey = "OUTPUT_MAPPING_TRANSFORM",
+      injectionKeyDescription = "MultiMappingMeta.Injection.OUTPUT_MAPPING_TRANSFORM")
+  private String inputTransformName;
+
+  @HopMetadataProperty(
+      key = "output_transform",
+      injectionKey = "OUTPUT_TARGET_TRANSFORM",
+      injectionKeyDescription = "MultiMappingMeta.Injection.OUTPUT_TARGET_TRANSFORM")
+  private String outputTransformName;
+
+  @HopMetadataProperty(
+      key = "description",
+      injectionKey = "OUTPUT_DESCRIPTION",
+      injectionKeyDescription = "MultiMappingMeta.Injection.OUTPUT_DESCRIPTION")
+  private String description;
+
+  @HopMetadataProperty(
+      key = "connector",
+      injectionGroupKey = "OUTPUT_RENAMES",
+      injectionGroupDescription = "MultiMappingMeta.Injection.OUTPUT_RENAMES")
+  private List<MultiMappingOutputRename> valueRenames;
+
+  @HopMetadataProperty(
+      key = "main_path",
+      injectionKey = "OUTPUT_MAIN_PATH",
+      injectionKeyDescription = "MultiMappingMeta.Injection.OUTPUT_MAIN_PATH")
+  private boolean mainDataPath;
+
+  @HopMetadataProperty(
+      key = "rename_on_output",
+      injectionKey = "OUTPUT_RENAME_ON_OUTPUT",
+      injectionKeyDescription = "MultiMappingMeta.Injection.OUTPUT_RENAME_ON_OUTPUT")
+  private boolean renamingOnOutput;
+
+  public MultiMappingOutputDefinition() {
+    this.valueRenames = new ArrayList<>();
+  }
+
+  public MultiMappingOutputDefinition(String inputTransformName, String outputTransformName) {
+    this();
+    this.inputTransformName = inputTransformName;
+    this.outputTransformName = outputTransformName;
+  }
+
+  public MultiMappingOutputDefinition(MultiMappingOutputDefinition d) {
+    this();
+    copyFrom(d);
+  }
+
+  public void copyFrom(MultiMappingOutputDefinition d) {
+    this.inputTransformName = d.inputTransformName;
+    this.outputTransformName = d.outputTransformName;
+    this.description = d.description;
+    this.mainDataPath = d.mainDataPath;
+    this.renamingOnOutput = d.renamingOnOutput;
+    this.valueRenames.clear();
+    for (MultiMappingOutputRename rename : d.valueRenames) {
+      this.valueRenames.add(new MultiMappingOutputRename(rename));
+    }
+  }
+
+  @Override
+  public MultiMappingOutputDefinition clone() {
+    return new MultiMappingOutputDefinition(this);
+  }
+
+  public MappingIODefinition toIoDefinition() {
+    MappingIODefinition definition =
+        new MappingIODefinition(inputTransformName, outputTransformName);
+    definition.setDescription(description);
+    definition.setMainDataPath(mainDataPath);
+    definition.setRenamingOnOutput(renamingOnOutput);
+    List<MappingValueRename> renames = new ArrayList<>();
+    for (MultiMappingOutputRename rename : valueRenames) {
+      renames.add(rename.toValueRename());
+    }
+    definition.setValueRenames(renames);
+    return definition;
+  }
+
+  public static MultiMappingOutputDefinition fromIoDefinition(MappingIODefinition d) {
+    MultiMappingOutputDefinition definition = new MultiMappingOutputDefinition();
+    if (d == null) {
+      return definition;
+    }
+    definition.inputTransformName = d.getInputTransformName();
+    definition.outputTransformName = d.getOutputTransformName();
+    definition.description = d.getDescription();
+    definition.mainDataPath = d.isMainDataPath();
+    definition.renamingOnOutput = d.isRenamingOnOutput();
+    if (d.getValueRenames() != null) {
+      for (MappingValueRename rename : d.getValueRenames()) {
+        definition.valueRenames.add(MultiMappingOutputRename.fromValueRename(rename));
+      }
+    }
+    return definition;
+  }
+}
diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingOutputRename.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingOutputRename.java
new file mode 100644
index 0000000000..a23b587cc1
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingOutputRename.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.multimapping;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.pipeline.transforms.mapping.MappingValueRename;
+
+@Getter
+@Setter
+public class MultiMappingOutputRename implements Cloneable {
+
+  @HopMetadataProperty(
+      key = "parent",
+      injectionKey = "OUTPUT_RENAME_SOURCE",
+      injectionKeyDescription = "MultiMappingMeta.Injection.OUTPUT_RENAME_SOURCE")
+  private String sourceValueName;
+
+  @HopMetadataProperty(
+      key = "child",
+      injectionKey = "OUTPUT_RENAME_TARGET",
+      injectionKeyDescription = "MultiMappingMeta.Injection.OUTPUT_RENAME_TARGET")
+  private String targetValueName;
+
+  public MultiMappingOutputRename() {}
+
+  public MultiMappingOutputRename(String sourceValueName, String targetValueName) {
+    this.sourceValueName = sourceValueName == null ? "" : sourceValueName;
+    this.targetValueName = targetValueName == null ? "" : targetValueName;
+  }
+
+  public MultiMappingOutputRename(MultiMappingOutputRename rename) {
+    this.sourceValueName = rename.sourceValueName;
+    this.targetValueName = rename.targetValueName;
+  }
+
+  @Override
+  public MultiMappingOutputRename clone() {
+    return new MultiMappingOutputRename(this);
+  }
+
+  public MappingValueRename toValueRename() {
+    return new MappingValueRename(sourceValueName, targetValueName);
+  }
+
+  public static MultiMappingOutputRename fromValueRename(MappingValueRename rename) {
+    if (rename == null) {
+      return new MultiMappingOutputRename();
+    }
+    return new MultiMappingOutputRename(rename.getSourceValueName(), rename.getTargetValueName());
+  }
+}
diff --git a/plugins/transforms/mapping/src/main/resources/MMAP.svg b/plugins/transforms/mapping/src/main/resources/MMAP.svg
new file mode 100644
index 0000000000..a986b31a31
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/resources/MMAP.svg
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
+     width="42px" height="42px" viewBox="0 0 42 42">
+    <g>
+        <rect x="14.753" y="8.232" fill="#0E3A5A" width="2.523" height="1.699"/>
+        <rect x="18.276" y="8.232" fill="#0E3A5A" width="2.524" height="1.699"/>
+        <rect x="28.85" y="8.232" fill="#0E3A5A" width="2.525" height="1.699"/>
+        <rect x="32.41" y="24.557" fill="#0E3A5A" width="1.699" height="2.396"/>
+        <rect x="25.325" y="8.232" fill="#0E3A5A" width="2.524" height="1.699"/>
+        <rect x="21.801" y="8.232" fill="#0E3A5A" width="2.524" height="1.699"/>
+        <rect x="32.41" y="17.767" fill="#0E3A5A" width="1.699" height="2.396"/>
+        <rect x="32.41" y="21.162" fill="#0E3A5A" width="1.699" height="2.395"/>
+        <rect x="32.41" y="10.977" fill="#0E3A5A" width="1.699" height="2.395"/>
+        <rect x="32.41" y="14.371" fill="#0E3A5A" width="1.699" height="2.396"/>
+        <polygon fill="#0E3A5A"
+                 points="32.41,9.977 34.109,9.977 34.109,8.232 32.375,8.232 32.375,9.932 32.41,9.932"/>
+        <rect x="7.167" y="7.296" fill="#FFFFFF" width="3.666" height="3.666"/>
+        <rect x="7.167" y="31.037" fill="#FFFFFF" width="3.666" height="3.667"/>
+        <rect x="31.167" y="31.037" fill="#FFFFFF" width="3.666" height="3.667"/>
+        <rect x="31.167" y="7.296" fill="#FFFFFF" width="3.666" height="3.666"/>
+        <path fill="#0E3A5A" d="M34.109,29.338v-1.386H32.41v1.386h-2.942v3.172H12.532v-3.172h-2.7V12.661h2.7V9.932h1.221V8.232h-1.221
+		V5.597H5.468v7.064h2.665v16.677H5.468v7.065h7.064v-2.194h16.936v2.194h7.064v-7.065H34.109z M7.167,10.962V7.296h3.666v3.666
+		H7.167z M10.833,34.704H7.167v-3.667h3.666V34.704z M34.833,34.704h-3.666v-3.667h3.666V34.704z M34.833,10.962h-3.666V7.296h3.666
+		V10.962z"/>
+    </g>
+</svg>
diff --git a/plugins/transforms/mapping/src/main/resources/org/apache/hop/pipeline/transforms/multimapping/messages/messages_en_US.properties b/plugins/transforms/mapping/src/main/resources/org/apache/hop/pipeline/transforms/multimapping/messages/messages_en_US.properties
new file mode 100644
index 0000000000..14b36b7420
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/resources/org/apache/hop/pipeline/transforms/multimapping/messages/messages_en_US.properties
@@ -0,0 +1,112 @@
+#
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+MultiMapping.Name=Multi Mapping
+MultiMapping.Description=Re-uses a child pipeline with zero or more inputs and outputs, including info and target streams
+MultiMappingMeta.keyword=mapping,multi,subpipeline,reusable,info,target
+MultiMappingMeta.ReferencedObject.Description=Mapping (sub-pipeline)
+MultiMappingMeta.InfoStream.Description=These rows are used as an info stream for the mapping
+MultiMappingMeta.InfoStream.New.Description=New info input
+MultiMappingMeta.TargetStream.Description=Target output: {0}
+MultiMappingMeta.TargetStream.New.Description=New target output
+MultiMappingMeta.Exception.UnableToLoadMappingPipeline=Unable to load the mapping pipeline
+MultiMappingMeta.Exception.UnableToFindField=Unable to find field : {0}
+MultiMappingMeta.Exception.UnableToFindMetadataInfo=Unable to find transform ''{0}'' to determine the row metadata for the mapping input.
+MultiMappingMeta.CheckResult.NoMappingSpecified=There is no mapping specified\!
+MultiMappingMeta.CheckResult.MappingPipelineSpecified=A mapping (pipeline) is specified.
+MultiMappingMeta.CheckResult.NotReceivingAnyFields=Not receiving any fields from previous transforms.
+MultiMappingMeta.CheckResult.TransformReceivingFields=Transform is connected to previous one, receiving {0} fields
+MultiMappingMeta.CheckResult.TransformReceivingFieldsFromOtherTransforms=Transform is receiving info from other transforms.
+MultiMappingMeta.CheckResult.NoInputReceived=No input received from other transforms (allowed for generator mappings).
+MultiMappingMeta.CheckResult.ParentTransformNotFound=Parent transform ''{0}'' could not be found.
+MultiMappingMeta.CheckResult.InfoMappingMissingSource=An info mapping has no source transform name.
+MultiMappingMeta.CheckResult.UnableToLoadMappingPipeline=Unable to load the mapping pipeline
+MultiMappingMeta.Injection.RUN_CONFIGURATION=Pipeline run configuration (local only)
+MultiMappingMeta.Injection.INPUT_MAPPINGS=Input mappings
+MultiMappingMeta.Injection.OUTPUT_MAPPINGS=Output mappings
+MultiMappingMeta.Injection.INPUT_SOURCE_TRANSFORM=Parent source transform for an input mapping
+MultiMappingMeta.Injection.INPUT_MAPPING_TRANSFORM=Child Mapping Input transform
+MultiMappingMeta.Injection.INPUT_DESCRIPTION=Input mapping description
+MultiMappingMeta.Injection.INPUT_MAIN_PATH=Input mapping is the main data path
+MultiMappingMeta.Injection.INPUT_RENAME_ON_OUTPUT=Rename input fields back on output
+MultiMappingMeta.Injection.INPUT_RENAMES=Input field renames
+MultiMappingMeta.Injection.INPUT_RENAME_SOURCE=Input rename source field
+MultiMappingMeta.Injection.INPUT_RENAME_TARGET=Input rename target field
+MultiMappingMeta.Injection.OUTPUT_MAPPING_TRANSFORM=Child Mapping Output transform
+MultiMappingMeta.Injection.OUTPUT_TARGET_TRANSFORM=Parent target transform for an output mapping
+MultiMappingMeta.Injection.OUTPUT_DESCRIPTION=Output mapping description
+MultiMappingMeta.Injection.OUTPUT_MAIN_PATH=Output mapping is the main data path
+MultiMappingMeta.Injection.OUTPUT_RENAME_ON_OUTPUT=Output rename on output flag
+MultiMappingMeta.Injection.OUTPUT_RENAMES=Output field renames
+MultiMappingMeta.Injection.OUTPUT_RENAME_SOURCE=Output rename source field
+MultiMappingMeta.Injection.OUTPUT_RENAME_TARGET=Output rename target field
+MultiMapping.Exception.UnableToPrepareExecutionOfMapping=Unable to prepare (allocate, initialize) the execution of the mapping (sub-pipeline)
+MultiMapping.Exception.MappingInputNotFound=Unable to find Mapping Input transform ''{0}'' in the mapping pipeline
+MultiMapping.Exception.MappingOutputNotFound=Unable to find Mapping Output transform ''{0}'' in the mapping pipeline
+MultiMapping.Exception.InfoRowSetNotFound=Unable to find the info row set from transform ''{0}''
+MultiMapping.Exception.InputRowSetNotFound=Unable to find the input row set from transform ''{0}''
+MultiMapping.Log.ErrorOccurredInSubPipeline=An error occurred in the sub-pipeline, halting processing
+MultiMappingDialog.Shell.Title=Multi Mapping
+MultiMappingDialog.Pipeline.Label=Pipeline
+MultiMappingDialog.Browse.Label=Browse...
+MultiMappingDialog.New.Label=New
+MultiMappingDialog.Edit.Label=Edit
+MultiMappingDialog.RunConfig.Label=Run configuration (local only)
+MultiMappingDialog.Parameters.Title=Parameters
+MultiMappingDialog.Parameters.Tooltip=You can specify the parameters (variables) to set for this mapping (sub-pipeline)
+MultiMappingDialog.Parameters.column.Variable=Variable name
+MultiMappingDialog.Parameters.column.ValueOrField=String value (can include variable expressions like '${VARIABLE}')
+MultiMappingDialog.Parameters.InheritAll=Inherit all variables from the parent pipeline
+MultiMappingDialog.InputTab.Title=Input
+MultiMappingDialog.InputTab.Tooltip=Defines an input stream for this mapping
+MultiMappingDialog.OutputTab.Title=Output
+MultiMappingDialog.OutputTab.Tooltip=Defines an output stream for this mapping
+MultiMappingDialog.button.AddInput=Add Input
+MultiMappingDialog.button.AddOutput=Add Output
+MultiMappingDialog.button.EnterMapping=Mapping...
+MultiMappingDialog.button.GetFields=Get Fields
+MultiMappingDialog.InputTab.label.InputSourceTransformName=Input source transform name
+MultiMappingDialog.InputTab.label.OutputTargetTransformName=Mapping Input transform name
+MultiMappingDialog.InputTab.label.Description=Description
+MultiMappingDialog.InputTab.column.SourceField=Fieldname from source transform
+MultiMappingDialog.InputTab.column.TargetField=Fieldname to mapping input transform
+MultiMappingDialog.OutputTab.label.InputSourceTransformName=Mapping Output transform name
+MultiMappingDialog.OutputTab.label.OutputTargetTransformName=Output target transform name
+MultiMappingDialog.OutputTab.label.Description=Description
+MultiMappingDialog.OutputTab.column.SourceField=Fieldname from mapping transform
+MultiMappingDialog.OutputTab.column.TargetField=Fieldname to target transform
+MultiMappingDialog.input.MainDataPath=Is this the main data path?
+MultiMappingDialog.input.RenamingOnOutput=Update mapped field names downstream
+MultiMappingDialog.CloseDefinitionTabAreYouSure.Title=Delete mapping definition
+MultiMappingDialog.CloseDefinitionTabAreYouSure.Message=Are you sure you want to delete this mapping definition?
+MultiMappingDialog.FilenameMissing.Header=Missing filename
+MultiMappingDialog.FilenameMissing.Message=Please specify a mapping pipeline filename.
+MultiMappingDialog.SelfReference.Header=Self reference
+MultiMappingDialog.SelfReference.Message=The mapping pipeline cannot be the same as the parent pipeline.
+MultiMappingDialog.ErrorLoadingPipeline.DialogTitle=Error loading pipeline
+MultiMappingDialog.ErrorLoadingPipeline.DialogMessage=There was an error loading the pipeline from file:
+MultiMappingDialog.ErrorLoadingSpecifiedPipeline.Title=Error
+MultiMappingDialog.ErrorLoadingSpecifiedPipeline.Message=There was an error during the loading and verification of the selected pipeline.
+MultiMappingDialog.Exception.NoMappingSpecified=No mapping (sub-pipeline) was specified or it was not a valid one.
+MultiMappingDialog.Exception.ErrorGettingMappingSourceAndTargetFields=There was an error getting the source and target fields : {0}
+MultiMappingDialog.SelectPipelineTransform.Title=Select a transform
+MultiMappingDialog.SelectPipelineTransform.Message=Select a transform from the parent pipeline:
+MultiMappingDialog.SelectMappingTransform.Title=Select a mapping transform
+MultiMappingDialog.SelectMappingTransform.Message=Select a transform from the mapping sub-pipeline:
+MultiMappingDialog.ErrorGettingPreviousFields.DialogTitle=Error getting previous fields
+MultiMappingDialog.ErrorGettingPreviousFields.DialogMessage=There was an error getting the fields entering this transform
diff --git a/plugins/transforms/mapping/src/main/samples/transforms/multi-mapping-child.hpl b/plugins/transforms/mapping/src/main/samples/transforms/multi-mapping-child.hpl
new file mode 100644
index 0000000000..51106875a4
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/samples/transforms/multi-mapping-child.hpl
@@ -0,0 +1,165 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+  <info>
+    <name>multi-mapping-child</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    <transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    <transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2020/09/09 16:13:22.075</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2020/09/09 16:13:22.075</modified_date>
+    <key_for_session_key>H4sIAAAAAAAAAAMAAAAAAAAAAAA=</key_for_session_key>
+    <is_key_private>N</is_key_private>
+  </info>
+  <notepads>
+    <notepad>
+      <note>This mapping pipeline accepts a first and last name field and 
+concatenates it with a space in between to output the full name.
+It can be re-used by other pipelines in a generic way.</note>
+      <xloc>128</xloc>
+      <yloc>160</yloc>
+      <width>339</width>
+      <heigth>58</heigth>
+      <fontname>Noto Sans</fontname>
+      <fontsize>11</fontsize>
+      <fontbold>N</fontbold>
+      <fontitalic>N</fontitalic>
+      <fontcolorred>14</fontcolorred>
+      <fontcolorgreen>58</fontcolorgreen>
+      <fontcolorblue>90</fontcolorblue>
+      <backgroundcolorred>201</backgroundcolorred>
+      <backgroundcolorgreen>232</backgroundcolorgreen>
+      <backgroundcolorblue>251</backgroundcolorblue>
+      <bordercolorred>14</bordercolorred>
+      <bordercolorgreen>58</bordercolorgreen>
+      <bordercolorblue>90</bordercolorblue>
+    </notepad>
+  </notepads>
+  <order>
+    <hop>
+      <from>Mapping Input</from>
+      <to>calculate name</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>calculate name</from>
+      <to>Mapping Output</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Mapping Input</name>
+    <type>MappingInput</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <name>firstName</name>
+        <type>String</type>
+        <length>-1</length>
+        <precision>-1</precision>
+      </field>
+      <field>
+        <name>lastName</name>
+        <type>String</type>
+        <length>-1</length>
+        <precision>-1</precision>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>160</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Mapping Output</name>
+    <type>MappingOutput</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>512</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>calculate name</name>
+    <type>ScriptValueMod</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <optimizationLevel>9</optimizationLevel>
+    <jsScripts>
+      <jsScript>
+        <jsScript_type>0</jsScript_type>
+        <jsScript_name>Script 1</jsScript_name>
+        <jsScript_script>
+var name = firstName + " " + lastName;
+
+</jsScript_script>
+      </jsScript>
+    </jsScripts>
+    <fields>
+      <field>
+        <name>name</name>
+        <rename>name</rename>
+        <type>String</type>
+        <length>-1</length>
+        <precision>-1</precision>
+        <replace>N</replace>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>336</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/plugins/transforms/mapping/src/main/samples/transforms/multi-mapping-parent.hpl b/plugins/transforms/mapping/src/main/samples/transforms/multi-mapping-parent.hpl
new file mode 100644
index 0000000000..8e71c8e509
--- /dev/null
+++ b/plugins/transforms/mapping/src/main/samples/transforms/multi-mapping-parent.hpl
@@ -0,0 +1,218 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+  <info>
+    <name>multi-mapping-parent</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    <transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    <transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2020/09/09 16:18:45.749</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2020/09/09 16:18:45.749</modified_date>
+    <key_for_session_key>H4sIAAAAAAAAAAMAAAAAAAAAAAA=</key_for_session_key>
+    <is_key_private>N</is_key_private>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>Data grid</from>
+      <to>id</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Multi Mapping</from>
+      <to>Results</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>id</from>
+      <to>Multi Mapping</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Data grid</name>
+    <type>DataGrid</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <name>first</name>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+      <field>
+        <name>last</name>
+        <type>String</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+    </fields>
+    <data>
+      <line>
+        <item>Johnny</item>
+        <item>Walker</item>
+      </line>
+      <line>
+        <item>Captain</item>
+        <item>Morgan</item>
+      </line>
+      <line>
+        <item>Jim</item>
+        <item>Bean</item>
+      </line>
+      <line>
+        <item>Evan</item>
+        <item>Williams</item>
+      </line>
+    </data>
+    <attributes/>
+    <GUI>
+      <xloc>160</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Results</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>608</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Multi Mapping</name>
+    <type>MultiMapping</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <filename>${PROJECT_HOME}/transforms/multi-mapping-child.hpl</filename>
+    <mappings>
+      <input>
+        <mapping>
+          <input_transform/>
+          <output_transform/>
+          <main_path>Y</main_path>
+          <rename_on_output>Y</rename_on_output>
+          <description/>
+          <connector>
+            <parent>first</parent>
+            <child>firstName</child>
+          </connector>
+          <connector>
+            <parent>last</parent>
+            <child>lastName</child>
+          </connector>
+        </mapping>
+      </input>
+      <output>
+        <mapping>
+          <input_transform/>
+          <output_transform/>
+          <main_path>Y</main_path>
+          <rename_on_output>N</rename_on_output>
+          <description/>
+          <connector>
+            <parent>name</parent>
+            <child>resultName</child>
+          </connector>
+        </mapping>
+      </output>
+      <parameters>
+        <inherit_all_vars>Y</inherit_all_vars>
+      </parameters>
+    </mappings>
+    <attributes/>
+    <GUI>
+      <xloc>432</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>id</name>
+    <type>Sequence</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <valuename>id</valuename>
+    <use_database>N</use_database>
+    <connection/>
+    <schema/>
+    <seqname>SEQ_</seqname>
+    <use_counter>Y</use_counter>
+    <counter_name/>
+    <start_at>1</start_at>
+    <increment_by>1</increment_by>
+    <max_value>999999999</max_value>
+    <attributes/>
+    <GUI>
+      <xloc>288</xloc>
+      <yloc>96</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/mapping/MappingTransformsTest.java b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/mapping/MappingTransformsTest.java
new file mode 100644
index 0000000000..2f6b396b5f
--- /dev/null
+++ b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/mapping/MappingTransformsTest.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hop.pipeline.transforms.mapping;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class MappingTransformsTest {
+
+  @Test
+  void infoMappingRequiresNonMainAndParentName() {
+    MappingIODefinition definition = new MappingIODefinition("Lookup", "Lookup input");
+    definition.setMainDataPath(false);
+    assertTrue(MappingTransforms.isInfoMapping(definition));
+    definition.setMainDataPath(true);
+    assertFalse(MappingTransforms.isInfoMapping(definition));
+    definition.setMainDataPath(false);
+    definition.setInputTransformName(null);
+    assertFalse(MappingTransforms.isInfoMapping(definition));
+    assertFalse(MappingTransforms.isInfoMapping(null));
+  }
+
+  @Test
+  void targetMappingRequiresNonMainAndParentName() {
+    MappingIODefinition definition = new MappingIODefinition("Case out", "Handle A");
+    definition.setMainDataPath(false);
+    assertTrue(MappingTransforms.isTargetMapping(definition));
+    definition.setMainDataPath(true);
+    assertFalse(MappingTransforms.isTargetMapping(definition));
+    definition.setOutputTransformName(null);
+    definition.setMainDataPath(false);
+    assertFalse(MappingTransforms.isTargetMapping(definition));
+  }
+}
diff --git a/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMetaInjectionTest.java b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMetaInjectionTest.java
new file mode 100644
index 0000000000..eca9c35fa4
--- /dev/null
+++ b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMetaInjectionTest.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hop.pipeline.transforms.multimapping;
+
+import org.apache.hop.core.injection.BaseMetadataInjectionTestJunit5;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class MultiMappingMetaInjectionTest extends BaseMetadataInjectionTestJunit5<MultiMappingMeta> {
+
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new RestoreHopEngineEnvironmentExtension();
+
+  @BeforeEach
+  void setup() throws Exception {
+    MultiMappingMeta meta = new MultiMappingMeta();
+    meta.getInputMappings().add(new MultiMappingInputDefinition());
+    meta.getInputMappings().get(0).getValueRenames().add(new MultiMappingInputRename());
+    meta.getOutputMappings().add(new MultiMappingOutputDefinition());
+    meta.getOutputMappings().get(0).getValueRenames().add(new MultiMappingOutputRename());
+    setup(meta);
+  }
+
+  @Test
+  void test() throws Exception {
+    check("filename", () -> meta.getFilename());
+    check("RUN_CONFIGURATION", () -> meta.getRunConfigurationName());
+    check("INPUT_SOURCE_TRANSFORM", () -> meta.getInputMappings().get(0).getInputTransformName());
+    check("INPUT_MAPPING_TRANSFORM", () -> meta.getInputMappings().get(0).getOutputTransformName());
+    check("INPUT_DESCRIPTION", () -> meta.getInputMappings().get(0).getDescription());
+    check("INPUT_MAIN_PATH", () -> meta.getInputMappings().get(0).isMainDataPath());
+    check("INPUT_RENAME_ON_OUTPUT", () -> meta.getInputMappings().get(0).isRenamingOnOutput());
+    check(
+        "OUTPUT_MAPPING_TRANSFORM", () -> meta.getOutputMappings().get(0).getInputTransformName());
+    check(
+        "OUTPUT_TARGET_TRANSFORM", () -> meta.getOutputMappings().get(0).getOutputTransformName());
+    check("OUTPUT_DESCRIPTION", () -> meta.getOutputMappings().get(0).getDescription());
+    check("OUTPUT_MAIN_PATH", () -> meta.getOutputMappings().get(0).isMainDataPath());
+    check("OUTPUT_RENAME_ON_OUTPUT", () -> meta.getOutputMappings().get(0).isRenamingOnOutput());
+    check("inherit_all_vars", () -> meta.getMappingParameters().isInheritingAllVariables());
+    skipPropertyTest("variable");
+    skipPropertyTest("input");
+    skipPropertyTest("INPUT_RENAME_SOURCE");
+    skipPropertyTest("INPUT_RENAME_TARGET");
+    skipPropertyTest("OUTPUT_RENAME_SOURCE");
+    skipPropertyTest("OUTPUT_RENAME_TARGET");
+    skipPropertyTest("mappings");
+    skipPropertyTest("parameters");
+    skipPropertyTest("mapping");
+    skipPropertyTest("connector");
+  }
+}
diff --git a/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMetaTest.java b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMetaTest.java
new file mode 100644
index 0000000000..e47f28b7b5
--- /dev/null
+++ b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingMetaTest.java
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hop.pipeline.transforms.multimapping;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.ICheckResult;
+import org.apache.hop.core.plugins.PluginRegistry;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
+import org.apache.hop.pipeline.PipelineMeta;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.apache.hop.pipeline.transform.TransformSerializationTestUtil;
+import org.apache.hop.pipeline.transform.stream.IStream;
+import org.apache.hop.pipeline.transform.stream.IStream.StreamType;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class MultiMappingMetaTest {
+
+  @BeforeEach
+  void setUp() throws Exception {
+    HopEnvironment.init();
+    PluginRegistry.init();
+  }
+
+  @Test
+  void testSerialization() throws Exception {
+    MultiMappingMeta meta =
+        TransformSerializationTestUtil.testSerialization(
+            "/multi-mapping-transform.xml", MultiMappingMeta.class);
+    assertEquals("${PROJECT_HOME}/multi-mapping-child.hpl", meta.getFilename());
+    assertEquals("local", meta.getRunConfigurationName());
+    assertEquals(2, meta.getInputMappings().size());
+    assertEquals(2, meta.getOutputMappings().size());
+    assertFalse(meta.getInputMappings().get(0).isMainDataPath());
+    assertEquals("Lookup source", meta.getInputMappings().get(0).getInputTransformName());
+    assertTrue(meta.getInputMappings().get(1).isMainDataPath());
+    assertTrue(meta.getInputMappings().get(1).isRenamingOnOutput());
+    assertEquals("Handle A", meta.getOutputMappings().get(1).getOutputTransformName());
+    assertTrue(meta.getMappingParameters().isInheritingAllVariables());
+    assertEquals(1, meta.getMappingParameters().getVariableMappings().size());
+    assertEquals("PARAM_X", meta.getMappingParameters().getVariableMappings().get(0).getName());
+  }
+
+  @Test
+  void testCloneAndDefault() {
+    MultiMappingMeta meta = new MultiMappingMeta();
+    meta.setDefault();
+    assertEquals(1, meta.getInputMappings().size());
+    assertEquals(1, meta.getOutputMappings().size());
+    assertTrue(meta.getInputMappings().get(0).isMainDataPath());
+    assertTrue(meta.getInputMappings().get(0).isRenamingOnOutput());
+
+    MultiMappingMeta copy = meta.clone();
+    assertEquals(1, copy.getInputMappings().size());
+    copy.getInputMappings().get(0).setInputTransformName("changed");
+    assertTrue(
+        meta.getInputMappings().get(0).getInputTransformName() == null
+            || !meta.getInputMappings().get(0).getInputTransformName().equals("changed"));
+  }
+
+  @Test
+  void testTransformIoMetaExposesInfoAndTargetStreams() {
+    MultiMappingMeta meta = new MultiMappingMeta();
+    meta.setDefault();
+
+    MultiMappingInputDefinition info = new MultiMappingInputDefinition("Lookup", "Lookup input");
+    info.setMainDataPath(false);
+    meta.getInputMappings().add(info);
+
+    MultiMappingOutputDefinition target = new MultiMappingOutputDefinition("Case out", "Handle A");
+    target.setMainDataPath(false);
+    meta.getOutputMappings().add(target);
+    meta.resetTransformIoMeta();
+
+    List<IStream> infoStreams = meta.getTransformIOMeta().getInfoStreams();
+    List<IStream> targetStreams = meta.getTransformIOMeta().getTargetStreams();
+    assertEquals(1, infoStreams.size());
+    assertEquals(StreamType.INFO, infoStreams.get(0).getStreamType());
+    assertEquals(1, targetStreams.size());
+    assertEquals(StreamType.TARGET, targetStreams.get(0).getStreamType());
+    assertNotNull(meta.getInfoTransforms());
+    assertEquals("Lookup", meta.getInfoTransforms()[0]);
+    assertNotNull(meta.getTargetTransforms());
+    assertEquals("Handle A", meta.getTargetTransforms()[0]);
+  }
+
+  @Test
+  void testSearchInfoAndTargetTransforms() {
+    MultiMappingMeta meta = new MultiMappingMeta();
+    MultiMappingInputDefinition info = new MultiMappingInputDefinition("Lookup", "Lookup input");
+    info.setMainDataPath(false);
+    meta.getInputMappings().add(info);
+    MultiMappingOutputDefinition target = new MultiMappingOutputDefinition("Case out", "Handle A");
+    target.setMainDataPath(false);
+    meta.getOutputMappings().add(target);
+    meta.resetTransformIoMeta();
+
+    TransformMeta lookup = new TransformMeta();
+    lookup.setName("Lookup");
+    TransformMeta handleA = new TransformMeta();
+    handleA.setName("Handle A");
+    meta.searchInfoAndTargetTransforms(List.of(lookup, handleA));
+    assertEquals(lookup, info.getInputTransform());
+    assertEquals(lookup, meta.getTransformIOMeta().getInfoStreams().get(0).getTransformMeta());
+    assertEquals(handleA, meta.getTransformIOMeta().getTargetStreams().get(0).getTransformMeta());
+  }
+
+  @Test
+  void testCheckWithoutFilenameIsError() {
+    MultiMappingMeta meta = new MultiMappingMeta();
+    List<ICheckResult> remarks = new ArrayList<>();
+    meta.check(
+        remarks,
+        new PipelineMeta(),
+        new TransformMeta(),
+        new RowMeta(),
+        new String[0],
+        new String[0],
+        new RowMeta(),
+        new Variables(),
+        new MemoryMetadataProvider());
+    assertTrue(remarks.stream().anyMatch(r -> r.getType() == ICheckResult.TYPE_RESULT_ERROR));
+  }
+
+  @Test
+  void testCheckZeroInputIsWarningNotError() {
+    MultiMappingMeta meta = new MultiMappingMeta();
+    meta.setFilename("dummy.hpl");
+    List<ICheckResult> remarks = new ArrayList<>();
+    meta.check(
+        remarks,
+        new PipelineMeta(),
+        new TransformMeta(),
+        new RowMeta(),
+        new String[0],
+        new String[0],
+        new RowMeta(),
+        new Variables(),
+        new MemoryMetadataProvider());
+    assertTrue(
+        remarks.stream()
+            .anyMatch(
+                r ->
+                    r.getType() == ICheckResult.TYPE_RESULT_WARNING
+                        && r.getText().contains("No input")));
+    assertFalse(
+        remarks.stream()
+            .anyMatch(
+                r ->
+                    r.getType() == ICheckResult.TYPE_RESULT_ERROR
+                        && r.getText().contains("No input")));
+  }
+
+  @Test
+  void testFindOutputDefinitionPrefersNamedTarget() {
+    MultiMappingMeta meta = new MultiMappingMeta();
+    MultiMappingOutputDefinition main = new MultiMappingOutputDefinition("Main out", null);
+    main.setMainDataPath(true);
+    MultiMappingOutputDefinition target = new MultiMappingOutputDefinition("Case out", "Handle A");
+    target.setMainDataPath(false);
+    meta.getOutputMappings().add(main);
+    meta.getOutputMappings().add(target);
+
+    TransformMeta next = new TransformMeta();
+    next.setName("Handle A");
+    assertEquals(target, meta.findOutputDefinition(next));
+    assertEquals(main, meta.findOutputDefinition(null));
+  }
+
+  @Test
+  void testOptionalStreamsAddDefinitions() {
+    MultiMappingMeta meta = new MultiMappingMeta();
+    List<IStream> optional = meta.getOptionalStreams();
+    assertEquals(2, optional.size());
+
+    TransformMeta lookup = new TransformMeta();
+    lookup.setName("Lookup");
+    optional.get(0).setTransformMeta(lookup);
+    meta.handleStreamSelection(optional.get(0));
+    assertEquals(1, meta.getInputMappings().size());
+    assertFalse(meta.getInputMappings().get(0).isMainDataPath());
+    assertEquals("Lookup", meta.getInputMappings().get(0).getInputTransformName());
+
+    TransformMeta handleA = new TransformMeta();
+    handleA.setName("Handle A");
+    optional.get(1).setTransformMeta(handleA);
+    meta.handleStreamSelection(optional.get(1));
+    assertEquals(1, meta.getOutputMappings().size());
+    assertEquals("Handle A", meta.getOutputMappings().get(0).getOutputTransformName());
+  }
+
+  @Test
+  void testCleanAfterHopRemove() {
+    MultiMappingMeta meta = new MultiMappingMeta();
+    MultiMappingInputDefinition info = new MultiMappingInputDefinition("Lookup", "Lookup input");
+    info.setMainDataPath(false);
+    meta.getInputMappings().add(info);
+    MultiMappingOutputDefinition target = new MultiMappingOutputDefinition("Case out", "Handle A");
+    target.setMainDataPath(false);
+    meta.getOutputMappings().add(target);
+
+    TransformMeta lookup = new TransformMeta();
+    lookup.setName("Lookup");
+    assertTrue(meta.cleanAfterHopToRemove(lookup));
+    assertTrue(
+        meta.getInputMappings().get(0).getInputTransformName() == null
+            || meta.getInputMappings().get(0).getInputTransformName().isEmpty());
+
+    TransformMeta handleA = new TransformMeta();
+    handleA.setName("Handle A");
+    assertTrue(meta.cleanAfterHopFromRemove(handleA));
+    assertTrue(
+        meta.getOutputMappings().get(0).getOutputTransformName() == null
+            || meta.getOutputMappings().get(0).getOutputTransformName().isEmpty());
+  }
+}
diff --git a/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingTest.java b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingTest.java
new file mode 100644
index 0000000000..36d28f83e1
--- /dev/null
+++ b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/multimapping/MultiMappingTest.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.hop.pipeline.transforms.multimapping;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.pipeline.engines.local.LocalPipelineEngine;
+import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class MultiMappingTest {
+
+  private TransformMockHelper<MultiMappingMeta, MultiMappingData> transformMockHelper;
+
+  @BeforeEach
+  void setup() {
+    transformMockHelper =
+        new TransformMockHelper<>(
+            "MULTI_MAPPING_TEST", MultiMappingMeta.class, MultiMappingData.class);
+    when(transformMockHelper.logChannelFactory.create(any(), any(ILoggingObject.class)))
+        .thenReturn(transformMockHelper.iLogChannel);
+    when(transformMockHelper.pipeline.isRunning()).thenReturn(true);
+  }
+
+  @AfterEach
+  void tearDown() {
+    transformMockHelper.cleanUp();
+  }
+
+  @Test
+  void disposeDoesNotNpeWhenInitFailed() {
+    MultiMappingData data = new MultiMappingData();
+    MultiMapping mapping =
+        new MultiMapping(
+            transformMockHelper.transformMeta,
+            transformMockHelper.iTransformMeta,
+            data,
+            0,
+            transformMockHelper.pipelineMeta,
+            transformMockHelper.pipeline);
+    assertDoesNotThrow(mapping::dispose);
+  }
+
+  @Test
+  void stopAllIsSafeWhenChildMissing() {
+    MultiMappingData data = new MultiMappingData();
+    MultiMapping mapping =
+        new MultiMapping(
+            transformMockHelper.transformMeta,
+            transformMockHelper.iTransformMeta,
+            data,
+            0,
+            transformMockHelper.pipelineMeta,
+            transformMockHelper.pipeline);
+    assertDoesNotThrow(mapping::stopAll);
+    assertDoesNotThrow(mapping::stopRunning);
+  }
+
+  @Test
+  void initFailsWithoutFilename() {
+    MultiMappingMeta meta = new MultiMappingMeta();
+    MultiMappingData data = new MultiMappingData();
+    when(transformMockHelper.iTransformMeta.getFilename()).thenReturn(null);
+    MultiMapping mapping =
+        new MultiMapping(
+            transformMockHelper.transformMeta,
+            meta,
+            data,
+            0,
+            transformMockHelper.pipelineMeta,
+            transformMockHelper.pipeline);
+    mapping.setMetadataProvider(transformMockHelper.pipeline.getMetadataProvider());
+    assertFalse(mapping.init());
+  }
+
+  @Test
+  void disposeFlagsErrorWhenChildHadErrors() {
+    MultiMappingData data = new MultiMappingData();
+    data.wasStarted = true;
+    LocalPipelineEngine child = org.mockito.Mockito.mock(LocalPipelineEngine.class);
+    when(child.isFinished()).thenReturn(true);
+    when(child.getErrors()).thenReturn(1);
+    data.mappingPipeline = child;
+    MultiMapping mapping =
+        new MultiMapping(
+            transformMockHelper.transformMeta,
+            transformMockHelper.iTransformMeta,
+            data,
+            0,
+            transformMockHelper.pipelineMeta,
+            transformMockHelper.pipeline);
+    mapping.dispose();
+  }
+}
diff --git a/plugins/transforms/mapping/src/test/resources/multi-mapping-transform.xml b/plugins/transforms/mapping/src/test/resources/multi-mapping-transform.xml
new file mode 100644
index 0000000000..37fd59d759
--- /dev/null
+++ b/plugins/transforms/mapping/src/test/resources/multi-mapping-transform.xml
@@ -0,0 +1,86 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  ~
+  -->
+
+<transform>
+    <name>Multi Mapping</name>
+    <type>MultiMapping</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+        <method>none</method>
+        <schema_name/>
+    </partitioning>
+    <filename>${PROJECT_HOME}/multi-mapping-child.hpl</filename>
+    <runConfiguration>local</runConfiguration>
+    <mappings>
+        <input>
+            <mapping>
+                <input_transform>Lookup source</input_transform>
+                <output_transform>Lookup input</output_transform>
+                <main_path>N</main_path>
+                <rename_on_output>N</rename_on_output>
+                <description>info hop</description>
+                <connector>
+                    <parent>id</parent>
+                    <child>lookup_id</child>
+                </connector>
+            </mapping>
+            <mapping>
+                <input_transform/>
+                <output_transform>Stream input</output_transform>
+                <main_path>Y</main_path>
+                <rename_on_output>Y</rename_on_output>
+                <description>main</description>
+            </mapping>
+        </input>
+        <output>
+            <mapping>
+                <input_transform>Main output</input_transform>
+                <output_transform/>
+                <main_path>Y</main_path>
+                <rename_on_output>N</rename_on_output>
+                <description>main out</description>
+                <connector>
+                    <parent>result</parent>
+                    <child>out</child>
+                </connector>
+            </mapping>
+            <mapping>
+                <input_transform>Case A output</input_transform>
+                <output_transform>Handle A</output_transform>
+                <main_path>N</main_path>
+                <rename_on_output>N</rename_on_output>
+                <description>target</description>
+            </mapping>
+        </output>
+        <parameters>
+            <inherit_all_vars>Y</inherit_all_vars>
+            <variablemapping>
+                <variable>PARAM_X</variable>
+                <input>'${Y}'</input>
+            </variablemapping>
+        </parameters>
+    </mappings>
+    <attributes/>
+    <GUI>
+        <xloc>288</xloc>
+        <yloc>112</yloc>
+    </GUI>
+</transform>