Make sure to follow https://cap.cloud.sap/docs/get-started/ to install global dependencies that are required for CAP aplication development.
npm install
npm i -g tsx
npm run build
cd /tests/bookshop
cds watch
- (in future): run
npm add @cap-js/process- before first release:
npm add git+https://github.com/cap-js/process.git
- before first release:
- Login to cf
cf login ... - Bind to process service instance:
cds bind ProcessService -to <sbpa-service-instance>
Start developing 🙂
@bpm.process.start-- Start a process (or classic workflow), either after entity creation, update, deletion, read, or any custom action including all entity elements unless at least one@bpm.process.inputis given- if no attribute is annotated with
@bpm.process.input, all attributes of that entity will be fetched and are part of the context for process input. Associations will not be expanded in that case @bpm.process.start.id-- definition ID for deployed process@bpm.process.start.on@bpm.process.start.if-- only starting process if expression is true
- if no attribute is annotated with
@bpm.process.start.inputs-- array of input mappings that control which entity fields are passed as process context (optional)
Important: The target process must have an input attribute businesskey of type string. The entity's key is used as the businesskey value, which links the process instance to the entity for later CANCEL/SUSPEND/RESUME operations.
The inputs array controls which entity fields are passed as context when starting a process.
When inputs is not specified, all direct attributes of the entity are fetched and passed as process context. Associations and compositions are not expanded - only scalar fields are included.
@bpm.process.start: {
id: 'orderProcess',
on: 'CREATE'
}
entity Orders {
key ID : UUID;
status : String(20);
total : Decimal(15, 2);
items : Composition of many OrderItems on items.order = $self;
};
// Context: { ID, status, total, businesskey }
// Note: 'items' composition is NOT includedUse $self.fieldName to include specific fields.
@bpm.process.start: {
id: 'orderProcess',
on: 'CREATE',
inputs: [
$self.ID,
$self.status
]
}
entity Orders {
key ID : UUID;
status : String(20);
total : Decimal(15, 2); // Not included
};
// Context: { ID, status, businesskey }Use $self alone (without a field name) to include all scalar fields of the entity. This is useful when you want all entity fields plus specific compositions.
@bpm.process.start: {
id: 'orderProcess',
on: 'CREATE',
inputs: [
$self, // All scalar fields: ID, status, shipmentDate, totalValue
$self.items // Plus the composition with all its scalar fields
]
}
entity Orders {
key ID : UUID;
status : String(20);
shipmentDate : Date;
totalValue : Decimal(15, 2);
items : Composition of many OrderItems on items.parent = $self;
};
// Context: { ID, status, shipmentDate, totalValue, businesskey, items: [{ ID, title, quantity, parent_ID }, ...] }Note: $self alone behaves identically to the default behavior (no inputs array), but allows you to combine it with explicit composition expansions in the same inputs array.
Use { path: $self.fieldName, as: 'TargetName' } to rename fields for the process.
@bpm.process.start: {
id: 'orderProcess',
on: 'CREATE',
inputs: [
$self.ID,
{ path: $self.total, as: 'OrderAmount' }
]
}
entity Orders {
key ID : UUID;
total : Decimal(15, 2);
};
// Context: { ID, OrderAmount, businesskey }Include composition without child field selection ($self.items):
When you include a composition without specifying any nested fields (e.g., $self.items alone), all direct attributes of the child entity are expanded. This behaves like the default behavior but for the nested entity.
@bpm.process.start: {
id: 'orderProcess',
on: 'CREATE',
inputs: [
$self.ID,
$self.items // Expands all direct attributes of OrderItems
]
}
entity Orders {
key ID : UUID;
items : Composition of many OrderItems on items.order = $self;
};
entity OrderItems {
key ID : UUID;
order : Association to Orders;
product : String(200);
quantity : Integer;
};
// Context: { ID, businesskey, items: [{ ID, product, quantity }, ...] }
// Note: 'order' association in child is NOT included (associations not expanded)Include composition with selected child fields ($self.items.field):
When you specify nested field paths like $self.items.ID or $self.items.product, only those specific fields are included from the child entity.
@bpm.process.start: {
id: 'orderProcess',
on: 'CREATE',
inputs: [
$self.ID,
$self.items.ID,
$self.items.product
// quantity is NOT included
]
}
entity Orders {
key ID : UUID;
items : Composition of many OrderItems on items.order = $self;
};
entity OrderItems {
key ID : UUID;
order : Association to Orders;
product : String(200);
quantity : Integer;
};
// Context: { ID, businesskey, items: [{ ID, product }, ...] }Alias composition and nested fields:
@bpm.process.start: {
id: 'orderProcess',
on: 'CREATE',
inputs: [
$self.ID,
{ path: $self.items, as: 'OrderLines' },
$self.items.ID,
{ path: $self.items.product, as: 'ProductName' }
]
}
entity Orders {
key ID : UUID;
items : Composition of many OrderItems on items.order = $self;
};
entity OrderItems {
key ID : UUID;
product : String(200);
};
// Context: { ID, businesskey, OrderLines: [{ ID, ProductName }, ...] }Combining wildcards with aliases:
You can combine wildcard expansion ($self or $self.items) with specific field aliases. The wildcard expands all fields, and the alias adds the field again with the new name.
@bpm.process.start: {
id: 'orderProcess',
on: 'CREATE',
inputs: [
$self, // All scalar fields: ID, status, total
{ path: $self.ID, as: 'OrderId' }, // Add ID again as 'OrderId'
$self.items, // All child fields: ID, product, quantity
{ path: $self.items.ID, as: 'ItemId' } // Add items.ID again as 'ItemId'
]
}
entity Orders {
key ID : UUID;
status : String(20);
total : Decimal(15, 2);
items : Composition of many OrderItems on items.order = $self;
};
entity OrderItems {
key ID : UUID;
product : String(200);
quantity : Integer;
};
// Context: {
// ID, OrderId, // ID appears twice (original + alias)
// status, total, businesskey,
// items: [{
// ID, ItemId, // ID appears twice in each item
// product, quantity
// }, ...]
// }For entities with cyclic relationships, explicit deep paths let you control exactly how deep to traverse without infinite loops.
@bpm.process.start: {
id: 'shipmentProcess',
on: 'CREATE',
inputs: [
$self.ID,
$self.items.ID,
$self.items.shipment.ID, // Back to parent
$self.items.shipment.items.ID // Back to items again
]
}
entity Shipments {
key ID : UUID;
items : Composition of many ShipmentItems on items.shipment = $self;
};
entity ShipmentItems {
key ID : UUID;
shipment : Association to Shipments;
};service MyService {
@bpm.process.start: {
id: '<projectId>.<processId>',
on: 'CREATE | UPDATE | DELETE | boundAction',
if: (<expression>),
inputs: [
$self.field1,
{ path: $self.field2, as: 'AliasName' },
$self.items,
$self.items.nestedField
]
}
entity MyEntity {
key ID : UUID;
field1 : String;
field2 : String;
items : Composition of many ChildEntity on items.parent = $self;
};
}@bpm.process.<cancel|resume|suspend>-- Cancel/Suspend/Resume any processes bound to the entity (using entityKey as businessKey in SBPA)@bpm.process.<cancel|resume|suspend>.on@bpm.process.<cancel|resume|suspend>.cascade-- boolean (optional, defaults to false)@bpm.process.<cancel|resume|suspend>.if-- only starting process if expression is true- example:
@bpm.process.suspend.if: (weight > 10)
- example:
Example:
service MyService {
@bpm.process.<cancel|suspend|resume>: {
on: 'CREATE | UPDATE | DELETE | boundAction',
cascade: true | false, // optional, defaults to false
when: (<expression>)
}
entity MyProjection as projection on MyEntity {
myElement,
myElement2,
myElement3
};
}
Validation occurs during cds build and produces errors (hard failures that stop the build) or warnings (soft failures that are logged but don't stop the build).
@bpm.process.start.idand@bpm.process.start.onare mutually required — if one is present, the other must also be present@bpm.process.start.idmust be a string@bpm.process.start.onmust be a string representing either:- A CRUD operation:
CREATE,READ,UPDATE, orDELETE - A bound action defined on the entity
- A CRUD operation:
@bpm.process.start.ifmust be a valid CDS expression (if present)
- Unknown annotations under
@bpm.process.start.*trigger a warning listing allowed annotations - If no imported process definition is found for the given
id, a warning is issued as input validation is skipped
When both @bpm.process.start.id and @bpm.process.start.on are present and the process definition is imported:
Errors:
- The process definition must have a
businesskeyinput - Entity attributes specified in
@bpm.process.start.inputs(or all direct attributes ifinputsis omitted) must exist in the process definition inputs - Mandatory inputs from the process definition must be present in the entity
Warnings:
- Type mismatches between entity attributes and process definition inputs
- Array cardinality mismatches (entity is array but process expects single value or vice versa)
- Mandatory flag mismatches (process input is mandatory but entity attribute is not marked as
@mandatory)
Note: Associations and compositions are recursively validated, and cycles in entity associations are detected and reported as errors.
@bpm.process.<cancel|suspend|resume>.onis required for cancel/suspend/resume operations and must be a string representing either:- A CRUD operation:
CREATE,READ,UPDATE, orDELETE - A bound action defined on the entity
- A CRUD operation:
@bpm.process.<cancel|suspend|resume>.cascadeis optional (defaults to false); if provided, must be a boolean@bpm.process.<cancel|suspend|resume>.ifmust be a valid CDS expression (if present)
- Unknown annotations under
@bpm.process.<cancel|suspend|resume>.*trigger a warning listing allowed annotations
To use the programmatic approach with types, you need to import an existing SBPA process. This requires credentials via cds bind and being logged in to Cloud Foundry.
Import your SBPA process directly from the API:
Note: For remote imports, you must have ProcessService credentials bound. Run with cds bind --exec if needed:
cds bind --exec -- cds-tsx import --from process --name eu12.bpm-horizon-walkme.sdshipmentprocessor.shipmentHandler --no-copyIf you want to have it as a cds instead of a csn you can add --as cds at the end. If you want to reimport the process use the --force flag at the end. The flag no-copy is very important, as otherwise the process will be saved locally on both ./workflowsand ./srv/external folder which would result in cds runtime issues, as the json is not a valid csn model and cannot be stored in the .srv/external directory.
If you already have a process definition JSON file (e.g., exported or previously fetched), you can generate the CSN model directly from it without needing credentials:
cds import --from process ./workflows/eu12.bpm-horizon-walkme.sdshipmentprocessor.shipmentHandler.json --no-copyThis will generate:
- A CDS service definition in
./workflows/ - Types via
cds-typerfor full TypeScript support - Generic handlers for the actions and functions in the imported service
const processService = await cds.connect.to(ShipmentHandlerService);
const processInstance = await processService.start({
businesskey: 'order-12345',
startingShipment: {
identifier: 'shipment_001',
items: [{ identifier: 'item_1', title: 'Laptop', quantity: 1, price: 1200.0 }],
},
});// Suspend
await processService.suspend({ businessKey: 'order-12345', cascade: false });
// Resume
await processService.resume({ businessKey: 'order-12345', cascade: false });
// Cancel
await processService.cancel({ businessKey: 'order-12345', cascade: false });const attributes = await processService.getAttributes({ processInstanceId: 'instance-uuid' });
const outputs = await processService.getOutputs({ processInstanceId: 'instance-uuid' });This project is open to feature requests/suggestions, bug reports etc. via GitHub 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.
If you find any bug that may be a security problem, please follow our instructions at in our security policy on how to report it. Please do not create GitHub issues for security-related doubts or problems.
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 at all times.
Copyright (20xx-)20xx SAP SE or an SAP affiliate company and contributors. Please see our LICENSE for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available via the REUSE tool.