Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
332 changes: 332 additions & 0 deletions .claude/commands/cap-build-process.md
Original file line number Diff line number Diff line change
@@ -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 <service-instance-name>
```

---

## 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/<ProcessName>.cds` — CDS service definition
- `srv/workflows/<processId>.json` — Process metadata
- `@cds-models/<ProcessName>/` — 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 })
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The typed service start method signature is wrong. Looking at processService.ts, the typed service's start handler receives req.data.inputs (not top-level named fields). The inputs object is what gets forwarded as context. The call svc.start({ orderId: '123', amount: 500 }) implies flat named parameters are the API, but the actual typed service generated after cds import wraps them under inputs. Verify the generated typed API before documenting this signature — it may need to be svc.start({ inputs: { orderId: '123', amount: 500 } }) or similar, depending on the generated CDS action signature.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

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'
})
Comment on lines +132 to +136
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: start on the generic ProcessService must use emit, but cancel/suspend/resume must also use emit — however the start payload shape is wrong.

Looking at the actual BTPProcessService.cds and BTPProcessService.ts, the start event expects { definitionId, context } — not a top-level businessKey field. The businessKey is passed as a messaging header (third argument to cds.queued(...).emit), not in the payload body. The example shown here incorrectly puts businessKey inside the payload object.

Should be:

await svc.emit('start', {
  definitionId: 'eu12.myorg.myproject.processId',
  context: { orderId: '123' }
})

The businessKey in the outbox header is handled internally by the plugin and is not a user-supplied field in this call.

Suggested change
await svc.emit('start', {
definitionId: 'eu12.myorg.myproject.processId',
context: { orderId: '123' },
businessKey: 'ORDER-123'
})
// Start (async, via outbox)
await svc.emit('start', {
definitionId: 'eu12.myorg.myproject.processId',
context: { orderId: '123' }
})

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful


// Cancel
await svc.emit('cancel', { businessKey: 'ORDER-123' })
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: cancel, suspend, and resume on the generic ProcessService are events (not actions), so they must also be dispatched with emit, not send. The CDS service definition (BTPProcessService.cds) declares them as event, and the implementation uses cds.queued(processService).emit(...). Calling svc.send('cancel', ...) would attempt a synchronous action invocation and fail at runtime.

Consider changing all three lifecycle operations to use emit:

await svc.emit('cancel', { businessKey: 'ORDER-123' })
await svc.emit('suspend', { businessKey: 'ORDER-123' })
await svc.emit('resume', { businessKey: 'ORDER-123' })
Suggested change
await svc.emit('cancel', { businessKey: 'ORDER-123' })
// Cancel
await svc.emit('cancel', { businessKey: 'ORDER-123' })
// Suspend
await svc.emit('suspend', { businessKey: 'ORDER-123' })
// Resume
await svc.emit('resume', { businessKey: 'ORDER-123' })

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful


// 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: '<processId>'` 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 <id>` |
| `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: <body>` | SBPA rejected the request. Check the `<body>` for the SBPA error detail. Common causes: invalid `definitionId`, bad credentials, missing inputs. |
| `Failed to retrieve workflow instances: <body>` | Query by business key failed. Check SBPA connectivity and credentials. |
| `Failed to update workflow instance: <body>` | Cancel/suspend/resume failed. Instance may already be in a terminal state. |
| `Failed to get workflow instance attributes/outputs: <body>` | 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 <service-instance-name>
```

---

## 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
Comment on lines +235 to +237
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic Error: The grep pattern is wrong for finding local process IDs. The annotation that carries the process definition ID is @bpm.process.start, @bpm.process.cancel, etc. — not a bare @bpm.process:. The pattern @bpm\.process\s*: would not match any actual annotation and would always return empty results, causing Step 5's cross-reference to show every remote process as ⬇️ not imported.

Consider scanning for the id: field within the annotation blocks, or use a pattern that matches the actual generated content of the .cds files (e.g., the @bpm.process: annotation on the service itself):

grep -rh "bpm\.process" srv/external/*.cds 2>/dev/null
ls srv/workflows/*.json 2>/dev/null | xargs -I{} basename {} .json

Using the workflow JSON filenames from srv/workflows/ is the most reliable source of local process IDs.

Suggested change
```bash
grep -rh "@bpm\.process\s*:" srv/external/*.cds 2>/dev/null
ls srv/workflows/ 2>/dev/null
ls srv/workflows/*.json 2>/dev/null | xargs -I{} basename {} .json

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

```

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 <serviceInstanceName> <keyName>
```

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 "<uaa.url>/oauth/token" \
-u "<clientid>:<clientsecret>" \
-d "grant_type=client_credentials" \
| jq -r '.access_token')
```

### Step 4 — List deployed workflow definitions

```bash
curl -s \
-H "Authorization: Bearer $TOKEN" \
"<endpoints.api>/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 <id>
```

---

## 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.
Loading