Orchestrate

CAP and side-by-side extensions for AI

Add AI to SAP S/4HANA without touching its core, by building a CAP extension on SAP BTP that reads released APIs, calls an AI service and checks who may ask.

Updated Oct 2, 2026Foundational 8 minDeep 40 min
Foundational layer · 8 min read

The 60-second version

A side-by-side extension is a separate application that adds a new capability to SAP S/4HANA without changing S/4HANA itself. It runs on SAP BTP, next to the ERP system rather than inside it. It reads and writes business data only through APIs that SAP has released for that purpose.

For AI, this is usually the right home. The AI part changes fast: models, prompts and providers move every few months. The ERP core should change slowly and predictably. A side-by-side extension keeps the two apart. You can swap a model on Tuesday without an S/4HANA transport, and an S/4HANA upgrade doesn't break your AI feature, as long as the released APIs stay stable.

CAP, SAP's Cloud Application Programming Model, is the framework SAP recommends for building these extensions. You describe your data and services in a short model; CAP turns it into a working service with standard OData endpoints, sign-in checks and a database. You write code only for the part that is truly yours, such as "ask the AI why this order is blocked".

Why it matters to the business

Take the course's running example: blocked sales orders in order-to-cash. The order desk wants a button that explains, in plain words, why an order is on hold and who should act.

There are three places you could build that button:

  • Inside S/4HANA, by changing SAP's code. Fast at first, expensive forever. Every upgrade has to be retested against your change. This is what "clean core" exists to stop.
  • Inside S/4HANA, using SAP's released extension points. Good for small rules and fields close to the data. Less suitable for calling outside model providers, handling model outages and changing prompts weekly.
  • Side by side, on SAP BTP. The extension reads the order through a released API, calls the AI, stores the answer and shows it. S/4HANA stays untouched.

The side-by-side choice has three business effects:

  • Upgrades stay cheap. SAP's learning material describes side-by-side extensions as having an independent lifecycle. The AI feature and the ERP release on their own schedules.
  • Risk is contained. If the model misbehaves or the provider goes down, the extension falls back to a rule-based answer. Order processing in S/4HANA carries on regardless.
  • Governance has one home. Who may use the AI, what it said, and whether it helped are stored in the extension's own database. That is your audit trail and, later, your evaluation data.

The cost is real too. A side-by-side extension is one more application to run, secure, monitor and pay for. It needs a BTP account, a connection to S/4HANA, and someone who owns it after go-live.

How SAP does it

As of October 2026, SAP's learning material on the S/4HANA Cloud extensibility model names four options:

Option Who builds it Where it runs Clean core?
Key-user extensibility Business users and citizen developers, with low-code tools Inside S/4HANA Yes
Developer extensibility Professional ABAP developers, with ABAP Cloud (RAP) Inside S/4HANA Yes, through released extension points
Side-by-side extensibility Professional developers, with CAP, Java or Node.js On SAP BTP Yes, through released APIs
Classic extensibility Experienced ABAP developers Inside S/4HANA; private edition and on-premise only Not guaranteed

For AI applications, SAP's Architecture Center (last updated 23 April 2026) describes a CAP-based backend that manages the application logic. It runs on Cloud Foundry or Kyma, connects to SAP back ends through destinations, and calls models through SAP Cloud SDK for AI, available for Java, JavaScript and Python. Model access needs SAP AI Core with the extended plan. SAP also recommends keeping prompts in the prompt registry rather than in code, and points to a CAP LLM Plugin for generative AI in CAP applications.

CAP's own tools have grown AI features too. As of October 2026, the CAP command line can add an AI plugin (@cap-js/ai) whose documentation describes value-help recommendations using SAP's RPT-1 model and simpler access to SAP AI Core. Treat it as a building block to evaluate, not a finished business feature.

For the platform basics (accounts, runtimes, destinations), see SAP BTP foundations for AI builders. For the model side, see SAP generative AI hub and the orchestration service.

A decision guide: where should this AI feature live?

Use these questions, in order, for each AI idea on your list.

If the honest answer is... Lean towards
"It's a new field, a simple rule or a UI tweak, and no model is involved" Key-user extensibility
"It's logic that must run inside the S/4HANA transaction, close to the data" Developer extensibility (ABAP Cloud)
"It calls a model, combines S/4HANA with other data, or needs its own users and history" Side-by-side on SAP BTP, usually with CAP
"SAP already ships it in Joule or as an SAP Business AI feature" Use SAP's feature first; extend only the gap
"It needs SAP code changed to work" Stop and redesign; this breaks clean core

Most AI features land in the third row. The fourth row matters more each release: check what SAP ships before you build, as covered in the SAP Business AI landscape.

A day in the life of the finished extension: at 9:05 a clerk opens the blocked-orders list in a Fiori app. The list comes from the extension, which asks S/4HANA through the Sales Order API. She clicks Explain on order 4711. The extension checks she holds the order-clerk role, calls the AI service, saves the answer under her name and shows it. At 11:00 the model provider is slow; she still gets a short rule-based answer, labeled as a fallback. On Friday her manager reviews which answers clerks marked as unhelpful.

Questions to ask

  • Which released APIs does the extension use? Get the list. Anything not released is a clean-core and upgrade risk.
  • Whose identity reaches S/4HANA? The end user's, or one technical user for everyone? This decides whether S/4HANA's own authorizations still apply.
  • Who may use the AI action, and where is that enforced? Ask to see the role names and the test that proves a user without the role is refused.
  • What does the extension store, and for how long? AI answers about customers are business data. They need retention rules and access rules.
  • What happens when the model or the S/4HANA connection is down? There should be a defined fallback for each.
  • Is there an SAP-delivered feature that already does this? Building what SAP ships next quarter is wasted money.
  • Who runs it after go-live? Monitoring, patching, CAP and Node.js upgrades, and BTP costs need an owner.

Common misconceptions

  • "Side by side means a copy of the ERP data." Not necessarily. The extension in this topic reads orders live through an API and stores only its own data: the AI answers.
  • "Clean core means no custom code." It means custom code only through released APIs and extension points. A side-by-side extension can hold a lot of custom code; it just doesn't sit inside SAP's.
  • "Signed in means allowed." Being signed in to the extension proves who you are. Roles in the extension, and authorizations in S/4HANA, decide what you may do.
  • "CAP is only for Fiori apps." CAP serves standard OData and is used for back-end services, integrations and AI services as well.
  • "Moving AI to BTP removes the S/4HANA risk." It removes the risk of breaking the core. Wrong answers, data exposure and cost still need controls, in the extension.

Key terms

Term Meaning
Side-by-side extension A separate application on SAP BTP that adds capability to S/4HANA through released APIs
Clean core Keeping SAP's code unmodified, extending only through released APIs and extension points
Released API An interface SAP has published and committed to keep stable for customer use
CAP SAP Cloud Application Programming Model: a framework for business services on BTP
CDS Core Data Services: the modeling language CAP uses to describe data and services
Consumption view Your own, smaller view of a remote API's entity, with the fields renamed to your terms
Action A custom operation on a service, such as "explain this order"
Destination A BTP setting that holds the address and sign-in details of a back-end system
Role A named set of permissions, such as "order clerk", assigned to users

Check yourself

Pick one answer for each question. The explanation appears after you choose.
  1. 1What makes a side-by-side extension a good home for an AI feature on S/4HANA?

    Answer: B. Models and prompts change every few months, while the ERP core should change slowly. A side-by-side extension on SAP BTP keeps the two apart and reads data through released APIs, so each side can release on its own schedule.
  2. 2A team proposes changing SAP's own code so the AI explanation appears faster. What is the main risk?

    Answer: C. Modifying SAP code breaks clean core. Upgrades then have to be checked against the change, which makes them slower and more expensive. SAP's learning material lists this kind of classic extensibility as not guaranteeing a clean core.
  3. 3Which extensibility option fits an AI feature that calls a model and keeps its own history of answers?

    Answer: D. Calling outside models, handling outages and storing answers with their own access rules fit best outside the core. The decision guide puts such features in the side-by-side row, with CAP as SAP's recommended framework.
  4. 4As of October 2026, what does SAP's Architecture Center describe as the backend for generative AI applications on BTP?

    Answer: A. The Architecture Center's AI golden path names a CAP-based backend on Cloud Foundry or Kyma, with SAP Cloud SDK for AI for model access and destinations to SAP back ends. Prompts belong in the prompt registry.
  5. 5A vendor says users must sign in, so the AI extension is secure. What should you ask next?

    Answer: D. Signing in only proves who someone is. Roles decide what they may do, and a test that shows a user without the role being refused is the proof. Ask for both.
  6. 6Why should the extension store each AI answer with the name of the person who asked?

    Answer: B. Stored answers show who asked what and what the AI said, which is your audit trail. With ratings added, the same records become evaluation data. The model doesn't learn from them by itself.
  7. 7Before funding a custom AI extension, what should a leader check first?

    Answer: C. Building what SAP already delivers, or will soon, wastes money and creates something to maintain. Check SAP's own features first and extend only the gap.
Deep layer · 40 min read

Mental model: a thin, owned layer between a stable core and a fast-moving model

Picture three boxes in a row. On the left, S/4HANA: the system of record, slow to change, upgraded by SAP. On the right, the model: fast to change, sometimes down, always paid per call. In the middle, your extension: a small CAP application you own.

The middle box has four jobs, and only four:

  1. Translate. Read S/4HANA through a released API and turn its fields into your terms (DeliveryBlockReason becomes deliveryBlock).
  2. Decide who may act. Check roles before reading or calling the model.
  3. Call the AI safely. With a timeout, a key kept out of code, and a fallback.
  4. Remember. Store what the AI said, for whom, and from which source.

Everything else stays where it belongs. Business rules for releasing an order stay in S/4HANA. Prompt and model logic stay in the AI service from Building an AI API, or in SAP's orchestration service. If you catch the middle box doing a fifth job, ask whether it belongs somewhere else.

CAP makes this layer small because it treats the remote S/4HANA API, your own database and your service the same way: as CAP services you query with the same commands. CAP's documentation calls this treating remote services as if they were local.

How it works

flowchart LR
  U[Fiori app or<br/>test script] -->|OData v4, signed in| A[AssistService<br/>CAP]
  A -->|role check| A
  A -->|OData v2, released API| S[S/4HANA<br/>Sales Order API]
  A -->|X-API-Key| M[AI API<br/>Unit 6]
  A -->|INSERT| D[(Explanations<br/>own database)]

The pieces of a CAP project

A CAP project is a folder with three kinds of files:

Folder Holds In this topic
db/ Your own data model in CDS Explanations: every AI answer
srv/ Services in CDS, plus optional JavaScript for custom logic AssistService and its handler
srv/external/ Models of remote APIs, usually generated by cds import A slice of the Sales Order API

package.json ties it together. Its cds.requires section lists what the app needs from outside: the remote API and the authentication method.

Importing and mocking a remote API

On a project you download the API's metadata file (an EDMX file) and run cds import on it. CAP's documentation shows this creates a CDS model in srv/external/, and you then register it in package.json with "kind": "odata-v2" for an OData V2 API like the S/4HANA Sales Order API.

The useful trick comes next. During development, cds watch mocks every imported service: it serves it locally, filled with CSV data from srv/external/data/. Your code can't tell the difference. It calls cds.connect.to('API_SALES_ORDER_SRV') and gets a service object, mock or real. For a more realistic test, cds mock API_SALES_ORDER_SRV runs the mock as a separate server, and your app then talks to it over HTTP, as it would to S/4HANA.

Consumption views

You rarely want the whole remote entity. A consumption view is a CDS projection that keeps the fields you need and renames them:

entity BlockedOrders as projection on S4.A_SalesOrder {
  key SalesOrder          as salesOrder,
      DeliveryBlockReason as deliveryBlock
}

When your handler sends a query on BlockedOrders to the remote service, CAP translates the names back. In our test, a filter on deliveryBlock reached the mock as an OData filter on DeliveryBlockReason. That keeps S/4HANA's names in one file.

Actions: where the AI goes

Reading data is generic, and CAP does it for you. The AI step is not, so it becomes a custom action. A bound action belongs to one record, so the URL names the order:

POST /odata/v4/assist/BlockedOrders('4711')/explain

In JavaScript you register a handler with this.on('explain', BlockedOrders, req => ...). The request object carries the key in req.params, any input in req.data, and the signed-in user in req.user. req.reject(404, '...') ends the request with an error.

Authorization in the model, not in the code

CAP checks permissions from annotations in the CDS file, before your handler runs:

  • @requires: 'authenticated-user' on the service: nobody anonymous gets in (401).
  • @(requires: 'OrderClerk') on the action: signed-in users without the role get 403.
  • @restrict with where: 'createdBy = $user' on Explanations: clerks see only the rows they created. CAP adds that filter to every read.

Because the rules sit in the model, they are visible in a code review and apply to every route into the data. In development, CAP uses mocked authentication: users and roles you list in package.json, signing in with basic authentication (a name and password sent with each request). In production, the same annotations are checked against real tokens from SAP's identity services; the deployment topic later in this unit sets that up.

Where does the model call happen?

There are two common shapes, and this topic builds the first:

Shape How Good when
CAP calls a separate AI service CAP sends the order to the Python AI API from the previous topic The AI logic is in Python, owned by an AI team, or reused by other callers
CAP calls the model itself The CAP handler uses SAP Cloud SDK for AI for JavaScript One small team, one app, and a simple prompt

Either way, CAP stays the place that knows the user, the roles and the business data.

Build it yourself: an AI side-by-side extension for blocked orders

You will build order-assist, a CAP extension. It reads sales orders from a mocked S/4HANA Sales Order API, lists only the blocked ones, lets order clerks ask for an AI explanation, and stores every answer with the name of the person who asked. By default it explains orders with simple rules, so it runs with no AI service at all. In Step 10 you connect it to the AI API you built in the previous topic.

flowchart LR
  T[test_order_assist.py<br/>9 checks] -->|clara, sam, maria, victor| C[order-assist<br/>CAP, port 4004]
  C --> S[Mocked Sales Order API<br/>CSV data]
  C -->|default| R[Rule-based answer<br/>source: sample]
  C -->|Step 10| A[ai_api.py<br/>port 8000]

Before you start: complete Set up your computer for this course and Set up for Unit 6. They create your orchestrate-course folder with its .venv, and install Node.js 24, the CAP development kit (cds) and FastAPI. Step 10 also uses unit06/ai_api.py and the ORCHESTRATE_API_KEY line in .env from Building an AI API. This walkthrough doesn't repeat those steps.

What you need

  • Your course folder with the Unit 6 setup done (python check_unit06.py ends with All set).
  • About 60 to 90 minutes.
  • Cost: free. Everything runs on your computer. Step 10 uses the sample model in your own AI API, also free.
  • No SAP system or BTP account is needed. S/4HANA is mocked.

Step 1: Open your course folder and check the tools

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

  2. Open a terminal: Terminal > New Terminal.

  3. Turn on the virtual environment if the prompt doesn't start with (.venv):

    • Windows (PowerShell):

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

      source .venv/bin/activate
  4. Check Node.js and CAP (the same on every system):

    node --version
    cds version

    You should see v24 (or v22) for Node.js, and a table that includes @sap/cds-dk with a version starting 10. If cds isn't found, go back to Step 3 of the Unit 6 setup.

Step 2: Create the CAP project

  1. Go into unit06 and create the project (the same on every system):

    cd unit06
    cds init order-assist --add nodejs
    cd order-assist

    What success looks like:

    Successfully initialized CAP project

    The option --add nodejs gives the project its own package.json, the file that lists its libraries and settings. In the Unit 6 setup you skipped it; this project needs it.

  2. In the folders VS Code shows under unit06/order-assist, check you have app, db, srv and a package.json.

From now on, run the commands in this topic from unit06/order-assist unless a step says otherwise.

Step 3: Describe the S/4HANA API you will read

On a real project you would download the Sales Order API's metadata from SAP Business Accelerator Hub and run cds import. Here you write a small slice of it by hand, with the same entity and field names as the real API, API_SALES_ORDER_SRV.

  1. In VS Code, inside unit06/order-assist/srv, create a folder named external.
  2. In srv/external, create API_SALES_ORDER_SRV.cds, paste this and save:
// A hand-written slice of SAP's Sales Order API (API_SALES_ORDER_SRV, entity A_SalesOrder).
// On a project you generate the full model with "cds import"; this keeps only the
// fields this topic uses, under the names the real API uses.
@cds.external : true
service API_SALES_ORDER_SRV {
  entity A_SalesOrder {
    key SalesOrder               : String(10);
        SoldToParty              : String(10);
        TotalNetAmount           : Decimal(16, 3);
        TransactionCurrency      : String(5);
        DeliveryBlockReason      : String(2);
        HeaderBillingBlockReason : String(2);
  }
}
  1. Inside srv/external, create a folder named data. In it, create API_SALES_ORDER_SRV-A_SalesOrder.csv, paste this and save. The name must match exactly: service name, a dash, entity name.
SalesOrder,SoldToParty,TotalNetAmount,TransactionCurrency,DeliveryBlockReason,HeaderBillingBlockReason
4711,17100001,12500.000,EUR,01,
4712,17100002,8300.500,GBP,02,
4713,17100003,21000.000,USD,01,
4714,17100004,640.000,CHF,03,
4715,17100001,990.000,EUR,,

These are made-up orders. 4711 to 4714 carry a delivery block; 4715 has none. The block codes 01 to 03 are made up too.

Step 4: Add your own data: the explanations

The extension keeps one table of its own. It lives in the extension's database, never in S/4HANA.

  1. In unit06/order-assist/db, create schema.cds, paste this and save:
namespace orderassist;
using { cuid, managed } from '@sap/cds/common';

// Our own data: every AI explanation we gave, who asked for it and where it came from.
// It lives in the extension's database, not in S/4HANA.
entity Explanations : cuid, managed {
  salesOrder  : String(10);
  explanation : String(800);
  nextStep    : String(30);
  source      : String(20);   // model, cache, fallback or sample
  model       : String(60);
  requestId   : String(40);
}

cuid gives each row a unique ID. managed adds createdAt, createdBy, modifiedAt and modifiedBy, which CAP fills in from the signed-in user. That is how each answer gets its author without any code.

Step 5: Define the service and who may use it

  1. In unit06/order-assist/srv, create assist-service.cds, paste this and save:
using { API_SALES_ORDER_SRV as S4 } from './external/API_SALES_ORDER_SRV';
using { orderassist as my } from '../db/schema';

@requires: 'authenticated-user'
service AssistService {

  // A consumption view: only the fields we need, renamed into our own words.
  // The handler adds the filter "only orders with a delivery block".
  @readonly
  entity BlockedOrders as projection on S4.A_SalesOrder {
    key SalesOrder          as salesOrder,
        SoldToParty         as customer,
        TotalNetAmount      as netValue,
        TransactionCurrency as currency,
        DeliveryBlockReason as deliveryBlock
  }
  actions {
    // Only order clerks may ask the AI; every answer is stored in Explanations.
    @(requires: 'OrderClerk')
    action explain() returns Explanations;
  };

  // Clerks see their own explanations; managers see all of them.
  @readonly
  @(restrict: [
    { grant: 'READ', to: 'OrderClerk', where: 'createdBy = $user' },
    { grant: 'READ', to: 'OrderManager' }
  ])
  entity Explanations as projection on my.Explanations;
}

Read it top to bottom; it is the whole design in 30 lines. Everyone must sign in. Anyone signed in may read blocked orders. Only OrderClerk may call explain. Clerks see their own explanations; OrderManager sees all of them.

Step 6: Write the handler: the AI step

  1. In unit06/order-assist/srv, create assist-service.js, paste this and save. The file name must match assist-service.cds; that is how CAP finds the code for the service.
// The logic behind AssistService: read orders from S/4HANA, ask the AI API, store the answer.
import cds from '@sap/cds'

const LOG = cds.log('assist')

// Keys stay in the course's .env file (two folders up), never in code.
try { process.loadEnvFile(new URL('../../../.env', import.meta.url)) } catch { /* no .env: sample mode */ }
const AI_API_URL = process.env.AI_API_URL          // e.g. http://127.0.0.1:8000; not set = sample mode
const AI_API_KEY = process.env.ORCHESTRATE_API_KEY
const TIMEOUT_MS = 8000

// Made-up texts for the made-up block codes in the mock data.
// Real codes and their texts are configured in each SAP system; read them, don't copy these.
const BLOCK_TEXTS = {
  '01': ['Credit limit exceeded', 'credit_review'],
  '02': ['Missing export documents', 'complete_documents'],
  '03': ['Incomplete delivery address', 'fix_master_data'],
}

function ruleAnswer (order, source) {
  const [text, nextStep] = BLOCK_TEXTS[order.deliveryBlock] ?? ['Delivery block ' + order.deliveryBlock, 'contact_customer']
  return {
    explanation: `Order ${order.salesOrder} is blocked for delivery. Reason: ${text}. ` +
      `It is worth ${order.netValue} ${order.currency}.`,
    nextStep, source, model: 'rules', requestId: null,
  }
}

async function askAiApi (order) {
  const response = await fetch(`${AI_API_URL}/v1/explain`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-API-Key': AI_API_KEY ?? '' },
    body: JSON.stringify({ sales_order: order.salesOrder }),
    signal: AbortSignal.timeout(TIMEOUT_MS),
  })
  if (!response.ok) throw new Error(`AI API answered ${response.status}`)
  const answer = await response.json()
  return {
    explanation: answer.explanation, nextStep: answer.next_step, source: answer.source,
    model: answer.model, requestId: answer.request_id,
  }
}

export default class AssistService extends cds.ApplicationService {
  async init () {
    const { BlockedOrders, Explanations } = this.entities
    const s4 = await cds.connect.to('API_SALES_ORDER_SRV')

    // Only orders with a delivery block. S/4HANA sends an empty text for "no block"; the mock sends null.
    const hasBlock = { deliveryBlock: { '!=': '' }, and: { deliveryBlock: { '!=': null } } }

    // Reading orders: add our filter and hand the query to S/4HANA (a mock on your laptop).
    this.on('READ', BlockedOrders, req => s4.run(req.query.where(hasBlock)))

    // The AI step, as a bound action: POST .../BlockedOrders('4711')/explain
    this.on('explain', BlockedOrders, async req => {
      const [key] = req.params
      const salesOrder = typeof key === 'object' ? key.salesOrder : key
      const order = await s4.run(SELECT.one.from(BlockedOrders).where({ salesOrder }).and(hasBlock))
      if (!order) return req.reject(404, `Order ${salesOrder} is not blocked or does not exist`)

      let answer
      if (!AI_API_URL) {
        answer = ruleAnswer(order, 'sample')
      } else {
        try {
          answer = await askAiApi(order)
        } catch (error) {
          LOG.warn(`AI API unavailable for order ${salesOrder}: ${error.message}`)
          answer = ruleAnswer(order, 'fallback')
        }
      }
      const ID = cds.utils.uuid()
      await INSERT.into(Explanations).entries({ ID, salesOrder, ...answer })
      LOG.info(`order ${salesOrder} explained for ${req.user.id} (source: ${answer.source})`)
      return SELECT.one.from(Explanations, ID)
    })

    return super.init()
  }
}

Step 7: Register the API and the test users

  1. Open unit06/order-assist/package.json. Replace its whole content with this and save:
{
  "name": "order-assist",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "@sap/cds": "^10"
  },
  "devDependencies": {
    "@cap-js/sqlite": "^3"
  },
  "scripts": {
    "start": "cds-serve"
  },
  "private": true,
  "cds": {
    "requires": {
      "API_SALES_ORDER_SRV": {
        "kind": "odata-v2",
        "model": "srv/external/API_SALES_ORDER_SRV"
      },
      "auth": {
        "kind": "mocked",
        "users": {
          "clara": {
            "password": "clara",
            "roles": [
              "OrderClerk"
            ]
          },
          "sam": {
            "password": "sam",
            "roles": [
              "OrderClerk"
            ]
          },
          "maria": {
            "password": "maria",
            "roles": [
              "OrderManager"
            ]
          },
          "victor": {
            "password": "victor",
            "roles": []
          },
          "*": false
        }
      }
    }
  }
}

The cds part does two things. API_SALES_ORDER_SRV tells CAP the app needs the Sales Order API, as OData V2, described by the file from Step 3. auth lists four made-up users for development, each with the password equal to the name. "*": false refuses any other name.

User Roles May
clara OrderClerk Read orders, ask the AI, see her own answers
sam OrderClerk The same, for his own answers
maria OrderManager Read orders, see every answer
victor none Read orders only
  1. Install the project's libraries (the same on every system):

    npm install

    What success looks like: a few lines ending with found 0 vulnerabilities. A node_modules folder appears. It is listed in the project's .gitignore, so Git ignores it.

Step 8: Start the extension and try it

  1. Start it:

    cds watch

What success looks like (trimmed; from our test):

[cds] - connect to db > sqlite { url: ':memory:' }
  > init from srv/external/data/API_SALES_ORDER_SRV-A_SalesOrder.csv
/> successfully deployed to in-memory database.
[cds] - using auth strategy { kind: 'mocked' }
[cds] - serving AssistService {
  at: [ '/odata/v4/assist' ],
  decl: 'srv/assist-service.cds:5',
  impl: 'srv/assist-service.js'
}
[cds] - mocking API_SALES_ORDER_SRV {
  at: [ '/odata/v4/api-sales-order-srv' ],
  decl: 'srv/external/API_SALES_ORDER_SRV.cds:5'
}
[cds] - server listening on { url: 'http://localhost:4004' }

mocking API_SALES_ORDER_SRV is the stand-in for S/4HANA. On Node.js 22 you may also see ExperimentalWarning: SQLite is an experimental feature. It is harmless here.

  1. Open http://localhost:4004/odata/v4/assist/BlockedOrders in your browser. A sign-in box appears. Enter victor as the user name and victor as the password. You see four orders, 4711 to 4714. Order 4715 is missing, because it has no block. That is the filter in the handler at work.

What success looks like (shortened to the first order):

{"@odata.context":"$metadata#BlockedOrders","value":[{"salesOrder":"4711","customer":"17100001","netValue":"12500.000","currency":"EUR","deliveryBlock":"01"}, ...]}
  1. Ask for an explanation. A browser can't send this kind of request, so use a second terminal (Terminal > New Terminal; the first one is busy running cds watch). First as victor, who has no clerk role:

    • Windows (PowerShell):

      $victor = @{ Authorization = "Basic " + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("victor:victor")) }
      Invoke-RestMethod -Method Post -Uri "http://localhost:4004/odata/v4/assist/BlockedOrders('4711')/explain" -Headers $victor -ContentType "application/json" -Body "{}"
    • macOS / Linux:

      curl -u victor:victor -X POST -H "Content-Type: application/json" -d '{}' "http://localhost:4004/odata/v4/assist/BlockedOrders('4711')/explain"

What success looks like: a refusal. That is the point.

{"error":{"message":"Forbidden","code":"403","@Common.numericSeverity":4}}

PowerShell shows the same as a red error mentioning 403 or Forbidden.

  1. Now as clara, who is a clerk:

    • Windows (PowerShell):

      $clara = @{ Authorization = "Basic " + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("clara:clara")) }
      Invoke-RestMethod -Method Post -Uri "http://localhost:4004/odata/v4/assist/BlockedOrders('4711')/explain" -Headers $clara -ContentType "application/json" -Body "{}"
    • macOS / Linux:

      curl -u clara:clara -X POST -H "Content-Type: application/json" -d '{}' "http://localhost:4004/odata/v4/assist/BlockedOrders('4711')/explain"

What success looks like (your ID and times differ):

{"@odata.context":"../$metadata#Explanations/$entity","ID":"19a65e05-2d5e-472a-9618-0f1c6723ac81","createdAt":"2026-10-02T12:37:46.109Z","createdBy":"clara","modifiedAt":"2026-10-02T12:37:46.109Z","modifiedBy":"clara","salesOrder":"4711","explanation":"Order 4711 is blocked for delivery. Reason: Credit limit exceeded. It is worth 12500.000 EUR.","nextStep":"credit_review","source":"sample","model":"rules","requestId":null}

"source":"sample" means the rule-based answer: no AI service is connected yet. "createdBy":"clara" came from CAP, not from your code.

  1. Look at the first terminal. The handler left a line:
[assist] - order 4711 explained for clara (source: sample)
  1. Leave cds watch running for the next step.

Step 9: Prove the rules with tests

Clicking around proves little. A test script proves every rule, every time. It uses only built-in Python, so nothing new to install.

  1. In VS Code, in unit06 (not inside order-assist), create test_order_assist.py, paste this and save:
"""Check the order-assist CAP extension: who may read, who may ask the AI, who sees which answers.

Start the extension first (in unit06/order-assist:  cds watch), then, from your course folder:
    python unit06/test_order_assist.py
It uses only built-in Python and the made-up users from package.json.
"""
import base64
import json
import sys
import urllib.error
import urllib.request

BASE = "http://localhost:4004/odata/v4/assist"


def call(path, user=None, method="GET", body=None):
    """Send one request; return (status code, parsed JSON or None)."""
    request = urllib.request.Request(BASE + path, method=method)
    if user:   # mocked users sign in with basic authentication: name and password
        token = base64.b64encode(f"{user}:{user}".encode()).decode()
        request.add_header("Authorization", "Basic " + token)
    if method == "POST":
        request.add_header("Content-Type", "application/json")
        request.data = json.dumps(body or {}).encode()
    try:
        with urllib.request.urlopen(request, timeout=20) as response:
            return response.status, json.loads(response.read() or b"null")
    except urllib.error.HTTPError as error:
        return error.code, None


def check(name, ok, detail=""):
    print(("PASS " if ok else "FAIL ") + name + ("" if ok else f"  ({detail})"))
    return ok


def main() -> None:
    try:
        call("/$metadata")
    except urllib.error.URLError:
        print("The extension isn't running. In a second terminal: cd unit06/order-assist, then cds watch.")
        sys.exit(1)

    results = []
    status, _ = call("/BlockedOrders")
    results.append(check("no sign-in -> 401", status == 401, status))

    status, body = call("/BlockedOrders", "victor")
    numbers = sorted(row["salesOrder"] for row in (body or {}).get("value", []))
    results.append(check("signed-in user sees only blocked orders", numbers == ["4711", "4712", "4713", "4714"], numbers))

    status, _ = call("/BlockedOrders('4711')/explain", "victor", "POST")
    results.append(check("user without OrderClerk may not ask the AI -> 403", status == 403, status))

    status, _ = call("/BlockedOrders('4715')/explain", "clara", "POST")
    results.append(check("order without a block -> 404", status == 404, status))

    status, answer = call("/BlockedOrders('4712')/explain", "clara", "POST")
    answer = answer or {}
    results.append(check("clerk asks the AI -> 200 with an explanation", status == 200 and bool(answer.get("explanation")), status))
    results.append(check("answer says where it came from", answer.get("source") in {"model", "cache", "fallback", "sample"}, answer.get("source")))
    results.append(check("answer is stored under the clerk's name", answer.get("createdBy") == "clara", answer.get("createdBy")))

    _, body = call("/Explanations", "sam")
    others = [row for row in (body or {}).get("value", []) if row.get("createdBy") != "sam"]
    results.append(check("another clerk can't read clara's answers", others == [], len(others)))

    _, body = call("/Explanations", "maria")
    results.append(check("manager can read every answer", any(row.get("createdBy") == "clara" for row in (body or {}).get("value", [])), body))

    print(f"\n{sum(results)} of {len(results)} passed")
    sys.exit(0 if all(results) else 1)


if __name__ == "__main__":
    main()
  1. In the second terminal, go back to the course folder and run it. Make sure (.venv) is on (Step 1).

    • Windows (PowerShell):

      cd ..\..
      python unit06\test_order_assist.py
    • macOS / Linux:

      cd ../..
      python unit06/test_order_assist.py

    cd goes up two folders, from unit06/order-assist to the course folder. A new terminal in VS Code already starts in the course folder; then skip the cd line.

What success looks like:

PASS no sign-in -> 401
PASS signed-in user sees only blocked orders
PASS user without OrderClerk may not ask the AI -> 403
PASS order without a block -> 404
PASS clerk asks the AI -> 200 with an explanation
PASS answer says where it came from
PASS answer is stored under the clerk's name
PASS another clerk can't read clara's answers
PASS manager can read every answer

9 of 9 passed

If you see The extension isn't running, start cds watch again in unit06/order-assist. The data lives in memory, so every restart begins empty; the tests don't depend on earlier runs.

Step 10: Connect the real AI API

Now replace the rules with the AI API from Building an AI API. It runs with its sample model, so this is still free.

  1. Open .env in the course folder. Add this line at the end and save:

    AI_API_URL="http://127.0.0.1:8000"

    ORCHESTRATE_API_KEY should already be in the same file. The extension sends it in the X-API-Key header, exactly as your earlier tests did.

  2. In the second terminal, in the course folder, start the AI API:

    python unit06/ai_api.py

    You should see Uvicorn running on http://127.0.0.1:8000.

  3. Go to the first terminal. Stop cds watch with Ctrl+C, then start it again so it reads the new .env line:

    cds watch
  4. Open a third terminal (Terminal > New Terminal). Ask for 4711 as clara twice, with the commands from Step 8, item 4. The $clara line must be run again in a new PowerShell terminal.

What success looks like (shortened):

..."explanation":"Order 4711 for Made-up Retail GmbH is on hold. Reason in SAP: Credit limit exceeded. It is worth 12,500.00 EUR. The responsible team must review it before it can ship.","nextStep":"credit_review","source":"model","model":"sample-model","requestId":"18abb262c8754bdea1b775208f956cfd"}
..."source":"cache","model":"sample-model","requestId":"f21ac979c1fa43c68a0324868f48cf9c"}

The first answer came from the AI API's model, the second from its cache. The AI API's requestId is stored with the answer, so you can find the call in both logs.

  1. Make it fail on purpose. Stop the AI API in the second terminal with Ctrl+C, then ask for 4713 as clara. You still get an answer, with "source":"fallback", and the first terminal shows why:
[assist] - AI API unavailable for order 4713: fetch failed
[assist] - order 4713 explained for clara (source: fallback)
  1. Run the tests again with the AI API on or off: 9 of 9 passed both ways. Stop everything with Ctrl+C in each terminal.

What the code does

Part What it does
API_SALES_ORDER_SRV.cds A slice of the real Sales Order API, with the real names; @cds.external marks it as a remote service
API_SALES_ORDER_SRV-A_SalesOrder.csv Mock data CAP serves in place of S/4HANA during development
schema.cds The extension's own table; managed records who created each row
BlockedOrders as projection on ... The consumption view: five fields, renamed into your terms
@requires, @restrict Who may read, who may call explain, and the per-user filter on Explanations
process.loadEnvFile(...) Reads the course's .env, so the key and URL stay out of code
hasBlock The filter "has a delivery block", covering both an empty text and a missing value
this.on('READ', BlockedOrders, ...) Sends list queries to the S/4HANA service, with the filter added
this.on('explain', BlockedOrders, ...) Reads the order, asks the AI API or the rules, stores and returns the answer
AbortSignal.timeout(TIMEOUT_MS) Gives up on the AI API after 8 seconds, so a slow model can't hang the clerk's screen
ruleAnswer The fallback: a plain, labeled answer from the block code
test_order_assist.py Signs in as each made-up user and checks every rule

Optional: run the mock as a separate server

To see what your extension really sends to S/4HANA, run the mock in its own process, as CAP's documentation describes.

  1. Stop cds watch. In one terminal, in unit06/order-assist:

    cds mock API_SALES_ORDER_SRV --port 4006
  2. In another terminal, in the same folder, run cds watch. It now shows connect to API_SALES_ORDER_SRV > odata { url: 'http://localhost:4006/...' }.

  3. List the blocked orders as victor (Step 8, item 2) and watch the mock's terminal. In our test it received:

'$filter': "DeliveryBlockReason ne '' and DeliveryBlockReason ne null"

Your filter on deliveryBlock arrived under S/4HANA's own field name. That is the consumption view doing its job.

If something goes wrong

What you see What it means What to do
cds: command not found or 'cds' is not recognized The CAP kit isn't installed, or the terminal is older than the install Open a new terminal; if it persists, redo Step 3 of the Unit 6 setup
python: command not found or 'python' is not recognized Python isn't on PATH, or .venv is off Turn on .venv (Step 1); on macOS/Linux try python3
Cannot find package '@sap/cds' npm install wasn't run in order-assist Run npm install inside unit06/order-assist
npm ERR! network or ETIMEDOUT during npm install The network or a company proxy blocks npm Try another network, or ask IT for the npm proxy settings
The browser shows no orders, "value":[] The CSV file name or folder is wrong Check srv/external/data/API_SALES_ORDER_SRV-A_SalesOrder.csv exactly
401 for every user The auth block in package.json is missing or mistyped Compare with Step 7; check the user and password are the same word
403 for clara on explain The role name differs between package.json and the CDS file Both must say OrderClerk, same spelling and case
"source":"fallback" and AI API answered 401 in the log The key in .env doesn't match the one the AI API reads Check ORCHESTRATE_API_KEY in .env; restart both servers
"source":"sample" after Step 10 cds watch didn't pick up AI_API_URL Check the line in .env in the course folder; restart cds watch
EADDRINUSE or address already in use Another cds watch or server is still running Stop it with Ctrl+C in its terminal, or close that terminal
The extension isn't running from the test cds watch isn't running, or runs on another port Start it in unit06/order-assist and wait for server listening

The SAP way

The extension runs on your laptop, against a mock, with made-up users. On SAP BTP, the same design maps to managed pieces. As of October 2026:

Concern In this topic On SAP BTP
Framework CAP on Node.js CAP on Cloud Foundry or Kyma; SAP's Architecture Center names a CAP-based backend for generative AI applications
S/4HANA API model A hand-written slice cds import of the API's EDMX into srv/external
S/4HANA connection cds watch mocks the service A destination in the production profile; for on-premise systems also the Connectivity service and Cloud Connector
Users and roles Mocked users in package.json Real sign-in through SAP's identity services; cds add xsuaa or cds add ias adds the configuration
Model access The Python AI API The same AI API on BTP, or SAP Cloud SDK for AI for JavaScript called from the handler
Prompts Inside the AI API The prompt registry, which SAP recommends over hard-coded prompts
Database SQLite in memory SAP HANA Cloud; cds add hana adds the configuration

Connecting to S/4HANA. CAP's documentation shows the remote service's real address going into a production profile, pointing at a destination by name. This sketch shows the shape for the Sales Order API, whose service path the SAP Cloud SDK lists as /sap/opu/odata/sap/API_SALES_ORDER_SRV:

"API_SALES_ORDER_SRV": {
  "kind": "odata-v2",
  "model": "srv/external/API_SALES_ORDER_SRV",
  "[production]": {
    "credentials": {
      "destination": "S4HANA",
      "path": "/sap/opu/odata/sap/API_SALES_ORDER_SRV"
    }
  }
}

Calling the model from CAP directly. SAP Cloud SDK for AI for JavaScript provides an OrchestrationClient in the @sap-ai-sdk/orchestration package. Its documentation shows a prompt template with placeholders, chatCompletion with placeholderValues, and getContent() for the answer. This sketch would replace askAiApi in the handler:

// Sketch: needs SAP AI Core (extended plan) and AICORE_SERVICE_KEY in .env, or a service binding on BTP.
// Install in order-assist with:  npm install @sap-ai-sdk/orchestration
import { OrchestrationClient } from '@sap-ai-sdk/orchestration'

const client = new OrchestrationClient({
  promptTemplating: {
    model: { name: 'gpt-5' },   // use a model name from your own catalog
    prompt: {
      template: [{ role: 'user', content: 'In under 60 words, explain why sales order {{?order}} is blocked: {{?reason}}' }],
    },
  },
})

async function askModel (order, reasonText) {
  const response = await client.chatCompletion({ placeholderValues: { order: order.salesOrder, reason: reasonText } })
  return { explanation: response.getContent(), nextStep: 'contact_customer', source: 'model', model: 'gpt-5', requestId: null }
}

CAP's AI plugin. As of October 2026, cds add ai in the CAP development kit (version 10.1) adds the @cap-js/ai plugin. Its README describes value-help recommendations using SAP-RPT-1 through SAP AI Core, and an AICore CAP service for resource groups and deployments. It needs an SAP AI Core binding in production. It does not replace your own action for a task like "explain this order".

Licensing notes. SAP AI Core's extended plan, the Destination service, identity services and SAP HANA Cloud are separate BTP entitlements. Check what your company's BTP contract includes before you design around any of them.

Build vs. SAP

Situation Build it yourself Use SAP's offering
Learning, a prototype, a demo CAP with mocks, as in this topic Not needed yet
The AI feature exists in Joule or SAP Business AI Don't rebuild it Use SAP's feature; extend only the gap
A new field or simple rule, no model Overkill Key-user extensibility
Logic that must run in the S/4HANA transaction Wrong place Developer extensibility with ABAP Cloud
AI over S/4HANA data with its own users and history CAP side-by-side, as here CAP on BTP with destinations and identity services
Field value suggestions in a Fiori form Weeks of work Evaluate @cap-js/ai recommendations
Prompts change weekly, by non-developers A code release each time The prompt registry
Many teams call the same AI service Hard to govern alone API Management in SAP Integration Suite, as in Building an AI API

Production concerns

  • Two layers of authorization. CAP roles decide who may use the extension. S/4HANA authorizations decide what data comes back. With one technical user for everyone, S/4HANA sees only that user, and every clerk can reach every order the technical user can. Unit 7 covers passing the user's identity through.
  • Instance checks for actions. CAP's documentation notes that bound actions support only simple, static where conditions in @restrict. For "only the author may change this", check in the handler, as the exercise does.
  • Mocks never ship. The mock serves the Sales Order API without sign-in on /odata/v4/api-sales-order-srv. That is fine on a laptop and must not exist in production. The production profile replaces it with the destination.
  • Mocked users never ship. Passwords in package.json are for development. Production uses SAP's identity services, with role collections assigned by administrators.
  • Released APIs only. Use APIs SAP has released for customer use, and check their state on SAP Business Accelerator Hub. Unreleased services are a clean-core and upgrade risk.
  • Data stored in the extension. Explanations holds customer-related text. Set a retention period, restrict who may read it, and include it in data-protection reviews.
  • Timeouts and fallbacks. The AI call has a timeout shorter than the user's patience, and the S/4HANA call needs one too. Every failure path returns a labeled answer or a clear error.
  • Evaluation. The tests prove the access rules. They don't prove the explanations are good. Ratings, added in the exercise, feed the evaluation harness in Unit 8.
  • Cost. Every explain may call a paid model. The AI API's cache and rate limit apply; also consider caching per order in the extension.
  • Operations. CAP, Node.js and the libraries need regular upgrades. CAP's release notes announce supported Node.js versions; plan for them, as the Unit 6 setup describes.
  • Clean core. The extension reads S/4HANA through a released API and stores its own data on BTP. Nothing in S/4HANA changes.

Pitfalls

  • Copying the whole remote entity into your service. Expose a consumption view with the fields you need, in your own names.
  • Putting authorization in if statements only. Use @requires and @restrict first; code checks are for what annotations can't express.
  • Forgetting that != keeps empty values. In our test, a filter on "not empty text" still returned the order with no block from the mock, which sends a missing value. Test filters against both.
  • Hard-coding block codes. Codes and texts differ per system. Read them from the system.
  • Calling the model in a READ handler. Listing 50 orders would make 50 paid calls. Keep AI behind an explicit action.
  • Trusting the mock too much. Real APIs bring paging, slow responses, authorization errors and CSRF tokens on writes. Test against a sandbox or test system before go-live.
  • Starting cds watch in the wrong folder. It serves whatever project it finds. Run it in unit06/order-assist.
  • Leaving the AI URL pointing at your laptop. 127.0.0.1 means "this computer". On BTP the AI service has its own address, set per environment.

Exercise: let clerks rate the AI's answers

You will add a rate action so a clerk can mark an explanation as helpful or not, with a short comment. Only the clerk who asked may rate it. These ratings become test data for the evaluation harness in Unit 8.

  1. Open unit06/order-assist/db/schema.cds. Below the requestId line, add two fields and save:

      helpful     : Boolean;       // the clerk's rating
      comment     : String(200);
  2. Open srv/assist-service.cds. Change the Explanations part at the end so it reads as follows, and save. The new grant: 'rate' line lets clerks call the action; the handler checks who wrote the answer.

      // Clerks see their own explanations; managers see all of them.
      @readonly
      @(restrict: [
        { grant: 'READ', to: 'OrderClerk', where: 'createdBy = $user' },
        { grant: 'READ', to: 'OrderManager' },
        { grant: 'rate', to: 'OrderClerk' }
      ])
      entity Explanations as projection on my.Explanations
      actions {
        action rate(helpful : Boolean, comment : String(200)) returns Explanations;
      };
    }
  3. Open srv/assist-service.js. Just above the line return super.init(), add this handler and save:

        // Exercise: a clerk rates one of their own explanations.
        this.on('rate', Explanations, async req => {
          const [key] = req.params
          const ID = typeof key === 'object' ? key.ID : key
          const row = await SELECT.one.from(Explanations, ID)
          if (!row) return req.reject(404, 'No such explanation')
          if (row.createdBy !== req.user.id) return req.reject(403, 'You can only rate your own explanations')
          await UPDATE(Explanations, ID).with({ helpful: req.data.helpful, comment: req.data.comment ?? null })
          return SELECT.one.from(Explanations, ID)
        })
  4. Open unit06/test_order_assist.py. Just above the line that starts with print(f"\n{sum(results)}, add these lines (indented like the lines around them) and save:

        status, _ = call(f"/Explanations({answer.get('ID')})/rate", "sam", "POST", {"helpful": True})
        results.append(check("a clerk can't rate someone else's answer -> 403", status == 403, status))
        status, rated = call(f"/Explanations({answer.get('ID')})/rate", "clara", "POST", {"helpful": False, "comment": "Wrong reason"})
        results.append(check("clerk rates her own answer", status == 200 and (rated or {}).get("helpful") is False, status))
  5. Start cds watch in unit06/order-assist (it may have restarted on its own), then run the tests from the course folder:

    python unit06/test_order_assist.py

    You should see 11 of 11 passed.

  6. In your browser, sign in as maria and open http://localhost:4004/odata/v4/assist/Explanations?$filter=helpful%20eq%20false. You see the answer clara marked as unhelpful, with her comment. If the browser remembers an earlier sign-in, open a private window first.

  7. In unit06, create extension_design.md with three short sections: Roles (each role and what it may do), Data stored (each field in Explanations and why), and APIs used (the S/4HANA API and the fields read). This goes into the solution design document later in this unit.

  8. Commit your work from the course folder:

    git add unit06/order-assist unit06/test_order_assist.py unit06/extension_design.md
    git commit -m "Add the order-assist CAP extension with roles, ratings and tests"

    git status should not list node_modules or .env.

Done when: python unit06/test_order_assist.py prints 11 of 11 passed, maria can filter the unhelpful answers in the browser, and extension_design.md is committed with all three sections filled in.

Check yourself

Pick one answer for each question. The explanation appears after you choose.
  1. 1In the order-assist design, which job does NOT belong in the CAP extension?

    Answer: C. The extension translates, checks roles, calls the AI safely and remembers. Business rules for releasing an order stay in S/4HANA, the system of record. If the extension takes on that job, the core and the extension start to disagree.
  2. 2What does CAP do with an imported remote service like API_SALES_ORDER_SRV during development?

    Answer: B. cds watch mocks imported services from files in srv/external/data. Your handler calls cds.connect.to('API_SALES_ORDER_SRV') and can't tell mock from real, so the same code later works against a destination.
  3. 3Why does the topic expose BlockedOrders as a consumption view instead of the whole A_SalesOrder entity?

    Answer: D. A consumption view keeps S/4HANA's names in one file and shows callers only what they need. CAP translates names back, so a filter on deliveryBlock reached the mock as DeliveryBlockReason.
  4. 4victor, who has no roles, calls the explain action. What happens, and where is that decided?

    Answer: B. victor is signed in, so it is not 401, and he can read orders, so not 404. The @(requires: 'OrderClerk') annotation makes CAP refuse him with 403 before the handler runs, which keeps the rule visible in the model.
  5. 5How does the extension make sure a clerk only sees their own explanations?

    Answer: C. The where: 'createdBy = $user' condition in @restrict is an instance-based rule. CAP adds it to every query, and createdBy is filled by the managed aspect from the signed-in user.
  6. 6The AI API is down when clara asks about order 4713. What does the extension return?

    Answer: D. askAiApi has an 8-second timeout, and the handler catches any failure. It stores and returns ruleAnswer(order, 'fallback') and logs why, so the clerk still gets a labeled answer.
  7. 7A colleague wants the extension to call the model in the READ handler, so every listed order shows an explanation. What would you advise?

    Answer: B. A READ handler runs for every list request, so 50 orders could mean 50 paid model calls and a slow screen. An explicit action keeps cost and control in the user's hands.
  8. 8Your extension reads S/4HANA through one technical user for all clerks. What is the main risk?

    Answer: D. With one technical user, S/4HANA only sees that user, so every clerk can reach everything it can. CAP roles control the extension, but S/4HANA authorizations need the real user's identity, which Unit 7 covers.

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