What is a BDEF?
A Behavior Definition is a design-time artifact in ABAP RAP that sits directly on top of a
CDS interface view and declares what a Business Object can do — which standard
operations are permitted, what custom actions and functions exist, and how the framework
should handle persistence, locking, numbering, drafts, and authorization. It does not contain
any executable ABAP code; it is purely declarative. The actual ABAP code lives in the
behavior implementation class (ZBP_*) that the BDEF references.
Think of the BDEF as a contract: it tells the RAP framework, the OData layer, and any
consuming service exactly what operations are available and under what conditions, without
spelling out how those operations work internally.
How to Create a BDEF
In ADT (Eclipse), right-click on your CDS interface view → New → Other ABAP
Repository Object → Core Data Services → Behavior Definition.
Name : ZI_SALESORDER
Description: Behavior for Sales Order BO
Alternatively, open the CDS view and use the quick-fix (Ctrl+1) — ADT will propose
"Create behavior definition for this view" and scaffold the skeleton automatically, including
the persistent table and mapping block derived from the CDS view's underlying table.
All Keywords — Detailed Reference
Header keywords
managed implementation in class zbp_salesorder unique;
Keyword Purpose
managed
RAP framework handles CRUD persistence. You only
implement custom logic.
unmanaged
You implement all persistence manually (legacy wrapping,
complex scenarios).
abstract No implementation class. Used for abstract actions or re-use
type definitions.
projection
Projection BDEF — exposes a subset of a base BO to a
specific service.
implementation in class Points to the ABAP behavior implementation class. unique
<classname> unique enforces one class per BO.
strict ( 2 ) Activates full strict-mode checks. Level 1 = basic; level 2 =
recommended for all new development.
Keyword Purpose
Registers an additional save hook — called during the save
with additional save sequence for side effects outside the BO (e.g. sending an
email, writing to a log table).
with privileged mode Internal calls to this BO skip authorization checks. Use with
caution.
Entity-level keywords
define behavior for ZI_SalesOrder alias SalesOrder
persistent table zsalesorder
draft table zsalesorder_d
lock master
authorization master ( instance )
etag master LastChangedAt
Keyword Purpose
persistent table The DDIC table where managed RAP writes data.
Draft-enabled BOs need a separate draft table (generated via
draft table @[Link]: #NOT_REQUIRED + table
wizard).
lock master This entity owns the lock. Child entities use lock dependent.
lock dependent by
_Parent Child entity — locking is delegated up to the root.
authorization
master ( instance
Authorization is checked per instance at runtime. ( global ) = one
) check for all instances.
authorization
dependent Authorization delegated to the root.
etag master Optimistic concurrency — the named field (usually
<field> LastChangedAt) is used as an ETag. Prevents lost updates.
etag dependent by
_Parent ETag control lives in the root entity.
Field controls
field ( readonly ) : UUID, CreatedAt, CreatedBy, LastChangedAt,
LastChangedBy;
field ( mandatory ) : SalesOrderID, CustomerID;
field ( readonly : create ) : SalesOrderID; " editable only on update
field ( readonly : update ) : CustomerID; " editable only on create
field ( features : instance ): NetAmount; " dynamic, evaluated at
runtime
features : instance requires an implementation method get_instance_features in the
behavior implementation class. This is how you disable a field based on document status,
user role, etc.
Standard operations
create;
update;
delete;
Can be individually feature-controlled:
create ( features : global ); " one check for all
update ( features : instance ); " per-document check
delete ( features : instance );
features : global requires get_global_features. features : instance requires
get_instance_features.
Numbering
" Early numbering: UUID assigned before save (managed scenario)
" Framework assigns UUID automatically if field is of type RAW(16) /
SYSUUID_X16
" Late numbering: number assigned during save sequence
late numbering;
For external number ranges (classic SAP number ranges), use early numbering and
implement earlynumbering_create in the implementation class.
Actions
" Instance action — acts on one or more selected instances
action ( features : instance ) submitOrder result [1] $self;
" Instance action with explicit parameter and result
action approveOrder parameter zi_approve_param result [1] $self;
" Static action — no instance context
static action refreshPricingForAll;
" Factory action — creates a new instance from an existing one
factory action copyOrder [1];
" Internal action — not exposed via OData, only callable within the BO
internal action recalculate;
" Draft action — part of draft lifecycle (see draft section)
draft action Edit;
draft action Activate optimized;
draft action Discard;
draft action Resume;
draft determine action Prepare;
result [1] $self means the action returns a single instance of its own entity type back to
the UI — enabling automatic UI refresh after the action.
result [1] structure <abstract_entity> returns a custom structure.
Functions
Functions are side-effect-free reads. They are exposed as OData functions (not actions).
function getOrderStatus result [1] structure zi_order_status_result;
function calculateTax result [1] structure zi_tax_result;
Determinations
Determinations run automatically when specific triggers fire. They modify data in the
transactional buffer.
" Trigger: on create only
determination setDefaults on modify { create; }
" Trigger: on change of specific fields
determination recalculateTotals on modify { field Quantity, UnitPrice; }
" Trigger: before save (create and update)
determination setDocumentStatus on save { create; update; }
Determinations run before validations in the save sequence.
Validations
Validations check data and report errors. They do not modify data.
" Fires on create and update, before save
validation checkMandatoryFields on save { create; update; }
" Fires only when specific fields change
validation checkDeliveryDate on save { field DeliveryDate, OrderDate; }
" Fires on delete
validation checkDeletionAllowed on save { delete; }
Errors are reported via APPEND VALUE #( ... ) TO reported-<entity> and APPEND
VALUE #( ... ) TO failed-<entity> in the implementation.
Associations and compositions
" Root entity
define behavior for ZI_SalesOrder alias SalesOrder
persistent table zsalesorder
lock master
...
{
create; update; delete;
association _Items { create; }
}
" Child entity
define behavior for ZI_SalesOrderItem alias SalesOrderItem
persistent table zsalesorderitem
lock dependent by _SalesOrder
authorization dependent by _SalesOrder
...
{
update; delete;
field ( readonly ) : SalesOrderID; " FK, set by parent
}
The child entity cannot expose create on its own — creation is always initiated from the
parent association.
Draft behavior
managed with additional save implementation in class zbp_salesorder unique;
define behavior for ZI_SalesOrder alias SalesOrder
persistent table zsalesorder
draft table zsalesorder_d
lock master total etag LastChangedAt
authorization master ( instance )
etag master LastChangedAt
{
create; update; delete;
draft action Edit; " locks the active instance,
creates draft
draft action Activate optimized; " promotes draft to active
draft action Discard; " deletes draft without saving
draft action Resume; " returns to an existing draft
draft determine action Prepare; " triggers validations before
activation
association _Items { create; with draft; }
}
total etag LastChangedAt covers both the active and draft states for concurrency control.
optimized in Activate optimized tells RAP to only write changed fields, improving
performance.
Projection BDEF
projection;
strict ( 2 );
define behavior for ZC_SalesOrder alias SalesOrder
{
use create;
use update;
use delete;
use action submitOrder;
use action approveOrder;
use function getOrderStatus;
use association _Items { create; }
}
A projection BDEF can restrict the base BDEF (e.g. expose create and update but
suppress delete), but it cannot extend it with new operations.
Mapping block
mapping for zsalesorder
{
SalesOrderID = sales_order_id;
CustomerID = customer_id;
NetAmount = net_amount;
Currency = currency;
CreatedAt = created_at;
CreatedBy = created_by;
LastChangedAt = last_changed_at;
LastChangedBy = last_changed_by;
}
Maps CDS field names to DDIC column names. Required in managed scenarios so RAP
knows which column to write to.
Real-Time Scenario 1 — Sales Order Header + Items
(Managed, Composition)
This is the most common RAP pattern: a root entity (header) with child entities (items), both
managed, with validations, determinations, and an action.
BDEF
managed implementation in class zbp_zi_salesorder unique;
strict ( 2 );
define behavior for ZI_SalesOrder alias SalesOrder
persistent table zsalesorder
lock master
authorization master ( instance )
etag master LastChangedAt
{
field ( readonly ) : SalesOrderID, CreatedAt, CreatedBy, LastChangedAt,
LastChangedBy, TotalAmount, Status;
field ( mandatory ) : CustomerID, SalesOrg, Currency;
field ( readonly : update ) : SalesOrg; " cannot change org
after creation
create;
update;
delete ( features : instance ); " controlled deletion
" Custom business logic
action ( features : instance ) submitOrder result [1] $self;
action ( features : instance ) cancelOrder result [1] $self;
" Automated triggers
determination setDefaults on modify { create; }
determination calcTotalAmount on modify { field Quantity, UnitPrice; }
" Data quality gates
validation checkCustomer on save { create; update; }
validation checkDeletionPolicy on save { delete; }
" Composition: parent owns items
association _Items { create; }
mapping for zsalesorder
{
SalesOrderID = sales_order_id;
CustomerID = customer_id;
SalesOrg = sales_org;
Currency = currency;
TotalAmount = total_amount;
Status = status;
CreatedAt = created_at;
CreatedBy = created_by;
LastChangedAt = last_changed_at;
LastChangedBy = last_changed_by;
}
}
define behavior for ZI_SalesOrderItem alias SalesOrderItem
persistent table zsalesorderitem
lock dependent by _SalesOrder
authorization dependent by _SalesOrder
etag dependent by _SalesOrder
{
field ( readonly ) : ItemID, SalesOrderID;
field ( mandatory ) : MaterialID, Quantity, UnitPrice;
update;
delete;
mapping for zsalesorderitem
{
ItemID = item_id;
SalesOrderID = sales_order_id;
MaterialID = material_id;
Quantity = quantity;
UnitPrice = unit_price;
}
}
Behavior implementation class (key methods)
CLASS zbp_zi_salesorder DEFINITION PUBLIC ABSTRACT FINAL
INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS setDefaults FOR DETERMINE
ON MODIFY
IMPORTING keys FOR SalesOrder~setDefaults.
METHODS calcTotalAmount FOR DETERMINE
ON MODIFY
IMPORTING keys FOR
SalesOrder~calcTotalAmount.
METHODS checkCustomer FOR VALIDATE
ON SAVE
IMPORTING keys FOR SalesOrder~checkCustomer.
METHODS submitOrder FOR MODIFY
IMPORTING keys FOR ACTION
SalesOrder~submitOrder
RESULT result.
METHODS get_instance_features FOR INSTANCE FEATURES
IMPORTING keys REQUEST requested_features
FOR SalesOrder
RESULT result.
ENDCLASS.
CLASS zbp_zi_salesorder IMPLEMENTATION.
METHOD setDefaults.
READ ENTITIES OF zi_salesorder IN LOCAL MODE
ENTITY SalesOrder FIELDS ( CustomerID ) WITH CORRESPONDING #( keys )
RESULT DATA(orders).
MODIFY ENTITIES OF zi_salesorder IN LOCAL MODE
ENTITY SalesOrder
UPDATE FIELDS ( Status CreatedBy CreatedAt LastChangedAt
LastChangedBy )
WITH VALUE #( FOR order IN orders
( %tky = order-%tky
Status = 'NEW'
CreatedBy = sy-uname
CreatedAt = utclong_current( )
LastChangedAt = utclong_current( )
LastChangedBy = sy-uname ) )
REPORTED DATA(reported_set)
FAILED DATA(failed_set).
reported = CORRESPONDING #( DEEP reported_set ).
failed = CORRESPONDING #( DEEP failed_set ).
ENDMETHOD.
METHOD checkCustomer.
READ ENTITIES OF zi_salesorder IN LOCAL MODE
ENTITY SalesOrder FIELDS ( CustomerID ) WITH CORRESPONDING #( keys )
RESULT DATA(orders).
LOOP AT orders INTO DATA(order).
" Check customer exists in master data
SELECT SINGLE @abap_true FROM zkna1
WHERE kunnr = @order-CustomerID
INTO @DATA(exists).
IF exists = abap_false.
APPEND VALUE #(
%tky = order-%tky
%element = VALUE #( CustomerID = if_abap_behv=>mk-on ) )
TO failed-salesorder.
APPEND VALUE #(
%tky = order-%tky
%msg = new_message_with_text( severity = 'E'
text = |Customer { order-
CustomerID } does not exist| ) )
TO reported-salesorder.
ENDIF.
ENDLOOP.
ENDMETHOD.
METHOD submitOrder.
READ ENTITIES OF zi_salesorder IN LOCAL MODE
ENTITY SalesOrder FIELDS ( Status ) WITH CORRESPONDING #( keys )
RESULT DATA(orders).
LOOP AT orders INTO DATA(order).
IF order-Status <> 'NEW'.
APPEND VALUE #( %tky = order-%tky ) TO failed-salesorder.
APPEND VALUE #(
%tky = order-%tky
%msg = new_message_with_text( severity = 'E'
text = 'Only NEW orders can be
submitted' ) )
TO reported-salesorder.
CONTINUE.
ENDIF.
ENDLOOP.
MODIFY ENTITIES OF zi_salesorder IN LOCAL MODE
ENTITY SalesOrder
UPDATE FIELDS ( Status LastChangedAt LastChangedBy )
WITH VALUE #( FOR order IN orders
( %tky = order-%tky
Status = 'SUBMITTED'
LastChangedAt = utclong_current( )
LastChangedBy = sy-uname ) ).
READ ENTITIES OF zi_salesorder IN LOCAL MODE
ENTITY SalesOrder ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(updated).
result = VALUE #( FOR order IN updated
( %tky = order-%tky
%param = order ) ).
ENDMETHOD.
METHOD get_instance_features.
READ ENTITIES OF zi_salesorder IN LOCAL MODE
ENTITY SalesOrder FIELDS ( Status ) WITH CORRESPONDING #( keys )
RESULT DATA(orders).
result = VALUE #( FOR order IN orders
( %tky = order-%tky
%features-%action-submitOrder =
COND #( WHEN order-Status = 'NEW'
THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
%features-%action-cancelOrder =
COND #( WHEN order-Status = 'SUBMITTED'
THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
%features-%delete =
COND #( WHEN order-Status = 'NEW'
THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled ) ) ).
ENDMETHOD.
ENDCLASS.
Real-Time Scenario 2 — Draft-Enabled Purchase
Requisition
Draft enables users to save incomplete documents without triggering validations, and
continue editing later (like a "save as draft" in an email client).
managed with additional save implementation in class zbp_zi_purchreq
unique;
strict ( 2 );
define behavior for ZI_PurchReq alias PurchReq
persistent table zpurchreq
draft table zpurchreq_d
lock master total etag LastChangedAt
authorization master ( instance )
etag master LastChangedAt
{
field ( readonly ) : ReqID, CreatedAt, CreatedBy, LastChangedAt,
LastChangedBy;
field ( mandatory ) : Plant, PurchOrg, ReqDate;
create; update; delete;
" Draft lifecycle actions (generated by framework)
draft action Edit;
draft action Activate optimized;
draft action Discard;
draft action Resume;
draft determine action Prepare; " triggers validations for UI-
side pre-check
" Custom actions (also work on draft)
action submitForApproval result [1] $self;
determination setReqDefaults on modify { create; }
validation checkPlant on save { create; update; }
validation checkReqDate on save { field ReqDate; }
association _Items { create; with draft; }
mapping for zpurchreq
{
ReqID = req_id;
Plant = plant;
PurchOrg = purch_org;
ReqDate = req_date;
Status = status;
CreatedAt = created_at;
CreatedBy = created_by;
LastChangedAt = last_changed_at;
LastChangedBy = last_changed_by;
}
}
define behavior for ZI_PurchReqItem alias PurchReqItem
persistent table zpurchreqitem
draft table zpurchreqitem_d
lock dependent by _PurchReq
authorization dependent by _PurchReq
etag dependent by _PurchReq
{
update; delete;
field ( readonly ) : ItemID, ReqID;
field ( mandatory ) : Material, Quantity, UoM;
mapping for zpurchreqitem
{
ItemID = item_id;
ReqID = req_id;
Material = material;
Quantity = quantity;
UoM = uom;
}
}
Real-Time Scenario 3 — Unmanaged BO (Wrapping a
Legacy FM)
When wrapping a classic BAPI or function module, you declare unmanaged and implement
all operations manually.
unmanaged implementation in class zbp_zi_classicorder unique;
strict ( 2 );
define behavior for ZI_ClassicOrder alias ClassicOrder
lock master
authorization master ( global )
{
field ( readonly ) : OrderID, Status, CreatedAt;
create;
update;
delete;
action releaseOrder result [1] $self;
validation checkReleaseAllowed on save { create; update; }
}
Unmanaged implementation (MODIFY handler)
METHOD modify.
" Create
LOOP AT entities-classicorder-create INTO DATA(new_order).
CALL FUNCTION 'ZBAPI_SALESORDER_CREATEFROMDAT2'
EXPORTING
order_header_in = VALUE bapisdhead1(
kunnr = new_order-CustomerID
vkorg = new_order-SalesOrg )
IMPORTING
salesdocument = DATA(created_id)
TABLES
return = DATA(bapi_return).
IF line_exists( bapi_return[ type = 'E' ] ).
APPEND VALUE #( %cid = new_order-%cid )
TO failed-classicorder.
APPEND VALUE #(
%cid = new_order-%cid
%msg = new_message_with_text(
severity = 'E'
text = bapi_return[ type = 'E' ]-message ) )
TO reported-classicorder.
ELSE.
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING wait = abap_true.
mapped-classicorder = VALUE #( ( %cid = new_order-%cid
OrderID = created_id ) ).
ENDIF.
ENDLOOP.
ENDMETHOD.
Real-Time Scenario 4 — Additional Save (Sending a
Notification)
with additional save registers a method called after the main save but still inside the
LUW (logical unit of work).
managed with additional save implementation in class zbp_zi_invoice unique;
METHOD save_modified.
" Called after all RAP-managed writes succeed.
" Use for: audit logs, external system calls, email notifications.
LOOP AT create-invoice INTO DATA(new_inv).
" Write to an audit log table outside the BO's own table
INSERT zaauditlog FROM VALUE #(
object_type = 'INVOICE'
object_id = new_inv-InvoiceID
action = 'CREATED'
changed_by = sy-uname
changed_at = utclong_current( ) ).
ENDLOOP.
LOOP AT update-invoice INTO DATA(upd_inv).
" Send workflow notification if status changed to POSTED
IF upd_inv-%control-Status = if_abap_behv=>mk-on
AND upd_inv-Status = 'POSTED'.
cl_swi_object=>start_workflow(
EXPORTING
wi_id = 'WS99000001'
wi_param = upd_inv-InvoiceID ).
ENDIF.
ENDLOOP.
ENDMETHOD.
RAP Save Sequence — Full Order of Events
Understanding this sequence is critical for placing determinations and validations correctly.
1. MODIFY (create / update / delete called by UI)
↓
2. Determinations ON MODIFY fire
(setDefaults, recalculate totals — in declaration order)
↓
3. SAVE sequence begins
↓
4. Determinations ON SAVE fire
(setStatus, setTimestamps — in declaration order)
↓
5. Validations ON SAVE fire
(checkMandatoryFields, checkBP, checkDates)
↓
6. If validations pass → WRITE to DB (managed: RAP does it; unmanaged: your
code)
↓
7. Additional save hook fires (save_modified)
↓
8. COMMIT WORK
Validations that fail at step 5 roll back steps 4–6. They never commit partial data.
Common Pitfalls and How to Avoid Them
Reading stale buffer data in determinations. Always use READ ENTITIES ... IN LOCAL
MODE inside determinations and validations — this reads from the transactional buffer, not the
DB, so you see the latest in-flight changes.
Forgetting %tky vs %key. %tky is the full transactional key (includes draft admin data in
draft scenarios). %key is just the semantic business key. In behavior implementation always
use %tky for safe identification.
Chaining determinations incorrectly. If determination B depends on data set by
determination A, declare A before B in the BDEF. RAP fires them in declaration order.
Not appending to both failed and reported in validations. failed tells RAP the
operation should not proceed. reported carries the actual user-visible message. You need
both — missing failed means the error message shows but the save still proceeds.
Exposing delete on child entities. Child entities in a composition should have delete
without features only if unconstrained delete is acceptable. Usually you want delete (
features : instance ) to block deletion of items when the header is in a locked status.
Forgetting with draft on child associations. In draft scenarios, if the child does not have
with draft on its association, draft edits to the child will not be persisted in the draft table.