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.
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:
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.
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.
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.
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.
"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.
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.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
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.
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 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 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.
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]
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
Open VS Code, choose File > Open Folder, and open orchestrate-course.
Open a terminal: Terminal > New Terminal.
If the prompt doesn't start with (.venv), turn it on:
Windows (PowerShell):
.venv\Scripts\Activate.ps1
macOS / Linux:
source .venv/bin/activate
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.
Open Eclipse and your ABAP Cloud project from Set up for Unit 13.
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.
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).
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.
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.
Press F5 on your package to refresh. You should see the objects listed in the table under How it works.
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.
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.
In the service binding editor, click the Service URL link. Your browser opens and may ask you to sign in with your trial user.
You see a short XML or JSON page, the service document. In the address bar, delete everything after /0001/, including any ?sap-client=....
Type $metadata at the end and press Enter. You should see a long XML page that starts with <edmx:Edmx Version="4.0".
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.
"""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()
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
Commit:
git add unit13
git commit -m "Unit 13: hide personal data from the RAP tool catalog"
Done whentool_catalog.json lists nine fields without CreatedBy, still marks three tools with "needs_approval": true, and REVIEW.md is committed.
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.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Related topics
Picked from the terms, links and tags this topic shares with others.
Exploring Released APIs (SAP Learning)— released interfaces stay stable from release to release; remote APIs, events, local APIs, local extension points; SAP Business Accelerator Hub lists RAP BO interfaces; Released Objects in ADT; syntax error for non-released objects, e.g. T005 and I_COUNTRY
Defining Business Services (SAP Learning)— service definition exposes CDS entities; service binding sets the binding type; OData V2 and V4 for UI or Web API, InA, SQL; naming with _UI_ or _API_; publishing in ADT is for testing
Create a transactional RAP UI service with the generator (SAP Developer Center, RAP100)— trial systems supported; package under ZLOCAL; table ZRAP100_ATRAV###; Generate ABAP Repository Objects, OData UI Service; generated objects including behavior pool, draft table, projection, service definition and binding ZRAP100_UI_TRAVEL_O4_###; Publish and preview
Implement validations (SAP Developer Center, RAP100)— validation validateCustomer on save { create; field CustomerID; }; draft determine action Prepare lists the validations; failed and reported; %msg, %element, %state_area
Implementing Authority Checks (SAP Learning)— read authorization through CDS access controls; modify authorization in the behavior; authorization master (global), (instance) or both; authorization dependent by; result flags with IF_ABAP_BEHV=>AUTH
Joule speaks ABAP (SAP Community blog by an SAP product expert)— ABAP AI SDK powered by ISLM as an ABAP client to the generative AI hub; needs the customer's own SAP AI Core instance; no separate ABAP licence, costs follow SAP AI Core; availability by product and the correction note for lower releases