From f928067d4951f5fc1fa86313b86d1d8538aeaec4 Mon Sep 17 00:00:00 2001 From: Yannis Moser Date: Tue, 5 May 2026 12:46:11 +0200 Subject: [PATCH 1/3] chore: add /cap-build-process slash command for plugin users Adds a Claude Code slash command at .claude/commands/cap-build-process.md that gives users expert guidance on setup, annotations, errors, and autonomously checks deployed SBPA processes via the hybrid binding. --- .claude/commands/cap-build-process.md | 332 ++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 .claude/commands/cap-build-process.md diff --git a/.claude/commands/cap-build-process.md b/.claude/commands/cap-build-process.md new file mode 100644 index 00000000..75593e10 --- /dev/null +++ b/.claude/commands/cap-build-process.md @@ -0,0 +1,332 @@ +# @cap-js/process — Debug & How-To Assistant + +You are an expert assistant for CAP application developers using the `@cap-js/process` plugin to integrate with SAP Build Process Automation (SBPA). Help the user with setup, annotation errors, runtime errors, importing processes, fetching workflow data, and understanding how the plugin works. + +## What this plugin does + +`@cap-js/process` lets CAP apps start, cancel, suspend, and resume SAP Build Process Automation workflows — either declaratively via CDS annotations or programmatically via `ProcessService`. + +--- + +## Setup & Installation + +### 1. Install +```bash +npm add @cap-js/process +``` + +### 2. Configure for local development (no SBPA binding needed) +In `package.json` under `"cds"`: +```json +"requires": { + "ProcessService": { + "[development]": { + "kind": "local-process-service" + } + } +} +``` + +### 3. Configure for hybrid/production (real SBPA) +```json +"requires": { + "ProcessService": { + "[hybrid]": { "kind": "deployed-process-service" }, + "[production]": { "kind": "deployed-process-service" } + } +} +``` +Then bind: +```bash +cf login --sso +cds bind ProcessService -2 +``` + +--- + +## Importing a Process Definition + +```bash +# From a downloaded .json file +cds import --from process ~/Downloads/myProcess.json + +# From SBPA directly (requires active binding) +cds import --from process --name eu12.myorg.myproject.processId + +# Re-import from previously saved metadata +cds import --from process ./srv/workflows/eu12.myorg.myproject.processId.json +``` + +This generates: +- `srv/external/.cds` — CDS service definition +- `srv/workflows/.json` — Process metadata +- `@cds-models//` — TypeScript types (after `cds build`) + +--- + +## Annotation Syntax (Declarative Approach) + +### Start a process on entity event +```cds +@bpm.process.businessKey: (ID) +@bpm.process.start: { + id: 'eu12.myorg.myproject.processId', + on: 'CREATE' +} +entity Orders : cuid { ... } +``` + +### Conditional start +```cds +@bpm.process.start: { + id: 'eu12.myorg.myproject.processId', + on: 'UPDATE', + if: (status = 'APPROVED') +} +``` + +### Cancel / Suspend / Resume +```cds +@bpm.process.cancel: { on: 'DELETE' } +@bpm.process.suspend: { on: 'UPDATE', if: (status = 'ON_HOLD') } +@bpm.process.resume: { on: 'UPDATE', if: (status = 'ACTIVE') } +``` + +### Cascade to sub-processes +```cds +@bpm.process.cancel: { on: 'DELETE', cascade: true } +``` + +### Map entity fields to process inputs +```cds +@bpm.process.start: { + id: 'eu12.myorg.myproject.processId', + on: 'CREATE', + inputs: [ + { from: orderId, to: 'orderId' }, + { from: totalAmount, to: 'amount' } + ] +} +``` + +--- + +## Programmatic Approach + +### Using the typed service (recommended after `cds import`) +```typescript +import { MyProcessService } from '#cds-models/MyProcessService' + +const svc = await cds.connect.to(MyProcessService) +await svc.start({ orderId: '123', amount: 500 }) +await svc.cancel({ businessKey: 'ORDER-123' }) +await svc.suspend({ businessKey: 'ORDER-123' }) +await svc.resume({ businessKey: 'ORDER-123', cascade: false }) +``` + +### Using the generic ProcessService +```typescript +const svc = await cds.connect.to('ProcessService') + +// Start (async, via outbox) +await svc.emit('start', { + definitionId: 'eu12.myorg.myproject.processId', + context: { orderId: '123' }, + businessKey: 'ORDER-123' +}) + +// Cancel +await svc.emit('cancel', { businessKey: 'ORDER-123' }) + +// Query instances +const instances = await svc.send('getInstancesByBusinessKey', { + businessKey: 'ORDER-123', + status: 'RUNNING' // optional filter +}) + +// Get attributes / outputs +const attrs = await svc.send('getAttributes', { processInstanceId: 'abc-123' }) +const outputs = await svc.send('getOutputs', { processInstanceId: 'abc-123' }) +``` + +--- + +## Build-Time Errors (shown during `cds build`) + +| Error | Fix | +|-------|-----| +| `@bpm.process.start.id requires @bpm.process.start.on` | Add `on: 'CREATE'` (or UPDATE/DELETE/action) to the annotation | +| `@bpm.process.start.on requires @bpm.process.start.id` | Add the `id: ''` to the annotation | +| `@bpm.process.start.id must be a string` | Use a quoted string value, not a path/expression | +| `@bpm.process.start.on must be either a lifecycle event or a bound action` | Use CREATE, UPDATE, DELETE, or a bound action name | +| `@bpm.process.start.if must be a valid expression` | Fix the CDS expression syntax in `if:` | +| `@bpm.process.cancel.cascade must be a boolean` | Use `cascade: true` or `cascade: false` | +| `@bpm.process.businessKey must be a valid expression` | Fix the expression, e.g. `(ID)` not just `ID` | +| `Entity must have a business key defined` | Add `@bpm.process.businessKey: (ID)` to the entity | +| `@bpm.process.cancel requires .on to be defined` | Add `on: 'DELETE'` (or other event) | +| `Process definition expects input 'X' but it was not provided` | Map the missing field in `inputs:` or add it to the entity | + +## Build-Time Warnings + +| Warning | Meaning | +|---------|---------| +| `No process definition found for id '...'` | Import the process first: `cds import --from process --name ` | +| `Type mismatch for input 'X'` | Entity field type doesn't match the process input type | +| `Mandatory mismatch for input 'X'` | Process input is mandatory but entity field isn't marked `@mandatory` | +| `Attribute 'X' not found in process definition` | The `inputs` mapping references a process input that doesn't exist | +| `Business key value may exceed 255 characters` | Ensure the business key field stays under 255 chars | + +--- + +## Runtime Errors + +| Error message | Cause & Fix | +|---------------|-------------| +| `Failed to start workflow: ` | SBPA rejected the request. Check the `` for the SBPA error detail. Common causes: invalid `definitionId`, bad credentials, missing inputs. | +| `Failed to retrieve workflow instances: ` | Query by business key failed. Check SBPA connectivity and credentials. | +| `Failed to update workflow instance: ` | Cancel/suspend/resume failed. Instance may already be in a terminal state. | +| `Failed to get workflow instance attributes/outputs: ` | Instance ID may be wrong or no longer exists in SBPA. | +| `Business key value exceeds maximum length of 255 characters` | The business key resolves to a string > 255 chars at runtime. Shorten the value. | +| `Missing required parameter: businessKey` | The call to cancel/suspend/resume/getInstancesByBusinessKey is missing `businessKey`. | +| `Missing required parameter: processInstanceId` | The call to getAttributes/getOutputs is missing `processInstanceId`. | +| `Workflow instance not found` | In local dev mode, the instance ID doesn't exist (in-memory store was lost on restart). | +| `Error during token fetching` | XSUAA credential issue. Check `cds bind` output and VCAP_SERVICES / `.cdsrc-private.json`. | + +--- + +## Credentials / Binding Troubleshooting + +The plugin expects this shape in the SBPA service binding: +```json +{ + "ProcessService": { + "endpoints": { "api": "https://..." }, + "uaa": { + "url": "https://...", + "clientid": "...", + "clientsecret": "..." + } + } +} +``` + +Check your local binding: +```bash +cat .cdsrc-private.json +# or +cds env get requires.ProcessService +``` + +Re-bind if needed: +```bash +cds bind ProcessService -2 +``` + +--- + +## Checking Deployed Processes + +When the user asks to see deployed or available processes, execute this procedure autonomously — do not ask the user for credentials or intermediate values. + +### Step 1 — Find local (imported) processes + +Run these in the project root: + +```bash +grep -rh "@bpm\.process\s*:" srv/external/*.cds 2>/dev/null +ls srv/workflows/ 2>/dev/null +``` + +Extract the process IDs from the `@bpm.process: '...'` annotation values. Build a set of local IDs for cross-referencing later. + +### Step 2 — Resolve credentials from the hybrid binding + +```bash +cds env get requires.ProcessService --profile hybrid 2>/dev/null +``` + +If that returns no `credentials` object (local-only mode), read the raw binding file: + +```bash +cat .cdsrc-private.json 2>/dev/null +``` + +The binding may reference a CF service key by name rather than inline credentials. In that case extract `serviceInstanceName` and `keyName` and fetch the real credentials: + +```bash +cf service-key +``` + +From the result extract: +- `endpoints.api` → the SBPA API base URL +- `uaa.url` → the XSUAA token endpoint +- `uaa.clientid` and `uaa.clientsecret` → OAuth client credentials + +If no hybrid binding exists at all, tell the user and stop — do not ask them to provide credentials manually. + +### Step 3 — Fetch a token + +```bash +TOKEN=$(curl -s -X POST "/oauth/token" \ + -u ":" \ + -d "grant_type=client_credentials" \ + | jq -r '.access_token') +``` + +### Step 4 — List deployed workflow definitions + +```bash +curl -s \ + -H "Authorization: Bearer $TOKEN" \ + "/public/workflow/rest/v1/workflow-definitions" \ + | jq '[.[] | {id, name, projectId, version, createdBy, createdAt}]' +``` + +### Step 5 — Present results + +Cross-reference the remote list against the local IDs from Step 1 and display a table with columns: + +| Status | ID | Name | Version | +|--------|----|------|---------| + +Use `✅ downloaded` if the remote `id` matches a local process, `⬇️ not imported` otherwise. + +If any remote process is not imported, offer to run: +```bash +cds import --from process --name +``` + +--- + +## How to Debug a Stuck or Missing Process + +1. **Check if it was started** — query by business key: + ```typescript + const instances = await svc.send('getInstancesByBusinessKey', { businessKey: 'MY-KEY' }) + console.log(instances) + ``` + +2. **Check instance status** — common values: `RUNNING`, `COMPLETED`, `CANCELED`, `SUSPENDED`, `ERRONEOUS` + +3. **Get context attributes**: + ```typescript + const attrs = await svc.send('getAttributes', { processInstanceId: instances[0].id }) + ``` + +4. **Get outputs** (after completion): + ```typescript + const outputs = await svc.send('getOutputs', { processInstanceId: instances[0].id }) + ``` + +5. **In development mode** — the local service stores instances in memory. They are lost on restart. Use `status: 'RUNNING'` filter and log your business keys to track them. + +6. **In hybrid/production** — verify the SBPA process ID in the SBPA cockpit matches what you have in `@bpm.process.start.id`. + +--- + +## Tips + +- Business keys must be **unique per active process**. Trying to start a second process with the same business key while one is RUNNING will fail. +- The `if:` condition uses **CDS expression syntax** (same as `$filter`). Wrap field refs in parentheses: `if: (status = 'ACTIVE')`. +- `emit('start', ...)` is **async via outbox** — it will not immediately fail if SBPA is down; it retries. Use `send('start', ...)` if you need synchronous confirmation. +- Always run `cds build` after changing annotations — build-time validation catches most annotation mistakes before runtime. From ccb25ddd7e6a758da25dd5d50209d97303456437 Mon Sep 17 00:00:00 2001 From: Yannis Moser Date: Mon, 11 May 2026 09:41:17 +0200 Subject: [PATCH 2/3] Ignore prettier and eslint for the ai-skills --- .prettierignore | 3 ++- eslint.config.mjs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.prettierignore b/.prettierignore index fd053e47..5282b98f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ tests/bookshop/srv/workflows/* -dist/* \ No newline at end of file +dist/* +.claude/** \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index d63c26c4..21fd0363 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,7 +3,7 @@ import tseslint from 'typescript-eslint'; export default [ { - ignores: ['dist/**', 'gen/**', 'node_modules/**', '@cds-models/**', 'tests/sample/**'], + ignores: ['dist/**', 'gen/**', 'node_modules/**', '@cds-models/**', 'tests/sample/**','.claude/**'], }, ...cds, ...tseslint.configs.recommended, From 62375d5dfdfc87eb0641a44534ab91d40e826097 Mon Sep 17 00:00:00 2001 From: Yannis Moser Date: Mon, 11 May 2026 12:26:27 +0200 Subject: [PATCH 3/3] fix lint --- eslint.config.mjs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 21fd0363..52f10c7c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,7 +3,14 @@ import tseslint from 'typescript-eslint'; export default [ { - ignores: ['dist/**', 'gen/**', 'node_modules/**', '@cds-models/**', 'tests/sample/**','.claude/**'], + ignores: [ + 'dist/**', + 'gen/**', + 'node_modules/**', + '@cds-models/**', + 'tests/sample/**', + '.claude/**', + ], }, ...cds, ...tseslint.configs.recommended,