Orchestrate

Report an issue

Something wrong, out of date or unclear? Tell us. Corrections are prioritized over new topics. You don't need to sign in.

0 / 2000

Don't include personal or confidential company details. Reports are reviewed with AI assistance.

ABAP Cloud and RAP for AI builders

Why RAP business objects are the safe door for AI into SAP logic, how to read a RAP service's contract like an agent, and how ABAP calls an LLM.

Updated Oct 9, 2026Foundational 8 minDeep 38 min
Foundational layer · 8 min read

The 60-second version

When an AI system needs to change something in SAP, the hard question is not the model. It is the door it uses.

SAP's modern answer for custom logic is a RAP business object. RAP is the ABAP RESTful Application Programming Model. A business object bundles three things: the data, the rules (checks that run before anything is saved) and the allowed operations, such as "release this order". It also says who may do each operation.

ABAP Cloud is the restricted, upgrade-safe way of writing ABAP. It allows only SAP's released, stable interfaces, and RAP is its model for apps and services.

For AI builders this matters in two directions:

  • AI calls RAP. An agent asks a RAP service to do something (built step by step here). The same rules and permission checks run as when a person clicks a button in a Fiori app.
  • RAP calls AI. ABAP code inside SAP asks a large language model for a suggestion through SAP's ABAP AI SDK, and stores it for a person to review.

Why it matters to the business

Take the course's running example: sales orders blocked for credit. You want an agent to propose releasing low-risk blocks, with a credit analyst approving each one.

There are three ways to let the agent's approved change into SAP:

  1. Write straight into the tables. Fast to build, and it skips every check SAP and your team have built. SAP does not consider this clean. It risks breaking on upgrades and leaves no business audit trail.
  2. Rebuild the rules in the AI app. The Python service checks credit limits and permissions itself. Now the rules live in two places and drift apart.
  3. Call a RAP action such as releaseBlock. The business object checks authorization, runs its validations and records the change. The agent gets the same treatment as a person.

Option 3 is the one to fund. It costs ABAP work up front, but every later AI use case reuses the same door. It also gives auditors one place to look.

The second direction pays off where data is already inside SAP. A RAP action can ask a model to summarise an order's history for the analyst, and save the text in a field the analyst reads. The model writes a suggestion. A person still decides.

How SAP does it

As of October 2026:

  • ABAP Cloud is the only ABAP development model in SAP BTP, ABAP environment and in SAP S/4HANA Cloud Public Edition, according to SAP Learning. In S/4HANA Cloud Private Edition and on-premise S/4HANA it sits beside classic ABAP, and customers choose. Developers mark objects with the language version "ABAP for Cloud Development".
  • Released APIs. SAP commits to keeping released interfaces stable from release to release. They include remote APIs (OData, SOAP), events, local APIs such as released CDS views, and extension points. The SAP Business Accelerator Hub lists them, including RAP business object interfaces.
  • Clean core levels. In August 2025 SAP described four levels. A uses only released interfaces, for example ABAP Cloud with released APIs. B adds classic APIs. C reads internal SAP objects and carries upgrade risk. D, which covers modifications and writing to SAP tables, is not considered clean.
  • RAP services. A service binding publishes a business object as OData V2 or V4, for a user interface or as a Web API. Fiori apps and outside callers, including agents, use the same business object.
  • ABAP AI SDK powered by Intelligent Scenario Lifecycle Management (ISLM). An ABAP client for the generative AI hub (Unit 5). An SAP product expert's blog says it needs your own SAP AI Core instance and has no separate ABAP licence; costs follow SAP AI Core usage. It lists BTP ABAP environment, S/4HANA Cloud Public and Private Edition and on-premise S/4HANA as of 2025, with a correction note for lower releases.
  • Joule for developers helps write ABAP Cloud and RAP code (Coding agents for SAP developers).

Where should an AI's change enter SAP?

Door Clean core level Rules that run Who builds it Use for AI when
Released SAP API or RAP BO interface A SAP's own SAP One exists for the change you need. Check the Business Accelerator Hub first
Your own RAP business object in ABAP Cloud A Yours and SAP's, through released APIs Your ABAP team You need a custom step, such as "release with review note"
Your RAP object wrapping a classic API (BAPI) B The classic API's Your ABAP team No released API covers the change yet
Program reading internal SAP tables C Whatever you write Anyone Read-only analysis, with upgrade risk accepted
Direct table update or modification D None Anyone Never for AI. No checks, no audit, breaks on upgrade

The decision for a leader: make "which door?" a design review question for every AI use case that writes. The answer should be level A or B.

Questions to ask

  1. Which released API or RAP business object will the AI's change go through? Show it on the Business Accelerator Hub or in the system.
  2. If we build our own RAP object, which validations and authorization checks does it run? Are they the same as for the Fiori app?
  3. Whose identity reaches SAP: the user's, or a technical user for the agent? See Agent permissions and SAP authorizations.
  4. Which actions does the agent only propose, and where does a person approve?
  5. If ABAP calls a model, which SAP AI Core instance and generative AI hub model does it use, and who pays for the tokens?
  6. Where are AI suggestions stored, and how do we tell them apart from human decisions in the data?
  7. Which clean core level does the design reach, and who signed that off?

Common misconceptions

  • "RAP is just a way to build Fiori apps." The same business object serves Fiori and outside callers through its service binding. That is what makes it useful for agents.
  • "An API means the AI is safe." The API is only as safe as the checks behind it. An action with no authorization or validation is an open door with a nice sign.
  • "ABAP Cloud is only for the cloud." It also runs in Private Edition and on-premise S/4HANA, beside classic ABAP.
  • "We'll let the AI write to the table, then tidy up later." That is clean core level D. It skips every rule and audit trail.
  • "If ABAP calls a model, the model's answer is a decision." It is a suggestion. Store it as such and keep the decision with a person.

Key terms

All course terms, A to Z, in the glossary

  • ABAP Cloud: SAP's development model for upgrade-safe ABAP. It uses the language version "ABAP for Cloud Development", released APIs only, and RAP.
  • RAP (ABAP RESTful Application Programming Model): SAP's model for building transactional services and apps in ABAP.
  • Business object (BO): in RAP, data plus its behavior: operations, actions, checks and authorization.
  • Action: a named business operation on a BO, such as "deduct discount" or "release block".
  • Validation: a check that runs before saving and can reject the change with a message.
  • Released API: an SAP interface with a stability promise across releases.
  • Service binding: the object that publishes a BO as an OData service for a UI or as a Web API.
  • ISLM / ABAP AI SDK: SAP's way to manage AI scenarios in ABAP systems and call generative AI hub models from ABAP code.

Check yourself

Pick one answer for each question. The explanation appears after you choose.

1Why is a RAP action a better door for an agent's change than a direct table update?

Answer: A. A RAP business object runs its validations and authorization checks for every caller. A direct table update skips them, which SAP classes as clean core level D.

2Your team plans to copy SAP's credit rules into the Python AI service. What is the main risk?

Answer: C. Rebuilding rules outside SAP means two copies to keep in step. Calling the business object keeps one set of rules that both people and agents go through.

3A design writes AI results straight into an SAP table. Which clean core level is that?

Answer: C. SAP's level D covers write access to SAP tables and modifications. It carries the highest upgrade risk and should never be the door for an AI system.

4Where does the ABAP AI SDK send its model calls?

Answer: A. The SDK is an ABAP client for the generative AI hub, and needs the customer's own SAP AI Core instance. Its costs follow SAP AI Core usage.

5A partner proposes an AI feature that writes through a custom RAP action. What should you ask first?

Answer: C. An API is only as safe as the checks behind it. Ask what the action enforces and whether it matches what the Fiori app enforces.

6An ABAP action asks a model to suggest how to resolve a blocked order. How should the result be treated?

Answer: A. A model's answer is a suggestion. Store it where the analyst can see it and keep the decision, and the record of who made it, with a person.
Deep layer · 38 min read

Mental model: a contract with a guard at the door

A RAP business object has two faces.

  • The contract is what callers can see: entities, fields, keys and actions, published as OData metadata. An agent's tools come from this contract.
  • The guard is what callers can't skip: authorization checks, read-only and mandatory fields, validations and locking. It runs inside SAP on every call, whoever the caller is.

As an AI builder, you read the contract to design tools, and you rely on the guard to stay safe. When the guard is missing something, you add it to the business object, not to the agent's prompt.

How it works

The layers of a RAP service

flowchart LR
  T[Table] --> R[CDS view entity<br/>data model]
  R --> B[Behavior definition<br/>and implementation]
  B --> P[Projection]
  P --> S[Service definition]
  S --> SB[Service binding<br/>OData V4]
  SB --> UI[Fiori app]
  SB --> AG[Agent or<br/>other caller]
  B --> E[ABAP code<br/>using EML]

SAP's RAP100 tutorial generates all of these from one table. For its Travel example the generator creates:

Layer What it is RAP100 example name
Data model CDS view entity over the table ZRAP100_R_TRAVELTP_###
Behavior Behavior definition plus an ABAP class with the logic (the behavior pool) ZRAP100_BP_TRAVELTP_###
Draft A table that holds unsaved edits ZRAP100_DTRAV###
Projection The slice of the BO this service exposes ZRAP100_C_TRAVELTP_###
Service definition Which entities the service includes ZRAP100_UI_TRAVEL_###
Service binding The protocol: here OData V4 for a UI ZRAP100_UI_TRAVEL_O4_###

The guard: behavior

The behavior definition declares what the BO allows. Three parts matter most for AI.

Actions are named operations. RAP100 defines one like this:

action deductDiscount result [1] $self;

A projection exposes it with use action deductDiscount;. The ABAP handler reads the instances, changes them with MODIFY ENTITIES, and reports any instance it rejects in failed and reported. For agents, an action is ideal: one clear business step, with its own checks, instead of a generic "update any field".

Validations run when data is saved. RAP100's:

validation validateCustomer on save { create; field CustomerID; }

The handler adds the key of a bad instance to failed, which blocks the save, and a message to reported. SAP Learning says a message for each rejected instance is mandatory. That message is exactly what an agent needs to explain a refusal. With draft, RAP100 also lists the validations under draft determine action Prepare, so drafts are checked before they become active.

Authorization comes in two parts, per SAP Learning:

  • Reading is controlled by CDS access controls, which filter the rows a CDS entity returns.
  • Changing is controlled in the behavior. Each entity is an authorization master or authorization dependent by another. A master can be ( global ), ( instance ) or both. The instance handler returns flags per instance and operation, for example marking %update as unauthorized for orders the user may not change.

This is the guard an agent inherits. If the agent calls through the user's identity, it can never do more than the user. Agent permissions and SAP authorizations covers which identity reaches SAP.

The contract: OData metadata

The service binding publishes the BO as OData. Every OData service has a metadata document at $metadata: an XML file that lists entity types, keys, properties with types and lengths, and actions with parameters. Annotations add meaning:

Annotation Meaning for an agent
Core.Computed SAP sets this field. Don't offer it as an input
Core.Immutable Set once on create, never changed
Core.OptimisticConcurrency The field used as the ETag. Changes must prove they saw the latest version
Common.FieldControl = Mandatory Required before save
Common.DraftRoot The entity uses draft. Lists the actions that edit, prepare and activate drafts

This file is the raw material for agent tools. Tool design for agents explains why you design tools rather than mirror every endpoint. The script below does the first pass: it turns the contract into a short catalog and marks every write.

Draft, briefly

Draft lets a user keep unsaved edits in a separate draft table, as the RAP100 generator sets up. In OData, draft-enabled entities usually carry an extra key, IsActiveEntity, and the Common.DraftRoot annotation names the actions to edit, prepare and activate. A person in Fiori never sees these steps. An agent calling the same UI service would have to perform them in the right order. That is a reason to give agents a purpose-built action, or a separate Web API binding, rather than the Fiori UI service. The next topic in this unit builds that call.

The other direction: RAP calls a model

Inside SAP, ABAP code reaches models through the ABAP AI SDK. SAP's April 2026 tutorial for S/4HANA sets it up in three steps:

  1. In the Intelligent Scenarios app (F4469), create a generative AI scenario and pick a provider and model.
  2. In the Intelligent Scenario Management app (F4470), deploy it. This creates an LLM deployment on SAP AI Core. Then activate it.
  3. In ABAP, create a client for the scenario and send messages.

The core of the tutorial's ABAP class:

DATA(api) = cl_aic_islm_compl_api_factory=>get( )->create_instance( islm_scenario = ints ).
...
container->set_system_role( system_role = system_role ).
DATA(res) = api->execute_for_messages( messages = container )->get_completion( ).

Errors arrive as cx_aic_api_factory and cx_aic_completion_api exceptions. The code names only the scenario. The model choice lives in the ISLM apps, not in the ABAP code.

Build it yourself: read a RAP service the way an agent will

You will look at a RAP service from the outside, as an agent does. First you create a real RAP service in your free ABAP trial with SAP's generator. Then a Python script reads its OData metadata, reviews it, and writes a tool catalog with every write marked "needs approval". The catalog is the input for the next topic, where an agent calls a RAP service.

flowchart LR
  G[Generator in<br/>ABAP trial] --> M[$metadata file]
  S[Built-in sample] --> P[rap_contract.py]
  M --> P
  P --> R[Review in<br/>terminal]
  P --> C[tool_catalog.json]

Before you start: complete Set up your computer for this course and Set up for Unit 13, which gives you the ABAP trial and Eclipse with ADT. This walkthrough doesn't repeat those steps.

What you need

  • Your orchestrate-course folder with Python and .venv from Unit 1.
  • For Part A: the SAP BTP ABAP environment trial and Eclipse with ADT from Set up for Unit 13. Free.
  • For Part B: nothing else. The script uses only Python's standard library, no keys and no network. It has a --sample path, so you can do Part B even if your trial is unavailable.
  • Time: about 60 minutes for Part A, 15 minutes for Part B.

Step 1: Open your course folder and turn on the virtual environment

  1. Open VS Code, choose File > Open Folder, and open orchestrate-course.

  2. Open a terminal: Terminal > New Terminal.

  3. If the prompt doesn't start with (.venv), turn it on:

    • Windows (PowerShell):

      .venv\Scripts\Activate.ps1
    • macOS / Linux:

      source .venv/bin/activate
  4. Create a folder for this topic's files:

    • Windows (PowerShell):

      New-Item -ItemType Directory -Force unit13\rap
    • macOS / Linux:

      mkdir -p unit13/rap

Step 2 (Part A): Generate a RAP service in your trial

This step follows SAP's RAP100 tutorial, which SAP says works on trial systems. Its table definition and data class are long, so copy them from the tutorial page rather than typing them. Replace ### with your own suffix everywhere, for example RT1.

  1. Open Eclipse and your ABAP Cloud project from Set up for Unit 13.
  2. Open SAP's tutorial Create a transactional RAP UI service in your browser.
  3. Package: in the Project Explorer, right-click ZLOCAL and choose New > ABAP Package. Name it ZRAP100_###, describe it RAP100 Package ###, tick Add to favorite packages, choose Next until you can pick a transport request, and choose Finish.
  4. Table: right-click your package and choose New > Other ABAP Repository Object. Search for database table, choose Next, name it ZRAP100_ATRAV### with description Travel data, and choose Finish. Replace the code with the table from the tutorial's step 2, replace ### (Ctrl+F, Replace All), then save (Ctrl+S) and activate (Ctrl+F3).
  5. Data: create the class ZCL_RAP100_GEN_DATA_### the same way (New > ABAP Class), paste the tutorial's code, replace ###, save and activate. Press F9 to run it. Then open the table ZRAP100_ATRAV### and press F8 to preview it. You should see up to 10 travel rows.
  6. Generate: right-click the table ZRAP100_ATRAV### and choose Generate ABAP Repository Objects. Enter the description Travel App ###, choose ABAP RESTful Application Programming Model: UI Service, then Next, then OData UI Service, then Next. Check that each proposed name contains your suffix, choose Next, and confirm.
  7. Press F5 on your package to refresh. You should see the objects listed in the table under How it works.
  8. Metadata extension: open ZRAP100_C_TRAVELTP_###, delete the @UI.selectionField block for the attachment field as the tutorial's step 5 says, save and activate.
  9. Publish: open the service binding ZRAP100_UI_TRAVEL_O4_### and choose Publish. Double-click the entity Travel and choose Go in the preview that opens. You should see up to 10 travels.

Step 3 (Part A): Save the service's metadata

  1. In the service binding editor, click the Service URL link. Your browser opens and may ask you to sign in with your trial user.
  2. You see a short XML or JSON page, the service document. In the address bar, delete everything after /0001/, including any ?sap-client=....
  3. Type $metadata at the end and press Enter. You should see a long XML page that starts with <edmx:Edmx Version="4.0".
  4. Save it: press Ctrl+S (Windows) or Cmd+S (macOS). Choose your orchestrate-course/unit13/rap folder and the file name travel_metadata.xml. If your browser offers a format, choose Webpage, HTML only or Page source, not "complete".

If the trial isn't available, skip to Step 5 and use --sample.

Step 4 (Part B): Create the script

  1. In VS Code, create a file unit13/rap_contract.py.
  2. Paste in the code below and save.
"""Read a RAP service's OData V4 metadata the way an agent will, and review it.

It prints what the service offers (entity sets, keys, fields, actions), flags
what an AI builder must handle (writes, draft, missing ETags), and writes a
tool catalog that the next topic's agent reads.

Commands (run from your course folder):
  python unit13/rap_contract.py --sample                        use the built-in made-up example
  python unit13/rap_contract.py --metadata unit13/rap/travel_metadata.xml
  python unit13/rap_contract.py --sample --out unit13/rap/my_catalog.json

Only Python's standard library is used. Nothing is sent anywhere.
"""
import argparse
import json
import os
import sys
import xml.etree.ElementTree as ET
from pathlib import Path

HERE = Path(__file__).resolve().parent
DEFAULT_OUT = HERE / "rap" / "tool_catalog.json"

EDMX = "http://docs.oasis-open.org/odata/ns/edmx"
EDM = "http://docs.oasis-open.org/odata/ns/edm"
CORE = "Org.OData.Core.V1"
COMMON = "com.sap.vocabularies.Common.v1"

# A made-up example, shaped like the metadata of a draft-enabled RAP UI service
# for the course's running example: sales orders with a credit block.
SAMPLE_METADATA = """<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
  <edmx:Reference Uri="https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Core.V1.xml">
    <edmx:Include Namespace="Org.OData.Core.V1" Alias="SAP__core"/>
  </edmx:Reference>
  <edmx:Reference Uri="https://sap.github.io/odata-vocabularies/vocabularies/Common.xml">
    <edmx:Include Namespace="com.sap.vocabularies.Common.v1" Alias="SAP__common"/>
  </edmx:Reference>
  <edmx:DataServices>
    <Schema Namespace="com.example.zrt1_ui_blocked_order" Alias="SAP__self" xmlns="http://docs.oasis-open.org/odata/ns/edm">
      <EntityType Name="BlockedOrderType">
        <Key>
          <PropertyRef Name="SalesOrder"/>
          <PropertyRef Name="IsActiveEntity"/>
        </Key>
        <Property Name="SalesOrder" Type="Edm.String" Nullable="false" MaxLength="10"/>
        <Property Name="SoldToParty" Type="Edm.String" MaxLength="10"/>
        <Property Name="NetAmount" Type="Edm.Decimal" Precision="15" Scale="2"/>
        <Property Name="Currency" Type="Edm.String" MaxLength="5"/>
        <Property Name="BlockReason" Type="Edm.String" MaxLength="2"/>
        <Property Name="BlockStatus" Type="Edm.String" MaxLength="1"/>
        <Property Name="ReviewNote" Type="Edm.String" MaxLength="255"/>
        <Property Name="AiSuggestion" Type="Edm.String" MaxLength="1000"/>
        <Property Name="CreatedBy" Type="Edm.String" MaxLength="12"/>
        <Property Name="LocalLastChangedAt" Type="Edm.DateTimeOffset" Precision="7"/>
        <Property Name="IsActiveEntity" Type="Edm.Boolean" Nullable="false"/>
        <Property Name="HasActiveEntity" Type="Edm.Boolean" Nullable="false"/>
        <Property Name="HasDraftEntity" Type="Edm.Boolean" Nullable="false"/>
      </EntityType>
      <Action Name="releaseBlock" IsBound="true" EntitySetPath="_it">
        <Parameter Name="_it" Type="SAP__self.BlockedOrderType" Nullable="false"/>
        <ReturnType Type="SAP__self.BlockedOrderType" Nullable="false"/>
      </Action>
      <Action Name="rejectOrder" IsBound="true" EntitySetPath="_it">
        <Parameter Name="_it" Type="SAP__self.BlockedOrderType" Nullable="false"/>
        <Parameter Name="Reason" Type="Edm.String" Nullable="false" MaxLength="255"/>
        <ReturnType Type="SAP__self.BlockedOrderType" Nullable="false"/>
      </Action>
      <Action Name="suggestResolution" IsBound="true" EntitySetPath="_it">
        <Parameter Name="_it" Type="SAP__self.BlockedOrderType" Nullable="false"/>
        <ReturnType Type="SAP__self.BlockedOrderType" Nullable="false"/>
      </Action>
      <Action Name="Activate" IsBound="true" EntitySetPath="_it">
        <Parameter Name="_it" Type="SAP__self.BlockedOrderType" Nullable="false"/>
        <ReturnType Type="SAP__self.BlockedOrderType" Nullable="false"/>
      </Action>
      <Action Name="Edit" IsBound="true" EntitySetPath="_it">
        <Parameter Name="_it" Type="SAP__self.BlockedOrderType" Nullable="false"/>
        <Parameter Name="PreserveChanges" Type="Edm.Boolean"/>
        <ReturnType Type="SAP__self.BlockedOrderType" Nullable="false"/>
      </Action>
      <Action Name="Prepare" IsBound="true" EntitySetPath="_it">
        <Parameter Name="_it" Type="SAP__self.BlockedOrderType" Nullable="false"/>
        <ReturnType Type="SAP__self.BlockedOrderType" Nullable="false"/>
      </Action>
      <EntityContainer Name="Container">
        <EntitySet Name="BlockedOrder" EntityType="SAP__self.BlockedOrderType"/>
      </EntityContainer>
      <Annotations Target="SAP__self.Container/BlockedOrder">
        <Annotation Term="SAP__core.OptimisticConcurrency">
          <Collection>
            <PropertyPath>LocalLastChangedAt</PropertyPath>
          </Collection>
        </Annotation>
        <Annotation Term="SAP__common.DraftRoot">
          <Record>
            <PropertyValue Property="ActivationAction" String="com.example.zrt1_ui_blocked_order.Activate"/>
            <PropertyValue Property="EditAction" String="com.example.zrt1_ui_blocked_order.Edit"/>
            <PropertyValue Property="PreparationAction" String="com.example.zrt1_ui_blocked_order.Prepare"/>
          </Record>
        </Annotation>
      </Annotations>
      <Annotations Target="SAP__self.BlockedOrderType/SalesOrder">
        <Annotation Term="SAP__core.Immutable"/>
      </Annotations>
      <Annotations Target="SAP__self.BlockedOrderType/AiSuggestion">
        <Annotation Term="SAP__core.Computed"/>
      </Annotations>
      <Annotations Target="SAP__self.BlockedOrderType/BlockStatus">
        <Annotation Term="SAP__core.Computed"/>
      </Annotations>
      <Annotations Target="SAP__self.BlockedOrderType/CreatedBy">
        <Annotation Term="SAP__core.Computed"/>
      </Annotations>
      <Annotations Target="SAP__self.BlockedOrderType/LocalLastChangedAt">
        <Annotation Term="SAP__core.Computed"/>
      </Annotations>
      <Annotations Target="SAP__self.BlockedOrderType/ReviewNote">
        <Annotation Term="SAP__common.FieldControl" EnumMember="SAP__common.FieldControlType/Mandatory"/>
      </Annotations>
    </Schema>
  </edmx:DataServices>
</edmx:Edmx>
"""


def q(ns, tag):
    """Qualified XML tag name, as ElementTree writes it."""
    return "{%s}%s" % (ns, tag)


class Names:
    """Turns aliases such as SAP__core.Computed into full names such as Org.OData.Core.V1.Computed."""

    def __init__(self, root):
        self.alias = {}
        for inc in root.iter(q(EDMX, "Include")):
            if inc.get("Alias"):
                self.alias[inc.get("Alias")] = inc.get("Namespace")
        for schema in root.iter(q(EDM, "Schema")):
            if schema.get("Alias"):
                self.alias[schema.get("Alias")] = schema.get("Namespace")

    def full(self, name):
        if not name:
            return name
        if name.startswith("Collection(") and name.endswith(")"):
            return "Collection(%s)" % self.full(name[11:-1])
        head, _, rest = name.partition(".")
        if rest and head in self.alias:
            return self.alias[head] + "." + rest
        return name

    def short(self, name):
        """The last part of a full name: com.x.y.BlockedOrderType -> BlockedOrderType."""
        name = self.full(name)
        return name.rsplit(".", 1)[-1] if name else name


def read_metadata(text):
    try:
        root = ET.fromstring(text)
    except ET.ParseError as err:
        sys.exit("This file is not valid XML (%s). Save the page with Save As, not copy and paste." % err)
    if root.tag != q(EDMX, "Edmx") or root.get("Version", "").split(".")[0] != "4":
        sys.exit("This is not OData V4 metadata. Check that the URL ends with /$metadata and the binding is OData V4.")
    return root


def annotations(root, names):
    """Collect annotations as {target: {term: element}} with full names."""
    found = {}
    for block in root.iter(q(EDM, "Annotations")):
        target = block.get("Target", "")
        head, sep, tail = target.partition("/")
        target = names.full(head) + sep + tail
        for ann in block.findall(q(EDM, "Annotation")):
            found.setdefault(target, {})[names.full(ann.get("Term"))] = ann
    # Inline annotations on properties, entity sets and entity types
    for schema in root.iter(q(EDM, "Schema")):
        ns = schema.get("Namespace")
        for et in schema.findall(q(EDM, "EntityType")):
            for prop in et.findall(q(EDM, "Property")):
                for ann in prop.findall(q(EDM, "Annotation")):
                    key = "%s.%s/%s" % (ns, et.get("Name"), prop.get("Name"))
                    found.setdefault(key, {})[names.full(ann.get("Term"))] = ann
        for cont in schema.findall(q(EDM, "EntityContainer")):
            for es in cont.findall(q(EDM, "EntitySet")):
                for ann in es.findall(q(EDM, "Annotation")):
                    key = "%s.%s/%s" % (ns, cont.get("Name"), es.get("Name"))
                    found.setdefault(key, {})[names.full(ann.get("Term"))] = ann
    return found


def is_true(ann):
    """An annotation with no value, or Bool="true", counts as true."""
    if ann is None:
        return False
    val = ann.get("Bool")
    return val is None or val.lower() == "true"


def analyse(root):
    names = Names(root)
    anns = annotations(root, names)
    types, operations, sets = {}, [], []

    for schema in root.iter(q(EDM, "Schema")):
        ns = schema.get("Namespace")
        for et in schema.findall(q(EDM, "EntityType")):
            full = "%s.%s" % (ns, et.get("Name"))
            keys = [r.get("Name") for r in et.iter(q(EDM, "PropertyRef"))]
            props = []
            for p in et.findall(q(EDM, "Property")):
                pa = anns.get("%s/%s" % (full, p.get("Name")), {})
                control = pa.get(COMMON + ".FieldControl")
                props.append({
                    "name": p.get("Name"),
                    "type": p.get("Type"),
                    "max_length": p.get("MaxLength"),
                    "key": p.get("Name") in keys,
                    "computed": is_true(pa.get(CORE + ".Computed")),
                    "immutable": is_true(pa.get(CORE + ".Immutable")),
                    "mandatory": control is not None and "Mandatory" in (control.get("EnumMember") or ""),
                })
            navs = [n.get("Name") for n in et.findall(q(EDM, "NavigationProperty"))]
            types[full] = {"keys": keys, "properties": props, "navigation": navs}
        for kind in ("Action", "Function"):
            for op in schema.findall(q(EDM, kind)):
                params = op.findall(q(EDM, "Parameter"))
                bound = op.get("IsBound") == "true"
                binding = names.full(params[0].get("Type")) if bound and params else None
                operations.append({
                    "kind": kind.lower(),
                    "name": op.get("Name"),
                    "full_name": "%s.%s" % (ns, op.get("Name")),
                    "bound_to": binding,
                    "parameters": [{"name": p.get("Name"), "type": names.full(p.get("Type")),
                                    "required": p.get("Nullable") == "false"}
                                   for p in (params[1:] if bound else params)],
                })
        for cont in schema.findall(q(EDM, "EntityContainer")):
            for es in cont.findall(q(EDM, "EntitySet")):
                sa = anns.get("%s.%s/%s" % (ns, cont.get("Name"), es.get("Name")), {})
                etag = [pp.text for pp in sa[CORE + ".OptimisticConcurrency"].iter(q(EDM, "PropertyPath"))] \
                    if CORE + ".OptimisticConcurrency" in sa else []
                draft_actions = []
                if COMMON + ".DraftRoot" in sa:
                    for pv in sa[COMMON + ".DraftRoot"].iter(q(EDM, "PropertyValue")):
                        if pv.get("String"):
                            draft_actions.append(names.full(pv.get("String")))
                sets.append({
                    "name": es.get("Name"),
                    "type": names.full(es.get("EntityType")),
                    "etag": etag,
                    "draft": COMMON + ".DraftRoot" in sa,
                    "draft_actions": draft_actions,
                })
    return names, types, operations, sets


def key_template(keys, props):
    by_name = {p["name"]: p for p in props}
    parts = []
    for k in keys:
        t = by_name.get(k, {}).get("type")
        if k == "IsActiveEntity":
            parts.append("IsActiveEntity=true")
        elif t == "Edm.String":
            parts.append("%s='{%s}'" % (k, k))
        else:
            parts.append("%s={%s}" % (k, k))
    return ",".join(parts)


def build(root):
    names, types, operations, sets = analyse(root)
    tools, findings = [], []
    for es in sets:
        et = types.get(es["type"])
        if not et:
            continue
        props = et["properties"]
        hidden = {"IsActiveEntity", "HasActiveEntity", "HasDraftEntity"}
        readable = [p["name"] for p in props if p["name"] not in hidden]
        writable = [p["name"] for p in props
                    if not (p["computed"] or p["key"] or p["immutable"] or p["name"] in hidden)]
        keys = [k for k in et["keys"] if k != "IsActiveEntity"]
        tools.append({
            "name": "list_%s" % es["name"], "kind": "read", "needs_approval": False,
            "http": "GET /%s?$select=...&$filter=...&$top=20" % es["name"],
            "fields": readable,
        })
        tools.append({
            "name": "get_%s" % es["name"], "kind": "read", "needs_approval": False,
            "http": "GET /%s(%s)" % (es["name"], key_template(et["keys"], props)),
            "inputs": keys,
        })
        if not es["etag"]:
            findings.append(("WARN", es["name"], "No ETag. Two writers could overwrite each other without noticing."))
        else:
            findings.append(("OK", es["name"], "ETag field %s. Send If-Match on every change." % ", ".join(es["etag"])))
        if es["draft"]:
            findings.append(("NOTE", es["name"], "Draft-enabled. Changes go through draft actions; the agent must not call them on its own."))
        mand = [p["name"] for p in props if p["mandatory"]]
        if mand:
            findings.append(("NOTE", es["name"], "Mandatory fields: %s." % ", ".join(mand)))
        computed = [p["name"] for p in props if p["computed"]]
        if computed:
            findings.append(("NOTE", es["name"], "Set by SAP, not by callers: %s." % ", ".join(computed)))
        if writable:
            findings.append(("NOTE", es["name"], "Fields a caller can change: %s." % ", ".join(writable)))

        for op in operations:
            if op["bound_to"] != es["type"]:
                continue
            if op["full_name"] in es["draft_actions"]:
                findings.append(("SKIP", op["name"], "Draft action, left out of the catalog."))
                continue
            is_read = op["kind"] == "function"
            tools.append({
                "name": "%s_%s" % ("call" if is_read else "propose", op["name"]),
                "kind": "read" if is_read else "write",
                "needs_approval": not is_read,
                "http": "%s /%s(%s)/%s" % ("GET" if is_read else "POST", es["name"],
                                          key_template(et["keys"], props), op["full_name"]),
                "inputs": keys + [p["name"] for p in op["parameters"]],
                "required": keys + [p["name"] for p in op["parameters"] if p["required"]],
            })
            if not is_read:
                findings.append(("WRITE", op["name"], "Changes SAP data. Catalog marks it needs_approval."))

    for op in operations:
        if op["bound_to"] is None:
            findings.append(("WARN", op["name"], "Unbound %s. Review by hand; not added to the catalog." % op["kind"]))
    return names, types, sets, tools, findings


def show(names, types, sets, tools, findings):
    print("Entity sets")
    for es in sets:
        et = types.get(es["type"], {"keys": [], "properties": []})
        print("  %s  (type %s, key %s)" % (es["name"], names.short(es["type"]), ", ".join(et["keys"])))
        for p in et["properties"]:
            flags = [f for f in ("key", "computed", "immutable", "mandatory") if p[f]]
            size = "(%s)" % p["max_length"] if p["max_length"] else ""
            print("    - %-20s %-22s %s" % (p["name"], p["type"] + size, " ".join(flags)))
    print("\nReview")
    for level, what, text in findings:
        print("  %-5s %-18s %s" % (level, what, text))
    print("\nTool catalog")
    for t in tools:
        print("  %-26s %-5s approval=%-5s %s" % (t["name"], t["kind"], str(t["needs_approval"]).lower(), t["http"]))


def main():
    ap = argparse.ArgumentParser(description="Review a RAP service's OData V4 metadata and write a tool catalog.")
    src = ap.add_mutually_exclusive_group(required=True)
    src.add_argument("--sample", action="store_true", help="use the built-in made-up example")
    src.add_argument("--metadata", help="path to a saved $metadata XML file")
    ap.add_argument("--out", default=str(DEFAULT_OUT), help="where to write the tool catalog")
    args = ap.parse_args()

    if args.sample:
        text, source = SAMPLE_METADATA, "built-in sample"
    else:
        path = Path(args.metadata)
        if not path.exists():
            sys.exit("File not found: %s. Check the folder and the file name." % path)
        text, source = path.read_text(encoding="utf-8-sig"), str(path)

    root = read_metadata(text)
    names, types, sets, tools, findings = build(root)
    if not sets:
        sys.exit("No entity sets found. Is this the $metadata of a service with entities?")
    show(names, types, sets, tools, findings)

    out = Path(args.out)
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text(json.dumps({"source": source, "tools": tools}, indent=2), encoding="utf-8")
    writes = sum(1 for t in tools if t["needs_approval"])
    try:
        shown = os.path.relpath(out)
    except ValueError:  # Windows: a different drive
        shown = str(out)
    print("\nWrote %d tools (%d need approval) to %s" % (len(tools), writes, shown))


if __name__ == "__main__":
    main()

Step 5: Review the built-in sample

The sample is a made-up example shaped like a draft-enabled RAP UI service for blocked sales orders. It has three business actions and three draft actions. Run:

python unit13/rap_contract.py --sample

On macOS or Linux, use python3 if python isn't found.

You should see:

Entity sets
  BlockedOrder  (type BlockedOrderType, key SalesOrder, IsActiveEntity)
    - SalesOrder           Edm.String(10)         key immutable
    - SoldToParty          Edm.String(10)         
    - NetAmount            Edm.Decimal            
    - Currency             Edm.String(5)          
    - BlockReason          Edm.String(2)          
    - BlockStatus          Edm.String(1)          computed
    - ReviewNote           Edm.String(255)        mandatory
    - AiSuggestion         Edm.String(1000)       computed
    - CreatedBy            Edm.String(12)         computed
    - LocalLastChangedAt   Edm.DateTimeOffset     computed
    - IsActiveEntity       Edm.Boolean            key
    - HasActiveEntity      Edm.Boolean            
    - HasDraftEntity       Edm.Boolean            

Review
  OK    BlockedOrder       ETag field LocalLastChangedAt. Send If-Match on every change.
  NOTE  BlockedOrder       Draft-enabled. Changes go through draft actions; the agent must not call them on its own.
  NOTE  BlockedOrder       Mandatory fields: ReviewNote.
  NOTE  BlockedOrder       Set by SAP, not by callers: BlockStatus, AiSuggestion, CreatedBy, LocalLastChangedAt.
  NOTE  BlockedOrder       Fields a caller can change: SoldToParty, NetAmount, Currency, BlockReason, ReviewNote.
  WRITE releaseBlock       Changes SAP data. Catalog marks it needs_approval.
  WRITE rejectOrder        Changes SAP data. Catalog marks it needs_approval.
  WRITE suggestResolution  Changes SAP data. Catalog marks it needs_approval.
  SKIP  Activate           Draft action, left out of the catalog.
  SKIP  Edit               Draft action, left out of the catalog.
  SKIP  Prepare            Draft action, left out of the catalog.

Tool catalog
  list_BlockedOrder          read  approval=false GET /BlockedOrder?$select=...&$filter=...&$top=20
  get_BlockedOrder           read  approval=false GET /BlockedOrder(SalesOrder='{SalesOrder}',IsActiveEntity=true)
  propose_releaseBlock       write approval=true  POST /BlockedOrder(SalesOrder='{SalesOrder}',IsActiveEntity=true)/com.example.zrt1_ui_blocked_order.releaseBlock
  propose_rejectOrder        write approval=true  POST /BlockedOrder(SalesOrder='{SalesOrder}',IsActiveEntity=true)/com.example.zrt1_ui_blocked_order.rejectOrder
  propose_suggestResolution  write approval=true  POST /BlockedOrder(SalesOrder='{SalesOrder}',IsActiveEntity=true)/com.example.zrt1_ui_blocked_order.suggestResolution

Wrote 5 tools (3 need approval) to unit13/rap/tool_catalog.json

Read it the way a reviewer would:

  • suggestResolution sounds harmless, but it is an action, and actions can change data. In this example it fills AiSuggestion. The script treats every action as a write until a person decides otherwise.
  • SoldToParty and NetAmount show as fields a caller can change. Would you want an agent to change an order's customer or value? Probably not. That is a reason to give the agent actions, not a general update tool.
  • The three draft actions are left out. They are plumbing for Fiori, not business steps.

Step 6 (Part A): Review your own service

python unit13/rap_contract.py --metadata unit13/rap/travel_metadata.xml --out unit13/rap/travel_catalog.json

You should see the entity set for travels, its fields, and review lines. A service generated by RAP100 uses draft, so expect a draft note and SKIP lines for draft actions. The generated service has no business actions yet, so its catalog holds only read tools. That is a valid result: nothing in it can change data.

If you completed RAP100's later tutorials and added actions such as deductDiscount, they appear as WRITE lines and propose_ tools.

Step 7: Save your work in Git

git add unit13/rap_contract.py unit13/rap
git commit -m "Unit 13: review a RAP service contract and build a tool catalog"

Check with git status first: you must not see .env.

How the code works

Part of the script What it does
SAMPLE_METADATA A made-up OData V4 metadata document for blocked orders, used by --sample
Names Turns aliases such as SAP__core.Computed into full names, so files with and without aliases both work
annotations() Collects annotations from Annotations blocks and from inside properties and entity sets
analyse() Reads entity types, keys, properties, actions, functions and entity sets, with ETag and draft information
build() Makes read tools for each entity set and a propose_ tool for each action, skips draft actions, and writes the review lines
key_template() Builds the key part of a URL, such as SalesOrder='{SalesOrder}',IsActiveEntity=true
show() and main() Print the result and write tool_catalog.json

The script reads the contract only. It can't see the guard: authorization checks and validations live in ABAP and never appear in metadata. That is why the catalog says "needs approval" for every write, and why you still read the behavior definition before trusting an action.

If something goes wrong

What you see What it means What to do
python is not recognized, or command not found Python isn't installed, or the terminal can't find it Windows: repeat Set up your computer, then open a new terminal. macOS/Linux: use python3 until .venv is active
No module named ... Shouldn't happen: the script uses only built-in modules Check you saved the whole script, and that you run rap_contract.py, not another file
File not found: ... The path to your metadata file is wrong Check the file is in unit13/rap and named travel_metadata.xml
This file is not valid XML The browser saved a web page around the XML, or you copied only part Save again with Ctrl+S and choose "HTML only" or "page source"; or use View page source, select all and save as a .xml file
This is not OData V4 metadata You saved the service document, or an OData V2 service Make sure the URL ends with /$metadata, and that the binding type is OData V4
The browser shows a sign-in error or 403 Your trial session expired Sign in to the trial again, then reload the $metadata URL
The browser can't reach the URL, or shows a proxy error A company network blocks the trial host Try another network, or ask IT to allow *.hana.ondemand.com; meanwhile use --sample
Generator names are already taken Another learner used the same suffix Delete your partial objects and start Step 2 again with a different suffix
The generator wizard isn't offered ADT is out of date In Eclipse, choose Help > Check for Updates and restart
Asked for an API key This script never needs one Check you are running rap_contract.py; keys are only for scripts with --llm

The SAP way

As of October 2026, these are the SAP pieces for each side of the contract.

Building the business object

  • Where: ABAP Cloud in SAP BTP, ABAP environment, S/4HANA Cloud Public Edition, or ABAP Cloud objects in Private Edition and on-premise S/4HANA. ADT in Eclipse is the main tool; ADT for VS Code is covered in Set up for Unit 13.
  • How: the RAP generator for a first version, then hand-written actions, validations and authorization. SAP's RAP100 tutorials walk through each, and Joule for developers can assist.
  • Reuse SAP's first: search the SAP Business Accelerator Hub for a released API or RAP BO interface before building your own. ADT's Released Objects view shows released local objects. Using a non-released object in ABAP Cloud is a syntax error; SAP Learning's example names the replacement, CDS entity I_COUNTRY instead of table T005.

Publishing it

  • Service binding types: OData V2 or V4, each for UI or Web API; also InA and SQL. SAP Learning gives the naming pattern <Namespace>_UI_<name>_04 or <Namespace>_API_<name>_04 for OData V4.
  • Publishing in ADT is for testing. For transportable registration SAP Learning points to the gateway admin transactions. SAP's RAP100 tutorial uses /IWFND/V4_ADMIN for on-premise and private cloud systems.
  • Outside callers such as an agent on BTP need a binding meant for them and a defined identity in SAP. The next topic in this unit sets this up.

Calling a model from ABAP

  • ABAP AI SDK powered by ISLM, as described above. Scenarios are created, deployed and activated in the ISLM apps, so model choice and activation are managed in the system, not hard-coded.
  • Sketch only: this needs an S/4HANA system or BTP ABAP environment connected to your own SAP AI Core with the generative AI hub, which the free trial doesn't include.
" Sketch: behavior definition lines for the blocked-order BO
authorization master ( instance )
...
field ( readonly ) AiSuggestion, BlockStatus;
action suggestResolution result [1] $self;     " writes AiSuggestion only
action releaseBlock result [1] $self;          " the decision, called after a person approves
validation validateReviewNote on save { create; field ReviewNote; }

" Sketch: inside the handler for suggestResolution
DATA(api) = cl_aic_islm_compl_api_factory=>get( )->create_instance( islm_scenario = 'ZRT1_BLOCK_SUGGEST' ).
" build a message container with the order facts read via READ ENTITIES,
" call execute_for_messages( ... )->get_completion( ),
" then MODIFY ENTITIES ... UPDATE FIELDS ( AiSuggestion ) with the text.
" Never change BlockStatus here: releaseBlock does that, and only after approval.

The design choice to copy: the model writes into a suggestion field, never into a status field. The decision is a separate action with its own authorization.

Licensing notes

  • The BTP ABAP environment trial is free and for learning only.
  • The ABAP AI SDK has no separate ABAP licence according to SAP's product expert; you pay for SAP AI Core and generative AI hub usage.
  • Joule for developers uses AI Units, as covered in Coding agents for SAP developers.
  • Productive BTP ABAP environment and S/4HANA Cloud subscriptions are priced separately. Ask your SAP account team.

Build vs. SAP

Situation Use Why
An SAP released API already does the change SAP's API or RAP BO interface SAP owns the rules and the stability promise
A custom business step for an agent, such as "release with review note" Your own RAP BO in ABAP Cloud with a purpose-built action Rules and checks run in SAP for every caller; level A
Only a classic BAPI can do the change Your RAP BO wrapping the BAPI One modern door with checks; level B, with a plan to move to a released API
Logic that lives outside SAP, with its own data CAP on BTP (CAP and side-by-side extensions) Keeps fast-moving AI code outside the ERP
A suggestion that must be computed next to SAP data RAP action calling the ABAP AI SDK The data stays in SAP; one place to secure and log
Learning the contract and designing tools The script in this topic Free, runs anywhere, shows exactly what an agent sees

Production concerns

Security and SAP authorizations

  • Check the guard, not the contract. Metadata can't show authorization. Read the behavior definition: is there an authorization master, and does the instance handler cover every action an agent may call?
  • Read access is separate. CDS access controls filter reads. A missing access control means an agent's read tool may see every row. Check each view layer has one, as SAP Learning advises.
  • Identity. A Web API called by a technical user runs with that user's rights, not the requester's. Decide which identity reaches SAP using Agent permissions and SAP authorizations.
  • Messages are input to the model. Validation messages in reported go back to the agent. Keep them factual and free of personal data, and treat text fields such as ReviewNote as possible prompt injection (Prompt injection and tool poisoning).
  • Model calls from ABAP send data to SAP AI Core. Send only the fields the prompt needs, and mask personal data as covered in SAP generative AI hub and orchestration and Data security and PII.

Evaluation

  • Test every action with the agent's identity: allowed, refused for authorization, and refused by validation. The refusal message should let the agent explain the outcome.
  • For suggestion actions, keep a labelled set of orders and score the suggestions as in LLM evaluation fundamentals. Track how often analysts accept them.

Cost

  • ABAP work is the main cost of a good door. It is paid once and reused by every later AI use case and by the Fiori app.
  • Model calls from ABAP cost tokens in SAP AI Core. Trigger them on demand from an action, not on every read.

Operations

  • Use ETags. Without them, an agent and a person can overwrite each other's changes silently.
  • Version the service. A new action or field is a contract change for the agent; re-run the review and update the tool catalog.
  • Transport RAP objects like any other ABAP code, with code review and ATC checks.

Clean core

Build on released APIs (level A), and wrap classic APIs only where nothing released exists (level B). Never let an AI path write to SAP tables (level D). This is the single most useful design review question for an AI feature that touches S/4HANA.

Pitfalls

  • Pointing the agent at the Fiori service. Draft steps, UI-only fields and generic updates make it awkward and risky. Give agents purpose-built actions.
  • A generic update tool. "Update any field" lets a model change customer or value. Offer narrow actions instead.
  • Trusting the action name. suggestResolution may still write. Read the handler.
  • Checks only in the prompt. "Don't release orders over 50,000" belongs in a validation, where it also protects against people and other systems.
  • Missing read authorization. Without CDS access controls, a read tool returns everything the technical user can see.
  • Model output in a decision field. Store suggestions apart from decisions, so reports and audits can tell them apart.
  • Shared trial confusion. Other learners' RAP100 objects look like yours. Check the suffix before you change anything.

Exercise

Hide a personal-data field from the agent and record your review. The catalog and notes feed the next topic, where an agent calls a RAP service.

Before you start: finish Build it yourself above, at least Steps 4 and 5.

  1. Open unit13/rap_contract.py in VS Code.

  2. Find this line (use Ctrl+F on Windows or Cmd+F on macOS):

            hidden = {"IsActiveEntity", "HasActiveEntity", "HasDraftEntity"}
  3. Change it to:

            hidden = {"IsActiveEntity", "HasActiveEntity", "HasDraftEntity", "CreatedBy"}
  4. Save, then run:

    python unit13/rap_contract.py --sample
  5. Open unit13/rap/tool_catalog.json. Under list_BlockedOrder, the fields list should no longer contain CreatedBy. It should list nine fields, from SalesOrder to LocalLastChangedAt.

  6. Create unit13/rap/REVIEW.md and write four short lines:

    • which actions the agent may propose,
    • which field you hid and why,
    • which field is the ETag,
    • one check you would add to the behavior before letting an agent call releaseBlock.
  7. Commit:

    git add unit13
    git commit -m "Unit 13: hide personal data from the RAP tool catalog"

Done when tool_catalog.json lists nine fields without CreatedBy, still marks three tools with "needs_approval": true, and REVIEW.md is committed.

Check yourself

Pick one answer for each question. The explanation appears after you choose.

1In the "contract and guard" model, what does the OData metadata of a RAP service give you?

Answer: C. Metadata is the contract: entities, keys, fields, actions and annotations. Authorization checks and validations are the guard, written in ABAP, and never appear in metadata.

2Why does the script mark every action, including suggestResolution, as needing approval?

Answer: A. An action may write, as suggestResolution does by filling AiSuggestion. Since metadata only shows the signature, the safe default is to treat every action as a write until a person has read its handler.

3A RAP validation rejects an instance. What must the handler do besides adding the key to failed?

Answer: B. SAP Learning says a message for each rejected instance is mandatory. That message is also what lets an agent explain a refusal.

4Where is read access to RAP data restricted?

Answer: D. Reads are filtered by CDS access controls; changes are checked in the behavior. A view layer without an access control can let a read tool see every row.

5Your agent must change blocked orders, and the only service is the draft-enabled Fiori UI service. What would you do?

Answer: C. Draft steps are UI plumbing, and a generic update lets a model change fields like the customer. A narrow action with its own checks is safer, and writing to tables is clean core level D.

6Why should a model called from ABAP write into AiSuggestion rather than BlockStatus?

Answer: C. Keeping the model's output in a suggestion field keeps decisions with people. The status change stays in releaseBlock, which has its own authorization and is called only after approval.

7What does the ETag in Core.OptimisticConcurrency protect against?

Answer: C. The ETag is a version stamp. A change must send the version it saw, so a stale write is refused instead of silently replacing someone else's newer change.

8In the SAP ISLM setup, what does deploying an intelligent scenario create?

Answer: B. SAP's tutorial says deploying in the Intelligent Scenario Management app creates an LLM deployment on SAP AI Core. ABAP code then calls it through the ABAP AI SDK.

Sources

Sign in to track your progress

We'll email you a one-time sign-in link. No password needed.

or

Tell us a little about you

Optional, every field. It helps us pitch answers to your questions at the right level and decide which topics to write next. It is never shown publicly, and you can change or clear it anytime from the account menu.

SAP areas you work in