cap-n8n
v0.1.0
Published
CDS plugin mapping CAP onto n8n: programmatic Workflow API, @n8n.trigger annotations via the transactional outbox, and a typed `cds import --from n8n`.
Maintainers
Readme
cap-n8n
A SAP CAP plugin that maps the @cap-js/process shape onto
n8n: a programmatic Workflow API, @n8n.trigger annotations routed
through the transactional outbox, and a typed cds import --from n8n.
The only thing you need to provide is an n8n service key (a base URL + API key from any n8n instance). Everything else — seeding workflows, activating, triggering, polling executions, asserting — runs autonomously.
At a glance
Annotations — fire n8n webhooks on lifecycle events, after commit, via the transactional outbox:
@n8n.trigger: 'book-ordered' // CREATE + UPDATE
entity Orders as projection on my.Orders;
@n8n.trigger: { workflow: 'book-saved', on: ['SAVE'] } // CRUD + Fiori draft events
@odata.draft.enabled entity Books as projection on my.Books;Programmatic API — drive workflows from your handlers:
const n8n = await cds.connect.to('n8n')
await n8n.trigger('book-ordered', { book: 'Moby Dick' }) // POST /webhook/:path
const execs = await n8n.listExecutions(workflowId) // poll & assert resultsTyped import — turn a workflow into a typed CDS service:
cds import workflow.json --from n8n
# → action trigger(book: String, quantity: Integer) returns { executionId: String };Console mode in development (no n8n needed), .env or BTP destination otherwise.
Getting started — the sample app
test/bookshop is the reference application: a minimal bookshop
showing every feature (all annotation forms, draft events, bound actions, the typed
import, and the test setup). It runs without any n8n instance:
npm install && npm run build
cd test/bookshop
npm run watch # console mode — triggers are logged, no n8n needed
npm run watch:live # real mode — needs an n8n service key in .env (see .env.example)Start there: test/bookshop/README.md walks through
firing each trigger with curl and using the app as a template for your own project.
Install
npm add cap-n8nConfigure connectivity
Local development — drop the service key into a .env file (gitignored):
N8N_BASE_URL=https://your-instance.app.n8n.cloud
N8N_API_KEY=eyJ...your-api-key...By default the plugin runs in console mode (kind: n8n-console): triggers are logged
and a synthetic executionId is returned, so the app runs with no instance at all. To talk
to a real instance locally, run with the REST kind, e.g.:
cds watch --profile hybrid # or set requires.n8n.kind = 'n8n-rest'Production (BTP) — user-provided service (simplest) — create and bind a
user-provided service instance carrying the service key. CAP merges its credentials
into cds.requires.n8n.credentials automatically:
cf create-user-provided-service n8n -p '{"baseUrl":"https://your.n8n.cloud","apiKey":"eyJ..."}'
cf bind-service <your-app> n8nIf the instance has a different name, point CAP at it from your app config:
{ "cds": { "requires": { "n8n": { "vcap": { "name": "my-n8n" } } } } }Production / hybrid (BTP) — destination — alternatively bind a destination named
n8n. Put the base URL in the destination URL and the API key in the additional
property URL.headers.X-N8N-API-KEY. The plugin resolves it via
@sap-cloud-sdk/connectivity, exactly like @cap-js/notifications.
Inline credentials — you can also put credentials directly in your app config and
keep the secret in an environment variable via the env: indirection:
{
"cds": {
"requires": {
"n8n": {
"credentials": { "baseUrl": "https://your.n8n.cloud", "apiKey": "env:N8N_API_KEY" }
}
}
}
}Resolution order: bound/inline credentials (incl. cds bind in hybrid mode) →
BTP destination → N8N_BASE_URL/N8N_API_KEY environment variables.
Programmatic API
const n8n = await cds.connect.to('n8n')
await n8n.trigger('book-ordered', { book: 'Moby Dick', buyer: '[email protected]' }) // POST /webhook/:path
await n8n.activate(workflowId) // POST /api/v1/workflows/:id/activate
await n8n.deactivate(workflowId) // POST /api/v1/workflows/:id/deactivate
await n8n.listExecutions(workflowId) // GET /api/v1/executions?workflowId=
await n8n.getExecution(executionId) // GET /api/v1/executions/:id?includeData=true
await n8n.createWorkflow(json) // POST /api/v1/workflows (seed helper)Annotations
Annotate an entity or event with the n8n production-webhook path. After the surrounding
transaction commits, the payload is POSTed to {baseUrl}/webhook/<path>, routed through
the CAP transactional outbox for resilience:
// string shorthand — fires on CREATE and UPDATE
@n8n.trigger: 'book-ordered'
entity Orders as projection on my.Orders;
// record form — pick the lifecycle events explicitly
@n8n.trigger: { workflow: 'book-archived', on: ['DELETE'] }
entity Books as projection on my.Books;
// Fiori draft-enabled entities: draft lifecycle events are supported
@odata.draft.enabled
@n8n.trigger: { workflow: 'book-saved', on: ['SAVE'] }
entity Books as projection on my.Books actions {
// bound actions fire when called (on the active or the draft row)
@n8n.trigger: 'book-archived'
action archive();
};
// unbound actions/functions and custom events work too
event OrderShipped @(n8n.trigger: 'order-shipped') { orderId: UUID; }Supported on events (entities):
| Event | Fires | Payload |
|---|---|---|
| CREATE / UPDATE | row written (incl. draft activation) | the resulting row |
| DELETE | row deleted | the deleted row's keys |
| SAVE | draft activated (Fiori "Save") — on non-draft entities = CREATE+UPDATE shortcut | the activated data |
| EDIT | user starts editing (draft created from active row) | the row being edited |
| NEW / PATCH / DISCARD | draft created / field changed / discarded | the draft data |
NEW/PATCH/SAVE/EDIT/DISCARD require @odata.draft.enabled (CANCEL is accepted
as the legacy alias for DISCARD). Bound-action payloads are the bound row's keys merged
with the action parameters; unbound actions send the call parameters.
Typed import
cds import workflow.json --from n8nGenerates a typed CDS service with a trigger action. The input contract is derived, most
reliable first:
- A node named
CAP Input Schemaholding a JSON Schema (recommended convention). - The webhook node's declared body fields.
- Pinned/sample data (
*.sample.jsonor the node'spinData). - Fallback: a single
body : LargeStringpassthrough.
Tests
npm test- The console integration suite always runs (no key needed).
- The REST integration suite runs the live seed → activate → trigger → poll → assert
loop and skips gracefully unless
N8N_BASE_URL+N8N_API_KEYare set. - For CI, run an
n8nio/n8ncontainer and pointN8N_BASE_URLat it.
License
Apache-2.0
