Anyone who has developed on SAP S/4HANA Public Cloud has run into the same wall eventually: the standard system is solid, consistent, and genuinely well designed — but it was built for the average customer, not for the one sitting across the table from you. Sooner or later a business requirement surfaces that no standard app supports, and you're left choosing between two bad options: hack around it with something fragile, or tell the business "that's just not possible in the cloud."
Extensibility exists precisely to remove that false choice. Done properly — using the RESTful ABAP Programming Model (RAP), CDS views, and SAP's officially released APIs — you can close a genuine functional gap while staying entirely inside the "clean core" boundaries that make Public Cloud upgrade-safe.
This article is a full technical walkthrough of a real extensibility case recently presented at an internal LeverX Masterclass: automatically creating fixed assets at the moment of goods receipt, something the standard Post Goods Movement app in S/4HANA Cloud simply does not support. We'll cover the business problem in detail, the reasoning behind choosing RAP and released APIs over any alternative, and then go layer by layer through the actual solution — custom tables, CDS views, metadata extensions, behavior definitions, communication scenarios, ABAP classes, and OData service exposure — finishing with the practical lessons any developer building a similar extension should take away.
If you work with SAP S/4HANA Cloud in any capacity — ABAP development, functional consulting, or logistics/finance process ownership — this is a case study worth reading closely, because the underlying pattern (standard gap → RAP business object → released API integration → Fiori Elements UI) repeats constantly in Public Cloud projects.
In a standard procurement flow, the asset associated with a purchase order is typically expected to be known — or at least finalized — by the time the PO is created. In practice, that assumption breaks down constantly. The exact assets a company will register often aren't clear until the goods physically show up.
For example, a company places a purchase order for 10 laptops. For whatever reason — a vendor shortage, a partial shipment, a supply constraint — only 8 units actually arrive at goods receipt. The business wants to create fixed assets for exactly those 8 delivered units, at the point of receipt, in one motion.
The problem is structural: the standard Post Goods Movement app in S/4HANA Cloud does not allow account assignment or asset-related fields to be edited during the goods receipt step. There's no configuration switch for this. It's a hard limitation of the standard application, which means the business is forced into a workaround.
When asset creation is decoupled from goods receipt, several problems cascade from that single gap:
Manual asset creation. Someone has to create the fixed assets separately, after the fact, which means extra clicks, extra screens, and extra room for mistakes.
Higher error risk. Manually re-entering data that already exists somewhere in the PO or the goods receipt document is a classic source of data-entry errors — wrong cost center, wrong asset class, wrong quantity.
Delayed asset availability. If assets aren't created immediately at receipt, they aren't available in the system when they should be, which pushes back the start of depreciation and can distort financial reporting for the period.
A structural disconnect between logistics and accounting. Logistics sees the goods arrive; accounting doesn't see the resulting assets until someone manually bridges the gap. Two teams end up working from two different pictures of the same event.
Stripped down to its essentials, the customer's expectation is simple to describe: a single, unified process where posting a goods receipt and creating/linking the resulting fixed assets happen together, in the same interface, in the same action. That's it. No parallel process, no manual reconciliation step, no separate asset-creation screen that someone has to remember to open. Just one workflow that mirrors how the business actually operates.
That gap between "simple to describe" and "impossible to configure in the standard system" is exactly where extensibility earns its place.
SAP S/4HANA Public Cloud deliberately restricts deep customization, which is the entire point of the Public Cloud model. Limiting how deeply customers can modify the core is what keeps upgrades painless and the system consistent across the entire customer base. But "limited customization" is not the same thing as "no customization." SAP's answer is governed, side-by-side extensibility: build what you need next to the standard system, not inside it.
That's the exact gap RAP fills. With the RESTful ABAP Programming Model, developers can:
Two release APIs do the heavy lifting for actual data operations in this solution: an API for fixed assets and an API for material documents. The reasoning for using released APIs instead of any direct data access is straightforward — released APIs are SAP's contract with you. They're maintained across releases, documented, and supported, which means the integration doesn't quietly break the next time SAP ships an update. RAP, meanwhile, gives the solution a business-object structure — data model, behavior, service exposure — that's consistent with how SAP itself builds cloud-ready applications, rather than a bespoke, one-off structure that only the original developer understands.
Put together: RAP builds something that feels native to SAP, behaves the way SAP expects a business object to behave, and survives the next release cycle — because it was never inside the core in the first place.
The solution presented is a custom RAP-based Fiori Elements application built to:
What follows is the architecture behind that app, broken down the same way it was built: data model first, then views, then UI metadata, then behavior, then the API integration layer, then service exposure.
Four purpose-built custom tables anchor the data model, each corresponding to a specific level of the business object:
zmc_poheader — header-level data per purchase order: supplier, PO type, and related header fields. This is the parent entity in the RAP hierarchy.
zmc_poitems — item-level data: materials, quantities, pricing. Since one purchase order can have many items, each item needs to be tracked individually, and this table is what drives both the asset-creation logic and the goods-receipt posting downstream.
zmc_asset — maps the fixed assets created during goods receipt back to their originating PO items, supporting the one-to-many relationship the laptop example requires (multiple assets per PO item).
zmc_count_assets — a supporting view used specifically to count how many assets have been created against how many are expected for a given PO item.
Each of the primary tables also has a draft counterpart (zmc_drf_*). Draft tables let a user start editing, save progress partway through, and return to finish later — without ever touching the active, committed version of the record. This is a small architectural decision with a disproportionately large impact on how forgiving the finished app feels: nobody loses work because they got interrupted mid-task, and nothing is written to the database until the user explicitly commits it.
On top of the tables sit two layers of CDS views, following RAP's standard separation of concerns:
Interface (basic) views — ZMC_PORDERHEADERS, ZMC_PORDERITEMS, ZMC_I_ASSETS — expose the raw custom tables in a clean, reusable structure. The header view selects from the standard purchase order CDS view joined with purchase order items, filtered so only purchase orders with relevant item data are included. It also carries associations to the purchase document type text (so the UI can show a readable description instead of a technical code) and to the company code name, plus a composition to the child items — each purchase order header can own multiple items.
The item-level basic view pulls material and quantity data and adds several associations that matter a lot for the UI logic:
An association to the parent header, establishing the parent-child relationship
A composition of zero-to-many assets, since one item can spawn multiple assets — again, exactly the "10 ordered, 8 received, 8 assets" scenario
An association to the material document item, linking back to the material document created once the goods receipt is posted
An association to a dedicated "count assets" view, which calculates how many assets exist for a given item against how many are expected
That count feeds directly into a calculated field that drives a traffic-light status indicator in the Fiori UI — zero assets created, some created, or all created. It's a small detail, but it's the difference between a business user having to open every line item to check progress and glancing at the list to see exactly where things stand. Every one of these views also filters explicitly on purchase orders created with the custom-asset account assignment category, so the app only ever surfaces the records it's actually meant to handle.
Projection views — ZMC_C_PORDERHEADERS, ZMC_C_PORDERITEMS, ZMC_C_ASSETS — sit on top of the basic views and define exactly which fields and associations get exposed to the consuming application. This is also where redirected to composition child annotations live, enabling navigation from header to items to assets inside the Fiori UI. Projection views are directly tied to the behavior definitions and service definitions built in the next steps, so this is the layer that determines what the outside world is actually allowed to see and do.
Metadata extensions are what turn a technically correct data model into something a business user can navigate without training. For each level — header, item, and asset (ZMC_C_PORDERHEADERS, ZMC_C_PORDERITEMS, ZMC_I_C_ASSETS) — the metadata extension defines:
Labels, grouping, and layout for individual fields
Which fields are shown or hidden entirely
Visual indicators, including the asset-creation traffic light described above
Line-item configuration and nested object pages, so users can drill from a purchase order down into its items and then into the assets each item generated
None of this changes the underlying data model — it's purely presentation — but it's the layer that decides whether the finished app is actually pleasant to use.
Behavior definitions define what the business object can actually do: which operations it supports (create, update, save, delete) and how the draft lifecycle behaves. The projection-level behavior definitions declare projection, strict mode, and draft handling, giving the app the full standard draft lifecycle — create, edit, activate, discard, resume — for header, item, and asset entities alike, using compositions (_Item for child PO items, _Asset for child assets) to keep nested object handling consistent.
The basic-view behavior definitions go a level deeper: each of the three (header, item, asset) declares its draft table, its persistent table, and its lock master — so the whole record is locked consistently during editing — along with authorization checks. Fields are marked read-only or mandatory as needed, associations are included where the logic requires navigation, and projection fields are explicitly mapped to their underlying table fields.
This is the layer that makes the API integration governed rather than ad hoc — SAP's mechanism for ensuring a custom app never talks to core APIs through some improvised, unmanaged connection.
Communication scenario (YY1_mc_API in this build) — declares intent: which APIs will be called, what protocol is used, what authentication method applies, and which technical user consumes the API. Two outbound services are defined under this one scenario: one for the fixed asset API, one for the material document API.
Communication arrangement — the concrete, system-specific configuration built from that scenario: which system, which username, which credentials. Once configured, it acts as the trusted bridge between the custom application and the external API — the actual "wire" the app uses at runtime.
A small but genuinely useful piece of the architecture: zi_mc_const, an interface holding reusable values like the scenario and service IDs used for communication. Centralizing these constants avoids hardcoding scenario names or service paths throughout the codebase, and it means a change to the communication setup later requires touching one place instead of hunting through every class that calls an API.
Two classes do the actual work:
zcl_mc_request_api — a generic API communication handler. It manages the technical mechanics of every outbound call: method_post for creating a record (asset or goods receipt), method_get for fetching data from SAP, and method_batch for bulk operations. It also handles CSRF token management and payload construction, and holds the host values from the communication arrangement so scenario names are never hardcoded into the calling logic.
zcl_mc_main — the actual business logic: fetching purchase order item data, posting the goods movement, building asset creation payloads, and writing results back into the custom tables.
Walking through the method sequence gives a clearer picture of how the pieces fit together:
A get-item method takes a purchase order and PO item and returns the relevant PO data — quantities, unit of measure — or does nothing if the identifiers are blank.
A create-post-goods-movement method takes a structured body of goods movement fields, validates the PO/item combination via the get-item method, builds a JSON payload with a constructed ISO timestamp, and posts it through the request-API class. A 201 response returns the created material document number; a 400 surfaces the error instead of failing silently.
A create-assets method validates the required fixed-asset fields, builds the payload, and posts it the same way. A 200 response returns the created asset's master data; anything else is handled as an error.
A handle-asset-creation method ties the previous three together: validate input, create a material document if one doesn't exist yet, retrieve the company code from the PO item's account assignment data, build the full asset-creation body, call create-assets, and — on success — write the result back into the custom tables.
Save logic follows the same discipline: on commit, the app loops through newly created asset records held in the draft and calls create-assets for each; a 400 anywhere in that loop warns the user that unsaved data will be lost rather than letting it disappear silently.
The final step exposes the whole data model to the outside world. Service definitions bundle the projection views for header, item, and asset levels; the service binding then exposes that service to the Fiori Launchpad (or any other OData consumer), and it's what makes the app previewable and testable — and eventually usable by the business.
The customer's challenge is resolved with extensibility, not a workaround. A real, structural gap — asset creation blocked during goods receipt — is closed cleanly rather than patched.
RAP enables modern, scalable extensions. Clean CDS-based architecture, managed behavior definitions, and built-in draft handling mean lifecycle management doesn't have to be reinvented from scratch for every custom object.
Released APIs allow secure integration with the SAP core. Both the fixed asset and material document integrations go through SAP's own supported APIs rather than direct table access, which is what keeps the solution stable across upgrades.
Clean core is maintained while enabling flexibility. Nothing about this solution modifies a standard object; everything lives alongside the core as a self-contained business object.
The approach follows SAP best practices for future-proof solutions. OData V4 exposure, draft handling, and custom actions built on RAP are exactly the pattern SAP itself uses for cloud-ready applications — which means this extension is built the same way SAP would build it.
A few principles from this case generalize well to almost any RAP-based extensibility project in S/4HANA Cloud:
Start from a genuine standard-app limitation, not a preference. The strongest sign a custom RAP object is worth building is a standard app that provably cannot do what the business needs.
Design the table and view hierarchy around the real parent-child relationships in the data. Header → item → asset, with explicit compositions at each level, is what makes navigation, draft handling, and status logic like the traffic light work consistently.
Never skip draft tables, even for a simple-looking object. Saving progress without committing to the active table is what separates an app that feels safe to use from one where every keystroke is a live edit.
Centralize integration constants early. A small constants interface for scenario and service IDs costs almost nothing and saves real time the first time a communication arrangement changes.
Treat error handling on external API calls as a first-class requirement. A 400 from a released API needs to surface to the end user in a way they can act on — silently failing is worse than not attempting it.
Extensibility is SAP's governed mechanism for adapting the standard system to business-specific needs without modifying core objects. It typically combines custom CDS-based business objects (often built with RAP), SAP's released APIs, and Fiori Elements UIs, all living alongside the standard system rather than inside it — which keeps the solution upgrade-safe.
The standard Post Goods Movement application does not allow account assignment or asset-related fields to be edited at the goods receipt step. This is a functional limitation of the standard app, not a configuration setting, which is why closing this gap requires a custom extension rather than a customizing change.
RAP is SAP's framework for building custom, cloud-ready business objects: data models (via CDS views), business logic and lifecycle behavior (via behavior definitions), and OData services, all exposed through Fiori Elements with minimal custom frontend work. It's the standard approach for extensibility on S/4HANA Cloud because it produces business objects that behave the way SAP's own applications behave.
Yes, provided they follow the clean core principles demonstrated in this case: custom tables and CDS views instead of modified standard objects, and released APIs instead of direct access to standard tables. Because none of the standard system is touched, and all integration happens through supported, versioned APIs, the extension is designed to remain stable across SAP's release cycles.
This walkthrough is based on an internal LeverX Masterclass — a recurring format where LeverX's SAP consultants and developers present project challenges to the wider team. The presenter, Sara Sulejmani, has been working as an SAP ABAP Developer on cloud projects for about a year, and this was her first time leading a session of this kind.
That's not incidental — it reflects how the team operates. Junior and mid-level developers at LeverX aren't handed isolated tickets and left to figure things out alone; they're expected to work through genuinely difficult extensibility problems — communication scenarios, behavior definitions, released-API integration — with senior colleagues around them, and then to turn around and teach what they learned to people across ABAP development, functional consulting, logistics, and asset accounting. Knowledge doesn't stay siloed inside whoever solved the problem first.
If working through problems like this — with senior engineers willing to dig into a RAP behavior definition alongside you, and a culture where you're expected to eventually stand up and present your own solution to the wider team — sounds like the environment you're looking for, LeverX's SAP teams are worth a look.