Skip to content

    Extending SaaS Applications

    This guide explains how subscribers of SaaS applications can extend these on tenant level, thereby customizing them for their specific needs.

    Note: This guide is for the new MTX services package @sap/cds-mtxs. Projects still working with the previous MTX version, see the previous extensibility guide, and the migration guide.


    Introduction & Overview

    Subscribers (customers) of SaaS solutions frequently need to tailor these to their specific needs, for example, by adding specific extension fields and entities. All CAP-based applications intrinsically support this out-of-the-box.

    The overall process is depicted in the following figure:

    customization process

    As a prerequisite, you need a CAP-based application, that was developed and deployed to SAP BTP as a multitenant application.


    Jumpstart (Optional)

    Instead of going for a manual step-by-step experience, you can also download Orders Management application in cap/samples with all walkthrough preparation steps applied upfront, you may checkout the ext branch:

    git clone -b ext


    The following sections describe and allow a step-by-step walkthrough to build your application bottom-up:

    1. Clone our cap/samples repository
      git clone
      cd cloud-cap-samples
      npm ci
    2. Ensure to have the latest version of @sap/cds-dk installed globally:
      npm i -g @sap/cds-dk

    As a SaaS Provider

    CAP provides intrinsic extensibility, which means all your entities and services are extensible by default. All you have to do is to switch on extensibility by a single flag in your project settings. In addition, however, you usually would want to restrict what can be extended, and also add corresponding documentation and templates for your customers.

    Enable Extensibility

    The easiest way to enable extensibility is to use cds add mtx in your project root, for example, like this in our tutorial walkthrough:

    cd orders
    cds add mtx
    npm update

    Essentially, this automates the following two steps…

    1. Add @sap/cds-mtxs package dependency:
      npm add @sap/cds-mtxs
    2. Switch on cds.requires.extensibility in your package.json:


      "cds": {
        "requires": {
          "extensibility": true

    Restrict Extension Points

    Usually you want to restrict, which services or entities your SaaS customers are supposed to extend, and to what degree they are allowed to do so. For example, in the package.json of the base app, you find the configuration to enforce the following restrictions:

    • All new elements have to start with x_ → to avoid name conflicts
    • Allows extending entities in namespace sap.capire.orders
      Restricted to 2 new elements maximum.
    • Allows extending the OrdersService
      Restricted to 2 new entities maximum.

    Add cds.xt.ExtensibilityService to your package.json:

    package.json: cds > requires

      "cds.xt.ExtensibilityService": {
        "element-prefix": ["x_"],
        "extension-allowlist": [
            "for": ["sap.capire.orders"],
            "kind": "entity",
            "new-fields": 2
            "for": ["OrdersService"],
            "new-entities": 2

    See source of this in the cap/samples/orders repo.

    Learn more about this in the MTX Services reference guide.

    Provide Extension Guides

    You should provide documentation to guide your customers through the steps to add extensions. This guide should provide application-specific information along the lines of the walkthrough steps presented in this guide.

    Here’s a rough checklist what this guide should cover:

    Provide Template Projects

    To jumpstart your customers with extension projects, you should provide respective template projects, as application-specific starting points. We recommend to ship that template with your application and offer it as downloadable archive.

    Create an Extension Project (Template)

    Extension projects are standard CAP projects extending the base application. Create one for your base app, following these steps:

    1. Create a new CAP project — orders-ext in our walkthrough:

      cd ..
      cds init orders-ext
      code orders-ext        # if you use VSCode
    2. Add this to your package.json:


      "name": "@capire/orders-ext",
      "extends": "@capire/orders"
    • As package name choose any reasonable name. It is used to identify this extension within a SaaS subscription. Your customers can adjust this.

    • Property extends is the package name of the base application. This is used as subdirectory of node_modules, inside the extension project, where the model of the SaaS app (the base model) will be stored by cds pull. This in turn allows us/the extenders to refer to the app’s base model, like using ... from '@capire/orders'.

    Add Sample Content

    Create a new file app/extensions.cds and fill in this content:


    namespace x_orders.ext; // for new entities, if any
    using { OrdersService, sap.capire.orders.Orders } from '@capire/orders';
    extend Orders with {
      x_new_field : String;
    // -------------------------------------------
    // Fiori Annotations
    annotate Orders:x_new_field with @title: 'New Field';
    annotate OrdersService.Orders with @UI.LineItem: [
      ... up to { Value: OrderNo },
      { Value : x_new_field },

    Learn more about what you can do in CDS extension models

    Names of the .cds file(s) can be freely chosen. Yet, for the build system to work out of the box, it must be in either the app, srv, or db folder. Learn more about project layouts.

    For the sake of simplicity, we recommend putting all extension files into ./app and removing the other folders ./srv and ./db from extension projects.

    You may want to consider separating concerns by putting all the Fiori annotations into a separate file ./app/fiori.cds.

    Add Test Data

    To support quick-turnaround tests of extensions using cds watch add some test data. In your template project, create a file test/data/sap.capire.orders-Orders.csv like that:



    Add a Readme

    Include additional documentation for the extension developer in a file inside the template project. For example like that in our cap/samples/orders-ext template:

    # Getting Started
    Welcome to your extension project to  `@capire/orders`.
    It contains these folders and files, following our recommended project layout:
    | File or Folder | Purpose                        |
    | `app/`         | all extensions content is here |
    | `test/`        | all test content is here       |
    | `package.json` | project configuration          |
    | ``    | this getting started guide     |
    ## Next Steps
    - `cds pull` latest models from your base application
    - edit [`./app/extensions.cds`](./app/extensions.cds) to add your extensions
    - `cds watch` your extension in local test-drives
    - `cds build && cds push` your extension to **test** tenant
    - `cds build && cds push` your extension to **prod** tenant
    ## Learn More
    Learn more at

    Deploy Application

    Return to your orders project:

    cd ../orders

    To make sure the tenant database is not reset after restarts of the application, add a configuration for a persistent SQL database:

    package.json: cds > requires

      "db": "sql-mt"

    With your application enabled and prepared for extensibility, you would now deploy the application as described in the Deployment Guide.

    Yet, to test-drive in your tutorial walkthrough, simulate this by simply running the app locally as usual with cds watch:

    cds watch
    ./orders $ cds watch
    cds serve all --with-mocks --in-memory?
    watching: cds,csn,csv,ts,mjs,cjs,js,json,properties,
    live reload enabled for browsers
    [cds] - model loaded from 8 file(s):
    [cds] - connect using bindings from: { registry: '~/.cds-services.json' }
    [cds] - connect to db > sqlite { url: 'db.sqlite' }
    [cds] - serving cds.xt.SaasProvisioningService { path: '/-/cds/saas-provisioning' }
    [cds] - serving cds.xt.ModelProviderService { path: '/-/cds/model-provider' }
    [cds] - serving cds.xt.DeploymentService { path: '/-/cds/deployment' }
    [cds] - serving OrdersService { path: '/orders', impl:'srv/orders-service.js' }
    [cds] - serving cds.xt.ExtensibilityService { path:'/-/cds/extensibility' }
    [cds] - server listening on { url:'http://localhost:4006' }
    [cds] - launched at 11/10/2022, 08:41:19, version: 6.2.0, in: 6.047s
    [cds] - [ terminate with ^C ]

    As a SaaS Customer

    The following sections provide step-by-step instructions about adding extensions. Instructions are given with respect to our local walkthrough setup.

    Subscribe to SaaS App

    It all starts with a customer subscribing to a SaaS application.
    In your local setup you simulate this as follows…

    1. Open a new terminal, and in there subscribe as customer tenant t1:
       cds subscribe t1 --to http://localhost:4006 -u carol:

      Learn more about tenant subscriptions

    2. Verify that it worked, by opening the Orders Management Fiori UI in a new private browser window and login as carol, which is assigned to tenant t1.


    Prepare an Extension Tenant

    You need a test tenant to validate the extension before activating to production.
    In your local setup you simulate this as follows…

    1. Set up a test tenant t1-ext
       cds subscribe t1-ext --to http://localhost:4006 -u carol:
    2. Assign extension developers for the test tenant.

      As you’re using mocked auth, simulate this step by adding the following to the base app’s package.json, assigning bob as extension developer for t1-ext:

    package.json: cds > requires > auth > users

        "bob": {
          "tenant": "t1-ext",
          "roles": ["cds.ExtensionDeveloper"]

    Start an Extension Project

    Extension projects are standard CAP projects extending the subscribed application. SaaS providers usually provide application-specific templates, that extension developers can download and open in their editor. That’s what you did in our walkthrough as SaaS provider or you already downloaded that extension template with cap/samples. Now, you can just open the orders-ext folder in your editor, for example in VS Code:

    code ../orders-ext


    Pull Latest Base Model

    Next you need to download the latest CDS models from the subscribed SaaS application. We refer to it as base model.

    cds pull --from http://localhost:4006 -u bob:

    Run cds pull --help to see all available options.

    This downloads the base model into node_modules/<extends>, as configured in your package.json.

    Write the Extension

    Edit the file app/extensions.cds and replace its content with the following:


    namespace x_orders.ext; // for new entities like SalesRegion below
    using { OrdersService, sap, sap.capire.orders.Orders } from '@capire/orders';
    extend Orders with { // 2 new fields....
      x_priority    : String enum {high; medium; low} default 'medium';
      x_salesRegion : Association to x_SalesRegion;
    entity x_SalesRegion : sap.common.CodeList { // Value Help
      key code : String(11);
    // -------------------------------------------
    // Fiori Annotations
    annotate Orders:x_priority with @title: 'Priority';
    annotate x_SalesRegion:name with @title: 'Sales Region';
    annotate OrdersService.Orders with @UI.LineItem: [
      ... up to { Value: OrderNo },
      { Value: x_priority },
      { Value: },

    Learn more about what you can do in CDS extension models

    Make sure no syntax errors are shown in the CDS editor before going on to the next steps.

    Test-Drive Locally

    To conduct an initial test of your extension, run it locally with cds watch:

    cds watch --port 4005

    This starts a local Node.js application server serving your extension along with the base model and supplied test data stored in an in-memory database.
    It does not include any custom application logic, though.

    Add Local Test Data

    To improve local test drives you can add local test data for extensions.

    Edit the template-provided file test/data/sap.capire.orders-Orders.csv and add data for the new fields as follows:



    Create a new file test/data/x_orders.ext-x_SalesRegion.csv with this content:


    AMER;Americas;North, Central and South America
    EMEA;Europe, the Middle East and Africa;Europe, the Middle East and Africa
    APJ;Asia Pacific and Japan;Asia Pacific and Japan

    Verify the Extension

    Verify your extensions are applied correctly by opening the Orders Fiori Preview in a new private browser window, login as bob, and see columns Priority and Sales Region filled as in the screenshot below:


    Note: the screenshot includes local test data, added as explained below.

    This test data will only be deployed to the local sandbox and not be processed during activation to the productive environment.

    Add Data

    After pushing your extension, you have seen that the column for Sales Region was added, but is not filled. To change this, you need to provide initial data with your extension. To do so, copy the data file that you created before from test/data/ to db/data/. Please also check further details about adding data

    Push to Test Tenant

    Let’s push your extension to the deployed application in your test tenant for final verification before pushing to production.

    cds build
    cds push --to http://localhost:4006 -u bob:

    Learn more about cds push

    Note: You pushed the extension with user bob, which in your local setup ensures they are sent to your test tenant t1-ext, not the production tenant t1.

    Verify the Extension

    Verify your extensions are applied correctly by opening the Order Management UI in a new private browser window, login as bob, and check that columns Priority and Sales Region are displayed as in the screenshot below. Also, check that there’s content with a proper label in the Sales Region column.


    Is some data missing? Make sure you copied the data.

    Activate the Extension

    Finally, after all tests, verifications and approvals are in place, you can push the extension to your production tenant:

    cds build
    cds push --to http://localhost:4006 -u carol:

    Note: You pushed the extension with user carol, which in your local setup ensures they are sent to your production tenant t1.


    About Extension Models

    This section explains in detail about the possibilities that the CDS languages provides for extension models.

    All names are subject to extension restrictions defined by the SaaS app.

    Extending the Data Model

    Following the extend directive and our “Reuse and Compose” cookbook it is pretty straightforward to extend the application with the following new artifacts:

    • Extend existing entities with new (simple) fields.
    • Create new entities.
    • Extend existing entities with new associations.
    • Add compositions to existing or new entities.
    • Supply new or existing fields with default values, range checks, or value list (enum) checks.
    • Define a mandatory check on new or existing fields.
    • Define new unique constraints on new or existing entities.
    using {sap.capire.bookshop, sap.capire.orders} from '@capire/fiori';
    using {
      cuid, managed, Country, sap.common.CodeList
    } from '@sap/cds/common';
    namespace x_bookshop.extension;
    // extend existing entity
    extend orders.Orders with {
      x_Customer    : Association to one x_Customers;
      x_SalesRegion : Association to one x_SalesRegion;
      x_priority    : String @assert.range enum {high; medium; low} default 'medium';
      x_Remarks     : Composition of many x_Remarks on x_Remarks.parent = $self;
    // new entity - as association target
    entity x_Customers : cuid, managed {
      email        : String;
      firstName    : String;
      lastName     : String;
      creditCardNo : String;
      dateOfBirth  : Date;
      status       : String   @assert.range enum {platinum; gold; silver; bronze} default 'bronze';
      creditScore  : Decimal  @assert.range: [ 1.0, 100.0 ] default 50.0;
      PostalAddresses : Composition of many x_CustomerPostalAddresses on PostalAddresses.Customer = $self;
    // new unique constraint (secondary index)
    annotate x_Customers with @assert.unique: { email: [ email ] } {
      email @mandatory;  // mandatory check
    // new entity - as composition target
    entity x_CustomerPostalAddresses : cuid, managed {
      Customer     : Association to one x_Customers;
      description  : String;
      street       : String;
      town         : String;
      country      : Country;
    // new entity - as code list
    entity x_SalesRegion: CodeList {
      key regionCode : String(11);
    // new entity - as composition target
    entity x_Remarks : cuid, managed {
      parent      : Association to one orders.Orders;
      number      : Integer;
      remarksLine : String;

    This example provides annotations for business logic handled automatically by CAP as documented in our cookbook “Providing Services”.
    Also, learn more about the basic syntax of the annotate directive.

    Extending the Service Model

    In the existing in OrdersService, the new entities x_CustomerPostalAddresses and x_Remarks are automatically included since they are targets of the corresponding compositions.

    The new entities x_Customers and x_SalesRegion are autoexposed in a read-only way as CodeLists. Only if wanted to change it, you would need to expose them explicitly:

    using { OrdersService } from '@capire/fiori';
    extend service OrdersService with {
      entity x_Customers   as projection on extension.x_Customers;
      entity x_SalesRegion as projection on extension.x_SalesRegion;

    Extending UI Annotations

    The following snippet demonstrates which UI annotations you need to expose your extensions to the SAP Fiori elements UI.

    Add UI annotations for the completely new entities x_Customers, x_CustomerPostalAddresses, x_SalesRegion, x_Remarks:

    using { OrdersService } from '@capire/fiori';
    // new entity -- draft enabled
    annotate OrdersService.x_Customers with @odata.draft.enabled;
    // new entity -- titles
    annotate OrdersService.x_Customers with {
      ID           @(
        Common : {Text : email}
      firstName    @title : 'First Name';
      lastName     @title : 'Last Name';
      email        @title : 'Email';
      creditCardNo @title : 'Credit Card No';
      dateOfBirth  @title : 'Date of Birth';
      status       @title : 'Status';
      creditScore  @title : 'Credit Score';
    // new entity -- titles
    annotate OrdersService.x_CustomerPostalAddresses with {
      ID          @(
        Common : {Text : description}
      description @title : 'Description';
      street      @title : 'Street';
      town        @title : 'Town';
      country     @title : 'Country';
    // new entity -- titles
    annotate x_SalesRegion : regionCode with @(
      title : 'Region Code',
      Common: { Text: name, TextArrangement: #TextOnly }
    // new entity in service -- UI
    annotate OrdersService.x_Customers with @(UI : {
      HeaderInfo       : {
        TypeName       : 'Customer',
        TypeNamePlural : 'Customers',
        Title          : { Value : email}
      LineItem         : [
        {Value : firstName},
        {Value : lastName},
        {Value : email},
        {Value : status},
        {Value : creditScore}
      Facets           : [
      {$Type: 'UI.ReferenceFacet', Label: 'Main', Target : '@UI.FieldGroup#Main'},
      {$Type: 'UI.ReferenceFacet', Label: 'Customer Postal Addresses', Target: 'PostalAddresses/@UI.LineItem'}
      FieldGroup #Main : {Data : [
        {Value : firstName},
        {Value : lastName},
        {Value : email},
        {Value : status},
        {Value : creditScore}
    // new entity -- UI
    annotate OrdersService.x_CustomerPostalAddresses with @(UI : {
      HeaderInfo       : {
        TypeName       : 'CustomerPostalAddress',
        TypeNamePlural : 'CustomerPostalAddresses',
        Title          : { Value : description }
      LineItem         : [
        {Value : description},
        {Value : street},
        {Value : town},
        {Value : country_code}
      Facets           : [
        {$Type: 'UI.ReferenceFacet', Label: 'Main', Target : '@UI.FieldGroup#Main'}
      FieldGroup #Main : {Data : [
        {Value : description},
        {Value : street},
        {Value : town},
        {Value : country_code}
    }) {};
    // new entity -- UI
    annotate OrdersService.x_SalesRegion with @(
      UI: {
        HeaderInfo: {
          TypeName       : 'Sales Region',
          TypeNamePlural : 'Sales Regions',
          Title          : { Value : regionCode }
        LineItem: [
          {Value: regionCode},
          {Value: name},
          {Value: descr}
        Facets: [
          {$Type: 'UI.ReferenceFacet', Label: 'Main', Target: '@UI.FieldGroup#Main'}
        FieldGroup#Main: {
          Data: [
            {Value: regionCode},
            {Value: name},
            {Value: descr}
    ) {};
    // new entity -- UI
    annotate OrdersService.x_Remarks with @(
      UI: {
        HeaderInfo: {
          TypeName       : 'Remark',
          TypeNamePlural : 'Remarks',
          Title          : { Value : number }
        LineItem: [
          {Value: number},
          {Value: remarksLine}
        Facets: [
          {$Type: 'UI.ReferenceFacet', Label: 'Main', Target: '@UI.FieldGroup#Main'}
        FieldGroup#Main: {
          Data: [
              {Value: number},
              {Value: remarksLine}
    ) {};

    Extending Array Values

    Extend the existing UI annotation of the existing Orders entity with new extension fields and new facets using the special syntax for array-valued annotations.

    // extend existing entity Orders with new extension fields and new composition
    annotate OrdersService.Orders with @(
      UI: {
        LineItem: [
          ... up to { Value: OrderNo },                             // head
          {Value: x_Customer_ID,            Label:'Customer'},     //> extension field
          {Value: x_SalesRegion.regionCode, Label:'Sales Region'}, //> extension field
          {Value: x_priority,               Label:'Priority'},     //> extension field
          ...,                                                     // rest
        Facets: [...,
          {$Type: 'UI.ReferenceFacet', Label: 'Remarks', Target: 'x_Remarks/@UI.LineItem'} // new composition
        FieldGroup#Details: {
          Data: [...,
            {Value: x_Customer_ID,            Label:'Customer'},      // extension field
            {Value: x_SalesRegion.regionCode, Label:'Sales Region'},  // extension field
            {Value: x_priority,               Label:'Priority'}       // extension field

    The advantage of this syntax is that you do not have to replicate the complete array content of the existing UI annotation, you only have to add the delta.

    Semantic IDs

    Finally, exchange the display ID (which is by default a GUID) of the new x_Customers entity with a human readable text which in your case is given by the unique property email.

    // new field in existing service -- exchange ID with text
    annotate OrdersService.Orders:x_Customer with @(
      Common: {
        //show email, not id for Customer in the context of Orders
        Text:  , TextArrangement: #TextOnly,
        ValueList: {
          Label: 'Customers',
          CollectionPath: 'x_Customers',
          Parameters: [
            { $Type: 'Common.ValueListParameterInOut',
              LocalDataProperty: x_Customer_ID,
              ValueListProperty: 'ID'
            { $Type: 'Common.ValueListParameterDisplayOnly',
              ValueListProperty: 'email'

    Localizable Texts

    To externalize translatable texts, use the same approach as for standard applications, that is, create a i18n/ file:


    SalesRegion_name_col = Sales Region
    Orders_priority_col = Priority

    Then replace texts with the corresponding {i18n>...} keys from the properties file. Make sure to run cds build again.

    Properties files must be placed in the i18n folder. If an entry with the same key exists in the SaaS application, the translation of the extension has preference.

    This feature is available with @sap/cds 6.3.0 or higher.

    Learn more about localization

    About cds push

    When run as cds push -s <subdomainName> (after logging in with another tenant), the command can activate the extension in other subdomains, effectively serving as a kind of cross-client transport mechanism.

    To push a pre-packed extension archive (.tar.gz or .tgz), run cds push … <archive or URL>. The argument can be a local path to the archive or a URL to have it downloaded from.

    Run cds push --help to see all available options.

    About cds login

    A passcode is a temporary authentication code, which is used to connect to the SaaS application. You received the Passcode URL from your tenant operator and it follows this pattern:

    <url> = https://<tenant-subdomain>.authentication.<landscape>

    A passcode can be used only once and within a limited period of time. When expired, a new passcode has to be generated and sent again. If you omit the passcode when executing cds login, it is queried interactively.

    The cds login command allows extension developers to save authentication tokens on the local machine with cds logout as its counterpart. It fetches a JWT token and save it for later use with this URL in either a plain-text file or a secure local storage. In order for secure storage to work, an additional Node.js module is needed:

    npm i -g keytar

    ❗ Warning
    In case of problems with keytar, you can add --plain, but this should only be done for non-productive tenants, since tokens are stored in plain text on the developer’s machine.

    CDS login usually works for several days, depending on configuration by the SaaS provider of the application. Once the prefetched token expires, commands requiring login (such as cds push) continue to work if a refresh token was originally obtained by cds login (again depending on SaaS configuration). In this case, a new token is automatically downloaded. Otherwise, you need to run cds login again with a fresh passcode.

    Run cds login --help to see all available options.

    About Adding Data to Extensions

    As described in Add Data, you can provide local test data and initial data for your extension. In this guide we copied local data from the test/data folder into the db/data folder. When using SQLite, this step can be further simplified. For sap.capire.orders-Orders.csv, just add the new columns along with the primary key:



    ❗ Warning
    Adding data only for the missing columns doesn’t work when using SAP HANA as a database. With SAP HANA, you always have to provide the full set of data.

    Predefined Mock Users

    The following users are available for local testing as part of the basic/mock authentication strategy:

      carol: { tenant: 't1', roles: ['admin', 'cds.Subscriber', 'cds.ExtensionDeveloper', 'cds.UIFlexDeveloper'] },
      dave:  { tenant: 't1', roles: ['cds.Subscriber'] },
      erin:  { tenant: 't2', roles: ['admin', 'cds.Subscriber', 'cds.ExtensionDeveloper'] },
      fred:  { tenant: 't2', roles: []  },
      yves:  { roles: ['internal-user'] }