In my previous articles, I introduced you to Camunda’s webhook connector and explained why payload validation is a hard requirement if you want stable and predictable process starts. Webhooks are an external entry point into your system. If you accept invalid payloads, you push errors deeper into the process where they are harder to detect and fix.
FEEL turned out to be a good fit for validation at this boundary. It’s expressive enough for structural checks and integrates cleanly with Camunda connectors. Using FEEL directly in the connector works well. Until you have to maintain it over time.
Note: FEEL stands for Friendly Enough Expression Language. It was defined by the Object Management Group as part of the DMN standard and is widely used in tools like Camunda. FEEL is designed to express business rules and decisions in a way that is readable for both developers and business users.
The Problem With Manual FEEL Validation
Validating webhook payloads with FEEL usually means writing expressions by hand. You check required fields, object structures, arrays, and types. None of this is complex, but it’s repetitive and easy to get wrong.
Webhook endpoints are defined directly in the Web Modeler. From a technical perspective, this is enough to receive requests. What is missing is an explicit description of what the payload is supposed to look like.
Without such a description, FEEL expressions become both contract and validation at the same time. That makes them hard to read, hard to review, and fragile when the payload evolves.
This leads to a simple question. What if each webhook had its own explicit description of the payload, even though the engine itself does not require it?
A Best Practice For Webhook Definitions
Defining webhook endpoints only in the process solves execution, but it does not solve three other problems. You still need a clear contract, usable documentation, and a reliable base for validation.
A simple best practice is to define each webhook endpoint using a structured description, even if it’s not technically required. OpenAPI is a natural choice here. It gives you a precise contract, human readable documentation, and a machine readable definition at the same time.
Once you have such a description, validation should not be written manually anymore. The OpenAPI spec already contains required fields and types. FEEL does not need to redefine that. It only needs to evaluate it.
This realization is what led from a small extraction script to a proper Maven plugin.
From Idea To Maven Plugin
I started with a small script that reads an OpenAPI spec and extracts validation relevant information. That experiment worked well enough to turn it into a Maven plugin.
A Maven plugin fits naturally into this workflow. Generation happens at build time. The output is deterministic, explicit, and easy to inspect. There is no runtime logic involved and no hidden behavior.
This became the FEEL Validation Generator, a Maven plugin that generates FEEL expressions from OpenAPI specifications for use in Camunda webhook connectors.
The project is public here: https://github.com/consid-germany/camunda-feel-validation-generator
What The Plugin Does In Practice
During the build, the plugin parses the OpenAPI specification and walks through the request body schema of each webhook endpoint. From that schema, it derives structural validation rules and renders them as FEEL expressions.
The generator understands referenced schemas and required properties. The result is FEEL you would normally write by hand, but kept aligned with the API contract.
To keep the example focused, here is a minimal setup.
OpenAPI Specification
openapi: 3.1.0
paths:
/orders:
post:
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
components:
schemas:
Order:
type: object
required:
- orderId
properties:
orderId:
type: string
note:
type: stringThis defines a webhook payload where orderId is required and note is optional.
Generated FEEL Expression
From this specification, the plugin generates a FEEL validation like this:
{
req: request.body,
rules: [
{
id: "orderId-invalid",
invalid: req.orderId=null or not(req.orderId instance of string) or is blank(req.orderId) }
],
isValid: count(rules[invalid=true])=0
}.isValidThis expression is meant to be pasted directly into the webhook connector’s activation condition.
Request That Validates To True
{
"orderId": "ORD-123",
"note": "priority order"
}The required field is present and valid, so the activation condition passes and the process starts.
Request That Validates To False
{
"note": "missing order id"
}The required orderId is missing. The FEEL expression evaluates to false and the webhook is rejected before the process is instantiated.
Beyond The Simple Case
The example above is intentionally minimal. The plugin also supports more complex OpenAPI constructs. This includes referenced schemas using allOf, oneOf, and anyOf, as well as hierarchical object structures with nested contexts and deeper validation paths.
This allows you to keep using expressive OpenAPI models without falling back to handwritten FEEL as complexity grows.
Activation Condition Or Response Expression
The same OpenAPI spec can also be used to generate a response expression instead of a boolean activation condition. In that mode, the FEEL returns a structured response with validation details and status codes:
# POST /orders: HTTP 400
{
"message": "Process creation failed.",
"processInstanceKey": null,
"details": [
{"id": "orderId-invalid", "field": "orderId", "invalid": true},
]
}The plugin supports both modes so you can decide whether you want early rejection or a detailed error response.
Why This Approach Works
The main benefit is consistency. The same definition serves as contract, documentation, and validation base. There is no second place where structural rules need to be maintained.
Reviews become simpler because changes are discussed at the level of the OpenAPI spec. Adding new webhook endpoints or evolving existing ones does not increase validation maintenance cost.
Scope And Limitations
This plugin focuses on structural validation. It does not attempt to encode complex business rules, and it should not. OpenAPI can express more than what makes sense to enforce at the validation boundary.
The goal is to automate the boring and error prone parts while keeping domain logic explicit.
Summary
Defining webhook endpoints only in the process solves execution, but it leaves contract, documentation, and validation loosely defined. Introducing an explicit description closes that gap.
Using OpenAPI as that description and generating FEEL validation from it gives you a single source of truth. If you want your webhook handling to be predictable, reviewable, and maintainable over time, this approach is worth considering.



