AlphaOak is developing a SAP extension app called 'Sigma' that is using S/4 as a foundation.
The concept of Sigma is at it's core to calculate some standard Asset and Work Management KPI's.
Why do you need CAP for it you ask?
A few reasons:
- Standard Analytics Engines like SAC or BDC don't have the capabilty to configure thresholds when lights turn green and other factors like weighting of multiple measures into a single KPI.
- We have lots of data points that don't exist or exist in another system, so we want to have standard interfaces in place to feed the information into out data model or (if no external system exists), use a Fiori app to enter it in reference to our main entity (in our case a Job).
At the end of the day the overall solution allows me to consume the data set through three different channels:
- For key decision makers in SAC with some standard Dashboards. Once I drill down I can then offer a jump into a Fiori App that provides all the line item details.
- If I want to provide the analytics team an additional level of flexibility, I can provide the information to BDC/Datasphere and ultimately to SAC.
- For the Field User and Supervisor I can use a simple Fiori Analytical app as third channel to consume the information.
Sigma is utilizing the following solution architecture.

+-----------------+
| @alphaoak/sigma |
+--------|--------+
|
+---------------------|
| |
| |
| +--------------------+
| | @alphaoak/eam-kpis |
| +----+---------------+
| |
| |-------------------------------------------------------------------------------------------------------+---------------------------+
| | | | | | |
| | +-----------|-----------+ +-----------|----------+ +--------------|-------------+ +--------------|--------------+ +----------|---------+
| | | @alphaoak/eam-quality | | @alphaoak/eam-safety | | @alphaoak/eam-customer-sat | | @alphaoak/eam-cost-burdened | | @alphaoak/workdate |
| | +-----------|-----------+ +-----------|----------+ +--------------|-------------+ +--------------|--------------+ +--------------------+
| | | | | |
| --------------------------------------------------------------------------------------------------------+
| +--------------------+
| | @alphaoak/eam-core |
| +--------------------+
| |
| |
| +----------------------------------------------------+
| | | |
| +--------------------+ +-------------------------+ +---------------------+
| | @alphaoak/cds-core | | @alphaoak/material-core | | @alphaoak/cost-core |
| +--------------------+ +-------------------------+ +---------------------+
+----------|----------+
| @alphaoak/cds-utils |
+---------------------+
We are using plugins to separate the different data models and extensions from each other. The easiest analogy would be modules in SAP S/4.
As we are re-using components in varying applications, we don't want to copy and paste data models around.
- Create an overall project folder. Let's say it is called
tapp - Create two sub folders. The first one called
capapp, the secondcapplugin - Initialize CAP projects in both folders
- in folder
capppluginexecute:cds init - in folder
capappexecute:cds init
- in folder
- You can then create a new workspace that couples the two projects by executing the following command in the project folder
tapp. The command is:npm init -w capplugin --include-workspace-root - Add
capapptoworkspacessection. - Change
package.jsonincapplugindirectory and change name to@alphaoak/capplugin - Change
package.jsonincapappdirectory and add@alphaoak/cappluginto dependencies - Change back to
t2and runnpm i - Validate that a symlink to
@alphaoak/cappluginexists innode_modulesdirectory
- Create a file called
cds-plugin.jswith the following content
const cds = require('@sap/cds')
const LOG = cds.log('recap-plugin')
cds.on('served', () => {
LOG.info('*** Recap Plugin Loaded ***');
})- Test if the plugin is working when you start it up.
- Run
cds win thecapappdirectory and see if you get an output line like the following in the console:
[recap-plugin] - *** Recap Plugin Loaded ***
- Run
- Create the plugin data model by creating a file called
eam-core.cdsin the[plugin]/dbfoldernamespace ao.recap; entity Jobs { key ID : UUID; sapWorkOrderId : String(50); description : String(500); effort : Double; startedAt : DateTime; finishedAt : DateTime; createdAt : DateTime; updatedAt : DateTime; }
- Create a default api service that exposes the data model
using {ao.recap as recap} from '../db/eam-core'; service ApiService { entity Jobs as projection on recap.Jobs; }
- Combining all the data model pieces in an index.cds
using from './db/eam-core'; using from './srv/api';
- Create the app data model supplmenting the core model defined in the plugin. Create a file in folder
dbof the application (not plugin) calledapp-dbmodel.cds
namespace ao.recap;
using {ao.recap as eam} from '@alphaoak/recap-plugin';
entity SafetyInspections {
key ID : Integer;
sapWorkOrderId : String(50);
description : String(500);
status : String(50);
createdAt : DateTime;
updatedAt : DateTime;
}
extend eam.Jobs with {
extensionField : String(50);
safetyInspections : Association to many SafetyInspections
on $self.sapWorkOrderId = safetyInspections.sapWorkOrderId;
};
- Create a new Admin Service by creating a new file
admin.cdsin thesrvfolder
using {ao.recap as eam }from '../db/app-dbmodel';
service AdminService{
entity Jobs as projection on eam.Jobs;
entity SafetyInspections as projection on eam.SafetyInspections;
}
-
Create some test data for entity SafetyInspections.
- Create a file called
ao.recap-SafetyInspections.csvin directorytest/data/ - Paste the following content
ID,sapWorkOrderId,description,status,createdAt,updatedAt 1,8100001,Inspection of safety equipment,Completed,2023-01-01T10:00:00Z,2023-01-02T12:00:00Z 2,8100001,Fire extinguisher check,In Progress,2023-01-03T14:00:00Z,2023-01-04T16:00:00Z 3,8100002,Emergency exit inspection,Pending,2023-01-05T18:00:00Z,2023-01-06T20:00:00Z
- Create a file called
-
Setup test
- Create folder
test/http - Create a file called
admin.httpwith the following content
@server=http://localhost:4004 @service=/odata/v4/admin ### Admin Service GET {{server}}{{service}}/Bla ### Admin Service GET {{server}}{{service}}/Jobs?$expand=safetyInspections ### Admin Service POST POST {{server}}{{service}}/Jobs Content-Type: application/json { "ID": "123", "sapWorkOrderId":"8100001", "description": "Test Job", "extensionField": "Extended Value" }
- Create folder
- Start watch with
cds win the application folder. - Execute the
POSTin the admin.http test file. - Execute the
GETin the admin.http test file. The result should look like this:
HTTP/1.1 200 OK
X-Powered-By: Express
X-Correlation-ID: ae695a94-7730-4f04-88cd-45a76eb57bcb
OData-Version: 4.0
Content-Type: application/json; charset=utf-8
Content-Length: 592
Date: Mon, 07 Jul 2025 03:36:24 GMT
Connection: close
{
"@odata.context": "$metadata#Jobs",
"value": [
{
"ID": "123",
"sapWorkOrderId": "8100001",
"description": "Test Job",
"effort": null,
"startedAt": null,
"finishedAt": null,
"createdAt": null,
"updatedAt": null,
"extensionField": "Extended Value",
"safetyInspections": [
{
"ID": 1,
"sapWorkOrderId": "8100001",
"description": "Inspection of safety equipment",
"status": "Completed",
"createdAt": "2023-01-01T10:00:00Z",
"updatedAt": "2023-01-02T12:00:00Z"
},
{
"ID": 2,
"sapWorkOrderId": "8100001",
"description": "Fire extinguisher check",
"status": "In Progress",
"createdAt": "2023-01-03T14:00:00Z",
"updatedAt": "2023-01-04T16:00:00Z"
}
]
}
]
}The current tooling from SAP does not allow you easily to make Fiori apps you have defined in a plugin in an overall application. The steps I took for making those apps available are the following.
- Create a directory with the same appname as the plugin in the application
appdirectory - in that directory create a symlink with the name
symwebappthat points to thewebappfolder in the imported plugin directory (i.e. node_modules/[cds plugin package name]/app/[appname]/webapp) - Add xs-app.json file to the directory.
- Adjust the first route and change
- The destination name
- The route to the proper service
- Adjust the first route and change
- Create a new file called
ui5-deploy.yaml- copy the file
- Replace metadata/name with metadata/name from ui5.yaml
- In the custom task
ui5-task-zipper, replace the configuration/archivename with the metadata/name stripped of all special characters.
- Adjust package.json
- Add this line to the scripts section of the package.json
build:cf": "ui5 build preload --clean-dest --config ui5-deploy.yaml --include-task=generateCachebusterInfo - Add devDependencies
- "@sap/ui5-builder-webide-extension": "^1.1.9",
- "ui5-task-zipper": "^3.3.1"
- Add this line to the scripts section of the package.json
As a product developer I want to make sure that all text labels are language supported.
We wrote a small tool that goes through the csn and checks if
- Are i18n placeholders specified where flexible texts are used.
- Do we have a value for the placeholder in the i18n text files.