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

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


The following commit(s) were added to refs/heads/main by this push:
     new 025f7cb97232 CAMEL-24744: camel-jbang - MCP scaffold tools, camel init 
templates and bundled examples generate canonical YAML
025f7cb97232 is described below

commit 025f7cb9723205bc7573a2f61f83e0738790f678
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 15 06:51:33 2026 +0200

    CAMEL-24744: camel-jbang - MCP scaffold tools, camel init templates and 
bundled examples generate canonical YAML
    
    Follow-up to CAMEL-24722 / CAMEL-24723: the YAML that camel-jbang generates 
is the
    starting point for users and AI agents, yet it was still written in the 
deprecated
    compact notation, so the first `camel run` warned about a Camel-generated 
file.
    
    The `camel init` templates (yaml, integration, kamelet-source, 
kamelet-action), the
    bundled examples and the `camel_openapi_scaffold` / 
`camel_ai_pipeline_scaffold` MCP
    tools now emit the canonical form (`expression:` wrapper, `log: {message: 
...}`,
    `to: {uri: ...}`). Running the canonical validator over the AI pipeline 
scaffold also
    surfaced two bugs where the generated route never loaded (`steps:` at the 
wrong
    level); both fixed. The stale `message-size/message-size.camel.yaml` copy 
is removed.
    
    Tests validate every template, bundled example and scaffold output against 
the
    canonical schema, and the upgrade guide documents the changed generated 
output.
    
    Closes #26444
    
    Co-Authored-By: Claude Opus 5 (1M context) <[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