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

davsclaus pushed a commit to branch fix/CAMEL-24744
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 4746e544b2a5e425ec222ce47499051679745c76
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Sep 14 21:48:48 2026 +0200

    CAMEL-24744: camel-jbang - the YAML the MCP scaffold tools, the camel init 
templates and the bundled examples generate is in the canonical format, not the 
deprecated compact notation
    
    What camel-jbang generates is the starting point of a user or an AI agent,
    and it was still written in the compact notation that CAMEL-24722/24723
    deprecated, so the first camel run warned about a Camel-generated file.
    
    - camel init templates: yaml.ftl (camel init foo.yaml), 
integration.yaml.ftl,
      kamelet-source.yaml.ftl and kamelet-action.yaml.ftl write the expression
      under expression:, log as {message: ...} and to as {uri: ...}.
    - bundled examples: tui-hello-world, content-based-router and
      message-size/orders rewritten; the stale message-size.camel.yaml (a copy
      of orders.camel.yaml that is not in the catalog) is removed.
    - MCP scaffold tools: camel_openapi_scaffold and camel_ai_pipeline_scaffold
      emit the canonical form. The canonical guard also caught that the AI
      pipeline scaffold put steps: at the route level instead of under from:,
      and the split steps beside split: rather than under it, so the generated
      route never loaded; both fixed.
    
    Guards: TemplateHelperTest validates every YAML template (unwrapping the
    Integration spec.flows and the Kamelet spec.template), ExampleRoutesLoadTest
    validates every bundled example, and the scaffold tool tests validate the
    generated routes, all against the canonical schema.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  10 ++
 .../beginner/tui-hello-world/tui-hello-world.yaml  |  14 +-
 .../content-based-router.camel.yaml                |  10 +-
 .../message-size/message-size.camel.yaml           | 102 ---------------
 .../observability/message-size/orders.camel.yaml   |  40 ++++--
 .../main/resources/templates/integration.yaml.ftl  |   7 +-
 .../resources/templates/kamelet-action.yaml.ftl    |   4 +-
 .../resources/templates/kamelet-source.yaml.ftl    |  11 +-
 .../src/main/resources/templates/yaml.ftl          |   7 +-
 .../jbang/core/commands/DependencyUpdateTest.java  |  16 ++-
 .../jbang/core/common/ExampleRoutesLoadTest.java   |  21 +++
 .../dsl/jbang/core/common/TemplateHelperTest.java  |  50 +++++++
 .../test/resources/dependency-update/route.yaml    |   7 +-
 .../core/commands/mcp/AiPipelineScaffoldTools.java | 145 +++++++++++----------
 .../dsl/jbang/core/commands/mcp/OpenApiTools.java  |  12 +-
 .../commands/mcp/AiPipelineScaffoldToolsTest.java  |  22 ++++
 .../jbang/core/commands/mcp/OpenApiToolsTest.java  |  37 +++++-
 17 files changed, 308 insertions(+), 207 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 2fcbbec5e74c..e973045d1446 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -663,6 +663,16 @@ Running an existing Maven project (`camel run pom.xml`) is 
unchanged, and still
 from the `pom.xml`. The other commands that take `--runtime` (such as `camel 
export`,
 `camel dependency list` and `camel version list`) accept `jbang` as an alias 
for `main`.
 
+The YAML that `camel init` writes (`camel init foo.yaml`, and the Integration 
and Kamelet templates), the
+bundled examples (`camel init --example`) and the routes the 
`camel_ai_pipeline_scaffold` and
+`camel_openapi_scaffold` MCP tools generate are now written in the canonical 
YAML DSL format, with an
+expression under `expression:` and a step as a map of its options
+(`setBody: {expression: {simple: {expression: "..."}}}`, `log: {message: 
"..."}`, `to: {uri: "..."}`).
+The compact notation they used to be written in (`setBody: {simple: "..."}`, 
`log: "..."`) is deprecated:
+`camel run` warns about it, `camel validate yaml --canonical` reports it, and 
`camel validate normalize`
+rewrites a file in the canonical format. Existing files keep working; only the 
generated starting point
+changed. Scripts that post-process the generated YAML by matching the old text 
need to be updated.
+
 === camel-jbang (MCP servers)
 
 The Camel authoring tools for AI agents are now defined once, in 
`camel-jbang-core`, and exposed under the
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/tui-hello-world/tui-hello-world.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/tui-hello-world/tui-hello-world.yaml
index 28db17370b76..b5bce65695db 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/tui-hello-world/tui-hello-world.yaml
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/tui-hello-world/tui-hello-world.yaml
@@ -6,14 +6,20 @@
         repeatCount: 1
       steps:
         - setBody:
-            constant: "Welcome! Open the TUI (camel tui) and press F2 → Send 
Message to say hello."
-        - log: "${body}"
+            expression:
+              constant:
+                expression: "Welcome! Open the TUI (camel tui) and press F2 → 
Send Message to say hello."
+        - log:
+            message: "${body}"
 
 - route:
     id: greet
     from:
       uri: direct:greet
       steps:
-        - log: "Received: ${body}"
+        - log:
+            message: "Received: ${body}"
         - setBody:
-            simple: "Hello ${body}!"
+            expression:
+              simple:
+                expression: "Hello ${body}!"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/content-based-router/content-based-router.camel.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/content-based-router/content-based-router.camel.yaml
index 698c2b1c39dc..d451fcfd93bf 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/content-based-router/content-based-router.camel.yaml
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/content-based-router/content-based-router.camel.yaml
@@ -14,8 +14,9 @@
             type: int
         - choice:
             when:
-              - simple:
-                  expression: "${body} >= 30"
+              - expression:
+                  simple:
+                    expression: "${body} >= 30"
                 steps:
                   - setHeader:
                       name: level
@@ -24,8 +25,9 @@
                           expression: hot
                   - log:
                       message: "Hot alert: ${body} C"
-              - simple:
-                  expression: "${body} >= 15"
+              - expression:
+                  simple:
+                    expression: "${body} >= 15"
                 steps:
                   - setHeader:
                       name: level
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/message-size.camel.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/message-size.camel.yaml
deleted file mode 100644
index 9b2e0361c3a4..000000000000
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/message-size.camel.yaml
+++ /dev/null
@@ -1,102 +0,0 @@
-#
-# 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.
-#
-
-# Producers: simulate different payload sizes using Content-Length header
-- route:
-    id: small-producer
-    from:
-      uri: timer:small
-      parameters:
-        period: 2000
-      steps:
-        - setHeader:
-            name: Content-Length
-            simple: "${random(100,1023)}"
-        - setHeader:
-            name: source
-            simple: small-producer
-        - to:
-            uri: seda:small
-
-- route:
-    id: medium-producer
-    from:
-      uri: timer:medium
-      parameters:
-        period: 3000
-      steps:
-        - setHeader:
-            name: Content-Length
-            simple: "${random(8192,18432)}"
-        - setHeader:
-            name: source
-            simple: medium-producer
-        - setHeader:
-            name: tracking-id
-            simple: "TRK-${random(10000,99999)}"
-        - to:
-            uri: seda:medium
-
-- route:
-    id: large-producer
-    from:
-      uri: timer:large
-      parameters:
-        period: 5000
-      steps:
-        - setHeader:
-            name: Content-Length
-            simple: "${random(10485760,20971520)}"
-        - setHeader:
-            name: source
-            simple: large-producer
-        - setHeader:
-            name: tracking-id
-            simple: "TRK-${random(10000,99999)}"
-        - setHeader:
-            name: batch-id
-            simple: "BATCH-${random(100,999)}"
-        - setHeader:
-            name: priority
-            simple: "${random(1,5)}"
-        - to:
-            uri: seda:large
-
-# Consumers: process messages from each seda endpoint
-- route:
-    id: process-small
-    from:
-      uri: seda:small
-      steps:
-        - log:
-            message: "Small: Content-Length=${header.Content-Length}"
-
-- route:
-    id: process-medium
-    from:
-      uri: seda:medium
-      steps:
-        - log:
-            message: "Medium: Content-Length=${header.Content-Length}"
-
-- route:
-    id: process-large
-    from:
-      uri: seda:large
-      steps:
-        - log:
-            message: "Large: Content-Length=${header.Content-Length}"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/orders.camel.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/orders.camel.yaml
index 10930065eb1d..c7d84fa0b866 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/orders.camel.yaml
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/orders.camel.yaml
@@ -8,10 +8,14 @@
       steps:
         - setHeader:
             name: Content-Length
-            simple: "${random(100,1023)}"
+            expression:
+              simple:
+                expression: "${random(100,1023)}"
         - setHeader:
             name: source
-            simple: small-producer
+            expression:
+              simple:
+                expression: small-producer
         - to:
             uri: seda:small
 
@@ -24,13 +28,19 @@
       steps:
         - setHeader:
             name: Content-Length
-            simple: "${random(8192,18432)}"
+            expression:
+              simple:
+                expression: "${random(8192,18432)}"
         - setHeader:
             name: source
-            simple: medium-producer
+            expression:
+              simple:
+                expression: medium-producer
         - setHeader:
             name: tracking-id
-            simple: "TRK-${random(10000,99999)}"
+            expression:
+              simple:
+                expression: "TRK-${random(10000,99999)}"
         - to:
             uri: seda:medium
 
@@ -43,19 +53,29 @@
       steps:
         - setHeader:
             name: Content-Length
-            simple: "${random(10485760,20971520)}"
+            expression:
+              simple:
+                expression: "${random(10485760,20971520)}"
         - setHeader:
             name: source
-            simple: large-producer
+            expression:
+              simple:
+                expression: large-producer
         - setHeader:
             name: tracking-id
-            simple: "TRK-${random(10000,99999)}"
+            expression:
+              simple:
+                expression: "TRK-${random(10000,99999)}"
         - setHeader:
             name: batch-id
-            simple: "BATCH-${random(100,999)}"
+            expression:
+              simple:
+                expression: "BATCH-${random(100,999)}"
         - setHeader:
             name: priority
-            simple: "${random(1,5)}"
+            expression:
+              simple:
+                expression: "${random(1,5)}"
         - to:
             uri: seda:large
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/integration.yaml.ftl
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/integration.yaml.ftl
index 50f8e8a2d2b4..3c200771ad7e 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/integration.yaml.ftl
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/integration.yaml.ftl
@@ -28,5 +28,8 @@ spec:
           period: "1000"
         steps:
           - setBody:
-              constant: "Hello Camel from yaml"
-          - log: "${body}"
+              expression:
+                constant:
+                  expression: "Hello Camel from yaml"
+          - log:
+              message: "${body}"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/kamelet-action.yaml.ftl
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/kamelet-action.yaml.ftl
index 26355e87bccd..959f39ead507 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/kamelet-action.yaml.ftl
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/kamelet-action.yaml.ftl
@@ -42,4 +42,6 @@ spec:
       uri: "kamelet:source"
       steps:
       - delay:
-          constant: "{{milliseconds}}"
+          expression:
+            constant:
+              expression: "{{milliseconds}}"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/kamelet-source.yaml.ftl
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/kamelet-source.yaml.ftl
index d40d80a6a827..fb2cd7b18f0d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/kamelet-source.yaml.ftl
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/kamelet-source.yaml.ftl
@@ -48,8 +48,13 @@ spec:
         period: "{{period}}"
       steps:
         - setBody:
-            constant: "{{message}}"
+            expression:
+              constant:
+                expression: "{{message}}"
         - setHeader:
             name: "Content-Type"
-            constant: "text/plain"
-        - to: "kamelet:sink"
+            expression:
+              constant:
+                expression: "text/plain"
+        - to:
+            uri: "kamelet:sink"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/yaml.ftl 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/yaml.ftl
index 48d5a88d02bf..808f5b7e79bd 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/yaml.ftl
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/yaml.ftl
@@ -23,5 +23,8 @@
         period: "1000"
       steps:
         - setBody:
-            simple: Hello Camel from ${routeId}
-        - log: ${body}
+            expression:
+              simple:
+                expression: "Hello Camel from ${routeId}"
+        - log:
+            message: "${body}"
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyUpdateTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyUpdateTest.java
index 2951d17f221c..9a0328045ffd 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyUpdateTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DependencyUpdateTest.java
@@ -389,11 +389,17 @@ class DependencyUpdateTest extends 
CamelCommandBaseTestSupport {
     private void addArangodbToCamelFile() throws Exception {
         File camelFile = new File(workingDir, 
"src/main/resources/camel/my.camel.yaml");
         String content = Files.readString(camelFile.toPath());
-        content = content.replace("- log: ${body}", """
-                - to:
-                             uri: arangodb
-                             parameters:
-                               database: demo
+        // the generated route ends with a canonical log step; swap it for an 
arangodb producer
+        String log = """
+                        - log:
+                            message: "${body}"
+                """;
+        assertThat(content).contains(log);
+        content = content.replace(log, """
+                        - to:
+                            uri: arangodb
+                            parameters:
+                              database: demo
                 """);
         Files.writeString(camelFile.toPath(), content);
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java
index abaa20f65836..003c76299bb8 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java
@@ -23,11 +23,13 @@ import java.nio.file.Path;
 import java.util.Properties;
 import java.util.stream.Stream;
 
+import org.apache.camel.dsl.yaml.validator.YamlValidator;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.camel.model.Model;
 import org.apache.camel.spi.Resource;
 import org.apache.camel.support.PluginHelper;
 import org.apache.camel.support.ResourceHelper;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Named;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
@@ -47,9 +49,21 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
  * fixture and the example's {@code application.properties} are required), 
whereas endpoints and their producers and
  * consumers are only created at start. Not starting therefore avoids opening 
external endpoints (JMS, Kafka, SQL, ...)
  * but also means endpoint wiring is not exercised.
+ *
+ * Every example is also validated against the canonical YAML schema: the 
examples are what a user or an AI agent starts
+ * from, so they must be written in the canonical format and not in the 
deprecated compact notation, which
+ * {@code camel run} warns about.
  */
 class ExampleRoutesLoadTest {
 
+    private static YamlValidator canonicalValidator;
+
+    @BeforeAll
+    static void setUpValidator() throws Exception {
+        canonicalValidator = new YamlValidator(true);
+        canonicalValidator.init();
+    }
+
     static Stream<Arguments> exampleRouteFiles() throws Exception {
         // resolve the examples from the classpath (target/classes/examples) 
so the test does not depend on the working
         // directory being the module base, which differs between Maven 
Surefire and IDE run configurations
@@ -100,6 +114,13 @@ class ExampleRoutesLoadTest {
         }
     }
 
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("exampleRouteFiles")
+    void shouldBeCanonicalYaml(Path file) throws Exception {
+        var report = canonicalValidator.validate(file.toFile());
+        assertTrue(report.isEmpty(), "Example is not in the canonical YAML 
format: " + file + "\n" + report);
+    }
+
     private static void loadExampleProperties(DefaultCamelContext context, 
Path file) throws Exception {
         Path properties = file.resolveSibling("application.properties");
         if (!Files.exists(properties)) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TemplateHelperTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TemplateHelperTest.java
index 41ba25f1e2ea..a7f91b4ef0d9 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TemplateHelperTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/TemplateHelperTest.java
@@ -23,15 +23,29 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.camel.dsl.yaml.validator.YamlValidator;
 import org.apache.camel.util.IOHelper;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.yaml.snakeyaml.Yaml;
 
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+@SuppressWarnings("unchecked")
 class TemplateHelperTest {
 
+    private static YamlValidator canonicalValidator;
+
+    @BeforeAll
+    static void setUpValidator() throws Exception {
+        canonicalValidator = new YamlValidator(true);
+        canonicalValidator.init();
+    }
+
     // ===== TemplateHelper.processTemplate() tests (FreeMarker-processed 
templates) =====
 
     @Test
@@ -485,6 +499,42 @@ class TemplateHelperTest {
     /**
      * Loads a template the same way Init.java does: raw text + header 
stripping.
      */
+    // ===== the YAML the templates generate is what a user or an AI agent 
starts from: it must be canonical =====
+
+    @ParameterizedTest
+    @ValueSource(strings = { "yaml.ftl", "rest-dsl.yaml.ftl" })
+    void testInitYamlTemplatesAreCanonical(String name) throws Exception {
+        String content = loadInitTemplate(name).replace("[=Spec]", 
"petstore.json");
+
+        assertCanonical(name, content);
+    }
+
+    @Test
+    void testInitIntegrationTemplateIsCanonical() throws Exception {
+        String content = 
loadInitTemplate("integration.yaml.ftl").replace("[=Name]", "my-integration");
+
+        // the Integration CR wraps the routes in spec.flows
+        Map<String, Object> cr = new Yaml().load(content);
+        Map<String, Object> spec = (Map<String, Object>) cr.get("spec");
+        assertCanonical("integration.yaml.ftl", new 
Yaml().dump(spec.get("flows")));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "kamelet-source.yaml.ftl", 
"kamelet-sink.yaml.ftl", "kamelet-action.yaml.ftl" })
+    void testInitKameletTemplatesAreCanonical(String name) throws Exception {
+        String content = loadInitTemplate(name).replace("[=Name]", 
"my-kamelet");
+
+        // the Kamelet CR wraps the route in spec.template as its from
+        Map<String, Object> cr = new Yaml().load(content);
+        Map<String, Object> spec = (Map<String, Object>) cr.get("spec");
+        assertCanonical(name, new Yaml().dump(List.of(spec.get("template"))));
+    }
+
+    private static void assertCanonical(String name, String yaml) throws 
Exception {
+        var report = canonicalValidator.validate(yaml);
+        assertTrue(report.isEmpty(), name + " is not in the canonical YAML 
format:\n" + yaml + "\n" + report);
+    }
+
     private String loadInitTemplate(String name) throws IOException {
         try (InputStream is = 
getClass().getClassLoader().getResourceAsStream("templates/" + name)) {
             String content = IOHelper.loadText(is);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/dependency-update/route.yaml
 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/dependency-update/route.yaml
index df0f91ed45f8..cb3a75391e6b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/dependency-update/route.yaml
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/dependency-update/route.yaml
@@ -22,5 +22,8 @@
         period: "1000"
       steps:
         - setBody:
-            simple: Hello Camel from my
-        - log: ${body}
+            expression:
+              simple:
+                expression: "Hello Camel from my"
+        - log:
+            message: "${body}"
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldTools.java
index 1a392ff181dd..18de55b1206f 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldTools.java
@@ -114,13 +114,13 @@ public class AiPipelineScaffoldTools {
         sb.append("    id: ai-summarization\n");
         sb.append("    from:\n");
         appendSourceEndpoint(sb, source);
-        sb.append("    steps:\n");
+        sb.append("      steps:\n");
         appendDocumentProcessing(sb, processor);
-        sb.append("      - process:\n");
-        sb.append("          ref: \"#buildSummarizationPrompt\"\n");
+        sb.append("        - process:\n");
+        sb.append("            ref: \"#buildSummarizationPrompt\"\n");
         appendBedrockConverse(sb, model, region);
-        sb.append("      - log:\n");
-        sb.append("          message: \"Summary generated: ${body}\"\n");
+        sb.append("        - log:\n");
+        sb.append("            message: \"Summary generated: ${body}\"\n");
         return sb.toString();
     }
 
@@ -132,16 +132,16 @@ public class AiPipelineScaffoldTools {
         sb.append("    id: ai-extraction\n");
         sb.append("    from:\n");
         appendSourceEndpoint(sb, source);
-        sb.append("    steps:\n");
+        sb.append("      steps:\n");
         appendDocumentProcessing(sb, processor);
-        sb.append("      - process:\n");
-        sb.append("          ref: \"#buildExtractionPrompt\"\n");
+        sb.append("        - process:\n");
+        sb.append("            ref: \"#buildExtractionPrompt\"\n");
         appendBedrockConverse(sb, model, region);
-        sb.append("      - unmarshal:\n");
-        sb.append("          json:\n");
-        sb.append("            unmarshalType: java.util.Map\n");
-        sb.append("      - log:\n");
-        sb.append("          message: \"Extracted data: ${body}\"\n");
+        sb.append("        - unmarshal:\n");
+        sb.append("            json:\n");
+        sb.append("              unmarshalType: java.util.Map\n");
+        sb.append("        - log:\n");
+        sb.append("            message: \"Extracted data: ${body}\"\n");
         return sb.toString();
     }
 
@@ -159,34 +159,40 @@ public class AiPipelineScaffoldTools {
         sb.append("    id: rag-ingestion\n");
         sb.append("    from:\n");
         appendSourceEndpoint(sb, source);
-        sb.append("    steps:\n");
+        sb.append("      steps:\n");
         appendDocumentProcessing(sb, processor);
-        sb.append("      # Chunk the extracted text for embedding\n");
-        sb.append("      - split:\n");
-        sb.append("          tokenize: \"\\n\\n\"\n");
-        sb.append("          streaming: true\n");
-        sb.append("        steps:\n");
-        sb.append("          - to:\n");
-        sb.append("              uri: \"langchain4j-embeddings:embed\"\n");
-        sb.append("              parameters:\n");
-        sb.append("                embeddingModelId: \"#bedrockEmbedding\"\n");
-        sb.append("          # TODO: Configure your vector store endpoint\n");
-        sb.append("          - to: 
\"log:ingested?showBody=false&showHeaders=true\"\n\n");
+        sb.append("        # Chunk the extracted text for embedding\n");
+        sb.append("        - split:\n");
+        sb.append("            expression:\n");
+        sb.append("              tokenize:\n");
+        sb.append("                token: \"\\n\\n\"\n");
+        sb.append("            streaming: true\n");
+        sb.append("            steps:\n");
+        sb.append("              - to:\n");
+        sb.append("                  uri: \"langchain4j-embeddings:embed\"\n");
+        sb.append("                  parameters:\n");
+        sb.append("                    embeddingModelId: 
\"#bedrockEmbedding\"\n");
+        sb.append("              # TODO: Configure your vector store 
endpoint\n");
+        sb.append("              - to:\n");
+        sb.append("                  uri: \"log:ingested\"\n");
+        sb.append("                  parameters:\n");
+        sb.append("                    showBody: false\n");
+        sb.append("                    showHeaders: true\n\n");
 
         // Query route
         sb.append("- route:\n");
         sb.append("    id: rag-query\n");
         sb.append("    from:\n");
         sb.append("      uri: \"direct:query\"\n");
-        sb.append("    steps:\n");
-        sb.append("      # TODO: Retrieve relevant chunks from vector 
store\n");
-        sb.append("      # - to: \"langchain4j-embeddings:embed\" # embed the 
query\n");
-        sb.append("      # - to: \"qdrant:search\"                # search 
vector store\n");
-        sb.append("      - process:\n");
-        sb.append("          ref: \"#buildRagPrompt\"\n");
+        sb.append("      steps:\n");
+        sb.append("        # TODO: Retrieve relevant chunks from vector 
store\n");
+        sb.append("        # - to: \"langchain4j-embeddings:embed\" # embed 
the query\n");
+        sb.append("        # - to: \"qdrant:search\"                # search 
vector store\n");
+        sb.append("        - process:\n");
+        sb.append("            ref: \"#buildRagPrompt\"\n");
         appendBedrockConverse(sb, model, region);
-        sb.append("      - log:\n");
-        sb.append("          message: \"RAG answer: ${body}\"\n");
+        sb.append("        - log:\n");
+        sb.append("            message: \"RAG answer: ${body}\"\n");
         return sb.toString();
     }
 
@@ -198,22 +204,29 @@ public class AiPipelineScaffoldTools {
         sb.append("    id: ai-classification\n");
         sb.append("    from:\n");
         appendSourceEndpoint(sb, source);
-        sb.append("    steps:\n");
+        sb.append("      steps:\n");
         appendDocumentProcessing(sb, processor);
-        sb.append("      - process:\n");
-        sb.append("          ref: \"#buildClassificationPrompt\"\n");
+        sb.append("        - process:\n");
+        sb.append("            ref: \"#buildClassificationPrompt\"\n");
         appendBedrockConverse(sb, model, region);
-        sb.append("      - choice:\n");
-        sb.append("          when:\n");
-        sb.append("            - simple: \"${body} contains 'invoice'\"\n");
-        sb.append("              steps:\n");
-        sb.append("                - to: \"direct:handle-invoice\"\n");
-        sb.append("            - simple: \"${body} contains 'contract'\"\n");
+        sb.append("        - choice:\n");
+        sb.append("            when:\n");
+        sb.append("              - expression:\n");
+        sb.append("                  simple:\n");
+        sb.append("                    expression: \"${body} contains 
'invoice'\"\n");
+        sb.append("                steps:\n");
+        sb.append("                  - to:\n");
+        sb.append("                      uri: \"direct:handle-invoice\"\n");
+        sb.append("              - expression:\n");
+        sb.append("                  simple:\n");
+        sb.append("                    expression: \"${body} contains 
'contract'\"\n");
+        sb.append("                steps:\n");
+        sb.append("                  - to:\n");
+        sb.append("                      uri: \"direct:handle-contract\"\n");
+        sb.append("            otherwise:\n");
         sb.append("              steps:\n");
-        sb.append("                - to: \"direct:handle-contract\"\n");
-        sb.append("          otherwise:\n");
-        sb.append("            steps:\n");
-        sb.append("              - to: \"direct:handle-other\"\n");
+        sb.append("                - to:\n");
+        sb.append("                    uri: \"direct:handle-other\"\n");
         return sb.toString();
     }
 
@@ -249,39 +262,39 @@ public class AiPipelineScaffoldTools {
             case "textract" -> appendTextractStep(sb);
             case "combined" -> {
                 appendDoclingStep(sb);
-                sb.append("      # Also extract tables/forms via Textract for 
structured data\n");
+                sb.append("        # Also extract tables/forms via Textract 
for structured data\n");
                 appendTextractStep(sb);
-                sb.append("      - process:\n");
-                sb.append("          ref: \"#mergeDoclingAndTextract\"\n");
+                sb.append("        - process:\n");
+                sb.append("            ref: \"#mergeDoclingAndTextract\"\n");
             }
             default -> appendDoclingStep(sb);
         }
     }
 
     private void appendDoclingStep(StringBuilder sb) {
-        sb.append("      - to:\n");
-        sb.append("          uri: \"docling:convert\"\n");
-        sb.append("          parameters:\n");
-        sb.append("            operation: CONVERT_TO_MARKDOWN\n");
-        sb.append("            useDoclingServe: true\n");
-        sb.append("            doclingServeUrl: \"{{docling.server.url}}\"\n");
+        sb.append("        - to:\n");
+        sb.append("            uri: \"docling:convert\"\n");
+        sb.append("            parameters:\n");
+        sb.append("              operation: CONVERT_TO_MARKDOWN\n");
+        sb.append("              useDoclingServe: true\n");
+        sb.append("              doclingServeUrl: 
\"{{docling.server.url}}\"\n");
     }
 
     private void appendTextractStep(StringBuilder sb) {
-        sb.append("      - to:\n");
-        sb.append("          uri: \"aws2-textract:detect\"\n");
-        sb.append("          parameters:\n");
-        sb.append("            operation: detectDocumentText\n");
-        sb.append("            region: \"{{aws.region}}\"\n");
+        sb.append("        - to:\n");
+        sb.append("            uri: \"aws2-textract:detect\"\n");
+        sb.append("            parameters:\n");
+        sb.append("              operation: detectDocumentText\n");
+        sb.append("              region: \"{{aws.region}}\"\n");
     }
 
     private void appendBedrockConverse(StringBuilder sb, String model, String 
region) {
-        sb.append("      - to:\n");
-        sb.append("          uri: \"aws-bedrock:label\"\n");
-        sb.append("          parameters:\n");
-        sb.append("            operation: converse\n");
-        sb.append("            modelId: \"").append(model).append("\"\n");
-        sb.append("            region: \"").append(region).append("\"\n");
+        sb.append("        - to:\n");
+        sb.append("            uri: \"aws-bedrock:label\"\n");
+        sb.append("            parameters:\n");
+        sb.append("              operation: converse\n");
+        sb.append("              modelId: \"").append(model).append("\"\n");
+        sb.append("              region: \"").append(region).append("\"\n");
     }
 
     // ---- Properties generation ----
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
index 8e50784a4beb..6a84882c8625 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
@@ -177,18 +177,24 @@ public class OpenApiTools {
             if (stub.contentType() != null) {
                 yaml.append("        - setHeader:\n");
                 yaml.append("            name: Content-Type\n");
-                yaml.append("            constant: 
").append(stub.contentType()).append("\n");
+                yaml.append("            expression:\n");
+                yaml.append("              constant:\n");
+                yaml.append("                expression: 
").append(stub.contentType()).append("\n");
             }
 
             // Set response code if we know it
             if (stub.responseCode() != null) {
                 yaml.append("        - setHeader:\n");
                 yaml.append("            name: CamelHttpResponseCode\n");
-                yaml.append("            constant: 
").append(stub.responseCode()).append("\n");
+                yaml.append("            expression:\n");
+                yaml.append("              constant:\n");
+                yaml.append("                expression: 
").append(stub.responseCode()).append("\n");
             }
 
             yaml.append("        - setBody:\n");
-            yaml.append("            constant: \"TODO: implement 
").append(stub.operationId()).append("\"\n");
+            yaml.append("            expression:\n");
+            yaml.append("              constant:\n");
+            yaml.append("                expression: \"TODO: implement 
").append(stub.operationId()).append("\"\n");
         }
 
         return new ScaffoldResult(yaml.toString(), stubs.size(), filename, 
mode, apiTitle);
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldToolsTest.java
index ef736764e4bf..84974e51e027 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldToolsTest.java
@@ -17,6 +17,8 @@
 package org.apache.camel.dsl.jbang.core.commands.mcp;
 
 import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.dsl.yaml.validator.YamlValidator;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
@@ -27,8 +29,16 @@ import static 
org.assertj.core.api.Assertions.assertThatThrownBy;
 
 class AiPipelineScaffoldToolsTest {
 
+    private static YamlValidator canonicalValidator;
+
     private AiPipelineScaffoldTools tools;
 
+    @BeforeAll
+    static void setUpValidator() throws Exception {
+        canonicalValidator = new YamlValidator(true);
+        canonicalValidator.init();
+    }
+
     @BeforeEach
     void setUp() {
         tools = new AiPipelineScaffoldTools();
@@ -76,6 +86,18 @@ class AiPipelineScaffoldToolsTest {
         assertThat(result.description()).isNotBlank();
     }
 
+    @ParameterizedTest
+    @ValueSource(strings = { "summarization", "extraction", "rag", 
"classification" })
+    void shouldGenerateCanonicalYamlForEachPipelineType(String type) throws 
Exception {
+        AiPipelineScaffoldTools.ScaffoldResult result
+                = tools.camel_ai_pipeline_scaffold(type, "docling", "file", 
null, null);
+
+        // the scaffold is the starting point of a user or an AI agent: it 
must not be in the deprecated compact notation
+        assertThat(canonicalValidator.validate(result.yamlRoute()))
+                .as("canonical validation of the %s scaffold:%n%s", type, 
result.yamlRoute())
+                .isEmpty();
+    }
+
     @Test
     void shouldGenerateSummarizationWithDocling() {
         AiPipelineScaffoldTools.ScaffoldResult result
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiToolsTest.java
index 7fcb93850c67..8dfc29830956 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiToolsTest.java
@@ -17,6 +17,7 @@
 package org.apache.camel.dsl.jbang.core.commands.mcp;
 
 import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.dsl.yaml.validator.YamlValidator;
 import org.junit.jupiter.api.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -286,15 +287,45 @@ class OpenApiToolsTest {
     void scaffoldResponseCodesFromSpec() {
         OpenApiTools.ScaffoldResult result = 
tools.camel_openapi_scaffold(MINIMAL_SPEC, null, null);
 
-        assertThat(result.yaml()).contains("constant: 200");
-        assertThat(result.yaml()).contains("constant: 201");
+        assertThat(result.yaml()).contains("""
+                        - setHeader:
+                            name: CamelHttpResponseCode
+                            expression:
+                              constant:
+                                expression: 200
+                """);
+        assertThat(result.yaml()).contains("""
+                        - setHeader:
+                            name: CamelHttpResponseCode
+                            expression:
+                              constant:
+                                expression: 201
+                """);
     }
 
     @Test
     void scaffoldContentTypeHeaders() {
         OpenApiTools.ScaffoldResult result = 
tools.camel_openapi_scaffold(MINIMAL_SPEC, null, null);
 
-        assertThat(result.yaml()).contains("constant: application/json");
+        assertThat(result.yaml()).contains("""
+                        - setHeader:
+                            name: Content-Type
+                            expression:
+                              constant:
+                                expression: application/json
+                """);
+    }
+
+    @Test
+    void scaffoldIsCanonicalYaml() throws Exception {
+        OpenApiTools.ScaffoldResult result = 
tools.camel_openapi_scaffold(MINIMAL_SPEC, null, null);
+
+        // the scaffold is the starting point of a user or an AI agent: it 
must not be in the deprecated compact notation
+        YamlValidator canonicalValidator = new YamlValidator(true);
+        canonicalValidator.init();
+        assertThat(canonicalValidator.validate(result.yaml()))
+                .as("canonical validation of the scaffold:%n%s", result.yaml())
+                .isEmpty();
     }
 
     @Test

Reply via email to