Serving SAP Fiori UIs
CAP provides out-of-the-box support for SAP Fiori elements. This guide explains how to add SAP Fiori elements apps to a CAP project and how to add SAP Fiori elements annotations to service definitions. Throughout this guide, "Fiori" refers to SAP Fiori elements.
Learn more about developing SAP Fiori elements and OData V4 (since 1.84.)
Getting Started
Using Fiori Previews
Fiori Preview
For entities exposed via OData V4, a Fiori preview link appears on the index page. It dynamically serves an SAP Fiori elements list page that allows you to quickly see the effect of annotation changes without having to create a UI application first.
Not for production
The preview is meant for quick tests and iterations during development, but not for production use, and hence automatically disabled in production. To also enable it in cloud deployments for test or demo purposes, set cds.fiori.preview:true for Node.js apps, or cds.index-page.enabled:true for Java
Adding Fiori Apps
As showcased in cap/samples, SAP Fiori apps should be added as subfolders of the app/ directory in a CAP project. Each subfolder constitutes an individual SAP Fiori application, with local annotations, manifest.json, etc. So, a typical folder layout would look like this:
| Folder/Sub Folder | Description |
|---|---|
app/ | All SAP Fiori apps should go in here |
browse/ | SAP Fiori app for end users |
orders/ | SAP Fiori app for order management |
admin/ | SAP Fiori app for admins |
index.html | For sandbox tests |
srv/ | All services |
db/ | Domain models and database artifacts |
Tip
Links to Fiori applications created in the app/ folder are automatically added to the index page of your CAP application for local development.
SAP Fiori Tools
The SAP Fiori tools provide advanced support for adding SAP Fiori apps to existing CAP projects as well as a wealth of productivity tools, for example for adding SAP Fiori annotations, or graphical modeling and editing. They can be used locally in Visual Studio Code (VS Code) or in SAP Business Application Studio.
OData Annotations Plugin
The SAP CDS language support plugin includes a plugin that helps you add and edit OData annotations in CDS syntax in VS Code. It provides the following features:
- Code completion
- Validation against the OData vocabularies and project metadata
- Navigation to the referenced annotations
- Quick view of vocabulary information
- Internationalization support
These features are available for OData annotations in CDS syntax but not yet for core data services common annotations.
The @sap/ux-cds-odata-language-server-extension module requires no manual installation. The latest version is fetched automatically from npmjs.com, as indicated by the user preference setting CDS > Contributions: Registry.
Learn more about the CDS extension for VS Code.
Fiori Annotations
SAP Fiori elements apps are generic front ends that construct and render pages and controls based on annotated metadata documents. The annotations provide the semantic information needed to render that content, for example:
annotate CatalogService.Books with @(
UI: {
SelectionFields: [ ID, price, currency_code ],
LineItem: [
{Value: title},
{Value: author, Label:'{i18n>Author}'},
{Value: genre.name},
{Value: price},
{Value: currency.symbol, Label:' '},
]
}
);Find this source and many more in capire/bookstore.Learn more about OData Annotations in CDS.
Where to Put Them?
Although CDS allows you to add annotations anywhere in your models, we recommend placing them in separate .cds files in your ./app/* folders, for example, as follows.
./app #> all your Fiori annotations should go here, for example:
./admin
fiori-service.cds #> annotating ../srv/admin-service.cds
./browse
fiori-service.cds #> annotating ../srv/cat-service.cds
services.cds #> imports ./admin/fiori-service and ./browse/fiori-service
./srv #> all service definitions should stay clean in here:
admin-service.cds
cat-service.cds
...See this also in capire/bookstore.
This recommendation follows the principles of Conceptual Modeling and Separation of Concerns.
Prefer @title and @description
Influenced by the JSON Schema, CDS supports the common annotations @title and @description, which are mapped to corresponding OData annotations as follows:
| CDS | JSON Schema | OData |
|---|---|---|
@title | title | @Common.Label |
@description | description | @Core.Description |
We recommend preferring these annotations over the OData ones in protocol-agnostic data models and service models, for example:
annotate my.Books with { //...
title @title: 'Book Title';
author @title: 'Author ID';
}Prefer @readonly, @mandatory, ...
CDS supports @readonly as a common annotation, which translates to respective OData annotations from the @Capabilities vocabulary. We recommend using the former for reasons of conciseness and comprehensibility as shown in this example:
@readonly entity Foo { // entity-level
@readonly foo : String // element-level
}is equivalent to:
entity Foo @(Capabilities:{
// entity-level
InsertRestrictions.Insertable: false,
UpdateRestrictions.Updatable: false,
DeleteRestrictions.Deletable: false
}) {
// element-level
@Core.Computed foo : String
}Similar recommendations apply to @mandatory and others → see Common Annotations.
Simple Value Helps
In addition to supporting the standard @Common.ValueList annotations as defined in the OData Vocabularies, CAP provides convenient support for Value Helps.
@cds.odata.valuelist
Simply add the @cds.odata.valuelist annotation to an entity, and all managed associations targeting this entity will automatically receive Value Lists in SAP Fiori clients. For example:
@cds.odata.valuelist
entity Currencies { key code ... }service BookshopService {
entity Books { //...
currency : Association to Currencies;
}
}This would be expanded by the compiler to the following OData annotations, in the EDMX documents generated for Fiori clients:
<Annotations Target="AdminService.Books/currency_code">
<Annotation Term="Common.ValueList">
<Record Type="Common.ValueListType">
<PropertyValue Property="CollectionPath" String="Currencies"/>
<PropertyValue Property="Label" String="Currency"/>
<PropertyValue Property="Parameters">
<Collection>
<Record Type="Common.ValueListParameterInOut">
<PropertyValue Property="ValueListProperty" String="code"/>
<PropertyValue Property="LocalDataProperty" PropertyPath="currency_code"/>
</Record>
<Record Type="Common.ValueListParameterDisplayOnly">
<PropertyValue Property="ValueListProperty" String="name"/>
</Record>
</Collection>
</PropertyValue>
</Record>
</Annotation>
</Annotations>@sap/cds/common
The reuse types in @sap/cds/common already have this added to base types and entities, so all uses automatically benefit from this. This is an effective excerpt of respective definitions in @sap/cds/common:
type Currencies : Association to sap.common.Currencies;context sap.common {
entity Currencies : CodeList {...};
entity CodeList { name : localized String; ... }
}annotate sap.common.CodeList with @(
UI.Identification: [name],
cds.odata.valuelist,
);In effect, usages of @sap/cds/common stay clean of any pollution, for example:
using { Currency } from '@sap/cds/common';
entity Books { //...
currency : Currency;
}Find this also in capire/bookstore.
With that, all UIs on all services exposing Books will automatically receive Value Help for currencies. You can also benefit from that when deriving your project-specific code list entities from sap.common.CodeList.
Fiori Draft Support
SAP Fiori uses drafts to let users save their progress while editing data and continue later without losing changes. Drafts are stored on the server and can be accessed from different devices and locations providing flexibility and convenience for users. CAP provides out-of-the-box support for drafts, making it easy to implement this functionality in your applications.
This documentation focuses on CAP only
For general information on the user experience and the technical details of drafts in SAP Fiori, refer to the SAP Fiori Design Guidelines and the SAP UI5 documentation.
Draft-Enabled Entities
All you need to do to serve an entity with draft support enabled is to annotate it with @odata.draft.enabled. For example, as we do in the capire/xtravels sample:
annotate TravelService.Travels with @odata.draft.enabled;Behind the scenes, CAP handles everything else. Most importantly, it adds a new .drafts entity next to the active entity with the same elements, used to store draft data. Think of it as a shadow entity, defined like this:
entity TravelService.Travels.drafts : TravelService.Travels { ... }You can access this entity definition from the model at runtime, for example, to add custom handlers to draft events or to access draft data. In a CAP Node.js service implementation, use the .drafts reference as a shortcut to access the draft entity:
const { Travels } = this.entities
SELECT.from (Travels) //> queries active data
SELECT.from (Travels.drafts) //> queries draft dataDraft Choreography
With @odata.draft.enabled entities in place, CAP automatically serves the Fiori draft choreography as illustrated in the following diagram:
In essence, the draft choreography defines the following flows:
- Creating drafts for new active entities, or for editing existing ones.
- Filling in draft data through a series of PATCH events.
- Saving the draft back to the active entity, or discarding it.
Drafts are isolated from any active data until they are saved/activated. When drafts are discarded, they are removed as if they never existed – with draft locks as the only exception to prevent conflicting changes.
Draft Locks
Whenever a draft is created to edit an active entity, this active entity is locked for any operation that could result in conflicting changes. In particular:
- No other draft can be created for the same active instance.
- No direct updates or deletes to the active instance are allowed.
The lock is released automatically when the draft is saved, activated, or discarded. Other users can manually reclaim it after a period of inactivity, which is 15 minutes by default, but can be configured via cds.fiori.draft_lock_timeout: 1h for CAP Node.js and cds.drafts.cancellationTimeout: 1h for CAP Java, respectively. See draft lock configuration for Node.js or Java.
Draft locks are not applied when creating drafts for new entities, as there is no active entity to be locked in this case.
Requests to Drafts
Fiori clients send the following HTTP requests for draft operations:
POST /Foo //> NEW
POST /Foo(ID,IsActiveEntity=true)/draftEdit //> EDIT
GET /Foo(ID,IsActiveEntity=false) //> READ
PATCH /Foo(ID,IsActiveEntity=false) {...} //> PATCH
POST /Foo(ID,IsActiveEntity=false)/draftActivate //> SAVE
DELETE /Foo(ID,IsActiveEntity=false) //> DISCARD2
3
4
5
6
The key parameter IsActiveEntity=false addresses draft data, with the exception of the empty POST and draftEdit for semantic reasons.
Full HTTP requests ...
The requests above are abbreviated for clarity. The actual HTTP requests include the service path, content-type headers, and JSON bodies as shown below.
POST /odata/v4/TravelService/Travels
Content-Type: application/json
{}POST /odata/v4/TravelService/Travels(ID=a11fb6f1-36ab-46ec-b00c-d379031e817a,IsActiveEntity=true)/draftEdit
Content-Type: application/jsonPATCH /odata/v4/TravelService/Travels(ID=a11fb6f1-36ab-46ec-b00c-d379031e817a,IsActiveEntity=false)
Content-Type: application/json
{ ... }... and so forth.
Requests to Active Data
Add IsActiveEntity=true as a key parameter to your requests to address active data directly, bypassing potentially existing drafts (draft locks still apply), for example:
POST /Books { IsActiveEntity:true, ... } //> CREATE
PATCH /Books(ID=201,IsActiveEntity=true) {...} //> UPDATE
DELETE /Books(ID=201,IsActiveEntity=true) //> DELETE
GET /Books(ID=201,IsActiveEntity=true) //> READ2
3
4
Available for CAP Node.js Since @sap/cds v10
While this was always possible in CAP Java before, it's available for CAP Node.js in the same way by default since v10. Can be disabled with cds.fiori.bypass_draft: false, which prevents bypassing the draft flow for CREATE and UPDATE operations entirely.
Draft locks still apply
Directly updating an active entity does not bypass draft locks. If an existing draft locks the entity, direct updates are blocked to prevent lost update situations.
Draft-agnostic Requests
Taking this further, through cds.fiori.draft_new_action: true IsActiveEntity=true is assumed by default, so clients that are unaware of drafts or don't need to handle them can ignore all draft-specific requests and parameters:
// creation of active instances
POST /Foo //> CREATE
GET /Foo(ID) //> READ
PATCH /Foo(ID) {...} //> UPDATE
DELETE /Foo(ID) //> DELETE
// creation of draft instances
POST /Foo/draftNew //> CREATE new draft
...2
3
4
5
6
7
8
The previously used POST /Foo requests without an IsActiveEntity parameter to create new drafts is now replaced by the collection bound action draftNew to resolve the ambiguities with requests to active data.
Available for CAP Node.js Since @sap/cds v10 – not yet for CAP Java
Draft-agnostic requests as above assume IsActiveEntity=true by default for all requests that don't explicitly specify it. This was possible in CAP Node.js, but not in CAP Java, which is still bound by the Olingo library. For CAP Java, explicitly add IsActiveEntity=true as a key parameter to address active data
Programmatic Access
You can also access draft data programmatically from custom code in JavaScript or Java.
In CAP Java, add IsActiveEntity as a key parameter to your queries (learn more):
Select.from(FOO).where(o -> o.ID().eq(201); //> reads active data
Select.from(FOO).where(o -> o.ID().eq(201) .and( //> reads draft data
o.IsActiveEntity().eq(false))
);In CAP Node.js, use the Foo.drafts references to access draft data:
const { Foo } = this.entities
SELECT.from (Foo, 201) // reads active data only
SELECT.from (Foo.drafts, 201) // reads draft data, if existsEven better, use req.subject, which automatically resolves to the correct entity instance - active or draft - based on the current request context. For example, in custom action handlers triggered for both active and draft data:
this.on ('approveTravel', req => UPDATE (req.subject) .with ({ status: 'A' }))
this.on ('rejectTravel', req => UPDATE (req.subject) .with ({ status: 'X' }))Draft Input Validation
Validating Drafts
During the draft phase - that is, on PATCH requests to draft data - all @asserts are validated and error messages are returned to the client. Unlike with active entities, the draft is still created or updated even with invalid data, so users can correct it later without losing their progress.
Custom Handlers for Draft Events
You can add custom handlers to draft events by referring to draft-specific events and .drafts entities, as shown for CAP Node.js (learn more):
const { Foo } = this.entities
this.before ('NEW', Foo.drafts, ...)
this.before ('EDIT', Foo, ...) //> note: refers to active entity
this.before ('PATCH', Foo.drafts, ...)
this.before ('SAVE', Foo.drafts, ...)
this.before ('DISCARD', Foo.drafts, ...)2
3
4
5
6
Similar in CAP Java (learn more):
@Before (event = DraftService.EVENT_DRAFT_CREATE)
@Before (event = DraftService.EVENT_DRAFT_EDIT)
@Before (event = DraftService.EVENT_DRAFT_PATCH)
@Before (event = DraftService.EVENT_DRAFT_SAVE)
@Before (event = DraftService.EVENT_DRAFT_CANCEL)2
3
4
5
Validation on Active Entities
When a draft is saved, all validations for active entities run as usual. Invalid data is rejected, so only valid data gets activated. This includes constraints such as @assert and @readonly, and all custom handlers registered to active entity events, for example:
const { Foo } = this.entities
this.before ([ 'CREATE', 'UPDATE' ], Foo, req => {/* validate all */})Validate on active entities, not only on drafts
Validations on draft entities alone are not sufficient, because active entities can be updated directly, bypassing drafts. Always perform all necessary validations on active entities, not only on drafts. Also note that updates to active entities can be partial, for example, updating only an individual OrderItem within an Order via PATCH /Orders(1)/Items(3). Make sure your validation logic covers such cases.
Persistent Messages
While in draft state, error messages are automatically persisted and remain visible even after you edit other fields or navigate away from the page.
CAP automatically generates corresponding side-effect annotations in the EDMX to instruct Fiori clients to fetch state messages after every PATCH request.
You can override generated side-effect annotations per entity, for example:
annotate MyService.Foo with @(
Common.SideEffects #alwaysFetchMessages: false
);Available Since CAP Java v3.8 and Since CAP Node.js v9.1
Can be disabled with cds.fiori.draft_messages: false.
Draft for Localized Data
Annotate the underlying base entity in the base model with @fiori.draft.enabled to also support drafts for localized data:
annotate sap.capire.bookshop.Books with @fiori.draft.enabled;Background
SAP Fiori drafts require single keys of type UUID, which is not the case for .texts entities, that are generated for localized data. The @fiori.draft.enabled annotation tells the compiler to add an additional technical primary key element named ID_texts.

See it live in capire/bookstore.
Fiori Tree Views
Following the same principle of convenience as for Value Helps, CAP provides a shortcut annotation to define hierarchies on entities with recursive associations, which are then rendered as Tree Views in SAP Fiori clients.
Recursive Associations
Hierarchies are most commonly parent-child structures created via recursive associations. For example, in the capire/bookshop sample, we have the Genres entity with a recursive association parent to itself:
entity Genres : cuid { //...
parent : Association to Genres;
}The @hierarchy Annotation
To get a Tree View in SAP Fiori clients, annotate the entity with @hierarchy, for example as we did in the Fiori Annotations of the capire/bookstore sample:
annotate AdminService.Genres with @hierarchy;If multiple associations can serve as the parent association, specify the one to use as the value of the @hierarchy annotation, for example:
annotate AdminService.Genres with @hierarchy: parent;Under the hood...
The @hierarchy annotation is a shortcut for the following Fiori-level annotate and extend statements, which you would otherwise have to write manually.
// declare a hierarchy with the qualifier "GenresHierarchy"
annotate AdminService.Genres with @Aggregation.RecursiveHierarchy #GenresHierarchy: {
NodeProperty : ID, // identifies a node, usually the key
ParentNavigationProperty : parent // navigates to a node's parent
};
extend AdminService.Genres with @(
// The computed properties expected by Fiori to be present in hierarchy entities
Hierarchy.RecursiveHierarchy #GenresHierarchy: {
LimitedDescendantCount : LimitedDescendantCount,
DistanceFromRoot : DistanceFromRoot,
DrillState : DrillState,
LimitedRank : LimitedRank
},
// Disallow filtering on these properties from Fiori UIs
Capabilities.FilterRestrictions.NonFilterableProperties: [
'LimitedDescendantCount', 'DistanceFromRoot', 'DrillState', 'LimitedRank'
],
// Disallow sorting on these properties from Fiori UIs
Capabilities.SortRestrictions.NonSortableProperties: [
'LimitedDescendantCount', 'DistanceFromRoot', 'DrillState', 'LimitedRank'
],
) columns { // Ensure we can query these columns from the database
null as LimitedDescendantCount : Int16,
null as DistanceFromRoot : Int16,
null as DrillState : String,
null as LimitedRank : Int16
};Note: When naming the hierarchy qualifier, use the following pattern:
<entity name in service>Hierarchy
UI5 manifest Configuration
In addition, you need to configure the TreeTable in UI5's manifest.json file:
"sap.ui5": { ...
"routing": { ...
"targets": { ...
"GenresList": { ...
"options": {
"settings": { ...
"controlConfiguration": {
"@com.sap.vocabularies.UI.v1.LineItem": {
"tableSettings": {
"hierarchyQualifier": "GenresHierarchy",
"type": "TreeTable"
}
}
}
}
}
},
},
},Note: construct the
hierarchyQualifierwith the following pattern:<entity name in service>Hierarchy
You can now start the server with cds watch and see the hierarchical tree view in action in the Browse Genres app.

The compiler automatically expands the shortcut annotation @hierarchy to the following annotate and extend statements.
Cache Control in Java
CAP lets you set a Cache-Control header with a max-age directive to indicate that a response remains fresh for n seconds after it was generated. In the CDS model, use the @http.CacheControl: {maxAge: <seconds>} annotation on stream properties. The header tells caches to store the response and reuse it for subsequent requests while it is fresh. max-age (in seconds) specifies how long the content remains fresh before becoming stale.
Elapsed time since the response was generated
max-age is the elapsed time since the response was generated on the origin server, not the time since the response was received.
Only Java
The Cache Control feature is currently supported only on the Java runtime.
Role-based Visibility
In addition to adding restrictions on services, entities, and actions/functions, there are cases where you want to hide certain UI elements for specific users. You can do this using annotations such as @UI.Hidden or @UI.CreateHidden together with $edmJson pointing to a singleton.
First, define the singleton in your service and annotate it with @cds.persistence.skip so that no database artifact is created:
@odata.singleton @cds.persistence.skip
entity Configuration {
key ID: String;
isAdmin : Boolean;
}A key is technically not required, but omitting it may cause issues for some consumers.
Then, define an on handler to serve the request:
srv.on('READ', 'Configuration', async req => {
req.reply({
isAdmin: req.user.is('admin') //admin is the role, which for example is also used in @requires annotation
});
});Finally, refer to the singleton in the annotation by using a dynamic expression:
annotate service.Books with @(
UI.CreateHidden : { $edmJson: {$Not: { $Path: '/CatalogService.EntityContainer/Configuration/isAdmin'} } },
UI.UpdateHidden : { $edmJson: {$Not: { $Path: '/CatalogService.EntityContainer/Configuration/isAdmin'} } },
);The Entity Container is OData-specific. It refers to the $metadata of the OData service, where all accessible entities are registered.
SAP Fiori elements also allows to not include it in the path
annotate service.Books with @(
UI.CreateHidden : { $edmJson: {$Not: { $Path: '/Configuration/isAdmin'} } },
UI.UpdateHidden : { $edmJson: {$Not: { $Path: '/Configuration/isAdmin'} } },
);