In SAP S/4HANA Cloud Public Edition (2602), I had a Service Management requirement that looked trivial on paper and turned into a nice little tour of the extensibility layers: the user enters a Customer Reference only at the service document header, and that value has to be copied down to a custom field at item level automatically. This post walks through why the obvious BAdIs don't get you there on their own, how to confirm developer extensibility is open to you, and the full working implementation including the gotchas that cost me time.
The requirement
Using key user extensibility (Custom Fields app), I created two text custom fields and exposed them on the Manage Service Contracts app via Adapt UI:
- Header field Customer Reference on business context Service Header (CRMS4_SERV_H). Its persistence component resolved to
YY1_CUSTOMERREFERENCE_SRH. - Item field Customer Reference Item on business context Service Item (CRMS4_SERV_I).
Both save and display correctly from the UI. The business rule: the user only maintains the header value, and the item field should mirror it without manual entry.
The BAdIs I planned to use
The enhancement spot for service one-order is ES_S4CRM_1O_EXTENSION, which contains, among others:
CRMS4_SERV_CUST_H_MODIFY(Service Document Header Modifications) — importingSERVICEHEADER,SERVICEHEADER_EXTENSION_IN; changingSERVICEHEADER_EXTENSION_OUT.CRMS4_SERV_CUST_I_MODIFY(Service Document Item Modifications) — importingSERVICEHEADER,SERVICEITEM,SERVICEITEM_EXTENSION_IN; changingSERVICEITEM_EXTENSION_OUT.
My first instinct was: read the header value in the item BAdI and write it to the item extension field. Simple.
Why that falls short (and why Custom Logic scope can't bridge it)
The item BAdI receives the standard header structure SERVICEHEADER (type CRMS4S_SERV_H_BADI) — but it does not receive the header extension fields. So inside CRMS4_SERV_CUST_I_MODIFY there is no way to read YY1_CUSTOMERREFERENCE from the header. Conversely, the header BAdI has the header custom field but cannot touch items.
So the value lives in one BAdI and has to be written in another. The standard pattern for that is a small in-memory buffer: the header BAdI stashes the value, the item BAdI reads it. The problem with doing this in key user Custom Logic scope is that you can't create a standalone global class or shared object to hold that state, and custom logic can't reference an arbitrary custom class — so a clean cross-header-to-item copy isn't feasible there. That pushed me to developer extensibility.
Checking the API Hub: is it released for Developer Extensibility?
Older SAP Community threads (2024) noted these BAdIs were released only for key user apps (USE_IN_KEY_USER_APPS). Release status changes by release, so check your own. On the SAP Business Accelerator Hub, open the BAdI (Developer Extensibility → BAdIs) and look at the Release State tiles. On 2602, CRMS4_SERV_CUST_I_MODIFY showed Release State Developer Extensibility: Released (and the same for the header BAdI). That's the green light — verify both the header and item definitions before you start.
The approach: developer extensibility + a static buffer bridge
Three things to build: a buffer class to carry the value across the two BAdI calls, plus an implementing class for each BAdI. The header BAdI runs first in the save sequence and fills the buffer; the item BAdI reads it.
Step 1 — Prerequisites
- Connect ADT to the Development tenant , not the Customizing tenant. Developer extensibility objects can only be created on Development tenant .
- Have a customer software component and package enabled for ABAP for Cloud Development.
Step 2 — The buffer class
Create and activate this first, since the implementing classes reference it. The key is the document character UUID (SERVICEDOCUMENTCHARUUID), which exists identically on the header and item BAdI's SERVICEHEADER structure and is stable from creation — unlike the document number, which can be empty for a new document.
abap
CLASS zcl_serv_custref_buf DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
CLASS-METHODS set
IMPORTING iv_guid TYPE crms4s_serv_h_badi-servicedocumentcharuuid iv_value TYPE crms4s_serv_h_incl_eew_ps-yy1_customerreference_srh.
CLASS-METHODS get
IMPORTING iv_guid TYPE crms4s_serv_h_badi-servicedocumentcharuuid RETURNING VALUE(rv_value) TYPE crms4s_serv_h_incl_eew_ps-yy1_customerreference_srh.
PRIVATE SECTION.
TYPES: BEGIN OF ty_row,
guid TYPE crms4s_serv_h_badi-servicedocumentcharuuid,
value TYPE crms4s_serv_h_incl_eew_ps-yy1_customerreference_srh,
END OF ty_row.
CLASS-DATA gt_buf TYPE SORTED TABLE OF ty_row WITH UNIQUE KEY guid.
ENDCLASS.
CLASS zcl_serv_custref_buf IMPLEMENTATION.
METHOD set.
DELETE gt_buf WHERE guid = iv_guid.
INSERT VALUE #( guid = iv_guid value = iv_value ) INTO TABLE gt_buf.
ENDMETHOD.
METHOD get.
READ TABLE gt_buf WITH KEY guid = iv_guid INTO DATA(ls).
IF sy-subrc = 0.
rv_value = ls-value.
ENDIF.
ENDMETHOD.
ENDCLASS.
Step 3 — Enhancement implementation and BAdI implementations
In ADT: New → Other ABAP Repository Object → Enhancement → Enhancement Implementation, against enhancement spot ES_S4CRM_1O_EXTENSION. Inside that single enhancement implementation you can host both BAdI implementations:
- Add BAdI Implementation for BAdI definition
CRMS4_SERV_CUST_I_MODIFY(e.g. nameZUPDATE_CUSTREF_H_TO_I), implementing classZCL_SERV_CUST_I_MODIFY, “Active implementation” ticked. - Add BAdI Implementation for BAdI definition
CRMS4_SERV_CUST_H_MODIFY, implementing classZCL_SERV_CUST_H_MODIFY.
Tip: in the Implementing Class field, type the class name and use the quick-fix (Ctrl+1 → create implementing class) so ADT generates the class already wired to the BAdI interface and the MODIFY method signature.
Step 4 — The header implementing class (fills the buffer)
Read the value from SERVICEHEADER_EXTENSION_OUT first (what the user entered in this interaction); fall back to SERVICEHEADER_EXTENSION_IN (the existing/persisted value) when OUT is empty. This matters — see the gotchas.
abap
CLASS zcl_serv_cust_h_modify DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_crms4_serv_cust_h_modify.
ENDCLASS.
CLASS zcl_serv_cust_h_modify IMPLEMENTATION.
METHOD if_crms4_serv_cust_h_modify~modify.
DATA(lv_value) = serviceheader_extension_out-yy1_customerreference_srh.
IF lv_value IS INITIAL.
lv_value = serviceheader_extension_in-yy1_customerreference_srh.
ENDIF.
zcl_serv_custref_buf=>set(
iv_guid = serviceheader-servicedocumentcharuuid iv_value = lv_value ).
ENDMETHOD.
ENDCLASS.
Step 5 — The item implementing class (reads the buffer)
abap
CLASS zcl_serv_cust_i_modify DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_crms4_serv_cust_i_modify.
ENDCLASS.
CLASS zcl_serv_cust_i_modify IMPLEMENTATION.
METHOD if_crms4_serv_cust_i_modify~modify.
DATA(lv_ref) = zcl_serv_custref_buf=>get( serviceheader-servicedocumentcharuuid ).
" confirm your item field's exact component name (context suffix) via code completion
serviceitem_extension_out-yy1_customerreferenitm_sri = lv_ref.
ENDMETHOD.
ENDCLASS.
Activate the buffer class first, then both implementing classes.
How H_MODIFY and I_MODIFY are triggered
This is the part that's easy to misjudge:
- The header MODIFY fires whenever the document is saved/touched.
- The item MODIFY fires per item, only for items that are created or changed in that save — not for untouched items.
- In the save sequence, the header MODIFY runs before the item MODIFY. That ordering is exactly what makes the buffer work: header sets, item gets.
Consequences:
- Create a contract with items and a header reference in the same save → both fire → the value copies.
- Touch an item on an existing contract → the item BAdI fires for that item, reads the buffer (which the header BAdI filled with the current header value via the OUT/IN logic), and copies.
- Edit only the header reference on an existing contract → the item BAdI does not fire for untouched items, so they won't resync. You cannot flag items as dirty from the header BAdI in this released set, so this is a design boundary to be aware of, not a bug.
Testing and the gotchas
Use the ADT debugger for this (Custom Logic Tracing is for key user logic, not your Z classes). Set breakpoints in both MODIFY methods and run a save from the Fiori UI.
The gotchas that actually bit me:
- There is no
GUIDfield onCRMS4S_SERV_H_BADI. The fields use CDS-style names; the usable key isSERVICEDOCUMENTCHARUUID. Don't use the document number — it can be blank for new documents. - Field component names carry a context suffix. The header field resolved to
YY1_CUSTOMERREFERENCE_SRH. The item field will have its own suffix (verify with code completion onserviceitem_extension_out-). Get these exact or it won't compile/copy. - Don't write
serviceheader_extension_out = serviceheader_extension_in. That line appears in SAP's sample code and overwrites the user's freshly entered value with the old one. - OUT vs IN. In my first test the buffer row existed with the right UUID but a blank VALUE. The reason: I had only changed a line item, not re-entered the header field, so
SERVICEHEADER_EXTENSION_OUTcame in empty while the actual value sat inSERVICEHEADER_EXTENSION_IN. Reading OUT with an IN fallback (as in Step 4) fixed it and makes the copy robust whether or not the header was touched in that save. - Edit-only-header limitation (above): if the business needs a later header-only change to resync existing items, this in-transaction pattern won't cover it — you'd need an API-driven update that actually touches the items.
Takeaways
The core lesson is that the service item modify BAdI deliberately doesn't expose header extension fields, so any header-to-item copy needs a bridge across the two BAdI calls — a static buffer keyed by the document UUID, populated in the header BAdI and read in the item BAdI. Add to that the OUT/IN value source, the correct UUID key, the context-suffixed field names, and the per-item trigger behavior, and you have a clean, upgrade-stable solution entirely within developer extensibility.
If you've solved the edit-only-header resync cleanly, I'd be glad to hear how in the comments.



