diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@
 
 ```bash
 cabal update
-cabal install --overwrite-policy=always phino-0.0.0.69
+cabal install --overwrite-policy=always phino-0.0.0.70
 phino --version
 ```
 
@@ -342,6 +342,13 @@
   because such bindings cannot be renamed and would produce duplicates
   when spliced at more than one position. When no binding matches the
   sentinel, the output equals the input unchanged.
+* `graft` - same call shape as `splice` (`𝐵-in`, sentinel, `𝐵-rep`) and the
+  same renaming guarantees, but every matched sentinel binding is replaced by
+  the renamed copy of `𝐵-rep` instead of preserved in place. Use it when the
+  marker must not survive the substitution — for example, when fusing two
+  bodies that both produce the same effect and the original marker would fire
+  a second time. When no binding matches the sentinel, the output equals the
+  input unchanged.
 
 ## Meta variables
 
@@ -385,55 +392,55 @@
 === parse/phi ===
   warmup:     3 iterations
   batches:    10 x 1
-  total:      1448938.560 μs
-  avg:        144893.856 μs
-  min:        129456.053 μs
-  max:        178640.839 μs
-  std dev:    16904.402 μs
+  total:      1283128.483 μs
+  avg:        128312.848 μs
+  min:        117090.148 μs
+  max:        159904.869 μs
+  std dev:    16799.275 μs
 === parse/xmir ===
   warmup:     3 iterations
   batches:    10 x 1
-  total:      7588032.720 μs
-  avg:        758803.272 μs
-  min:        700986.244 μs
-  max:        821408.758 μs
-  std dev:    37918.602 μs
+  total:      7638674.160 μs
+  avg:        763867.416 μs
+  min:        696616.654 μs
+  max:        834131.587 μs
+  std dev:    46054.709 μs
 === rewrite/normalize ===
   warmup:     3 iterations
   batches:    10 x 1
-  total:      388634.740 μs
-  avg:        38863.474 μs
-  min:        36132.373 μs
-  max:        42039.094 μs
-  std dev:    1775.255 μs
+  total:      410101.450 μs
+  avg:        41010.145 μs
+  min:        37322.375 μs
+  max:        46790.558 μs
+  std dev:    3315.786 μs
 === print/sweet/multiline ===
   warmup:     3 iterations
   batches:    10 x 1
-  total:      4021506.769 μs
-  avg:        402150.677 μs
-  min:        396372.506 μs
-  max:        409920.442 μs
-  std dev:    5075.454 μs
+  total:      4536347.235 μs
+  avg:        453634.724 μs
+  min:        436266.864 μs
+  max:        484874.805 μs
+  std dev:    14385.579 μs
 === print/sweet/flat ===
   warmup:     3 iterations
   batches:    10 x 1
-  total:      3909169.505 μs
-  avg:        390916.950 μs
-  min:        359212.755 μs
-  max:        408481.102 μs
-  std dev:    17796.878 μs
+  total:      4391468.284 μs
+  avg:        439146.828 μs
+  min:        421949.972 μs
+  max:        461548.132 μs
+  std dev:    12926.007 μs
 === print/salty/multiline ===
   warmup:     3 iterations
   batches:    10 x 1
-  total:      13573961.600 μs
-  avg:        1357396.160 μs
-  min:        1334521.176 μs
-  max:        1370074.545 μs
-  std dev:    12356.510 μs
+  total:      13872760.194 μs
+  avg:        1387276.019 μs
+  min:        1347848.058 μs
+  max:        1435996.600 μs
+  std dev:    28902.131 μs
 ```
 
 The results were calculated in [this GHA job][benchmark-gha]
-on 2026-05-21 at 11:38,
+on 2026-05-24 at 14:39,
 on Linux with 4 CPUs.
 
 <!-- benchmark_end -->
@@ -469,4 +476,4 @@
 [jna]: https://github.com/java-native-access/jna
 [jna-native]: https://github.com/java-native-access/jna/blob/master/src/com/sun/jna/Native.java
 [jeo]: https://github.com/objectionary/jeo-maven-plugin
-[benchmark-gha]: https://github.com/objectionary/phino/actions/runs/26223356318
+[benchmark-gha]: https://github.com/objectionary/phino/actions/runs/26364073830
diff --git a/phino.cabal b/phino.cabal
--- a/phino.cabal
+++ b/phino.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.0
 name: phino
-version: 0.0.0.70
+version: 0.0.0.71
 license: MIT
 synopsis: Command-Line Manipulator of 𝜑-Calculus Expressions
 description: Please see the README on GitHub at <https://github.com/objectionary/phino#readme>
diff --git a/src/Functions.hs b/src/Functions.hs
--- a/src/Functions.hs
+++ b/src/Functions.hs
@@ -45,6 +45,7 @@
 buildTerm' "sum" = _sum
 buildTerm' "join" = _join
 buildTerm' "splice" = _splice
+buildTerm' "graft" = _graft
 buildTerm' func = _unsupported func
 
 argToBytes :: Y.ExtraArgument -> Subst -> IO Bytes
@@ -250,14 +251,20 @@
         _ -> AtLabel "unknown"
 
 _splice :: BuildTermMethod
-_splice [Y.ArgBinding inArg, Y.ArgExpression sentExpr, Y.ArgBinding repArg] subst = do
+_splice = _spliceLike "splice" True
+
+_graft :: BuildTermMethod
+_graft = _spliceLike "graft" False
+
+_spliceLike :: String -> Bool -> BuildTermMethod
+_spliceLike name keepMarker [Y.ArgBinding inArg, Y.ArgExpression sentExpr, Y.ArgBinding repArg] subst = do
   inBds <- buildBindingThrows inArg subst
   repBds <- buildBindingThrows repArg subst
   mapM_ validateRep repBds
   (sentinel, _) <- buildExpressionThrows sentExpr subst
   let avoid = map Y.ArgAttribute (attributesFromBindings (inBds ++ repBds))
-  spliced <- splice inBds sentinel repBds avoid
-  pure (TeBindings spliced)
+  result <- walk inBds sentinel repBds avoid
+  pure (TeBindings result)
   where
     validateRep :: Binding -> IO ()
     validateRep (BiTau (AtLabel _) _) = pure ()
@@ -266,19 +273,20 @@
       throwIO
         ( userError
             ( printf
-                "Function splice() can only rename τ-labelled bindings in the replacement group, but got '%s' which would produce duplicates when spliced more than once"
+                "Function %s() can only rename τ-labelled bindings in the replacement group, but got '%s' which would produce duplicates when applied at more than one position"
+                name
                 (printBinding bd)
             )
         )
-    splice :: [Binding] -> Expression -> [Binding] -> [Y.ExtraArgument] -> IO [Binding]
-    splice [] _ _ _ = pure []
-    splice (bd : rest) sent rep avoid
+    walk :: [Binding] -> Expression -> [Binding] -> [Y.ExtraArgument] -> IO [Binding]
+    walk [] _ _ _ = pure []
+    walk (bd : rest) sent rep avoid
       | matches sent bd = do
           fresh <- traverse (renamed avoid) rep
-          tail' <- splice rest sent rep avoid
-          pure (fresh ++ bd : tail')
+          tail' <- walk rest sent rep avoid
+          pure (fresh ++ (if keepMarker then bd : tail' else tail'))
       | otherwise = do
-          tail' <- splice rest sent rep avoid
+          tail' <- walk rest sent rep avoid
           pure (bd : tail')
     matches :: Expression -> Binding -> Bool
     matches sent (BiTau _ (ExFormation bds)) = any (isPhi sent) bds
@@ -291,14 +299,15 @@
       term <- _randomTau avoid subst
       case term of
         TeAttribute attr -> pure (BiTau attr expr)
-        _ -> throwIO (userError "Failed to generate fresh tau attribute for splice")
+        _ -> throwIO (userError (printf "Failed to generate fresh tau attribute for %s" name))
     renamed avoid (BiVoid (AtLabel _)) = do
       term <- _randomTau avoid subst
       case term of
         TeAttribute attr -> pure (BiVoid attr)
-        _ -> throwIO (userError "Failed to generate fresh tau attribute for splice")
+        _ -> throwIO (userError (printf "Failed to generate fresh tau attribute for %s" name))
     renamed _ bd = pure bd
-_splice _ _ = throwIO (userError "Function splice() requires exactly 3 arguments: input bindings, sentinel expression, replacement bindings")
+_spliceLike name _ _ _ =
+  throwIO (userError (printf "Function %s() requires exactly 3 arguments: input bindings, sentinel expression, replacement bindings" name))
 
 _unsupported :: BuildTermFunc
 _unsupported func _ _ = throwIO (userError (printf "Function %s() is not supported or does not exist" func))
diff --git a/test/FunctionsSpec.hs b/test/FunctionsSpec.hs
--- a/test/FunctionsSpec.hs
+++ b/test/FunctionsSpec.hs
@@ -145,6 +145,108 @@
         ) ::
         IO (Either IOError ())
     outcome `shouldSatisfy` isLeft
+  Test.Hspec.it "grafts replacement in place of every sentinel match" $ do
+    let foo = ExDispatch ExGlobal (AtLabel "foo")
+        bar = ExDispatch ExGlobal (AtLabel "bar")
+        cpsBody =
+          [ phiBinding "x" foo
+          , phiBinding "emit1" emit
+          , phiBinding "y" bar
+          , phiBinding "emit2" emit
+          ]
+        autoBody =
+          [ phiBinding "add1" foo
+          , phiBinding "add2" bar
+          ]
+        subst =
+          Subst
+            ( Map.fromList
+                [ ("B-in", MvBindings cpsBody)
+                , ("B-rep", MvBindings autoBody)
+                ]
+            )
+    TeBindings result <-
+      buildTerm
+        "graft"
+        [ ArgBinding (BiMeta "B-in")
+        , ArgExpression emit
+        , ArgBinding (BiMeta "B-rep")
+        ]
+        subst
+    bds <- uniqueBindings' result
+    logDebug (printf "Grafted bindings:\n%s" (printExpression (ExFormation bds)))
+    map bodyOf bds
+      `shouldBe` [ Just foo
+                 , Just foo
+                 , Just bar
+                 , Just bar
+                 , Just foo
+                 , Just bar
+                 ]
+  Test.Hspec.it "returns the input unchanged when graft finds no sentinel" $ do
+    let foo = ExDispatch ExGlobal (AtLabel "foo")
+        body = [phiBinding "x" foo, phiBinding "y" foo]
+        subst =
+          Subst
+            ( Map.fromList
+                [ ("B-in", MvBindings body)
+                , ("B-rep", MvBindings [phiBinding "z" foo])
+                ]
+            )
+    TeBindings result <-
+      buildTerm
+        "graft"
+        [ ArgBinding (BiMeta "B-in")
+        , ArgExpression emit
+        , ArgBinding (BiMeta "B-rep")
+        ]
+        subst
+    result `shouldBe` body
+  Test.Hspec.it "produces only unique attributes for many graft positions" $ do
+    let foo = ExDispatch ExGlobal (AtLabel "foo")
+        body = [phiBinding (T.pack ('e' : show i)) emit | i <- [1 .. 5 :: Int]]
+        rep = [phiBinding "a" foo, phiBinding "b" foo]
+        subst =
+          Subst
+            ( Map.fromList
+                [ ("B-in", MvBindings body)
+                , ("B-rep", MvBindings rep)
+                ]
+            )
+    TeBindings result <-
+      buildTerm
+        "graft"
+        [ ArgBinding (BiMeta "B-in")
+        , ArgExpression emit
+        , ArgBinding (BiMeta "B-rep")
+        ]
+        subst
+    bds <- uniqueBindings' result
+    bds `shouldSatisfy` ((== 5 * length rep) . length)
+  Test.Hspec.it "fails fast when graft replacement contains a non-label binding" $ do
+    let body = [phiBinding "e1" emit, phiBinding "e2" emit]
+        rep = [phiBinding "a" emit, BiVoid AtRho]
+        subst =
+          Subst
+            ( Map.fromList
+                [ ("B-in", MvBindings body)
+                , ("B-rep", MvBindings rep)
+                ]
+            )
+    outcome <-
+      try
+        ( void
+            ( buildTerm
+                "graft"
+                [ ArgBinding (BiMeta "B-in")
+                , ArgExpression emit
+                , ArgBinding (BiMeta "B-rep")
+                ]
+                subst
+            )
+        ) ::
+        IO (Either IOError ())
+    outcome `shouldSatisfy` isLeft
   where
     isLeft :: Either a b -> Bool
     isLeft (Left _) = True
