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

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

commit 92d30481b8c41ed3fdf741bf71370cd561f3a268
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 6 14:31:09 2026 +0100

    CAMEL-26689: camel-chaos - a model of the chaos rules in camel-catalog: 
targets, hooks, faults, parameters and known rules
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/catalog/chaos-model.json      | 1078 ++++++++++++++++++++
 .../org/apache/camel/catalog/docs/chaos.adoc       |   23 +
 .../org/apache/camel/catalog/CamelCatalog.java     |   10 +
 .../apache/camel/catalog/DefaultCamelCatalog.java  |    5 +
 .../org/apache/camel/catalog/CamelCatalogTest.java |   11 +
 .../org/apache/camel/chaos/chaos-model.json        | 1078 ++++++++++++++++++++
 components/camel-chaos/src/main/docs/chaos.adoc    |   23 +
 .../apache/camel/component/chaos/ChaosModel.java   |  602 +++++++++++
 .../camel/component/chaos/ChaosRuleParser.java     |   61 +-
 .../camel/component/chaos/ChaosModelTest.java      |  194 ++++
 .../modules/ROOT/pages/camel-catalog.adoc          |    9 +
 .../camel/maven/packaging/PrepareCatalogMojo.java  |   17 +
 12 files changed, 3094 insertions(+), 17 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
new file mode 100644
index 000000000000..7505fb834508
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/chaos-model.json
@@ -0,0 +1,1078 @@
+{
+  "chaos": {
+    "description": "The chaos rules of camel-chaos: a rule is one line, the 
target, the fault and its parameters as name=value, then the options (such as 
at, rate, from, until) and optionally an expectation",
+    "syntax": "<target>=<pattern> <fault> [<parameter>=<value> ...] 
[<option>=<value> ...]",
+    "targets": [
+      {
+        "name": "route",
+        "description": "A route id, with * for any characters; one route id 
(no pattern) for stall and lifecycle",
+        "pattern": true,
+        "pick": "routes"
+      },
+      {
+        "name": "endpoint",
+        "description": "An endpoint URI pattern, with * for any characters; 
matched with or without the query parameters, and the \/\/ after the scheme 
does not count (kafka:orders* matches kafka:\/\/orders?brokers=b1)",
+        "pattern": true,
+        "pick": "endpoints"
+      },
+      {
+        "name": "component",
+        "description": "The scheme of the endpoint URIs, with * for any 
characters (no ':')",
+        "pattern": true,
+        "pick": "components"
+      },
+      {
+        "name": "step",
+        "description": "A step id (to1, or the id set with id(\"enrich\")), 
with * for any characters",
+        "pattern": true,
+        "pick": "steps"
+      }
+    ],
+    "hooks": [
+      {
+        "name": "send",
+        "description": "The sends to a matching endpoint (to, toD, wireTap, 
enrich, recipientList, a ProducerTemplate...), before the producer is called, 
inside the error handler",
+        "targets": [
+          "endpoint",
+          "component"
+        ],
+        "faults": [
+          "error",
+          "delay",
+          "drop",
+          "corrupt"
+        ],
+        "onlyWithAt": false
+      },
+      {
+        "name": "reply",
+        "description": "The reply of such a send, after the producer 
succeeded: its side effect happened (only with at=reply)",
+        "targets": [
+          "endpoint",
+          "component"
+        ],
+        "faults": [
+          "error",
+          "corrupt"
+        ],
+        "onlyWithAt": true
+      },
+      {
+        "name": "dlq",
+        "description": "The send of the dead letter channel to a matching 
endpoint (only with at=dlq)",
+        "targets": [
+          "endpoint",
+          "component"
+        ],
+        "faults": [
+          "error",
+          "delay",
+          "drop",
+          "corrupt"
+        ],
+        "onlyWithAt": true
+      },
+      {
+        "name": "poll",
+        "description": "The polls of the scheduled poll consumers of the 
routes",
+        "targets": [
+          "endpoint",
+          "component"
+        ],
+        "faults": [
+          "error",
+          "delay",
+          "drop"
+        ],
+        "onlyWithAt": false
+      },
+      {
+        "name": "entry",
+        "description": "Each exchange that enters a matching route, before the 
first step",
+        "targets": [
+          "route"
+        ],
+        "faults": [
+          "corrupt"
+        ],
+        "onlyWithAt": false
+      },
+      {
+        "name": "step",
+        "description": "Each matching step of a route, right before its 
processor is called",
+        "targets": [
+          "step"
+        ],
+        "faults": [
+          "error",
+          "delay"
+        ],
+        "onlyWithAt": false
+      }
+    ],
+    "faults": [
+      {
+        "name": "error",
+        "description": "Fails the send, the poll or the step with an exception 
(a ChaosException without exception=), or the failure of an HTTP component for 
status=",
+        "targets": [
+          "component",
+          "endpoint",
+          "step"
+        ],
+        "hooks": [
+          "send",
+          "reply",
+          "dlq",
+          "poll",
+          "step"
+        ],
+        "parameters": [
+          {
+            "name": "after",
+            "type": "duration",
+            "required": false,
+            "description": "Waits that long first, then fails, as a call that 
times out; the producer is not called",
+            "example": "5s",
+            "notAt": [
+              "reply"
+            ]
+          },
+          {
+            "name": "body",
+            "type": "string",
+            "required": false,
+            "description": "The body of the HTTP response (needs status=)",
+            "example": "no stock"
+          },
+          {
+            "name": "exception",
+            "type": "class",
+            "required": false,
+            "description": "The class of the exception: a public Throwable 
with a public constructor taking a String or none (not an 
InterruptedException)",
+            "example": "java.net.ConnectException"
+          },
+          {
+            "name": "header.<name>",
+            "type": "string",
+            "required": false,
+            "description": "A header of the HTTP response, such as 
header.Retry-After=5 (needs status=)",
+            "example": "5",
+            "prefix": "header."
+          },
+          {
+            "name": "message",
+            "type": "string",
+            "required": false,
+            "description": "The message of the exception (the class needs a 
constructor taking a String)",
+            "example": "broker down"
+          },
+          {
+            "name": "status",
+            "type": "integer",
+            "required": false,
+            "description": "The failure of an HTTP component for this status 
(not with exception= or message=)",
+            "example": "503",
+            "min": 300,
+            "max": 599,
+            "schemes": [
+              "http",
+              "https",
+              "netty-http",
+              "undertow",
+              "vertx-http"
+            ]
+          }
+        ],
+        "options": [
+          "at",
+          "expect",
+          "from",
+          "rate",
+          "until",
+          "within"
+        ]
+      },
+      {
+        "name": "delay",
+        "description": "Waits before the send, the poll or the step, as for a 
slow endpoint",
+        "targets": [
+          "component",
+          "endpoint",
+          "step"
+        ],
+        "hooks": [
+          "send",
+          "dlq",
+          "poll",
+          "step"
+        ],
+        "parameters": [
+          {
+            "name": "duration",
+            "type": "duration",
+            "required": true,
+            "description": "How long to wait, or a range such as 100ms..2s for 
a draw between the two times (from the seeded sequence of the rule)",
+            "example": "500ms",
+            "range": true
+          }
+        ],
+        "options": [
+          "at",
+          "expect",
+          "from",
+          "rate",
+          "until",
+          "within"
+        ]
+      },
+      {
+        "name": "drop",
+        "description": "A send that reports success without calling the 
producer, or a poll that is skipped",
+        "targets": [
+          "component",
+          "endpoint"
+        ],
+        "hooks": [
+          "send",
+          "dlq",
+          "poll"
+        ],
+        "parameters": [
+          
+        ],
+        "options": [
+          "at",
+          "expect",
+          "from",
+          "rate",
+          "until",
+          "within"
+        ]
+      },
+      {
+        "name": "corrupt",
+        "description": "Changes the message: its headers, the charset of its 
body, or its body",
+        "targets": [
+          "component",
+          "endpoint",
+          "route"
+        ],
+        "hooks": [
+          "send",
+          "reply",
+          "dlq",
+          "entry"
+        ],
+        "parameters": [
+          {
+            "name": "header",
+            "type": "string",
+            "required": false,
+            "description": "The headers mode=remove-header removes: a name or 
a pattern with *, ignoring case (without it every header that is not Camel*)",
+            "example": "X-Trace-*"
+          },
+          {
+            "name": "mode",
+            "type": "enum",
+            "required": true,
+            "description": "headers: the headers that are not Camel* get 
random values; charset: a text body becomes UTF-16 bytes; truncate: a String, 
byte[] or stream body ends after half of it; empty: the body becomes empty of 
its kind (or null); remove-header: removes headers",
+            "example": "headers",
+            "enum": [
+              "headers",
+              "charset",
+              "truncate",
+              "empty",
+              "remove-header"
+            ]
+          }
+        ],
+        "options": [
+          "at",
+          "expect",
+          "from",
+          "rate",
+          "until",
+          "within"
+        ]
+      },
+      {
+        "name": "lifecycle",
+        "description": "Suspends, stops or removes the route of a route=<id> 
target, and brings it back after the duration; repeat= takes it down more than 
once",
+        "targets": [
+          "route"
+        ],
+        "hooks": [
+          
+        ],
+        "parameters": [
+          {
+            "name": "action",
+            "type": "enum",
+            "required": true,
+            "description": "What to do to the route",
+            "example": "stop",
+            "enum": [
+              "suspend",
+              "stop",
+              "remove"
+            ]
+          },
+          {
+            "name": "duration",
+            "type": "duration",
+            "required": true,
+            "description": "How long the route stays down (at most 
camel.chaos.maxDuration)",
+            "example": "20s"
+          },
+          {
+            "name": "every",
+            "type": "duration",
+            "required": false,
+            "description": "The time from one time the route goes down to the 
next, longer than duration=",
+            "example": "1m"
+          },
+          {
+            "name": "repeat",
+            "type": "integer",
+            "required": false,
+            "description": "How many times to take the route down (needs 
every= when more than 1)",
+            "example": "3",
+            "min": 1,
+            "max": 100
+          }
+        ],
+        "options": [
+          "expect",
+          "from",
+          "within"
+        ]
+      },
+      {
+        "name": "stall",
+        "description": "Short for lifecycle action=suspend: the route stalls 
for the duration",
+        "targets": [
+          "route"
+        ],
+        "hooks": [
+          
+        ],
+        "shortFor": "lifecycle action=suspend",
+        "parameters": [
+          {
+            "name": "duration",
+            "type": "duration",
+            "required": true,
+            "description": "How long the route stays down (at most 
camel.chaos.maxDuration)",
+            "example": "20s"
+          },
+          {
+            "name": "every",
+            "type": "duration",
+            "required": false,
+            "description": "The time from one time the route goes down to the 
next, longer than duration=",
+            "example": "1m"
+          },
+          {
+            "name": "repeat",
+            "type": "integer",
+            "required": false,
+            "description": "How many times to take the route down (needs 
every= when more than 1)",
+            "example": "3",
+            "min": 1,
+            "max": 100
+          }
+        ],
+        "options": [
+          "expect",
+          "from",
+          "within"
+        ]
+      }
+    ],
+    "options": [
+      {
+        "name": "at",
+        "type": "enum",
+        "required": false,
+        "description": "Narrows the rule to one hook of its target (reply and 
dlq are reached only with at=)",
+        "example": "send",
+        "enum": [
+          "send",
+          "reply",
+          "dlq",
+          "poll",
+          "entry",
+          "step"
+        ]
+      },
+      {
+        "name": "expect",
+        "type": "expectation",
+        "required": false,
+        "description": "An expectation of a route after the rule is added: 
<route>:<count> (at least that many completed) or <route>:<kind><op><count>",
+        "example": "orders:handled<=0"
+      },
+      {
+        "name": "from",
+        "type": "duration",
+        "required": false,
+        "description": "When the rule starts after it is added, such as after 
the warm-up (+30s or 30s)",
+        "example": "+30s"
+      },
+      {
+        "name": "rate",
+        "type": "rate",
+        "required": false,
+        "description": "How often the rule fires: a probability above 0 and at 
most 1, every Nth match, or bursts (every match during the first part of each 
period); without it every match fires",
+        "example": "0.25",
+        "forms": [
+          "0.25",
+          "every:4",
+          "burst:5s\/30s"
+        ]
+      },
+      {
+        "name": "until",
+        "type": "countOrDuration",
+        "required": false,
+        "description": "When the rule ends: a number of matches or a time from 
its start (at most camel.chaos.maxDuration, its default)",
+        "example": "10m"
+      },
+      {
+        "name": "within",
+        "type": "duration",
+        "required": false,
+        "description": "The time of the expectation (by default until the rule 
ends)",
+        "example": "2m"
+      }
+    ],
+    "expectation": {
+      "kinds": [
+        {
+          "name": "completed",
+          "description": "The exchanges completed in the route"
+        },
+        {
+          "name": "handled",
+          "description": "The exchanges handled by the error handler in the 
route"
+        },
+        {
+          "name": "failed",
+          "description": "The exchanges failed in the route"
+        },
+        {
+          "name": "redeliveries",
+          "description": "The redeliveries in the route"
+        }
+      ],
+      "ops": [
+        {
+          "name": ">=",
+          "description": "at least"
+        },
+        {
+          "name": "<=",
+          "description": "at most"
+        },
+        {
+          "name": "=",
+          "description": "exactly"
+        }
+      ]
+    },
+    "limits": {
+      "maxLength": 2000,
+      "maxRepeat": 100,
+      "maxDuration": "camel.chaos.maxDuration (default 10m): the longest 
until, delay, after, from, burst period and stall or lifecycle duration",
+      "time": "a number with a unit (ms, s, m, h, d), also combined as 1m30s; 
a number alone is milliseconds",
+      "quoting": "a value with spaces or ; is quoted with \" or ' (\\\", \\' 
and \\\\ inside quotes)"
+    },
+    "recipes": [
+      {
+        "id": "http-error",
+        "group": "Downstream",
+        "title": "Returns an error (HTTP status)",
+        "description": "An HTTP call fails with a status, as for a service 
that is down or overloaded: tests onException, redelivery and the dead letter 
channel",
+        "rule": "endpoint=${endpoint} error status=${status} rate=${rate} 
until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The HTTP endpoint the route calls",
+            "example": "https:\/\/inventory\/stock",
+            "pick": {
+              "from": "producers",
+              "schemes": [
+                "http",
+                "https",
+                "netty-http",
+                "undertow",
+                "vertx-http"
+              ]
+            }
+          },
+          {
+            "name": "status",
+            "type": "integer",
+            "description": "The HTTP status",
+            "default": "503",
+            "example": "503"
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.25",
+            "example": "0.25"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "connection-refused",
+        "group": "Downstream",
+        "title": "Refuses the connection",
+        "description": "The send fails as when the other side is not there",
+        "rule": "endpoint=${endpoint} error 
exception=java.net.ConnectException rate=${rate} until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint the route sends to",
+            "example": "kafka:orders",
+            "pick": {
+              "from": "producers"
+            }
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.25",
+            "example": "0.25"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "slow",
+        "group": "Downstream",
+        "title": "Is slow",
+        "description": "Each send waits a different time within a range, as a 
slow service does",
+        "rule": "endpoint=${endpoint} delay duration=${duration} rate=${rate} 
until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint the route sends to",
+            "example": "kafka:orders",
+            "pick": {
+              "from": "producers"
+            }
+          },
+          {
+            "name": "duration",
+            "type": "duration",
+            "description": "The time, or a range of times",
+            "default": "200ms..3s",
+            "example": "200ms..3s"
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.5",
+            "example": "0.5"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "timeout",
+        "group": "Downstream",
+        "title": "Times out",
+        "description": "The send waits, then fails without calling the 
producer, as a call that times out",
+        "rule": "endpoint=${endpoint} error after=${after} 
exception=java.net.SocketTimeoutException rate=${rate} until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint the route sends to",
+            "example": "kafka:orders",
+            "pick": {
+              "from": "producers"
+            }
+          },
+          {
+            "name": "after",
+            "type": "duration",
+            "description": "How long before it times out",
+            "default": "5s",
+            "example": "5s"
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.1",
+            "example": "0.1"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "failed-after-success",
+        "group": "Downstream",
+        "title": "Did the work, then failed",
+        "description": "The send succeeds and then fails, as a reply that is 
lost: a redelivery sends it again, so this tests that a duplicate does no harm",
+        "rule": "endpoint=${endpoint} error at=reply until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint the route sends to",
+            "example": "kafka:orders",
+            "pick": {
+              "from": "producers"
+            }
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "How many sends fail",
+            "default": "1",
+            "example": "1"
+          }
+        ]
+      },
+      {
+        "id": "outage",
+        "group": "Downstream",
+        "title": "Is down for a while",
+        "description": "Every send fails during the first part of each period, 
as outage windows",
+        "rule": "endpoint=${endpoint} error rate=burst:${outage}\/${period} 
until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint the route sends to",
+            "example": "kafka:orders",
+            "pick": {
+              "from": "producers"
+            }
+          },
+          {
+            "name": "outage",
+            "type": "duration",
+            "description": "How long each outage lasts",
+            "default": "10s",
+            "example": "10s"
+          },
+          {
+            "name": "period",
+            "type": "duration",
+            "description": "The time from one outage to the next",
+            "default": "1m",
+            "example": "1m"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "corrupt-entry",
+        "group": "Messages",
+        "title": "Arrives corrupt",
+        "description": "The messages that enter the route are changed: random 
headers, the wrong charset, cut in half or empty",
+        "rule": "route=${route} corrupt mode=${mode} rate=${rate} 
until=${until}",
+        "placeholders": [
+          {
+            "name": "route",
+            "type": "route",
+            "description": "The route",
+            "example": "orders",
+            "pick": {
+              "from": "routes"
+            }
+          },
+          {
+            "name": "mode",
+            "type": "enum",
+            "description": "How the message is changed",
+            "default": "headers",
+            "example": "headers",
+            "pick": {
+              "from": "values",
+              "values": [
+                "headers",
+                "charset",
+                "truncate",
+                "empty",
+                "remove-header"
+              ]
+            }
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.25",
+            "example": "0.25"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "empty-body",
+        "group": "Messages",
+        "title": "Is sent with an empty body",
+        "description": "The message sent to the endpoint has an empty body",
+        "rule": "endpoint=${endpoint} corrupt mode=empty rate=${rate} 
until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint the route sends to",
+            "example": "kafka:orders",
+            "pick": {
+              "from": "producers"
+            }
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.25",
+            "example": "0.25"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "missing-header",
+        "group": "Messages",
+        "title": "Is sent without a header",
+        "description": "A header is missing from the message sent to the 
endpoint",
+        "rule": "endpoint=${endpoint} corrupt mode=remove-header 
header=${header} rate=${rate} until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint the route sends to",
+            "example": "kafka:orders",
+            "pick": {
+              "from": "producers"
+            }
+          },
+          {
+            "name": "header",
+            "type": "string",
+            "description": "The header, or a pattern with *",
+            "example": "X-Trace-Id",
+            "pick": {
+              "from": "headers"
+            }
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.25",
+            "example": "0.25"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "poll-error",
+        "group": "Consumers and routes",
+        "title": "Polls fail",
+        "description": "The polls of a scheduled poll consumer fail, as when 
the source is not reachable",
+        "rule": "endpoint=${endpoint} error at=poll rate=${rate} 
until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint a route consumes with a scheduled 
poll",
+            "example": "file:inbox",
+            "pick": {
+              "from": "pollingConsumers"
+            }
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.5",
+            "example": "0.5"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "poll-skip",
+        "group": "Consumers and routes",
+        "title": "Polls are skipped",
+        "description": "The polls of a scheduled poll consumer are skipped, as 
a consumer that falls behind",
+        "rule": "endpoint=${endpoint} drop at=poll rate=${rate} 
until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint a route consumes with a scheduled 
poll",
+            "example": "file:inbox",
+            "pick": {
+              "from": "pollingConsumers"
+            }
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.5",
+            "example": "0.5"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "stall",
+        "group": "Consumers and routes",
+        "title": "The route stalls",
+        "description": "The route is suspended for a while, and its messages 
wait",
+        "rule": "route=${route} stall duration=${duration}",
+        "placeholders": [
+          {
+            "name": "route",
+            "type": "route",
+            "description": "The route",
+            "example": "orders",
+            "pick": {
+              "from": "routes"
+            }
+          },
+          {
+            "name": "duration",
+            "type": "duration",
+            "description": "How long it stalls",
+            "default": "20s",
+            "example": "20s"
+          }
+        ]
+      },
+      {
+        "id": "flap",
+        "group": "Consumers and routes",
+        "title": "The route flaps",
+        "description": "The route stops and starts again several times",
+        "rule": "route=${route} lifecycle action=stop duration=${duration} 
repeat=${repeat} every=${every}",
+        "placeholders": [
+          {
+            "name": "route",
+            "type": "route",
+            "description": "The route",
+            "example": "orders",
+            "pick": {
+              "from": "routes"
+            }
+          },
+          {
+            "name": "duration",
+            "type": "duration",
+            "description": "How long it is down each time",
+            "default": "10s",
+            "example": "10s"
+          },
+          {
+            "name": "repeat",
+            "type": "integer",
+            "description": "How many times",
+            "default": "3",
+            "example": "3"
+          },
+          {
+            "name": "every",
+            "type": "duration",
+            "description": "The time from one time to the next",
+            "default": "1m",
+            "example": "1m"
+          }
+        ]
+      },
+      {
+        "id": "removed",
+        "group": "Consumers and routes",
+        "title": "The route is removed",
+        "description": "The route is removed, and added back after a while",
+        "rule": "route=${route} lifecycle action=remove duration=${duration}",
+        "placeholders": [
+          {
+            "name": "route",
+            "type": "route",
+            "description": "The route",
+            "example": "orders",
+            "pick": {
+              "from": "routes"
+            }
+          },
+          {
+            "name": "duration",
+            "type": "duration",
+            "description": "How long it is gone",
+            "default": "30s",
+            "example": "30s"
+          }
+        ]
+      },
+      {
+        "id": "dlq-unreachable",
+        "group": "Error handling",
+        "title": "The dead letter channel is unreachable",
+        "description": "The send of the dead letter channel fails, so the 
messages it handles are lost (add it before the rule that makes them fail)",
+        "rule": "endpoint=${endpoint} error at=dlq",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint the route sends to",
+            "example": "kafka:orders",
+            "pick": {
+              "from": "producers"
+            }
+          }
+        ]
+      },
+      {
+        "id": "step-error",
+        "group": "Error handling",
+        "title": "A step fails",
+        "description": "A step of a route fails, as when its processor throws",
+        "rule": "step=${step} error rate=${rate} until=${until}",
+        "placeholders": [
+          {
+            "name": "step",
+            "type": "step",
+            "description": "The step",
+            "example": "enrich",
+            "pick": {
+              "from": "steps"
+            }
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.1",
+            "example": "0.1"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "step-slow",
+        "group": "Error handling",
+        "title": "A step is slow",
+        "description": "A step of a route waits before its processor is 
called",
+        "rule": "step=${step} delay duration=${duration} rate=${rate} 
until=${until}",
+        "placeholders": [
+          {
+            "name": "step",
+            "type": "step",
+            "description": "The step",
+            "example": "enrich",
+            "pick": {
+              "from": "steps"
+            }
+          },
+          {
+            "name": "duration",
+            "type": "duration",
+            "description": "The time, or a range of times",
+            "default": "1s",
+            "example": "1s"
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.25",
+            "example": "0.25"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      }
+    ]
+  }
+}
+
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
index 8032ef961160..5edfa1a8982a 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
@@ -617,6 +617,29 @@ exchange gets which draw: with several threads (concurrent 
consumers, parallel p
 so two runs with the same seed can give different reports. With one thread and 
the same exchanges in the same order,
 the same seed gives the same faults.
 
+== The model of the rules
+
+The rules are also described as data, for the tools that help to write them (a 
form in a terminal UI, an AI that turns
+a request into rules, the docs): 
`META-INF/org/apache/camel/chaos/chaos-model.json` in the camel-chaos JAR, also 
in the
+Camel catalog (`CamelCatalog.chaosModelJSon()`). It has the targets, the hooks 
of each target and the faults each one
+offers (and whether it is reached only with `at=`), the parameters of each 
fault with their types and limits, the
+options, the kinds and ops of an expectation, the limits, and recipes: known 
rules in plain words, grouped by what goes
+wrong, each a rule with placeholders (`${endpoint}`, `${rate}`) and what to 
pick them from (the endpoints the routes
+send to, the routes, the steps), such as:
+
+[source,json]
+----
+{
+  "id": "failed-after-success",
+  "group": "Downstream",
+  "title": "Did the work, then failed",
+  "rule": "endpoint=${endpoint} error at=reply until=${until}"
+}
+----
+
+The model is made from the tables of the parser, so it says what the parser 
accepts; a tool still adds a rule through
+`ChaosManager` or the CLI connector, which check it as for any other rule.
+
 == Limits
 
 * A rule that ended stays in the rules (and the report) as `ended`. The hooks 
skip it at once, and the next change
diff --git 
a/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/CamelCatalog.java
 
b/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/CamelCatalog.java
index a0bbd7f2e0e4..fd9e8d349236 100644
--- 
a/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/CamelCatalog.java
+++ 
b/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/CamelCatalog.java
@@ -679,6 +679,16 @@ public interface CamelCatalog {
      */
     String devConsolesOpenApiSpec();
 
+    /**
+     * The model of the chaos rules of camel-chaos, as JSON: the targets, the 
hooks of each target and the faults each
+     * offers, the parameters of each fault, the options, the expectations, 
the limits, and the recipes (known rules as
+     * templates), for the tools that help to write rules.
+     *
+     * @return the model, or null when the catalog has none
+     * @since  4.24
+     */
+    String chaosModelJSon();
+
     /**
      * Lists all the models (EIPs) summary details in JSon
      */
diff --git 
a/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/DefaultCamelCatalog.java
 
b/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/DefaultCamelCatalog.java
index 9dd4534fa102..71f78886ae58 100644
--- 
a/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/DefaultCamelCatalog.java
+++ 
b/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/DefaultCamelCatalog.java
@@ -563,6 +563,11 @@ public class DefaultCamelCatalog extends 
AbstractCachingCamelCatalog implements
         return cache(BASE_RESOURCE_DIR + "/dev-consoles-openapi.json", 
this::loadResource);
     }
 
+    @Override
+    public String chaosModelJSon() {
+        return cache(BASE_RESOURCE_DIR + "/chaos-model.json", 
this::loadResource);
+    }
+
     @Override
     public String listModelsAsJson() {
         return cache(LIST_MODELS_AS_JSON, () -> 
JsonMapper.serialize(findModelNames().stream()
diff --git 
a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
 
b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
index f7dfaf283c34..bef4e38d383f 100644
--- 
a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
+++ 
b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
@@ -2197,6 +2197,17 @@ public class CamelCatalogTest {
         Assertions.assertTrue(advisory.getComponents().contains("camel-bean"));
     }
 
+    @Test
+    public void chaosModel() {
+        String json = catalog.chaosModelJSon();
+        Assertions.assertNotNull(json);
+
+        JsonObject chaos = JsonMapper.deserialize(json).getJsonObject("chaos");
+        Assertions.assertNotNull(chaos);
+        Assertions.assertFalse(chaos.getCollection("faults").isEmpty());
+        Assertions.assertFalse(chaos.getCollection("recipes").isEmpty());
+    }
+
     @Test
     public void devConsolesOpenApiSpec() {
         String json = catalog.devConsolesOpenApiSpec();
diff --git 
a/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
 
b/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
new file mode 100644
index 000000000000..7505fb834508
--- /dev/null
+++ 
b/components/camel-chaos/src/generated/resources/META-INF/org/apache/camel/chaos/chaos-model.json
@@ -0,0 +1,1078 @@
+{
+  "chaos": {
+    "description": "The chaos rules of camel-chaos: a rule is one line, the 
target, the fault and its parameters as name=value, then the options (such as 
at, rate, from, until) and optionally an expectation",
+    "syntax": "<target>=<pattern> <fault> [<parameter>=<value> ...] 
[<option>=<value> ...]",
+    "targets": [
+      {
+        "name": "route",
+        "description": "A route id, with * for any characters; one route id 
(no pattern) for stall and lifecycle",
+        "pattern": true,
+        "pick": "routes"
+      },
+      {
+        "name": "endpoint",
+        "description": "An endpoint URI pattern, with * for any characters; 
matched with or without the query parameters, and the \/\/ after the scheme 
does not count (kafka:orders* matches kafka:\/\/orders?brokers=b1)",
+        "pattern": true,
+        "pick": "endpoints"
+      },
+      {
+        "name": "component",
+        "description": "The scheme of the endpoint URIs, with * for any 
characters (no ':')",
+        "pattern": true,
+        "pick": "components"
+      },
+      {
+        "name": "step",
+        "description": "A step id (to1, or the id set with id(\"enrich\")), 
with * for any characters",
+        "pattern": true,
+        "pick": "steps"
+      }
+    ],
+    "hooks": [
+      {
+        "name": "send",
+        "description": "The sends to a matching endpoint (to, toD, wireTap, 
enrich, recipientList, a ProducerTemplate...), before the producer is called, 
inside the error handler",
+        "targets": [
+          "endpoint",
+          "component"
+        ],
+        "faults": [
+          "error",
+          "delay",
+          "drop",
+          "corrupt"
+        ],
+        "onlyWithAt": false
+      },
+      {
+        "name": "reply",
+        "description": "The reply of such a send, after the producer 
succeeded: its side effect happened (only with at=reply)",
+        "targets": [
+          "endpoint",
+          "component"
+        ],
+        "faults": [
+          "error",
+          "corrupt"
+        ],
+        "onlyWithAt": true
+      },
+      {
+        "name": "dlq",
+        "description": "The send of the dead letter channel to a matching 
endpoint (only with at=dlq)",
+        "targets": [
+          "endpoint",
+          "component"
+        ],
+        "faults": [
+          "error",
+          "delay",
+          "drop",
+          "corrupt"
+        ],
+        "onlyWithAt": true
+      },
+      {
+        "name": "poll",
+        "description": "The polls of the scheduled poll consumers of the 
routes",
+        "targets": [
+          "endpoint",
+          "component"
+        ],
+        "faults": [
+          "error",
+          "delay",
+          "drop"
+        ],
+        "onlyWithAt": false
+      },
+      {
+        "name": "entry",
+        "description": "Each exchange that enters a matching route, before the 
first step",
+        "targets": [
+          "route"
+        ],
+        "faults": [
+          "corrupt"
+        ],
+        "onlyWithAt": false
+      },
+      {
+        "name": "step",
+        "description": "Each matching step of a route, right before its 
processor is called",
+        "targets": [
+          "step"
+        ],
+        "faults": [
+          "error",
+          "delay"
+        ],
+        "onlyWithAt": false
+      }
+    ],
+    "faults": [
+      {
+        "name": "error",
+        "description": "Fails the send, the poll or the step with an exception 
(a ChaosException without exception=), or the failure of an HTTP component for 
status=",
+        "targets": [
+          "component",
+          "endpoint",
+          "step"
+        ],
+        "hooks": [
+          "send",
+          "reply",
+          "dlq",
+          "poll",
+          "step"
+        ],
+        "parameters": [
+          {
+            "name": "after",
+            "type": "duration",
+            "required": false,
+            "description": "Waits that long first, then fails, as a call that 
times out; the producer is not called",
+            "example": "5s",
+            "notAt": [
+              "reply"
+            ]
+          },
+          {
+            "name": "body",
+            "type": "string",
+            "required": false,
+            "description": "The body of the HTTP response (needs status=)",
+            "example": "no stock"
+          },
+          {
+            "name": "exception",
+            "type": "class",
+            "required": false,
+            "description": "The class of the exception: a public Throwable 
with a public constructor taking a String or none (not an 
InterruptedException)",
+            "example": "java.net.ConnectException"
+          },
+          {
+            "name": "header.<name>",
+            "type": "string",
+            "required": false,
+            "description": "A header of the HTTP response, such as 
header.Retry-After=5 (needs status=)",
+            "example": "5",
+            "prefix": "header."
+          },
+          {
+            "name": "message",
+            "type": "string",
+            "required": false,
+            "description": "The message of the exception (the class needs a 
constructor taking a String)",
+            "example": "broker down"
+          },
+          {
+            "name": "status",
+            "type": "integer",
+            "required": false,
+            "description": "The failure of an HTTP component for this status 
(not with exception= or message=)",
+            "example": "503",
+            "min": 300,
+            "max": 599,
+            "schemes": [
+              "http",
+              "https",
+              "netty-http",
+              "undertow",
+              "vertx-http"
+            ]
+          }
+        ],
+        "options": [
+          "at",
+          "expect",
+          "from",
+          "rate",
+          "until",
+          "within"
+        ]
+      },
+      {
+        "name": "delay",
+        "description": "Waits before the send, the poll or the step, as for a 
slow endpoint",
+        "targets": [
+          "component",
+          "endpoint",
+          "step"
+        ],
+        "hooks": [
+          "send",
+          "dlq",
+          "poll",
+          "step"
+        ],
+        "parameters": [
+          {
+            "name": "duration",
+            "type": "duration",
+            "required": true,
+            "description": "How long to wait, or a range such as 100ms..2s for 
a draw between the two times (from the seeded sequence of the rule)",
+            "example": "500ms",
+            "range": true
+          }
+        ],
+        "options": [
+          "at",
+          "expect",
+          "from",
+          "rate",
+          "until",
+          "within"
+        ]
+      },
+      {
+        "name": "drop",
+        "description": "A send that reports success without calling the 
producer, or a poll that is skipped",
+        "targets": [
+          "component",
+          "endpoint"
+        ],
+        "hooks": [
+          "send",
+          "dlq",
+          "poll"
+        ],
+        "parameters": [
+          
+        ],
+        "options": [
+          "at",
+          "expect",
+          "from",
+          "rate",
+          "until",
+          "within"
+        ]
+      },
+      {
+        "name": "corrupt",
+        "description": "Changes the message: its headers, the charset of its 
body, or its body",
+        "targets": [
+          "component",
+          "endpoint",
+          "route"
+        ],
+        "hooks": [
+          "send",
+          "reply",
+          "dlq",
+          "entry"
+        ],
+        "parameters": [
+          {
+            "name": "header",
+            "type": "string",
+            "required": false,
+            "description": "The headers mode=remove-header removes: a name or 
a pattern with *, ignoring case (without it every header that is not Camel*)",
+            "example": "X-Trace-*"
+          },
+          {
+            "name": "mode",
+            "type": "enum",
+            "required": true,
+            "description": "headers: the headers that are not Camel* get 
random values; charset: a text body becomes UTF-16 bytes; truncate: a String, 
byte[] or stream body ends after half of it; empty: the body becomes empty of 
its kind (or null); remove-header: removes headers",
+            "example": "headers",
+            "enum": [
+              "headers",
+              "charset",
+              "truncate",
+              "empty",
+              "remove-header"
+            ]
+          }
+        ],
+        "options": [
+          "at",
+          "expect",
+          "from",
+          "rate",
+          "until",
+          "within"
+        ]
+      },
+      {
+        "name": "lifecycle",
+        "description": "Suspends, stops or removes the route of a route=<id> 
target, and brings it back after the duration; repeat= takes it down more than 
once",
+        "targets": [
+          "route"
+        ],
+        "hooks": [
+          
+        ],
+        "parameters": [
+          {
+            "name": "action",
+            "type": "enum",
+            "required": true,
+            "description": "What to do to the route",
+            "example": "stop",
+            "enum": [
+              "suspend",
+              "stop",
+              "remove"
+            ]
+          },
+          {
+            "name": "duration",
+            "type": "duration",
+            "required": true,
+            "description": "How long the route stays down (at most 
camel.chaos.maxDuration)",
+            "example": "20s"
+          },
+          {
+            "name": "every",
+            "type": "duration",
+            "required": false,
+            "description": "The time from one time the route goes down to the 
next, longer than duration=",
+            "example": "1m"
+          },
+          {
+            "name": "repeat",
+            "type": "integer",
+            "required": false,
+            "description": "How many times to take the route down (needs 
every= when more than 1)",
+            "example": "3",
+            "min": 1,
+            "max": 100
+          }
+        ],
+        "options": [
+          "expect",
+          "from",
+          "within"
+        ]
+      },
+      {
+        "name": "stall",
+        "description": "Short for lifecycle action=suspend: the route stalls 
for the duration",
+        "targets": [
+          "route"
+        ],
+        "hooks": [
+          
+        ],
+        "shortFor": "lifecycle action=suspend",
+        "parameters": [
+          {
+            "name": "duration",
+            "type": "duration",
+            "required": true,
+            "description": "How long the route stays down (at most 
camel.chaos.maxDuration)",
+            "example": "20s"
+          },
+          {
+            "name": "every",
+            "type": "duration",
+            "required": false,
+            "description": "The time from one time the route goes down to the 
next, longer than duration=",
+            "example": "1m"
+          },
+          {
+            "name": "repeat",
+            "type": "integer",
+            "required": false,
+            "description": "How many times to take the route down (needs 
every= when more than 1)",
+            "example": "3",
+            "min": 1,
+            "max": 100
+          }
+        ],
+        "options": [
+          "expect",
+          "from",
+          "within"
+        ]
+      }
+    ],
+    "options": [
+      {
+        "name": "at",
+        "type": "enum",
+        "required": false,
+        "description": "Narrows the rule to one hook of its target (reply and 
dlq are reached only with at=)",
+        "example": "send",
+        "enum": [
+          "send",
+          "reply",
+          "dlq",
+          "poll",
+          "entry",
+          "step"
+        ]
+      },
+      {
+        "name": "expect",
+        "type": "expectation",
+        "required": false,
+        "description": "An expectation of a route after the rule is added: 
<route>:<count> (at least that many completed) or <route>:<kind><op><count>",
+        "example": "orders:handled<=0"
+      },
+      {
+        "name": "from",
+        "type": "duration",
+        "required": false,
+        "description": "When the rule starts after it is added, such as after 
the warm-up (+30s or 30s)",
+        "example": "+30s"
+      },
+      {
+        "name": "rate",
+        "type": "rate",
+        "required": false,
+        "description": "How often the rule fires: a probability above 0 and at 
most 1, every Nth match, or bursts (every match during the first part of each 
period); without it every match fires",
+        "example": "0.25",
+        "forms": [
+          "0.25",
+          "every:4",
+          "burst:5s\/30s"
+        ]
+      },
+      {
+        "name": "until",
+        "type": "countOrDuration",
+        "required": false,
+        "description": "When the rule ends: a number of matches or a time from 
its start (at most camel.chaos.maxDuration, its default)",
+        "example": "10m"
+      },
+      {
+        "name": "within",
+        "type": "duration",
+        "required": false,
+        "description": "The time of the expectation (by default until the rule 
ends)",
+        "example": "2m"
+      }
+    ],
+    "expectation": {
+      "kinds": [
+        {
+          "name": "completed",
+          "description": "The exchanges completed in the route"
+        },
+        {
+          "name": "handled",
+          "description": "The exchanges handled by the error handler in the 
route"
+        },
+        {
+          "name": "failed",
+          "description": "The exchanges failed in the route"
+        },
+        {
+          "name": "redeliveries",
+          "description": "The redeliveries in the route"
+        }
+      ],
+      "ops": [
+        {
+          "name": ">=",
+          "description": "at least"
+        },
+        {
+          "name": "<=",
+          "description": "at most"
+        },
+        {
+          "name": "=",
+          "description": "exactly"
+        }
+      ]
+    },
+    "limits": {
+      "maxLength": 2000,
+      "maxRepeat": 100,
+      "maxDuration": "camel.chaos.maxDuration (default 10m): the longest 
until, delay, after, from, burst period and stall or lifecycle duration",
+      "time": "a number with a unit (ms, s, m, h, d), also combined as 1m30s; 
a number alone is milliseconds",
+      "quoting": "a value with spaces or ; is quoted with \" or ' (\\\", \\' 
and \\\\ inside quotes)"
+    },
+    "recipes": [
+      {
+        "id": "http-error",
+        "group": "Downstream",
+        "title": "Returns an error (HTTP status)",
+        "description": "An HTTP call fails with a status, as for a service 
that is down or overloaded: tests onException, redelivery and the dead letter 
channel",
+        "rule": "endpoint=${endpoint} error status=${status} rate=${rate} 
until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The HTTP endpoint the route calls",
+            "example": "https:\/\/inventory\/stock",
+            "pick": {
+              "from": "producers",
+              "schemes": [
+                "http",
+                "https",
+                "netty-http",
+                "undertow",
+                "vertx-http"
+              ]
+            }
+          },
+          {
+            "name": "status",
+            "type": "integer",
+            "description": "The HTTP status",
+            "default": "503",
+            "example": "503"
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.25",
+            "example": "0.25"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "connection-refused",
+        "group": "Downstream",
+        "title": "Refuses the connection",
+        "description": "The send fails as when the other side is not there",
+        "rule": "endpoint=${endpoint} error 
exception=java.net.ConnectException rate=${rate} until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint the route sends to",
+            "example": "kafka:orders",
+            "pick": {
+              "from": "producers"
+            }
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.25",
+            "example": "0.25"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "slow",
+        "group": "Downstream",
+        "title": "Is slow",
+        "description": "Each send waits a different time within a range, as a 
slow service does",
+        "rule": "endpoint=${endpoint} delay duration=${duration} rate=${rate} 
until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint the route sends to",
+            "example": "kafka:orders",
+            "pick": {
+              "from": "producers"
+            }
+          },
+          {
+            "name": "duration",
+            "type": "duration",
+            "description": "The time, or a range of times",
+            "default": "200ms..3s",
+            "example": "200ms..3s"
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.5",
+            "example": "0.5"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "timeout",
+        "group": "Downstream",
+        "title": "Times out",
+        "description": "The send waits, then fails without calling the 
producer, as a call that times out",
+        "rule": "endpoint=${endpoint} error after=${after} 
exception=java.net.SocketTimeoutException rate=${rate} until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint the route sends to",
+            "example": "kafka:orders",
+            "pick": {
+              "from": "producers"
+            }
+          },
+          {
+            "name": "after",
+            "type": "duration",
+            "description": "How long before it times out",
+            "default": "5s",
+            "example": "5s"
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.1",
+            "example": "0.1"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "failed-after-success",
+        "group": "Downstream",
+        "title": "Did the work, then failed",
+        "description": "The send succeeds and then fails, as a reply that is 
lost: a redelivery sends it again, so this tests that a duplicate does no harm",
+        "rule": "endpoint=${endpoint} error at=reply until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint the route sends to",
+            "example": "kafka:orders",
+            "pick": {
+              "from": "producers"
+            }
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "How many sends fail",
+            "default": "1",
+            "example": "1"
+          }
+        ]
+      },
+      {
+        "id": "outage",
+        "group": "Downstream",
+        "title": "Is down for a while",
+        "description": "Every send fails during the first part of each period, 
as outage windows",
+        "rule": "endpoint=${endpoint} error rate=burst:${outage}\/${period} 
until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint the route sends to",
+            "example": "kafka:orders",
+            "pick": {
+              "from": "producers"
+            }
+          },
+          {
+            "name": "outage",
+            "type": "duration",
+            "description": "How long each outage lasts",
+            "default": "10s",
+            "example": "10s"
+          },
+          {
+            "name": "period",
+            "type": "duration",
+            "description": "The time from one outage to the next",
+            "default": "1m",
+            "example": "1m"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "corrupt-entry",
+        "group": "Messages",
+        "title": "Arrives corrupt",
+        "description": "The messages that enter the route are changed: random 
headers, the wrong charset, cut in half or empty",
+        "rule": "route=${route} corrupt mode=${mode} rate=${rate} 
until=${until}",
+        "placeholders": [
+          {
+            "name": "route",
+            "type": "route",
+            "description": "The route",
+            "example": "orders",
+            "pick": {
+              "from": "routes"
+            }
+          },
+          {
+            "name": "mode",
+            "type": "enum",
+            "description": "How the message is changed",
+            "default": "headers",
+            "example": "headers",
+            "pick": {
+              "from": "values",
+              "values": [
+                "headers",
+                "charset",
+                "truncate",
+                "empty",
+                "remove-header"
+              ]
+            }
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.25",
+            "example": "0.25"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "empty-body",
+        "group": "Messages",
+        "title": "Is sent with an empty body",
+        "description": "The message sent to the endpoint has an empty body",
+        "rule": "endpoint=${endpoint} corrupt mode=empty rate=${rate} 
until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint the route sends to",
+            "example": "kafka:orders",
+            "pick": {
+              "from": "producers"
+            }
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.25",
+            "example": "0.25"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "missing-header",
+        "group": "Messages",
+        "title": "Is sent without a header",
+        "description": "A header is missing from the message sent to the 
endpoint",
+        "rule": "endpoint=${endpoint} corrupt mode=remove-header 
header=${header} rate=${rate} until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint the route sends to",
+            "example": "kafka:orders",
+            "pick": {
+              "from": "producers"
+            }
+          },
+          {
+            "name": "header",
+            "type": "string",
+            "description": "The header, or a pattern with *",
+            "example": "X-Trace-Id",
+            "pick": {
+              "from": "headers"
+            }
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.25",
+            "example": "0.25"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "poll-error",
+        "group": "Consumers and routes",
+        "title": "Polls fail",
+        "description": "The polls of a scheduled poll consumer fail, as when 
the source is not reachable",
+        "rule": "endpoint=${endpoint} error at=poll rate=${rate} 
until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint a route consumes with a scheduled 
poll",
+            "example": "file:inbox",
+            "pick": {
+              "from": "pollingConsumers"
+            }
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.5",
+            "example": "0.5"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "poll-skip",
+        "group": "Consumers and routes",
+        "title": "Polls are skipped",
+        "description": "The polls of a scheduled poll consumer are skipped, as 
a consumer that falls behind",
+        "rule": "endpoint=${endpoint} drop at=poll rate=${rate} 
until=${until}",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint a route consumes with a scheduled 
poll",
+            "example": "file:inbox",
+            "pick": {
+              "from": "pollingConsumers"
+            }
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.5",
+            "example": "0.5"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "stall",
+        "group": "Consumers and routes",
+        "title": "The route stalls",
+        "description": "The route is suspended for a while, and its messages 
wait",
+        "rule": "route=${route} stall duration=${duration}",
+        "placeholders": [
+          {
+            "name": "route",
+            "type": "route",
+            "description": "The route",
+            "example": "orders",
+            "pick": {
+              "from": "routes"
+            }
+          },
+          {
+            "name": "duration",
+            "type": "duration",
+            "description": "How long it stalls",
+            "default": "20s",
+            "example": "20s"
+          }
+        ]
+      },
+      {
+        "id": "flap",
+        "group": "Consumers and routes",
+        "title": "The route flaps",
+        "description": "The route stops and starts again several times",
+        "rule": "route=${route} lifecycle action=stop duration=${duration} 
repeat=${repeat} every=${every}",
+        "placeholders": [
+          {
+            "name": "route",
+            "type": "route",
+            "description": "The route",
+            "example": "orders",
+            "pick": {
+              "from": "routes"
+            }
+          },
+          {
+            "name": "duration",
+            "type": "duration",
+            "description": "How long it is down each time",
+            "default": "10s",
+            "example": "10s"
+          },
+          {
+            "name": "repeat",
+            "type": "integer",
+            "description": "How many times",
+            "default": "3",
+            "example": "3"
+          },
+          {
+            "name": "every",
+            "type": "duration",
+            "description": "The time from one time to the next",
+            "default": "1m",
+            "example": "1m"
+          }
+        ]
+      },
+      {
+        "id": "removed",
+        "group": "Consumers and routes",
+        "title": "The route is removed",
+        "description": "The route is removed, and added back after a while",
+        "rule": "route=${route} lifecycle action=remove duration=${duration}",
+        "placeholders": [
+          {
+            "name": "route",
+            "type": "route",
+            "description": "The route",
+            "example": "orders",
+            "pick": {
+              "from": "routes"
+            }
+          },
+          {
+            "name": "duration",
+            "type": "duration",
+            "description": "How long it is gone",
+            "default": "30s",
+            "example": "30s"
+          }
+        ]
+      },
+      {
+        "id": "dlq-unreachable",
+        "group": "Error handling",
+        "title": "The dead letter channel is unreachable",
+        "description": "The send of the dead letter channel fails, so the 
messages it handles are lost (add it before the rule that makes them fail)",
+        "rule": "endpoint=${endpoint} error at=dlq",
+        "placeholders": [
+          {
+            "name": "endpoint",
+            "type": "endpoint",
+            "description": "The endpoint the route sends to",
+            "example": "kafka:orders",
+            "pick": {
+              "from": "producers"
+            }
+          }
+        ]
+      },
+      {
+        "id": "step-error",
+        "group": "Error handling",
+        "title": "A step fails",
+        "description": "A step of a route fails, as when its processor throws",
+        "rule": "step=${step} error rate=${rate} until=${until}",
+        "placeholders": [
+          {
+            "name": "step",
+            "type": "step",
+            "description": "The step",
+            "example": "enrich",
+            "pick": {
+              "from": "steps"
+            }
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.1",
+            "example": "0.1"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      },
+      {
+        "id": "step-slow",
+        "group": "Error handling",
+        "title": "A step is slow",
+        "description": "A step of a route waits before its processor is 
called",
+        "rule": "step=${step} delay duration=${duration} rate=${rate} 
until=${until}",
+        "placeholders": [
+          {
+            "name": "step",
+            "type": "step",
+            "description": "The step",
+            "example": "enrich",
+            "pick": {
+              "from": "steps"
+            }
+          },
+          {
+            "name": "duration",
+            "type": "duration",
+            "description": "The time, or a range of times",
+            "default": "1s",
+            "example": "1s"
+          },
+          {
+            "name": "rate",
+            "type": "rate",
+            "description": "How often: a probability, every:N or 
burst:<time>\/<period>",
+            "default": "0.25",
+            "example": "0.25"
+          },
+          {
+            "name": "until",
+            "type": "countOrDuration",
+            "description": "When the rule ends: a number of matches or a time",
+            "default": "10m",
+            "example": "10m"
+          }
+        ]
+      }
+    ]
+  }
+}
+
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc 
b/components/camel-chaos/src/main/docs/chaos.adoc
index 8032ef961160..5edfa1a8982a 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -617,6 +617,29 @@ exchange gets which draw: with several threads (concurrent 
consumers, parallel p
 so two runs with the same seed can give different reports. With one thread and 
the same exchanges in the same order,
 the same seed gives the same faults.
 
+== The model of the rules
+
+The rules are also described as data, for the tools that help to write them (a 
form in a terminal UI, an AI that turns
+a request into rules, the docs): 
`META-INF/org/apache/camel/chaos/chaos-model.json` in the camel-chaos JAR, also 
in the
+Camel catalog (`CamelCatalog.chaosModelJSon()`). It has the targets, the hooks 
of each target and the faults each one
+offers (and whether it is reached only with `at=`), the parameters of each 
fault with their types and limits, the
+options, the kinds and ops of an expectation, the limits, and recipes: known 
rules in plain words, grouped by what goes
+wrong, each a rule with placeholders (`${endpoint}`, `${rate}`) and what to 
pick them from (the endpoints the routes
+send to, the routes, the steps), such as:
+
+[source,json]
+----
+{
+  "id": "failed-after-success",
+  "group": "Downstream",
+  "title": "Did the work, then failed",
+  "rule": "endpoint=${endpoint} error at=reply until=${until}"
+}
+----
+
+The model is made from the tables of the parser, so it says what the parser 
accepts; a tool still adds a rule through
+`ChaosManager` or the CLI connector, which check it as for any other rule.
+
 == Limits
 
 * A rule that ended stays in the rules (and the report) as `ended`. The hooks 
skip it at once, and the next change
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
new file mode 100644
index 000000000000..00ab7dc974e2
--- /dev/null
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosModel.java
@@ -0,0 +1,602 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.chaos;
+
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.camel.component.chaos.ChaosRule.Action;
+import org.apache.camel.component.chaos.ChaosRule.CorruptMode;
+import org.apache.camel.component.chaos.ChaosRule.Expectation;
+import org.apache.camel.component.chaos.ChaosRule.Fault;
+import org.apache.camel.component.chaos.ChaosRule.Hook;
+import org.apache.camel.component.chaos.ChaosRule.Target;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * The model of the chaos rules as data, for the tools that help to write them 
(the chaos screen of the terminal UI, an
+ * AI that turns a prompt into rules, the docs): the targets, the hooks of 
each target and the faults each hook offers,
+ * the parameters of each fault, the general options, the expectations, the 
limits, and the recipes (known rules in
+ * plain words, as templates).
+ * <p/>
+ * The structure comes from the tables of {@link ChaosRuleParser}, so it 
cannot drift from what the parser accepts; the
+ * descriptions and the recipes are written here. {@code ChaosModelTest} 
writes {@link #FILE} from it (and fails when
+ * the committed file differs), and checks that every recipe parses. The 
catalog copies the file
+ * ({@code CamelCatalog.chaosModelJSon()}).
+ */
+final class ChaosModel {
+
+    /** Where the model is, in the jar and under {@code 
src/generated/resources}. */
+    static final String FILE = 
"META-INF/org/apache/camel/chaos/chaos-model.json";
+
+    private ChaosModel() {
+    }
+
+    /** The model as JSON, pretty printed (the content of {@link #FILE}). */
+    static String toJson() {
+        return Jsoner.prettyPrint(model().toJson(), 2) + "\n";
+    }
+
+    static JsonObject model() {
+        JsonObject chaos = new JsonObject();
+        chaos.put("description", "The chaos rules of camel-chaos: a rule is 
one line, the target, the fault and its"
+                                 + " parameters as name=value, then the 
options (such as at, rate, from, until) and"
+                                 + " optionally an expectation");
+        chaos.put("syntax", "<target>=<pattern> <fault> [<parameter>=<value> 
...] [<option>=<value> ...]");
+        chaos.put("targets", targets());
+        chaos.put("hooks", hooks());
+        chaos.put("faults", faults());
+        chaos.put("options", options());
+        chaos.put("expectation", expectation());
+        chaos.put("limits", limits());
+        chaos.put("recipes", recipes());
+        JsonObject root = new JsonObject();
+        root.put("chaos", chaos);
+        return root;
+    }
+
+    // ---- the targets and the hooks ----
+
+    private static JsonArray targets() {
+        JsonArray answer = new JsonArray();
+        for (Target target : Target.values()) {
+            JsonObject jo = new JsonObject();
+            jo.put("name", target.key);
+            switch (target) {
+                case ENDPOINT -> {
+                    jo.put("description", "An endpoint URI pattern, with * for 
any characters; matched with or without"
+                                          + " the query parameters, and the // 
after the scheme does not count"
+                                          + " (kafka:orders* matches 
kafka://orders?brokers=b1)");
+                    jo.put("pattern", true);
+                    jo.put("pick", "endpoints");
+                }
+                case COMPONENT -> {
+                    jo.put("description", "The scheme of the endpoint URIs, 
with * for any characters (no ':')");
+                    jo.put("pattern", true);
+                    jo.put("pick", "components");
+                }
+                case ROUTE -> {
+                    jo.put("description", "A route id, with * for any 
characters; one route id (no pattern) for"
+                                          + " stall and lifecycle");
+                    jo.put("pattern", true);
+                    jo.put("pick", "routes");
+                }
+                case STEP -> {
+                    jo.put("description", "A step id (to1, or the id set with 
id(\"enrich\")), with * for any"
+                                          + " characters");
+                    jo.put("pattern", true);
+                    jo.put("pick", "steps");
+                }
+                default -> throw new IllegalStateException("Unknown target " + 
target);
+            }
+            answer.add(jo);
+        }
+        return answer;
+    }
+
+    private static JsonArray hooks() {
+        // the targets and the faults of each hook, from the hooks of each 
target the parser offers
+        Map<Hook, List<String>> targets = new LinkedHashMap<>();
+        Map<Hook, Set<Fault>> faults = new LinkedHashMap<>();
+        for (Target target : Target.values()) {
+            for (Map.Entry<Hook, Set<Fault>> entry : 
ChaosRuleParser.offered(target).entrySet()) {
+                targets.computeIfAbsent(entry.getKey(), k -> new 
ArrayList<>()).add(target.key);
+                faults.computeIfAbsent(entry.getKey(), k -> 
EnumSet.noneOf(Fault.class)).addAll(entry.getValue());
+            }
+        }
+        JsonArray answer = new JsonArray();
+        for (Hook hook : Hook.values()) {
+            JsonObject jo = new JsonObject();
+            jo.put("name", hook.key);
+            jo.put("description", switch (hook) {
+                case SEND -> "The sends to a matching endpoint (to, toD, 
wireTap, enrich, recipientList, a"
+                             + " ProducerTemplate...), before the producer is 
called, inside the error handler";
+                case REPLY -> "The reply of such a send, after the producer 
succeeded: its side effect happened (only"
+                              + " with at=reply)";
+                case DLQ -> "The send of the dead letter channel to a matching 
endpoint (only with at=dlq)";
+                case POLL -> "The polls of the scheduled poll consumers of the 
routes";
+                case ENTRY -> "Each exchange that enters a matching route, 
before the first step";
+                case STEP -> "Each matching step of a route, right before its 
processor is called";
+            });
+            jo.put("targets", new JsonArray(targets.getOrDefault(hook, 
List.of())));
+            jo.put("faults", keys(faults.getOrDefault(hook, Set.of())));
+            jo.put("onlyWithAt", ChaosRuleParser.onlyWithAt(hook));
+            answer.add(jo);
+        }
+        return answer;
+    }
+
+    // ---- the faults and their parameters ----
+
+    private static JsonArray faults() {
+        JsonArray answer = new JsonArray();
+        for (Fault fault : Fault.values()) {
+            if (fault == Fault.LIFECYCLE) {
+                answer.add(fault("lifecycle", fault, false,
+                        "Suspends, stops or removes the route of a route=<id> 
target, and brings it back after the"
+                                                            + " duration; 
repeat= takes it down more than once"));
+                answer.add(fault("stall", fault, true, "Short for lifecycle 
action=suspend: the route stalls for the"
+                                                       + " duration"));
+            } else {
+                answer.add(fault(fault.key, fault, false, switch (fault) {
+                    case ERROR -> "Fails the send, the poll or the step with 
an exception (a ChaosException without"
+                                  + " exception=), or the failure of an HTTP 
component for status=";
+                    case DELAY -> "Waits before the send, the poll or the 
step, as for a slow endpoint";
+                    case DROP -> "A send that reports success without calling 
the producer, or a poll that is skipped";
+                    case CORRUPT -> "Changes the message: its headers, the 
charset of its body, or its body";
+                    default -> throw new IllegalStateException("Unknown fault 
" + fault);
+                }));
+            }
+        }
+        return answer;
+    }
+
+    private static JsonObject fault(String name, Fault fault, boolean stall, 
String description) {
+        JsonObject jo = new JsonObject();
+        jo.put("name", name);
+        jo.put("description", description);
+        // the targets that have a hook offering the fault; stall and 
lifecycle act on a route at no hook
+        Set<String> targets = new TreeSet<>();
+        Set<Hook> hooks = EnumSet.noneOf(Hook.class);
+        for (Target target : Target.values()) {
+            for (Map.Entry<Hook, Set<Fault>> entry : 
ChaosRuleParser.offered(target).entrySet()) {
+                if (entry.getValue().contains(fault)) {
+                    targets.add(target.key);
+                    hooks.add(entry.getKey());
+                }
+            }
+        }
+        if (fault == Fault.LIFECYCLE) {
+            targets.add(Target.ROUTE.key);
+        }
+        jo.put("targets", new JsonArray(List.copyOf(targets)));
+        jo.put("hooks", keys(hooks));
+        if (stall) {
+            jo.put("shortFor", "lifecycle action=suspend");
+        }
+        JsonArray parameters = new JsonArray();
+        for (String key : new TreeSet<>(ChaosRuleParser.faultKeys(fault, 
stall))) {
+            parameters.add(parameter(fault, key));
+        }
+        jo.put("parameters", parameters);
+        jo.put("options", new JsonArray(List.copyOf(new 
TreeSet<>(ChaosRuleParser.optionKeys(fault)))));
+        return jo;
+    }
+
+    private static JsonObject parameter(Fault fault, String key) {
+        String scope = fault.key + " " + key;
+        return switch (scope) {
+            case "error exception" -> param("exception", "class", false,
+                    "The class of the exception: a public Throwable with a 
public constructor taking a String or none"
+                                                                         + " 
(not an InterruptedException)",
+                    "java.net.ConnectException");
+            case "error message" -> param("message", "string", false,
+                    "The message of the exception (the class needs a 
constructor taking a String)", "broker down");
+            case "error after" -> withNote(param("after", "duration", false,
+                    "Waits that long first, then fails, as a call that times 
out; the producer is not called", "5s"),
+                    "notAt", new JsonArray(List.of(Hook.REPLY.key)));
+            case "error status" -> {
+                JsonObject p = param("status", "integer", false,
+                        "The failure of an HTTP component for this status (not 
with exception= or message=)", "503");
+                p.put("min", 300);
+                p.put("max", 599);
+                p.put("schemes", new JsonArray(List.copyOf(new 
TreeSet<>(ChaosRuleParser.HTTP_SCHEMES))));
+                yield p;
+            }
+            case "error body" -> param("body", "string", false, "The body of 
the HTTP response (needs status=)",
+                    "no stock");
+            case "error header." -> {
+                JsonObject p = param("header.<name>", "string", false,
+                        "A header of the HTTP response, such as 
header.Retry-After=5 (needs status=)", "5");
+                p.put("prefix", ChaosRuleParser.HEADER_PREFIX);
+                yield p;
+            }
+            case "delay duration" -> {
+                JsonObject p = param("duration", "duration", true,
+                        "How long to wait, or a range such as 100ms..2s for a 
draw between the two times (from the"
+                                                                   + " seeded 
sequence of the rule)",
+                        "500ms");
+                p.put("range", true);
+                yield p;
+            }
+            case "corrupt mode" -> {
+                JsonObject p = param("mode", "enum", true,
+                        "headers: the headers that are not Camel* get random 
values; charset: a text body becomes"
+                                                           + " UTF-16 bytes; 
truncate: a String, byte[] or stream body ends"
+                                                           + " after half of 
it; empty: the body becomes empty of its kind"
+                                                           + " (or null); 
remove-header: removes headers",
+                        CorruptMode.HEADERS.key);
+                p.put("enum", new JsonArray(corruptModes()));
+                yield p;
+            }
+            case "corrupt header" -> param("header", "string", false,
+                    "The headers mode=remove-header removes: a name or a 
pattern with *, ignoring case (without it"
+                                                                      + " 
every header that is not Camel*)",
+                    "X-Trace-*");
+            case "lifecycle action" -> {
+                JsonObject p = param("action", "enum", true, "What to do to 
the route", Action.STOP.key);
+                JsonArray actions = new JsonArray();
+                for (Action action : Action.values()) {
+                    actions.add(action.key);
+                }
+                p.put("enum", actions);
+                yield p;
+            }
+            case "lifecycle duration" -> param("duration", "duration", true,
+                    "How long the route stays down (at most 
camel.chaos.maxDuration)", "20s");
+            case "lifecycle repeat" -> {
+                JsonObject p = param("repeat", "integer", false,
+                        "How many times to take the route down (needs every= 
when more than 1)", "3");
+                p.put("min", 1);
+                p.put("max", ChaosRuleParser.MAX_REPEAT);
+                yield p;
+            }
+            case "lifecycle every" -> param("every", "duration", false,
+                    "The time from one time the route goes down to the next, 
longer than duration=", "1m");
+            default -> throw new IllegalStateException("The chaos model has no 
description of the parameter " + scope);
+        };
+    }
+
+    // ---- the options, the expectation and the limits ----
+
+    private static JsonArray options() {
+        JsonArray answer = new JsonArray();
+        for (String key : 
ChaosRuleParser.GENERAL_KEYS.stream().sorted().toList()) {
+            answer.add(switch (key) {
+                case "at" -> {
+                    JsonObject p = param("at", "enum", false,
+                            "Narrows the rule to one hook of its target (reply 
and dlq are reached only with at=)",
+                            Hook.SEND.key);
+                    JsonArray hooks = new JsonArray();
+                    for (Hook hook : Hook.values()) {
+                        hooks.add(hook.key);
+                    }
+                    p.put("enum", hooks);
+                    yield p;
+                }
+                case "rate" -> {
+                    JsonObject p = param("rate", "rate", false,
+                            "How often the rule fires: a probability above 0 
and at most 1, every Nth match, or bursts"
+                                                                + " (every 
match during the first part of each period);"
+                                                                + " without it 
every match fires",
+                            "0.25");
+                    p.put("forms", new JsonArray(List.of("0.25", "every:4", 
"burst:5s/30s")));
+                    yield p;
+                }
+                case "until" -> param("until", "countOrDuration", false,
+                        "When the rule ends: a number of matches or a time 
from its start (at most"
+                                                                         + " 
camel.chaos.maxDuration, its default)",
+                        "10m");
+                case "from" -> param("from", "duration", false,
+                        "When the rule starts after it is added, such as after 
the warm-up (+30s or 30s)", "+30s");
+                case "expect" -> param("expect", "expectation", false,
+                        "An expectation of a route after the rule is added: 
<route>:<count> (at least that many"
+                                                                       + " 
completed) or <route>:<kind><op><count>",
+                        "orders:handled<=0");
+                case "within" -> param("within", "duration", false,
+                        "The time of the expectation (by default until the 
rule ends)", "2m");
+                default -> throw new IllegalStateException("The chaos model 
has no description of the option " + key);
+            });
+        }
+        return answer;
+    }
+
+    private static JsonObject expectation() {
+        JsonObject jo = new JsonObject();
+        JsonArray kinds = new JsonArray();
+        for (Expectation.Kind kind : Expectation.Kind.values()) {
+            JsonObject k = new JsonObject();
+            k.put("name", kind.key);
+            k.put("description", "The " + kind.words(2) + " in the route");
+            kinds.add(k);
+        }
+        jo.put("kinds", kinds);
+        JsonArray ops = new JsonArray();
+        for (Expectation.Op op : Expectation.Op.values()) {
+            JsonObject o = new JsonObject();
+            o.put("name", op.key);
+            o.put("description", op.words);
+            ops.add(o);
+        }
+        jo.put("ops", ops);
+        return jo;
+    }
+
+    private static JsonObject limits() {
+        JsonObject jo = new JsonObject();
+        jo.put("maxLength", ChaosRuleParser.MAX_LENGTH);
+        jo.put("maxRepeat", ChaosRuleParser.MAX_REPEAT);
+        jo.put("maxDuration", "camel.chaos.maxDuration (default 10m): the 
longest until, delay, after, from, burst period"
+                              + " and stall or lifecycle duration");
+        jo.put("time", "a number with a unit (ms, s, m, h, d), also combined 
as 1m30s; a number alone is milliseconds");
+        jo.put("quoting", "a value with spaces or ; is quoted with \" or ' 
(\\\", \\' and \\\\ inside quotes)");
+        return jo;
+    }
+
+    // ---- the recipes ----
+
+    /**
+     * A placeholder of a recipe: {@code ${name}} in its rule. The example is 
what the model test fills in to parse the
+     * recipe; the default is what a tool offers first.
+     */
+    record Placeholder(String name, String type, String description, String 
example, String defaultValue,
+            JsonObject pick) {
+    }
+
+    /** A known rule in plain words, as a template. */
+    record Recipe(String id, String group, String title, String description, 
String rule, List<Placeholder> placeholders) {
+    }
+
+    static List<Recipe> recipeList() {
+        List<Recipe> answer = new ArrayList<>();
+        // downstream
+        answer.add(new Recipe(
+                "http-error", "Downstream", "Returns an error (HTTP status)",
+                "An HTTP call fails with a status, as for a service that is 
down or overloaded: tests onException,"
+                                                                              
+ " redelivery and the dead letter channel",
+                "endpoint=${endpoint} error status=${status} rate=${rate} 
until=${until}",
+                List.of(httpEndpoint(), new Placeholder("status", "integer", 
"The HTTP status", "503", "503", null),
+                        rate("0.25"), until("10m"))));
+        answer.add(new Recipe(
+                "connection-refused", "Downstream", "Refuses the connection",
+                "The send fails as when the other side is not there",
+                "endpoint=${endpoint} error 
exception=java.net.ConnectException rate=${rate} until=${until}",
+                List.of(producer(), rate("0.25"), until("10m"))));
+        answer.add(new Recipe(
+                "slow", "Downstream", "Is slow",
+                "Each send waits a different time within a range, as a slow 
service does",
+                "endpoint=${endpoint} delay duration=${duration} rate=${rate} 
until=${until}",
+                List.of(producer(), new Placeholder(
+                        "duration", "duration", "The time, or a range of 
times", "200ms..3s",
+                        "200ms..3s", null), rate("0.5"), until("10m"))));
+        answer.add(new Recipe(
+                "timeout", "Downstream", "Times out",
+                "The send waits, then fails without calling the producer, as a 
call that times out",
+                "endpoint=${endpoint} error after=${after} 
exception=java.net.SocketTimeoutException rate=${rate}"
+                                                                               
                      + " until=${until}",
+                List.of(producer(), new Placeholder("after", "duration", "How 
long before it times out", "5s", "5s", null),
+                        rate("0.1"), until("10m"))));
+        answer.add(new Recipe(
+                "failed-after-success", "Downstream", "Did the work, then 
failed",
+                "The send succeeds and then fails, as a reply that is lost: a 
redelivery sends it again, so this tests"
+                                                                               
    + " that a duplicate does no harm",
+                "endpoint=${endpoint} error at=reply until=${until}",
+                List.of(producer(), new Placeholder("until", 
"countOrDuration", "How many sends fail", "1", "1", null))));
+        answer.add(new Recipe(
+                "outage", "Downstream", "Is down for a while",
+                "Every send fails during the first part of each period, as 
outage windows",
+                "endpoint=${endpoint} error rate=burst:${outage}/${period} 
until=${until}",
+                List.of(producer(), new Placeholder("outage", "duration", "How 
long each outage lasts", "10s", "10s", null),
+                        new Placeholder("period", "duration", "The time from 
one outage to the next", "1m", "1m", null),
+                        until("10m"))));
+        // messages
+        answer.add(new Recipe(
+                "corrupt-entry", "Messages", "Arrives corrupt",
+                "The messages that enter the route are changed: random 
headers, the wrong charset, cut in half or"
+                                                                + " empty",
+                "route=${route} corrupt mode=${mode} rate=${rate} 
until=${until}",
+                List.of(route(), new Placeholder(
+                        "mode", "enum", "How the message is changed", 
"headers", "headers",
+                        pick("values", "values", corruptModes())),
+                        rate("0.25"), until("10m"))));
+        answer.add(new Recipe(
+                "empty-body", "Messages", "Is sent with an empty body",
+                "The message sent to the endpoint has an empty body",
+                "endpoint=${endpoint} corrupt mode=empty rate=${rate} 
until=${until}",
+                List.of(producer(), rate("0.25"), until("10m"))));
+        answer.add(new Recipe(
+                "missing-header", "Messages", "Is sent without a header",
+                "A header is missing from the message sent to the endpoint",
+                "endpoint=${endpoint} corrupt mode=remove-header 
header=${header} rate=${rate} until=${until}",
+                List.of(producer(), new Placeholder(
+                        "header", "string", "The header, or a pattern with *", 
"X-Trace-Id",
+                        null, pick("headers", null, null)), rate("0.25"), 
until("10m"))));
+        // consumers and routes
+        answer.add(new Recipe(
+                "poll-error", "Consumers and routes", "Polls fail",
+                "The polls of a scheduled poll consumer fail, as when the 
source is not reachable",
+                "endpoint=${endpoint} error at=poll rate=${rate} 
until=${until}",
+                List.of(polled(), rate("0.5"), until("10m"))));
+        answer.add(new Recipe(
+                "poll-skip", "Consumers and routes", "Polls are skipped",
+                "The polls of a scheduled poll consumer are skipped, as a 
consumer that falls behind",
+                "endpoint=${endpoint} drop at=poll rate=${rate} 
until=${until}",
+                List.of(polled(), rate("0.5"), until("10m"))));
+        answer.add(new Recipe(
+                "stall", "Consumers and routes", "The route stalls",
+                "The route is suspended for a while, and its messages wait",
+                "route=${route} stall duration=${duration}",
+                List.of(route(), new Placeholder("duration", "duration", "How 
long it stalls", "20s", "20s", null))));
+        answer.add(new Recipe(
+                "flap", "Consumers and routes", "The route flaps",
+                "The route stops and starts again several times",
+                "route=${route} lifecycle action=stop duration=${duration} 
repeat=${repeat} every=${every}",
+                List.of(route(), new Placeholder("duration", "duration", "How 
long it is down each time", "10s", "10s", null),
+                        new Placeholder("repeat", "integer", "How many times", 
"3", "3", null),
+                        new Placeholder("every", "duration", "The time from 
one time to the next", "1m", "1m", null))));
+        answer.add(new Recipe(
+                "removed", "Consumers and routes", "The route is removed",
+                "The route is removed, and added back after a while",
+                "route=${route} lifecycle action=remove duration=${duration}",
+                List.of(route(), new Placeholder("duration", "duration", "How 
long it is gone", "30s", "30s", null))));
+        // error handling
+        answer.add(new Recipe(
+                "dlq-unreachable", "Error handling", "The dead letter channel 
is unreachable",
+                "The send of the dead letter channel fails, so the messages it 
handles are lost (add it before the rule"
+                                                                               
                + " that makes them fail)",
+                "endpoint=${endpoint} error at=dlq",
+                List.of(producer())));
+        answer.add(new Recipe(
+                "step-error", "Error handling", "A step fails",
+                "A step of a route fails, as when its processor throws",
+                "step=${step} error rate=${rate} until=${until}",
+                List.of(step(), rate("0.1"), until("10m"))));
+        answer.add(new Recipe(
+                "step-slow", "Error handling", "A step is slow",
+                "A step of a route waits before its processor is called",
+                "step=${step} delay duration=${duration} rate=${rate} 
until=${until}",
+                List.of(step(), new Placeholder("duration", "duration", "The 
time, or a range of times", "1s", "1s", null),
+                        rate("0.25"), until("10m"))));
+        return answer;
+    }
+
+    private static JsonArray recipes() {
+        JsonArray answer = new JsonArray();
+        for (Recipe recipe : recipeList()) {
+            JsonObject jo = new JsonObject();
+            jo.put("id", recipe.id());
+            jo.put("group", recipe.group());
+            jo.put("title", recipe.title());
+            jo.put("description", recipe.description());
+            jo.put("rule", recipe.rule());
+            JsonArray placeholders = new JsonArray();
+            for (Placeholder p : recipe.placeholders()) {
+                JsonObject po = new JsonObject();
+                po.put("name", p.name());
+                po.put("type", p.type());
+                po.put("description", p.description());
+                if (p.defaultValue() != null) {
+                    po.put("default", p.defaultValue());
+                }
+                po.put("example", p.example());
+                if (p.pick() != null) {
+                    po.put("pick", p.pick());
+                }
+                placeholders.add(po);
+            }
+            jo.put("placeholders", placeholders);
+            answer.add(jo);
+        }
+        return answer;
+    }
+
+    private static Placeholder producer() {
+        return new Placeholder(
+                "endpoint", "endpoint", "The endpoint the route sends to", 
"kafka:orders", null,
+                pick("producers", null, null));
+    }
+
+    private static Placeholder httpEndpoint() {
+        return new Placeholder(
+                "endpoint", "endpoint", "The HTTP endpoint the route calls", 
"https://inventory/stock";,
+                null, pick("producers", "schemes", List.copyOf(new 
TreeSet<>(ChaosRuleParser.HTTP_SCHEMES))));
+    }
+
+    private static Placeholder polled() {
+        return new Placeholder(
+                "endpoint", "endpoint", "The endpoint a route consumes with a 
scheduled poll",
+                "file:inbox", null, pick("pollingConsumers", null, null));
+    }
+
+    private static Placeholder route() {
+        return new Placeholder("route", "route", "The route", "orders", null, 
pick("routes", null, null));
+    }
+
+    private static Placeholder step() {
+        return new Placeholder("step", "step", "The step", "enrich", null, 
pick("steps", null, null));
+    }
+
+    private static Placeholder rate(String value) {
+        return new Placeholder(
+                "rate", "rate", "How often: a probability, every:N or 
burst:<time>/<period>", value, value,
+                null);
+    }
+
+    private static Placeholder until(String value) {
+        return new Placeholder(
+                "until", "countOrDuration", "When the rule ends: a number of 
matches or a time", value,
+                value, null);
+    }
+
+    /**
+     * Where a tool picks the value of a placeholder from: the endpoints the 
routes send to (producers), the endpoints
+     * the routes consume with a scheduled poll (pollingConsumers), the 
routes, the steps, the headers, or the values
+     * listed.
+     */
+    private static JsonObject pick(String from, String key, List<String> 
values) {
+        JsonObject jo = new JsonObject();
+        jo.put("from", from);
+        if (values != null) {
+            jo.put(key, new JsonArray(values));
+        }
+        return jo;
+    }
+
+    private static List<String> corruptModes() {
+        List<String> answer = new ArrayList<>();
+        for (CorruptMode mode : CorruptMode.values()) {
+            answer.add(mode.key);
+        }
+        return answer;
+    }
+
+    // ---- helpers ----
+
+    private static JsonObject param(String name, String type, boolean 
required, String description, String example) {
+        JsonObject jo = new JsonObject();
+        jo.put("name", name);
+        jo.put("type", type);
+        jo.put("required", required);
+        jo.put("description", description);
+        jo.put("example", example);
+        return jo;
+    }
+
+    private static JsonObject withNote(JsonObject jo, String key, Object 
value) {
+        jo.put(key, value);
+        return jo;
+    }
+
+    private static <E extends Enum<E>> JsonArray keys(Set<E> values) {
+        JsonArray answer = new JsonArray();
+        for (E value : values) {
+            answer.add(switch (value) {
+                case Hook h -> h.key;
+                case Fault f -> f.key;
+                default -> value.name();
+            });
+        }
+        return answer;
+    }
+}
diff --git 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
index 879e027485f9..32165cb3a225 100644
--- 
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
+++ 
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosRuleParser.java
@@ -20,7 +20,6 @@ import java.lang.reflect.Constructor;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
 import java.util.EnumSet;
-import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -56,11 +55,16 @@ final class ChaosRuleParser {
      */
     static final Set<String> HTTP_SCHEMES = Set.of("http", "https", 
"netty-http", "undertow", "vertx-http");
 
-    private static final Set<String> GENERAL_KEYS = Set.of("at", "rate", 
"until", "from", "expect", "within");
-    private static final Set<String> ERROR_KEYS = Set.of("exception", 
"message", "status", "body", "after");
+    // the options of a rule besides the parameters of its fault: all of them 
for the faults at a hook, and those of a
+    // stall or lifecycle rule, which acts on its route at no hook
+    static final Set<String> GENERAL_KEYS = Set.of("at", "rate", "until", 
"from", "expect", "within");
+    static final Set<String> LIFECYCLE_KEYS = Set.of("from", "expect", 
"within");
+    // the prefix of the response headers of error status=: 
header.<name>=<value>
+    static final String HEADER_PREFIX = "header.";
+    private static final Set<String> ERROR_KEYS
+            = Set.of("exception", "message", "status", "body", "after", 
HEADER_PREFIX);
     // the most times a lifecycle rule acts (repeat=)
     static final int MAX_REPEAT = 100;
-    private static final String HEADER_PREFIX = "header.";
     // the parameters of every fault, to tell a misspelt target from a 
parameter
     private static final Set<String> FAULT_KEYS
             = Set.of("exception", "message", "status", "body", "after", 
"duration", "mode", "header", "action", "repeat",
@@ -72,6 +76,36 @@ final class ChaosRuleParser {
     private ChaosRuleParser() {
     }
 
+    /**
+     * The parameters of a fault, as the parser accepts them ({@code header.} 
stands for the {@code header.<name>}
+     * parameters of {@code error}). The chaos model ({@link ChaosModel}) is 
built from it.
+     *
+     * @param stall for {@code Fault.LIFECYCLE}: the rule says stall, which 
takes no action
+     */
+    static Set<String> faultKeys(Fault fault, boolean stall) {
+        return switch (fault) {
+            case ERROR -> ERROR_KEYS;
+            case DELAY -> Set.of("duration");
+            case DROP -> Set.of();
+            case CORRUPT -> Set.of("mode", "header");
+            case LIFECYCLE -> stall ? Set.of("duration", "repeat", "every") : 
Set.of("action", "duration", "repeat", "every");
+        };
+    }
+
+    /** The options a rule with the fault takes besides the parameters of the 
fault. */
+    static Set<String> optionKeys(Fault fault) {
+        return fault == Fault.LIFECYCLE ? LIFECYCLE_KEYS : GENERAL_KEYS;
+    }
+
+    /**
+     * Whether a rule reaches the hook only with {@code at=} naming it: the 
send of the dead letter channel, so
+     * {@code endpoint=*} does not lose the messages that the dead letter 
channel should keep, and the reply, so a rule
+     * fails a send before the producer is called unless it says otherwise.
+     */
+    static boolean onlyWithAt(Hook hook) {
+        return hook == Hook.DLQ || hook == Hook.REPLY;
+    }
+
     /**
      * Splits the rules of {@code camel.chaos.rules}, which are separated by 
{@code ;}. A {@code ;} inside a quoted
      * value does not separate; empty parts are left out.
@@ -215,7 +249,7 @@ final class ChaosRuleParser {
                 }
             }
             case DELAY -> {
-                checkKeys(params, Set.of("duration"), b.fault);
+                checkKeys(params, faultKeys(Fault.DELAY, false), b.fault);
                 String duration = params.get("duration");
                 if (duration == null) {
                     throw new IllegalArgumentException(
@@ -243,9 +277,9 @@ final class ChaosRuleParser {
                     checkMax("duration", duration, b.duration, maxDuration);
                 }
             }
-            case DROP -> checkKeys(params, Set.of(), b.fault);
+            case DROP -> checkKeys(params, faultKeys(Fault.DROP, false), 
b.fault);
             case CORRUPT -> {
-                checkKeys(params, Set.of("mode", "header"), b.fault);
+                checkKeys(params, faultKeys(Fault.CORRUPT, false), b.fault);
                 String mode = params.get("mode");
                 if (mode == null) {
                     throw new IllegalArgumentException("the fault corrupt 
needs mode=<mode> (" + MODES + ")");
@@ -452,9 +486,7 @@ final class ChaosRuleParser {
      * The parameters of {@code error}. Returns whether the error is the 
failure of an HTTP component.
      */
     private static boolean errorParameters(ChaosRule.Builder b, Map<String, 
String> params, ClassResolver classResolver) {
-        Set<String> keys = new HashSet<>(ERROR_KEYS);
-        keys.add(HEADER_PREFIX);
-        checkKeys(params, keys, Fault.ERROR);
+        checkKeys(params, faultKeys(Fault.ERROR, false), Fault.ERROR);
 
         Map<String, String> headers = new LinkedHashMap<>();
         params.forEach((k, v) -> {
@@ -561,8 +593,7 @@ final class ChaosRuleParser {
                     "stall is short for lifecycle action=suspend, so it takes 
no action=; use lifecycle action=...");
         }
         // at, rate and until are rejected above
-        checkKeys(params, b.stall ? Set.of("duration", "repeat", "every") : 
Set.of("action", "duration", "repeat", "every"),
-                word, Set.of("from", "expect", "within"));
+        checkKeys(params, faultKeys(Fault.LIFECYCLE, b.stall), word, 
optionKeys(Fault.LIFECYCLE));
         if (b.target != Target.ROUTE) {
             throw new IllegalArgumentException(
                     "the fault " + word + " acts on a route, so it takes only 
a route=<id> target, not " + b.target.key
@@ -706,11 +737,7 @@ final class ChaosRuleParser {
         }
         Set<Hook> hooks = EnumSet.noneOf(Hook.class);
         for (Map.Entry<Hook, Set<Fault>> entry : offered.entrySet()) {
-            // the send of the dead letter channel is reached only with 
at=dlq, so endpoint=* does not lose the messages
-            // that the dead letter channel should keep; the reply only with 
at=reply, so a rule fails a send before the
-            // producer is called unless it says otherwise
-            boolean asked = b.at == null
-                    ? entry.getKey() != Hook.DLQ && entry.getKey() != 
Hook.REPLY : b.at == entry.getKey();
+            boolean asked = b.at == null ? !onlyWithAt(entry.getKey()) : b.at 
== entry.getKey();
             if (asked && entry.getValue().contains(b.fault)) {
                 hooks.add(entry.getKey());
             }
diff --git 
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosModelTest.java
 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosModelTest.java
new file mode 100644
index 000000000000..924b3a036795
--- /dev/null
+++ 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosModelTest.java
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.chaos;
+
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.component.chaos.ChaosModel.Placeholder;
+import org.apache.camel.component.chaos.ChaosModel.Recipe;
+import org.apache.camel.component.chaos.ChaosRule.Hook;
+import org.apache.camel.component.chaos.ChaosRule.Target;
+import org.apache.camel.impl.engine.DefaultClassResolver;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The model of the chaos rules ({@link ChaosModel}): the committed file is 
what the model gives (write it again with
+ * {@code -Dcamel.chaos.updateModel=true}), it agrees with the parser, and 
every recipe parses.
+ */
+class ChaosModelTest {
+
+    private static final Path GENERATED = 
Path.of("src/generated/resources").resolve(ChaosModel.FILE);
+    private static final Pattern PLACEHOLDER = 
Pattern.compile("\\$\\{([a-zA-Z]+)}");
+    private static final long MAX = TimeUnit.MINUTES.toMillis(10);
+
+    @Test
+    void theCommittedFileIsTheModel() throws Exception {
+        String expected = ChaosModel.toJson();
+        if (Boolean.getBoolean("camel.chaos.updateModel")) {
+            Files.createDirectories(GENERATED.getParent());
+            Files.writeString(GENERATED, expected, StandardCharsets.UTF_8);
+        }
+        assertThat(Files.readString(GENERATED, StandardCharsets.UTF_8))
+                .as("%s is out of date: run the test with 
-Dcamel.chaos.updateModel=true and commit the file", GENERATED)
+                .isEqualTo(expected);
+        // and it is in the jar, where the catalog finds it
+        try (InputStream in = 
getClass().getClassLoader().getResourceAsStream(ChaosModel.FILE)) {
+            assertThat(in).isNotNull();
+        }
+    }
+
+    @Test
+    void theModelIsValidJsonWithTheChaosKey() throws Exception {
+        JsonObject root = (JsonObject) Jsoner.deserialize(ChaosModel.toJson());
+        assertThat(root).containsOnlyKeys("chaos");
+        assertThat(chaos()).containsKeys("targets", "hooks", "faults", 
"options", "expectation", "limits", "recipes");
+    }
+
+    @Test
+    void theHooksAndFaultsAreThoseOfTheParser() {
+        JsonArray hooks = (JsonArray) chaos().get("hooks");
+        assertThat(hooks).hasSize(Hook.values().length);
+        for (Object o : hooks) {
+            JsonObject hook = (JsonObject) o;
+            Hook h = hook(hook.getString("name"));
+            assertThat(hook.getBoolean("onlyWithAt")).isEqualTo(h == Hook.DLQ 
|| h == Hook.REPLY);
+            for (Object t : (JsonArray) hook.get("targets")) {
+                Target target = target((String) t);
+                Set<String> faults = new HashSet<>();
+                ChaosRuleParser.offered(target).get(h).forEach(f -> 
faults.add(f.key));
+                assertThat(new HashSet<>((JsonArray) 
hook.get("faults"))).containsAll(faults);
+            }
+        }
+        assertThat(names("faults")).containsExactly("error", "delay", "drop", 
"corrupt", "lifecycle", "stall");
+        JsonObject stall = byName("faults", "stall");
+        assertThat(stall.getString("shortFor")).isEqualTo("lifecycle 
action=suspend");
+        assertThat(paramNames(stall)).containsExactly("duration", "every", 
"repeat");
+        assertThat(paramNames(byName("faults", 
"lifecycle"))).containsExactly("action", "duration", "every", "repeat");
+        assertThat(paramNames(byName("faults", "error")))
+                .containsExactly("after", "body", "exception", 
"header.<name>", "message", "status");
+        assertThat(strings(byName("faults", 
"lifecycle").get("options"))).containsExactly("expect", "from", "within");
+        assertThat(strings(byName("faults", 
"error").get("hooks"))).containsExactly("send", "reply", "dlq", "poll", "step");
+        assertThat(names("options")).containsExactly("at", "expect", "from", 
"rate", "until", "within");
+    }
+
+    @Test
+    void theRecipesHaveUniqueIdsAndTheirPlaceholders() {
+        Set<String> ids = new HashSet<>();
+        for (Recipe recipe : ChaosModel.recipeList()) {
+            assertThat(ids.add(recipe.id())).as("recipe id %s", 
recipe.id()).isTrue();
+            Set<String> used = new LinkedHashSet<>();
+            Matcher m = PLACEHOLDER.matcher(recipe.rule());
+            while (m.find()) {
+                used.add(m.group(1));
+            }
+            Set<String> declared = new LinkedHashSet<>();
+            recipe.placeholders().forEach(p -> declared.add(p.name()));
+            assertThat(declared).as("the placeholders of recipe %s", 
recipe.id()).isEqualTo(used);
+        }
+    }
+
+    static List<Recipe> recipes() {
+        return ChaosModel.recipeList();
+    }
+
+    @ParameterizedTest
+    @MethodSource("recipes")
+    void everyRecipeParsesWithItsExamplesAndDefaults(Recipe recipe) {
+        // the examples
+        ChaosRuleParser.parse(fill(recipe, false), new DefaultClassResolver(), 
MAX);
+        // the defaults, with the examples where there is no default
+        ChaosRuleParser.parse(fill(recipe, true), new DefaultClassResolver(), 
MAX);
+    }
+
+    @Test
+    void theModeOfACorruptRecipeOffersEveryMode() {
+        Recipe recipe = ChaosModel.recipeList().stream().filter(r -> 
r.id().equals("corrupt-entry")).findFirst()
+                .orElseThrow();
+        Placeholder mode = recipe.placeholders().stream().filter(p -> 
p.name().equals("mode")).findFirst().orElseThrow();
+        for (Object value : (JsonArray) mode.pick().get("values")) {
+            String rule = fill(recipe, true).replace("mode=headers", "mode=" + 
value);
+            ChaosRuleParser.parse(rule, new DefaultClassResolver(), MAX);
+        }
+    }
+
+    private static String fill(Recipe recipe, boolean defaults) {
+        String rule = recipe.rule();
+        for (Placeholder p : recipe.placeholders()) {
+            String value = defaults && p.defaultValue() != null ? 
p.defaultValue() : p.example();
+            rule = rule.replace("${" + p.name() + "}", value);
+        }
+        assertThat(rule).doesNotContain("${");
+        return rule;
+    }
+
+    private static JsonObject chaos() {
+        return (JsonObject) ChaosModel.model().get("chaos");
+    }
+
+    private static List<String> names(String key) {
+        return ((JsonArray) chaos().get(key)).stream().map(o -> ((JsonObject) 
o).getString("name")).toList();
+    }
+
+    private static JsonObject byName(String key, String name) {
+        return (JsonObject) ((JsonArray) chaos().get(key)).stream()
+                .filter(o -> name.equals(((JsonObject) 
o).getString("name"))).findFirst().orElseThrow();
+    }
+
+    private static List<String> paramNames(JsonObject fault) {
+        return ((JsonArray) fault.get("parameters")).stream().map(o -> 
((Map<?, ?>) o).get("name").toString()).toList();
+    }
+
+    private static List<String> strings(Object array) {
+        return ((List<?>) array).stream().map(Object::toString).toList();
+    }
+
+    private static Hook hook(String key) {
+        for (Hook h : Hook.values()) {
+            if (h.key.equals(key)) {
+                return h;
+            }
+        }
+        throw new AssertionError("No hook " + key);
+    }
+
+    private static Target target(String key) {
+        for (Target t : Target.values()) {
+            if (t.key.equals(key)) {
+                return t;
+            }
+        }
+        throw new AssertionError("No target " + key);
+    }
+}
diff --git a/docs/user-manual/modules/ROOT/pages/camel-catalog.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-catalog.adoc
index 5b231a985486..7dc5f8090f11 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-catalog.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-catalog.adoc
@@ -48,6 +48,7 @@ org
   └── camel
     └── catalog
       ├── beans (JSON schema)
+      ├── chaos-model.json (the model of the chaos rules)
       ├── components (JSON schema)
       ├── dataformats (JSON schema)
       ├── dev-consoles (JSON schema)
@@ -77,6 +78,11 @@ covering every dev console endpoint, so the same contract 
used internally to ser
 dev console is also available standalone for tooling. A 
`dev-consoles.properties` index file lists
 all available dev console names.
 
+The `chaos-model.json` file, since Camel 4.24, is the model of the rules of
+xref:next@components:others:chaos.adoc[camel-chaos]: the targets, the hooks of 
each target and the faults each one
+offers, the parameters of each fault, the options, the expectations, the 
limits, and recipes (known rules in plain
+words, as templates). It is for the tools that help to write rules, such as a 
form or an AI.
+
 The `docs` directory contains the AsciiDoc documentation pages for all 
components, EIPs,
 data formats, languages, and other artifacts. A `docs.properties` index file 
lists all
 available documentation names.
@@ -244,4 +250,7 @@ String summary = catalog.listDevConsolesAsJson();
 
 // the OpenAPI 3.0 specification aggregated across every dev console endpoint
 String openApiSpec = catalog.devConsolesOpenApiSpec();
+
+// the model of the chaos rules of camel-chaos
+String chaosModel = catalog.chaosModelJSon();
 ----
diff --git 
a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCatalogMojo.java
 
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCatalogMojo.java
index 2fcac3ef4933..d286557b733d 100644
--- 
a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCatalogMojo.java
+++ 
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCatalogMojo.java
@@ -437,6 +437,7 @@ public class PrepareCatalogMojo extends AbstractMojo {
             Set<String> beans = executeBeans();
             executeApis();
             Set<String> consoles = executeDevConsoles();
+            executeChaosModel();
             Set<String> others = executeOthers();
             executeDocuments(components, dataformats, languages, others);
             executeDocs();
@@ -917,6 +918,22 @@ public class PrepareCatalogMojo extends AbstractMojo {
         return apiNames;
     }
 
+    /**
+     * Copies the model of the chaos rules of camel-chaos into the catalog 
(CamelCatalog.chaosModelJSon()).
+     */
+    protected void executeChaosModel() throws Exception {
+        List<Path> models = allJsonFiles.stream()
+                .filter(p -> 
p.endsWith("META-INF/org/apache/camel/chaos/chaos-model.json"))
+                .toList();
+        if (models.size() != 1) {
+            getLog().warn("Expected one chaos-model.json (camel-chaos), found 
" + models.size());
+            return;
+        }
+        Path target = consolesOutDir.toPath().resolve("../chaos-model.json");
+        FileUtil.updateFile(target, Files.readString(models.get(0)));
+        getLog().info("Copied the chaos model of camel-chaos");
+    }
+
     protected Set<String> executeDevConsoles() throws Exception {
         Path consolesOutDir = this.consolesOutDir.toPath();
 

Reply via email to