# SAP Cloud Application Programming Model, n8n plugin for Java [![REUSE status](https://api.reuse.software/badge/github.com/cap-java/cds-feature-n8n)](https://api.reuse.software/info/github.com/cap-java/cds-feature-n8n) CAP Plugin to automatically trigger and interact with [n8n](https://n8n.io/) workflow automation tool - declaratively via `@n8n.process.start` annotations and programmatically via the `n8n` service. ## Table of Contents - [About this project](#about-this-project) - [Requirements](#requirements) - [Quick Start](#quick-start) - [Connect to n8n](#connect-to-n8n) - [Local n8n](#local-n8n) - [Environment Variables](#environment-variables) - [BTP Destination](#btp-destination) - [Credential Resolution](#credential-resolution) - [Webhook Requests](#webhook-requests) - [Test Webhooks](#test-webhooks) - [Annotations](#annotations) - [Triggering a workflow](#triggering-a-workflow) - [Conditions](#conditions) - [Multiple Triggers](#multiple-triggers) - [Programmatic API](#programmatic-api) - [Trigger a Workflow](#trigger-a-workflow) - [Delivery Behaviour](#delivery-behaviour) - [Console Mode](#console-mode) - [Tests](#tests) - [Support, Feedback, Contributing](#support-feedback-contributing) - [Security / Disclosure](#security--disclosure) - [Code of Conduct](#code-of-conduct) - [Licensing](#licensing) ## About this project `cds-feature-n8n` is a Spring Boot auto-configuration plugin for CAP Java applications. It listens to CDS events (CREATE, DELETE, and custom actions) annotated with `@n8n.process.start` and fires HTTP requests to configured n8n webhook URLs — enabling you to trigger n8n workflows directly from your CAP service layer. **Features:** - Annotation-driven: no boilerplate code needed in your service handlers - Supports entity CRUD events (CREATE, DELETE) and custom actions/functions - Configurable HTTP method per trigger (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`) — defaults to `POST` - Reliable delivery via CAP persistent outbox with configurable retry - Optional API key header (`X-N8N-API-KEY`) for authentication ## Requirements - Java 21+ - CAP Java 5+ - Spring Boot 4+ - A running n8n instance ## Quick Start Follow these steps to get a fully working local environment from scratch. 1. Build and install the plugin locally: ```zsh mvn clean install ``` 2. Add the dependency to your CAP Java application's `pom.xml`: ```xml com.sap.cds cds-feature-n8n 0.0.1 ``` 3. Annotate an entity in your CDS model: ```cds annotate AdminService.Books with @n8n.process.start: [ {on: 'DELETE', path: 'book-deleted'} ]; ``` 4. Configure the n8n base URL in `application.yaml`: ```yaml n8n: base-url: ${N8N_BASE_URL:http://localhost:5678} api-key: ${N8N_API_KEY:} ``` 5. Start the sample app ```zsh cd samples/bookshop/srv mvn spring-boot:run ``` The app starts on `http://localhost:8080` and points at `http://localhost:5678/webhook` by default. Any annotated CDS event will fire a webhook to your local n8n instance. 6. Smoke test 1. In n8n, create a workflow with a **Webhook** node, path `book-deleted`, save (Cmd+S), click **"Listen for Test Event"** 2. Delete any book at `http://localhost:8080` → Admin → Books 3. n8n should show a green execution with `{ "ID": "...", "title": "...", "author_ID": "..." }` > **Alternative (test mode):** If you prefer one-shot manual testing, set `n8n.use-test-webhook: true` in `application.yaml`, restart the app, then click **"Listen for Test Event"** in n8n instead of activating the workflow. ## Connect to n8n ### Local n8n ```zsh docker volume create n8n_data docker run -it --rm \ --name n8n \ -p 5678:5678 \ -e GENERIC_TIMEZONE="Europe/Berlin" \ -e TZ="Europe/Berlin" \ -e N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true \ -e N8N_RUNNERS_ENABLED=true \ -v n8n_data:/home/node/.n8n \ docker.n8n.io/n8nio/n8n ``` Replace `Europe/Berlin` with your local timezone (e.g. `America/New_York`). The named volume `n8n_data` persists your workflows across container restarts. n8n will be available at `http://localhost:5678`. > **First run only:** open `http://localhost:5678` in a browser and create an owner account before proceeding. ### Environment Variables Configure the n8n base URL and optional API key in `application.yaml`: ```yaml n8n: base-url: ${N8N_BASE_URL:http://localhost:5678} api-key: ${N8N_API_KEY:} ``` `N8N_API_KEY` is sent as `X-N8N-API-KEY` on every webhook request — this is the same header n8n uses for its public REST API. Set `N8N_API_KEY` in your environment (or `~/.zshrc`) and configure the n8n Webhook node with **Authentication: Header Auth**, Name: `X-N8N-API-KEY`, Value: same string. Without it, n8n must have **Authentication: None** — otherwise it returns 403. ### BTP Destination For production deployments — especially SAP managed n8n instances behind a proxy — you can configure a BTP destination instead of a plain base URL. The destination takes priority over `base-url`: ```yaml n8n: destination: my-n8n-dest # BTP destination name (takes priority over base-url) api-key: ${N8N_API_KEY:} # optional; sent as X-N8N-API-KEY in addition to any proxy auth ``` When `destination` is set, the plugin resolves it via the SAP Cloud SDK at startup. The destination's URL and auth headers (e.g. `Authorization: Bearer …` for OAuth2 destinations) are merged into every request. `X-N8N-API-KEY` is then added on top — so both the outer proxy auth and the n8n-level API key are sent. The destination can also carry the API key as a custom property (`URL.headers.X-N8N-API-KEY`) instead of setting `n8n.api-key` — though `n8n.api-key` takes precedence if both are set. To use destinations, add `cloudplatform-connectivity` to your application's dependencies: ```xml com.sap.cloud.sdk.cloudplatform cloudplatform-connectivity ``` ### Credential Resolution The plugin resolves connection details in this order: 1. `n8n.use-console: true` — console mode, takes precedence over all other configuration (see [Console Mode](#console-mode)) 2. `n8n.destination` — BTP destination (takes priority over base-url) 3. `n8n.base-url` — explicit base URL in application.yaml 4. Dev-only fallback: `http://localhost:5678` (development profile only — throws at startup in any other profile) `n8n.api-key` is independent of the above and is always sent as X-N8N-API-KEY when set. ## Webhook Requests ### Test Webhooks The `path` value in each annotation is appended after `/webhook` to form the full webhook URL: `path: 'book-deleted'` → `http://localhost:5678/webhook/book-deleted` **Test vs. production webhooks** | Mode | n8n URL | When to use | |------|---------|-------------| | Production (default) | `/webhook` | Workflows are active and handle every call | | Test | `/webhook-test` | One-off manual testing; requires clicking "Listen for Test Event" in the n8n UI each time, and cannot handle bulk calls | Toggle test mode via `use-test-webhook` in `application.yaml`: ```yaml n8n: use-test-webhook: true # set to false (default) for production webhooks ``` ## Annotations Annotate entities or actions in your CDS model with `@n8n.process.start`. No additional Java code is needed — the plugin detects the annotation and fires the webhook automatically. ### Triggering a workflow Each trigger entry supports the following properties: | Property | Required | Description | |----------|----------|-------------| | `on` | yes | Event name — `CREATE`, `READ`, `UPDATE`, `DELETE`, or the action name | | `path` | yes | Appended to `n8n.base-url` + `/webhook` (or `/webhook-test` if `use-test-webhook: true` in application.yaml) to form the full webhook URL | | `method` | no | HTTP method to use when calling the n8n webhook — one of `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`. Defaults to `POST`. Must match the method configured on the n8n Webhook node. | | `inputs` | no | Fields to include in the payload; defaults to all direct entity attributes when omitted | **Entity events (CRUD):** ```cds annotate AdminService.Books with @n8n.process.start: [ {on: 'DELETE', path: 'book-deleted', inputs: [$self.ID, $self.title, $self.stock]}, {on: 'UPDATE', path: 'book-updated', method: 'PUT', inputs: [$self.ID, $self.title]} ]; ``` An invalid value (e.g. `method: 'YOLO'`) causes the application to fail at startup with a descriptive error: ``` IllegalStateException: @n8n.process.start[0] on entity 'MyService.Books' has invalid 'method' value 'YOLO'. Allowed values: GET, POST, PUT, PATCH, DELETE, HEAD. ``` > **Note:** `GET` sends no request body — the payload is serialized as query parameters instead. All other methods send a JSON body. **For custom actions:** ```cds annotate CatalogService.submitOrder with @n8n.process.start.on: 'submitOrder' @n8n.process.start.path: 'order-submitted'; ``` When the annotated event fires, the plugin posts the selected `inputs` fields as a flat JSON object to the configured webhook URL. For example, with the `inputs` list above: ```json { "ID": "abc123", "title": "The Hobbit", "stock": 42 } ``` When `inputs` is omitted, all direct fields of the entity are included in the payload. Specify `inputs` explicitly to limit which fields are sent — useful to avoid exposing sensitive or large fields. **Association fields** can be included using dot notation — the plugin issues a single expanded query to fetch the associated data: ```cds annotate AdminService.Books with @n8n.process.start: [ {on: 'DELETE', path: 'book-deleted', inputs: [$self.ID, $self.title, $self.author.name]} ]; ``` This produces a payload with the leaf field name as the key: ```json { "ID": "abc123", "title": "The Hobbit", "name": "Tolkien" } ``` > **Note:** > 1. Only one level of association traversal is supported (`$self.author.name`). Deeper paths (`$self.author.address.city`) are skipped with a warning. This is a known limitation compared to the Node.js plugin — contributions welcome. > 2. Association paths are *not* resolved for `CREATE` events. For these, the plugin uses the raw request payload (the data as submitted), so association fields like `$self.author.name` will be `null`. Use direct FK fields (e.g. `$self.author_ID`) for `CREATE` triggers instead. ### Conditions Add an `if` expression to a trigger entry to fire the webhook only when the condition is met: ```cds annotate AdminService.Books with @n8n.process.start: [ {on: 'DELETE', path: 'book-deleted', if: (stock = 0), inputs: [$self.ID, $self.title, $self.author_ID]}, {on: 'UPDATE', path: 'book-updated', inputs: [$self.ID, $self.title]}, {on: 'UPDATE', path: 'book-low-stock', inputs: [$self.ID, $self.title, $self.stock], if: (stock < 10)} ]; ``` Only deletes where stock is 0 fire `book-deleted`; every update fires `book-updated`; only updates that bring stock below 10 also fire `book-low-stock` — e.g. to trigger a reorder workflow in n8n. The `if` property is optional. When present, the webhook fires only when the condition evaluates to true against the entity row: | Property | Required | Description | |----------|----------|-------------| | `if` | no | CDS expression — webhook fires only when the condition is true | Supported operators: `=`, `==`, `!=`, `<>`, `<`, `<=`, `>`, `>=`, `in`, `like`, `between`, `is null`, `is not null`, `not`, `and`, `or`. > **Note:** The `if` condition is evaluated in application code against the entity row available at the time the event fires — it is not pushed to the database. This means it uses the same data the plugin already has: the CQN payload for CREATE, and the prefetched row for UPDATE and DELETE. Complex expressions involving subqueries or navigation paths that aren't part of the prefetched columns will not work. ### Multiple Triggers Multiple trigger entries for the same event on the same entity are supported — all matching entries fire. This allows you to route to different n8n workflows from one event, optionally with different `if` conditions. ## Programmatic API ### Trigger a Workflow For cases where you need full control over when and what is sent, inject `N8nService` directly into any CAP event handler and call `.trigger()`: ```java @Component @ServiceName(AdminService_.CDS_NAME) public class AdminServiceHandler implements EventHandler { @Autowired private N8nService n8nService; @After(event = CqnService.EVENT_CREATE, entity = "AdminService.Books") public void afterCreateBook(List books) { books.forEach(book -> n8nService.trigger("book-created", Map.of( "ID", book.getId(), "title", book.getTitle() ))); // defaults to POST when no HTTP Method is specified } } ``` The first argument to `.trigger()` is the webhook path — appended after `/webhook` to form the full URL (e.g. `"book-created"` → `http://localhost:5678/webhook/book-created`). The second argument is the payload — any `Map` you choose to send. The third argument is the HTTP method — pass `POST`, `PUT`, `PATCH`, `DELETE`, `GET`, or `HEAD`. It must match the method configured on the n8n Webhook node. If you omit the third argument, `POST` is used by default. > **Note:** The annotation-based and programmatic approaches are independent. You can use both in the same application, but take care not to fire duplicate webhooks for the same event. ## Delivery Behaviour The plugin retries failed webhook calls only on **network-level errors** — when n8n is unreachable (connection refused, timeout). HTTP error responses are not retried: | Response | Meaning | Retried? | |----------|---------|----------| | Network error / timeout | n8n is down or unreachable | Yes | | 5xx | n8n responded but the workflow itself failed | No | | 4xx | Misconfiguration (wrong URL, bad auth) | No | Retry behavior is managed by the CAP persistent outbox. Configure it under `cds.outbox.services.N8nOutbox` in your `application.yaml`: ```yaml cds: outbox: services: N8nOutbox: maxAttempts: 10 # total attempts before the message is marked as failed ordered: true # process messages in submission order (default: true) ``` ### Console Mode The plugin ships a built-in console mode for local development and CI environments where no n8n instance is available. When enabled, webhook calls are logged instead of sent via the specified HTTP Method — the app behaves normally but never makes an HTTP request to n8n. Delivery is synchronous and skips the persistent outbox entirely, so no rows are written to `cds_outbox_Messages`. ```yaml n8n: use-console: true ``` No `base-url` is needed. Console mode takes precedence over all other configuration. You'll see: ``` INFO ConsoleN8NWebhookService - [console-n8n-service]: would POST /webhook/book-deleted ``` Inject `ConsoleN8NWebhookService` to assert on webhook calls without a real n8n instance: ```java @SpringBootTest @TestPropertySource(properties = "n8n.use-console=true") class MyServiceTest { @Autowired ConsoleN8NWebhookService consoleWebhookService; @Test void deleteBook_triggersWebhook() { // ... trigger a delete ... assertThat(consoleWebhookService.getExecutions()).hasSize(1); Map exec = consoleWebhookService.getExecutions().get(0); assertThat(exec.get("path")).isEqualTo("book-deleted"); assertThat(exec.get("status")).isEqualTo("success"); } } ``` Each execution record contains: `id`, `path`, `method`, `payload`, `startedAt`, `finishedAt`, `status`. If `n8n.use-console` is `false` (the default) and `n8n.base-url` is not set: | Profile | Behaviour | |---------|-----------| | `development` | Warns at startup and falls back to `http://localhost:5678`. HTTP calls fail gracefully if n8n is not running — the outbox retries with backoff. | | any other | Throws `IllegalStateException` at startup: set `N8N_BASE_URL` or use `n8n.use-console=true`. | ## Tests Run the unit tests from the project root: ```zsh mvn test ``` To also generate a JaCoCo coverage report (unit tests only): ```zsh mvn verify ``` To include the retry integration test in coverage: ```zsh mvn verify -pl cds-feature-n8n -am -Dtest="N8nHandlerTest,N8nWebhookServiceRetryIT" ``` The report is written to `cds-feature-n8n/target/site/jacoco/index.html`. ### Integration tests The retry integration test (`N8nWebhookServiceRetryIT`) is excluded from the default `mvn test` run because Maven Surefire skips `*IT.java` classes by default. Run it explicitly: ```zsh mvn test -pl cds-feature-n8n -am -Dtest=N8nWebhookServiceRetryIT ``` This test uses [WireMock](https://wiremock.org) to start a local HTTP server that stands in for n8n, verifying that the retry logic fires the webhook up to three times before giving up. ### Testing with the sample application The `samples/bookshop` directory contains a complete CAP bookshop app that demonstrates the plugin with real webhook triggers. For a full walkthrough including starting n8n locally in Docker, see [Local n8n](#local-n8n). **Quick start (assumes n8n is already running):** ```zsh cd samples/bookshop/srv mvn spring-boot:run ``` The sample configures three webhook triggers on `AdminService.Books`: `DELETE` with `if: (stock = 0)` fires `book-deleted`; every `UPDATE` fires `book-updated`; and updates that bring stock below 10 also fire `book-low-stock`. See [Local n8n](#local-n8n) for the full walkthrough. 404 → listener expired or workflow not saved. 403 → `X-N8N-API-KEY` mismatch. ## Support, Feedback, Contributing This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/cap-java/cds-feature-n8n/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md). ## Security / Disclosure If you find any bug that may be a security problem, please follow our instructions at [in our security policy](https://github.com/cap-java/cds-feature-n8n/security/policy) on how to report it. Please do not create GitHub issues for security-related doubts or problems. ## Code of Conduct We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](https://github.com/cap-java/.github/blob/main/CODE_OF_CONDUCT.md) at all times. ## Licensing Copyright 2026 SAP SE or an SAP affiliate company and cds-feature-n8n contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/cap-java/cds-feature-n8n).