How a Cloud BAdI plus SAP AI Core (via the ISLM Intelligent Scenario framework) lets you replace ABAP if/else trees with a natural-language prompt — and adjust your approval routing strategy without redeploying code.
Why this matters
If you run S/4HANA Public Cloud and use the standard Sales Document approval workflow, you have probably hit this wall: the workflow framework evaluates conditions in order and assigns one approval reason. The “last reason whose condition is met” wins. As long as your business logic is “price changed → pricing manager approves; incoterm changed → incoterm manager approves,” the standard mechanism works. The moment your business says “and if both changed at the same time, I want a different routing,” the limits of the framework show up.
This is a recurring pain point: businesses want dynamic, multi-reason routing. Different field changes should pick different approvers, and combinations should be detected explicitly — not collapsed into whichever rule happened to be evaluated last.
In this blog, I'll walk through one of the approaches we explored in a proof of concept: an AI-enhanced BAdI that calls a Large Language Model synchronously during the sales order save to pick the most appropriate approval reason, based on what actually changed.
The idea in one picture
SO Save ──▶ Cloud BAdI (SD_APM_SET_APPROVAL_REASON)
│
▼
Z helper class (released for Key User Ext.)
│
├── Read DB old values from I_SalesOrder (released CDS)
├── Compare with new values from BAdI → list of changes
├── Call SAP AI Core via ISLM Intelligent Scenario
└── Validate response (whitelist) + fallback
│
▼
salesdocapprovalreason = LLM-chosen code
Instead of writing branching ABAP logic that hard-codes “if field X changed and field Y didn't, return reason Z,” we describe the routing rules in natural language and let the LLM apply them. The BAdI returns a single, validated reason code; the rest of the standard workflow continues unchanged.
What's the value?
- No ABAP redeployment to change routing strategy. The mapping from “what changed” to “which approver” lives in a prompt. Editing the prompt is a configuration change, not a transport.
- Business-readable rules. A line like “If both price AND incoterm changed → return ZBOT” can be reviewed and signed off by a process owner who does not read ABAP.
- Graceful escalation for combinations. When multiple categories change at once, the LLM can be instructed to return a dedicated “external” reason that hands the case off to a parallel approval flow (for example, in SAP Build Process Automation).
- Standard, released building blocks only. The solution sits on top of the Cloud BAdI
SD_APM_SET_APPROVAL_REASON, the released CDS viewI_SalesOrder, and the ISLM completion API. Nothing relies on private interfaces.
Prerequisites
- S/4HANA Public Cloud Edition with SAP AI Core / Generative AI Hub configured, and an existing ISLM Intelligent Scenario reachable from ABAP.
- An ABAP Z helper class released for Key User Extensibility, so the Cloud BAdI implementation (which runs on ABAP Cloud tier) is allowed to call it.
Walking through the implementation
Step 1 — Detect what actually changed
The Cloud BAdI receives the new values of the sales order. To know what changed, we need the old values too. On ABAP Cloud tier, CDHDR and CDPOS are not directly accessible — that's a deliberate restriction. The released CDS view I_SalesOrder (annotated #PUBLIC_LOCAL_API) is the supported alternative: it gives us the database state before the current save commits, and we compare it against the new values the BAdI hands us.
The output of this step is a plain-text list of human-readable change descriptions, e.g.:
Incoterm changed: EXW -> CIP
Net amount changed: 96.53 -> 150.00
Step 2 — Ask the LLM to pick the reason
We use the ISLM completion API (CL_AIC_ISLM_COMPL_API_FACTORY) to call the LLM. The system prompt encodes the routing rules in plain English; the user message contains only the detected changes:
System: You are an SAP sales order approval routing engine.
Rules:
1. If ONLY price/amount fields changed -> return ZPR1
2. If ONLY incoterm fields changed -> return ZINC
3. If BOTH price AND incoterm changed -> return ZBOT
4. If unsure -> return ZEXT
Return exactly one code as plain text, nothing else.
User: Changes detected in sales order {SO}:
Incoterm changed: EXW -> CIP
Net amount changed: 96.53 -> 150.00
Two LLM parameters matter here:
temperature = 0— we want deterministic, repeatable routing decisions, not creative ones.max_tokens = 10— the answer is a 4-character reason code; capping the output keeps latency and token cost low.
Step 3 — Validate, then trust
An LLM response is a string. Before assigning it to salesdocapprovalreason, we whitelist it against the known reason codes. Anything outside the whitelist — and any exception from the API call — falls back to a safe default (ZEXT) that routes through an external/parallel approval flow rather than silently approving or failing the save.
CASE lv_result.
WHEN 'ZPR1' OR 'ZINC' OR 'ZBOT' OR 'ZEXT'.
rv_reason = lv_result.
WHEN OTHERS.
rv_reason = 'ZEXT'. " Whitelist miss → safe fallback
ENDCASE.
Step 4 — Wire it into the BAdI
The Cloud BAdI implementation stays small and boring on purpose — all the intelligence sits behind the released helper class:
METHOD if_sd_apm_set_approval_reason~set_approval_reason.
CHECK salesdocument-sddocumentcategory = 'C'.
DATA(lv_reason) = zcl_apm_ai_reason_helper=>get_ai_reason(
VALUE #( salesdocument = salesdocument-salesdocument
totalnetamount = salesdocument-totalnetamount
incotermsclassification = salesdocument-incotermsclassification
incotermstransferlocation = salesdocument-incotermstransferlocation
incotermslocation1 = salesdocument-incotermslocation1 ) ).
IF lv_reason IS NOT INITIAL.
salesdocapprovalreason = lv_reason.
ENDIF.
ENDMETHOD.
What we observed in testing
Unit tests against the helper class
Scenario LLM returned Expected Result
| Only Incoterm changed (EXW → CIP) | ZINC | ZINC | PASS |
| Only price changed (96.53 → 150.00) | ZPR1 | ZPR1 | PASS |
| Both price and Incoterm changed | ZBOT | ZBOT | PASS |
End-to-end tests (real sales order save triggering the BAdI)
Sales Order Change made LLM returned Workflow status
| 8390 | Price-only change (TotalNetAmount = 40) | ZPR1 | In Approval ✓ |
| 8391 | Multi-category change (TotalNetAmount = 38) | ZBOT | In Approval ✓ |
Honest limitations
I want to be upfront — this approach is powerful, but it is not magic.
- The BAdI still returns a single reason. This is “use AI to pick the best single reason,” not true parallel multi-reason approval. If your real business need is parallel approvers running concurrently, the BAdI must hand off to an external workflow engine (for example, SAP Build Process Automation triggered by the sales order's ApprovalStsChanged event) — the AI's job here is to recognize the multi-category case and route it correctly.
- Synchronous LLM call costs ~1–2 seconds at save time. Acceptable for an approval-triggering save in our tests, but you should validate this against your own end-user expectations and peak-load behavior.
- Determinism is bounded, not absolute. Even at
temperature=0, an LLM is not a deterministic state machine. The whitelist + fallback are not optional — they are how you keep an unexpected response from breaking the save path. - Released-state for custom classes is a real topic. The Z helper class must be released for Key User Extensibility so the Cloud BAdI is allowed to call it. Plan for this in your transport / governance process.
When should you consider this pattern?
This pattern fits well when:
- Your routing rules change often, and you want process owners — not developers — to own them.
- The combinations of “what changed” are too many to express cleanly as a workflow condition matrix.
- You already operate SAP AI Core / Generative AI Hub and want a small, contained first use case to start building experience.
It is probably not the right fit when your routing logic is genuinely simple and stable, or when sub-second save latency is a hard requirement.
Closing thoughts
What I find most interesting about this pattern is not the AI itself — it's the shape of the extension. The BAdI implementation stays small. The helper class encapsulates one decision. The “business rules” leave ABAP and live as a prompt. When the rules change, you adjust the prompt and re-test, instead of opening a transport.
For anyone exploring how Generative AI fits into real S/4HANA Public Cloud extensibility — not as a chatbot, but as a small, replaceable decision component inside a standard extension point — this is one of the cleanest entry points I've seen.
Happy to discuss in the comments: where else in the sales order lifecycle would you trade a hardcoded rule for a prompt?



