In this workshop, we will build a simple Item business object using the ABAP RESTful Application Programming Model (RAP) on an SAP BTP trial system.
RAP is SAP’s strategic programming model for developing cloud-ready, Fiori-enabled business applications in ABAP. It provides a clean, end-to-end programming model where you can define business objects, behaviors, services, and UI-ready projections in a structured way.
In this blog, we will:
Create a database table for items.
Define CDS root and projection views.
Implement behavior definitions with CRUD, and enhance it with actions, validations and determinations.
Expose the business object as an OData service.
Preview it in Fiori.
Model relationships using associations and compositions between entities.
Implement value helps to improve user input and Fiori UX.
By the end, you will understand how RAP objects are structured, connected, and how Clean Core principles are applied in a real example.
Cloud-ready: SAP’s standard for S/4HANA cloud extensions and custom apps.
Clean Core compliant: No modifications to standard objects; all extensibility is via APIs.
End-to-end model: Business logic, UI, draft handling, and authorizations all in one place.
Consistency: Automatic CRUD, locking, and draft handling.
Fiori-ready: Generates OData services consumable by Fiori Elements.
Productivity: Faster development than classical ABAP or BOPF.
For this demo, we use ABAP Development Tools (ADT) in Eclipse connected to an ABAP on Cloud project on an SAP BTP trial system. This setup gives us access to all RAP development tools and lets us preview Fiori apps directly from Eclipse.
If you’re setting this up for the first time, SAP provides a step-by-step tutorial to install Eclipse IDE, add the ABAP Development Tools (ADT) plugin and connect to your SAP system. Download the Eclipse IDE and add the ABAP Development Tools (ADT) Plugin | SAP Tutorials
Here are some useful ADT keyboard shortcuts for the ABAP development in Eclipse.
Info: You can display the full list of available shortcuts in the Show Key Assit in ADT by pressing Ctrl+Shift+L.
Before creating RAP objects, we need a package to organize them. Packages are like folders in Eclipse—they help manage all the development objects together.
1. In Eclipse, right-click your ABAP on Cloud project → New → ABAP Package.
2. Enter package details
Why this matters:
All RAP objects (tables, CDS views, behavior definitions, service definitions) should be in the same package.
It ensures clean core compliance, meaning your custom objects don’t modify standard SAP objects directly.
The database table is the foundation of your RAP business object. For this example, we’ll create a table called ZITEM02 for our Item BO.
1. Right-click your package → New → Other Repository Object → search for "Database Table".
2. Add the fields and mark item_id as the primary key.
@EndUserText.label : 'Item Table'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zitem_02 {
key client : abap.clnt not null;
key item_id : sysuuid_x16 not null;
name : abap.char(50) not null;
description : abap.char(100);
price : abap.dec(10,2);
status_code : abap.char(5) not null;
local_created_by : abp_creation_user;
local_created_at : abp_creation_tstmpl;
local_last_changed_by : abp_locinst_lastchange_user;
local_last_changed_at : abp_locinst_lastchange_tstmpl;
last_changed_at : abp_lastchange_tstmpl;
}3. Activate the table.
The table ZITEM_02 serves as the persistent layer for your RAP business object. All data is stored here.
Administrative Fields:
local_created_by/ local_created_at → track who created the record and when.
local_last_changed_by / local_last_changed_at → track local changes before activation (important for draft-enabled objects).
last_changed_at → tracks the last committed change; used as the ETag for locking and consistency.
Why Admin Fields Matter:
RAP automatically uses them for draft management, locking, and optimistic concurrency control. Even if your app is simple, including them makes your BO ready for enhancements such as validations, actions, and side effects.
The root view sits on top of the database table and defines the entity structure for your business object. It is the source for the behavior definition.
1. Right-click the database table → New Data Definition
2. Select the Root View Entity template.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Root View Entity for Item'
@Metadata.allowExtensions: true
define root view entity ZR_ITEM_02
as select from zitem_02
{
key item_id as ItemId,
name as Name,
description as Description,
price as Price,
status_code as StatusCode,
local_created_by as LocalCreatedBy,
local_created_at as LocalCreatedAt,
local_last_changed_by as LocalLastChangedBy,
local_last_changed_at as LocalLastChangedAt,
last_changed_at as LastChangedAt
}Table = raw persistence.
Root view = business-level representation, enriched with semantics, annotations, and ready for behavior logic.
The root view defines the primary key item_id to match the table.
The projection view is built on top of the root view and serves as the entry point for service exposure. Here, we select which fields are exposed in the transactional OData service. We can also include UI annotations to guide Fiori Elements in rendering the app automatically.
1. Right-click the Root View CDS → New Data Definition
2. Select the Projection View template.
The root view contains the core business data and logic, while the projection only exposes what the frontend needs. This way, we don’t modify or clutter the core data model with UI-specific details, keeping the core stable and extensible.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Projection View for Item'
@Metadata.allowExtensions: true
define root view entity ZC_ITEM_02
provider contract transactional_query
as projection on ZR_ITEM_02
{
key ItemId,
Name,
Description,
Price,
StatusCode
}For today, I’ll keep the UI annotations minimal here and show how to add them cleanly in a metadata extension later, which keeps our projection view tidy and focused on data exposure.
The behavior definition specifies how this entity behaves at runtime: which operations are allowed, It maps fields from the root view to the database table, and how draft actions are handled.
1. Right-click the Root View CDS → New Behavior Definition
Since the purpose of this first example is to display the list report, we only need to map fields from the root view to the database table.
managed implementation in class zbp_r_item_02 unique;
strict ( 2 );
define behavior for ZR_ITEM_02 alias Item
persistent table zitem_02
lock master
authorization master ( instance )
etag master LastChangedAt
{
create ( authorization : global );
update;
delete;
field ( readonly, numbering : managed ) ItemId;
mapping for zitem_02
{
ItemId = item_id;
Name = name;
Description = description;
Price = price;
StatusCode = status_code;
LastChangedAt = last_changed_at;
LocalCreatedAt = local_created_at;
LocalCreatedBy = local_created_by;
LocalLastChangedAt = local_last_changed_at;
LocalLastChangedBy = local_last_changed_by;
}
}
readonly → The field cannot be changed by the user. RAP ensures it is protected in the frontend and backend.
numbering : managed → RAP automatically generates unique values for this field when a new entity is created. You don’t need to write your own logic for primary keys like UUIDs or sequences.
The behavior projection allows us to expose only a subset of behavior from the main entity for a specific service. This is useful if we want different services to expose different parts of the behavior without modifying the core behavior definition.
projection;
strict ( 2 );
define behavior for ZC_ITEM_02 alias Item
{
use create;
use update;
use delete;
}
The service definition declares which projection views are exposed as OData services. Here, we expose our projection view, making our User BO available for Fiori or API consumption. Service definitions are protocol-agnostic and can later be consumed via different bindings.
@EndUserText.label: 'Service Definition for Item'
define service ZSD_ITEM {
expose ZC_ITEM_02 as Item;
}
Right-click the Service Definition → New Service Binding
Finally, the service binding connects the service definition to a specific protocol, such as OData V2 or V4. After activation, Publish the service and it can be accessed directly from Fiori elements or any OData client.
At this stage, our Business Object is not draft-enabled and does not support create/update/delete yet.
This means the Fiori Elements application shows a read-only list report, and because the table is empty, the preview will be blank.
This step is intentional — it confirms that:
the CDS views are correct
the projection is exposed
the service binding is active
the app renders successfully with no errors
Even with no data, seeing the list report appear is an important milestone. In the next sections, we will add draft support and enable full CRUD operations.
In Part 1 of this series, we created the foundation of our RAP application:
A clean core–compliant database table
The CDS root view entity
The projection view
A minimal behavior definition
A service binding that exposes our object to Fiori
At that stage, the generated application was intentionally read-only, serving as a good starting point to verify that the service and CDS structure work correctly.
Now, in Part 2, we take the next major step: turning our read-only object into a full transactional Business Object using the managed RAP BO approach. This includes:
Activating Create, Update, and Delete support
Enabling Draft handling for safe data entry
Allowing the Fiori Elements app to automatically render UI flows such as Edit, Save, Delete, and Discard Draft
Preparing the BO for further business logic enhancements (which we will add in Part 3)
Draft is one of the key features of Fiori Elements applications.
Before enabling it, it’s important to explain why draft is needed.
In modern SAP Fiori apps, users do not commit their changes immediately. Instead:
They can start editing an object.
Leave the page.
Navigate back later.
Recover their unsaved changes.
This improves the UX significantly and eliminates accidental data loss.
Draft ensures that:
Partial or inconsistent data is never saved.
Users can enter complex data in multiple steps.
Locks on active instances are minimized.
This is especially important in enterprise systems with many concurrent users.
Thanks to RAP’s managed BO runtime, draft functionality requires no custom code — only a few declarations in the behavior definition.
1. Add with draft;
2. Add draft table zitem_02_d;
This associates the BO with a draft table.
If the draft table does not exist yet, ADT will offer to create it automatically.
3. Click on "Quick assist" ( CTRL + 1 )
@EndUserText.label : 'Draft table for entity ZR_ITEM_02'
@AbapCatalog.enhancement.category : #EXTENSIBLE_ANY
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zitem_02_d {
key mandt : mandt not null;
key itemid : sysuuid_x16 not null;
name : abap.char(50);
description : abap.char(100);
price : abap.dec(10,2);
statuscode : abap.char(5);
localcreatedby : abp_creation_user;
localcreatedat : abp_creation_tstmpl;
locallastchangedby : abp_locinst_lastchange_user;
locallastchangedat : abp_locinst_lastchange_tstmpl;
lastchangedat : abp_lastchange_tstmpl;
"%admin" : include sych_bdl_draft_admin_inc;
}Draft tables contain:
Table fields
Administrator fields
Temporary user changes
This table supports parallel editing and safe drafts.
4. Additional Draft Actions and What They Do
In RAP, the draft handling pattern can be extended beyond the basic actions .
For more advanced scenarios — especially when working with optimized draft flows, background checks, or multi-step editing — we can include the following actions:
draft action Activate optimized;
draft action Edit;
draft action Discard;
draft action Resume;
draft determine action Prepare { }Beyond the basic Edit / Activate / Discard pattern, RAP provides additional draft actions to improve performance and user experience.
Resume – restores an existing draft so users can continue where they left off.
Prepare – pre-fills data before the user sees the draft instance.
These actions require no manual coding unless you want to enhance them, and together they deliver a modern, safe, Fiori-compliant editing workflow.
5. Add total etag LastChangedAt;
This ETag is used by RAP to guarantee concurrency and data consistency.
It prevents situations where one user accidentally overwrites changes made by another user.
It is also required for draft handling and for the Fiori Elements UI, which relies on this timestamp to detect outdated edits.
RAP automatically manages this field, updating it whenever the record is changed.
6. Update the Behavior Projection
The projection behavior definition exposes the business behavior to the Fiori UI and the OData service. Even if the root BO supports create, update, delete, and draft actions, they must be explicitly enabled in the behavior projection. Without this step, Fiori Elements will not show the Create/Edit/Delete or draft buttons. The projection layer ensures clean core separation by keeping internal logic in the root BO while exposing only the required operations to the UI.
projection;
strict ( 2 );
use draft;
define behavior for ZC_ITEM_02 alias Item
{
use create;
use update;
use delete;
use action Activate;
use action Edit;
use action Discard;
use action Resume;
use action Prepare;
}7. Create Behavior Implementation Class
Eclipse generates a global ABAP class with the naming convention.
Understanding What “Managed Implementation” Means
Standard CRUD is generated automatically
You do not need to write: create/ update/ delete logic, draft save/ load logic, locking logic, ETag management
RAP provides all of this out of the box.
The class is only for custom behavior
This is where we will later implement validations (check input before save), determinations (auto-fill fields), actions (mark item as completed), unmanaged logic, feature control, authorization checks, if needed.
But for Part 2 (Draft + CRUD), the class stays empty.
8. Creating the Metadata Extension (UI Annotations)
After creating the projection view and exposing the BO behavior, we need to define how this data should be displayed in Fiori Elements apps.
This is where metadata extensions come into play. They allow you to add UI annotations without touching the core CDS view, keeping your implementation clean and following Clean Core principles.
Right-click on the Consumption View → New → Metadata Extension
@Metadata.layer: #CORE
annotate entity ZC_ITEM_02 with
{
@UI.facet: [
{
purpose: #STANDARD,
type: #IDENTIFICATION_REFERENCE,
label: 'Items',
position: 10
} ]
ItemId;
@UI.lineItem: [{ position: 10, label: 'Name' }]
@UI.identification: [{ position: 10, label: 'Name' }]
Name;
@UI.lineItem: [{ position: 20, label: 'Description' }]
@UI.identification: [{ position: 20, label: 'Description' }]
Description;
@UI.lineItem: [{ position: 30, label: 'Price' }]
@UI.identification: [{ position: 30, label: 'Price' }]
Price;
@UI.lineItem: [{ position: 40, label: 'Status Code' }]
@UI.identification: [{ position: 40, label: 'Status Code' }]
StatusCode;
}The metadata extension defines how the Item BO is displayed in Fiori Elements. Using annotations like @ui.lineItem and @ui.Identification, we specify which fields appear in the list report and object header. The @ui.facet groups the fields into a standard identification card. Placing these annotations in a metadata extension layer ensures Clean Core compliance, separating UI concerns from business logic, while enabling a ready-to-use Fiori preview.
After adding draft-enabled behavior and the metadata extension, let’s see the results in the Fiori preview:
Create Button appears automatically at the top of the list report and clicking it opens a draft object, ready for entry.
Delete Button appears at the top of the list report.
List Report displays all annotated fields without manually selecting columns, shows all items.
Object Page: clicking a list entry opens the object page.
Edit Button is visible on the object page, clicking it opens the draft for editing, supporting safe modifications before activation.
This confirms that draft-enabled RAP objects, combined with metadata annotations, automatically generate a full-featured Fiori UI: list report, object page, and CRUD actions—all without writing UI code manually.
In Part 1 and Part 2, we created the basic structure of a RAP Business Object, enabled draft handling, activated CRUD operations, and prepared a clean UI using metadata extensions.
Now, in Part 3, we introduce the most important part of RAP:
Business logic — implemented through:
Validations (check before save)
Determinations (auto-fill fields)
Actions (custom operations triggered by the user)
Side Effects (UI refresh logic after actions)
To demonstrate these features in a realistic scenario, we switch to a second simple Business Object: Leave Requests
This BO includes fields such as employee name, requested days, and status.
With it, we implement:
A validation to ensure the requested days are > 0
A determination that sets the default status
A custom action Approve Leave Request
Metadata annotations to show an action button in the UI
Side effects to refresh the UI automatically after approval
Here is the behavior definition, which we will explain piece by piece:
managed implementation in class zbp_r_leavereq unique;
strict ( 2 );
with draft;
define behavior for ZR_LEAVEREQ
persistent table zleavereq
draft table zleavereq_d
lock master
total etag LastChangedAt
authorization master ( instance )
etag master LastChangedAt
{
create ( authorization : global );
update;
delete;
field ( readonly, numbering : managed ) Leaveguid;
draft action Activate optimized;
draft action Edit;
draft action Discard;
draft action Resume;
draft determine action Prepare { }
validation checkDays on save { create; update; field DaysReq; }
determination setDefaultStatus on modify { create; }
action approveLeaveRequest;
mapping for zleavereq
{
Leaveguid = leaveguid;
EmployeeName = employee_name;
DaysReq = days_req;
Status = status;
LocalCreatedAt = local_created_at;
LocalCreatedBy = local_created_by;
LocalLastChangedAt = local_last_changed_at;
LocalLastChangedBy = local_last_changed_by;
LastChangedAt = last_changed_at;
}
}A validation allows you to stop a save operation if business rules are violated.
In this example, we ensure that requested days > 0.
METHOD checkDays.
READ ENTITIES OF zr_leavereq IN LOCAL MODE
ENTITY zr_leavereq
ALL FIELDS
WITH CORRESPONDING #( keys )
RESULT DATA(lt_leave_req).
LOOP AT lt_leave_req ASSIGNING FIELD-SYMBOL(<fs_lr>).
IF <fs_lr>-DaysReq < 1.
APPEND VALUE #( %tky = <fs_lr>-%tky ) TO failed-zr_leavereq.
APPEND VALUE #(
%tky = <fs_lr>-%tky
%msg = new_message_with_text(
text = 'Number of days should be > 0.'
severity = if_abap_behv_message=>severity-error )
) TO reported-zr_leavereq.
ENDIF.
ENDLOOP.
ENDMETHOD.The validation is triggered on save, both for create and update.
A determination is used to auto-fill or derive values without UI involvement.
Here, whenever a leave request is created, we automatically set its initial status:
METHOD setDefaultStatus.
DATA(key) = VALUE #( keys[ 1 ] OPTIONAL ).
MODIFY ENTITIES OF zr_leavereq IN LOCAL MODE
ENTITY zr_leavereq
UPDATE FIELDS ( Status )
WITH VALUE #( ( %tky = key-%tky
Status = 'Pending' ) ).
ENDMETHOD.Actions implement custom operations that the user triggers manually from the UI.
Here, we add an Approve action.
METHOD approveLeaveRequest.
DATA(key) = VALUE #( keys[ 1 ] OPTIONAL ).
MODIFY ENTITIES OF zr_leavereq IN LOCAL MODE
ENTITY zr_leavereq
UPDATE FIELDS ( Status )
WITH VALUE #( ( %tky = key-%tky
Status = 'Approved' ) ).
ENDMETHOD.Use an action when:
Without adding it to the projection, the UI cannot call the action.
projection;
strict ( 2 );
use draft;
define behavior for ZC_LEAVEREQ
{
use create;
use update;
use delete;
use action approveLeaveRequest;
side effects
{
action approveLeaveRequest affects $self, field Status;
}
use action Activate;
use action Edit;
use action Discard;
use action Resume;
use action Prepare;
}Side effects tell RAP which fields or entities might change when an action runs.
This ensures the UI refreshes automatically after the action is executed.
This informs Fiori Elements:
After pressing “Approve”, refresh the Status field
Update list report and object page automatically
Without side effects, the UI does not update until a manual refresh.
In the Metadata Extension, you expose the action as a button:
@UI.lineItem: [{
position: 40,
type: #FOR_ACTION,
dataAction: 'approveLeaveRequest',
label: 'Approve',
inline: true
}]✔ The approve button appears directly in the table row
✔ User can approve without navigating to the object page
✔ Great for workflow-like processes
In this part, we move to a more realistic RAP scenario, where our business object is no longer a single table.
Instead, we have Projects and Tasks, and these objects need to be connected.
RAP uses CDS associations and compositions to model these relationships.
An association is basically a link between two view entities — similar to a JOIN — but it is not executed until we actually navigate to it.
In our app, each Task is assigned to a User.
We represent this using an association:
define view entity ZI_TASK
as select from ztask_01
association [0..1] to ZR_USER as _User on _User.UserCode = $projection.Assignee
{association → create a link between Task and User
ZR_USER → the target entity (the User root)
_User → the name we will use to navigate to it
on ... → join condition ( Assignee equals the UserCode )
Cardinality describes how many related records the association can return.
This means:
0 → the task might not have an assigned user (Assignee is empty)
1 → if a user is assigned, there can be only one matching UserCode
This is a typical “optional 1-to-1” relationship.
If Assignee = 'USR01', we get that one user.
If Assignee is empty, we get nothing.
Inside the projection view ZC_TASK, we read the user’s name through the _User association:
_User.UserName as AssigneeName,This is only read, not stored anywhere in the Task table.
To make SAP Fiori show the text automatically, we add:
@ObjectModel.text.element: ['AssigneeName']
Assignee,The UI displays UserCode + UserName
They avoid duplicate data
They allow “on-demand” navigation
They plug naturally into Fiori Elements
(value helps, display texts, navigation links)
Now that we’ve seen simple associations, let’s move to something more powerful in RAP:
composition.
A composition is not just a link — it models a parent/child relationship where the child’s lifecycle depends entirely on the parent.
In our example:
Project is the parent
Tasks are the children
This is a very common RAP pattern used in real applications.
In the Project root view (ZR_PROJECT), we define:
composition [0..*] of ZI_TASK as _Taskscomposition → strong parent–child relationship
[0..*] → a project can have zero, one, or many tasks
of ZI_TASK → tasks are the child entity
_Tasks → the navigation property we will use in the UI
This tells RAP that:
Tasks belong to a project
Tasks cannot exist without the project
If the project is deleted, all its tasks must be deleted too
Tasks must always reference a ProjectId (its parent)
This is different from a normal association — it defines ownership.
In the Project projection (ZC_PROJECT), we expose the composition:
_Tasks : redirected to composition child ZC_TASKThis allows the UI to show the list of Tasks inside the Project object page, including:
Create Task
Edit Task
Delete Task
All directly in the Project UI.
Without this redirection, the Tasks table wouldn't appear in the Fiori Elements app.
Since compositions define hierarchy, the behavior must reflect that hierarchy.
define behavior for ZR_PROJECT alias Project
{
create;
update;
delete;
association _Tasks { create; with draft; }
}The Project behavior controls the entire object
It is allowed to create children (_Tasks { create; })
Children are created in the context of the parent
In Fiori:
When you open a Project → Task table appears
“Create” inside the Tasks table creates a child of this project
define behavior for ZI_TASK alias Task
{
update;
delete;
field (readonly) ParentId;
association _Project { with draft; }
}Tasks cannot create their own parent
Tasks inherit locking and authorization from the Project
Tasks cannot change the ParentId (because it is read-only)
The lifecycle looks like this:
You create a Project
Inside the project, you add Tasks
Tasks cannot exist outside the Project
It gives RAP a clear structure of your data model
It enables nested object pages (Projects → Tasks)
It simplifies locking, authorization, and lifecycle management
Fiori Elements automatically understands parent/child UI patterns
Child objects cannot “float” or become inconsistent
So far, we built our entities, associations, and composition.
But in a real Fiori Elements app, users shouldn’t manually type in IDs.
Instead, they should select values from a value help (similar to a search help in classical ABAP).
In RAP, value helps are implemented using CDS view entities and the annotation:
@Consumption.valueHelpDefinitionLet’s look at the value help for selecting a User.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Value Help for User'
@Metadata.ignorePropagatedAnnotations: true
define view entity ZI_USER_VH
as select from ZR_USER
{
@UI.hidden: true
key UserId,
UserCode,
UserName
}It selects from ZR_USER, our user root entity.
The value help will show UserCode and UserName in the popup search dialog.
This view is not a projection — it's a standalone VH definition recommended for RAP apps.
This view is now ready to be used by any field that needs a value help for Users.
In the Task projection, we attach the value help to the Assignee field:
@Consumption.valueHelpDefinition: [{
entity: { name: 'ZI_USER_VH', element: 'UserCode' },
useForValidation: true
}]
Assignee,Opens a value help popup
Shows columns from ZI_USER_VH (UserCode, UserName)
Ensures the selected UserCode is valid (useForValidation: true)
Prevents users from entering invalid users
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
| User | Count |
|---|---|
| 48 | |
| 38 | |
| 23 | |
| 21 | |
| 21 | |
| 18 | |
| 16 | |
| 13 | |
| 12 | |
| 12 |