Orchestrate

Report an issue

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

0 / 2000

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

AI in S/4HANA migration projects

Where AI speeds up a move to S/4HANA (custom code, data, testing, documentation, cutover), what SAP offers in November 2026, and how people stay in charge.

Updated Nov 1, 2026Foundational 9 minDeep 40 min
Foundational layer · 9 min read

The 60-second version

Moving from SAP ECC to S/4HANA is mostly hard, repetitive work: checking thousands of custom programs, cleaning and mapping years of data, writing and running test cases, and planning a weekend cutover. Most of that work is reading, comparing and drafting. AI is good at drafting.

As of November 2026, SAP sells AI for most of these jobs. It has a custom code migration agent that finds and fixes code problems, and assistants for data quality, testing and configuration that early adopters are using.

The catch is that a migration is where confident mistakes are most expensive. A wrong code fix that "looks right" goes live with everything else. So the rule for leaders is simple: AI drafts, people decide, and every decision leaves a record. Measure what the AI saved, and keep the checks that existed before it.

Why it matters to the business

Take the running example. The fictional Calder Pumpworks is moving its German company CP20 from ECC into CP10's S/4HANA system, with go-live planned for 5 July 2027 (Data migration strategy). Before then, the project team must:

  • Assess every custom program in CP20's ECC system. Some are no longer used and can be retired. Some read tables that S/4HANA hides behind new views. A few write directly into SAP tables, which must be redesigned.
  • Clean and map the data: customers, suppliers, products and open items, through several mock loads.
  • Test the processes end to end on migrated data: order-to-cash, procure-to-pay, intercompany.
  • Document what the old system does, because the people who wrote some of the code have left.
  • Plan and run the cutover, the weekend when the old system stops and the new one starts.

Each of these costs consultant days, and consultant days drive the budget. AI that drafts a code fix, a test case or a cleansing rule saves time only if checking the draft is faster than writing it. That depends on how good the draft is and how easy it is to check.

The risk side is just as real:

  • Confident wrong changes. An AI fix can compile and still change what the program does, for example by dropping a filter so a report sums the wrong orders.
  • Gaps in testing. AI-generated tests can confirm what the code does today, mistakes included, rather than what the business needs.
  • No audit trail. Auditors and the business owners who sign off the migration need to know who approved each change and why.

How SAP does it

As of November 2026, SAP's AI for migration projects comes in three groups. Availability changes quickly, so check each item for your contract and release.

1. The custom code migration agent. SAP's Q2 2026 release highlights list the SAP S/4HANA custom code migration agent as generally available, delivered in Joule for developers (ABAP AI). It runs SAP's readiness checks across custom code packages, applies standard quick fixes, and proposes AI changes with a confidence score. High-confidence fixes are applied, lower-confidence ones become comments for a developer, and all changes are recorded in transport requests (SAP News, July 2026). SAP said in August 2026 that a second agent, for clean core transformation, was planned for Q3 2026 (SAP News, August 2026). We found no general availability announcement for it by 1 November 2026.

2. Migration and modernization assistants. On 28 October 2026 SAP announced four assistants available to early adopters: a Data Management Assistant (ranks data quality issues by business impact and helps correct them), a Test Management Assistant (test scope, critical scenarios, test cases and test data, with execution in SAP Test Automation by Tricentis), a Custom Code Assistant and a Business Configuration Assistant. A System Analysis, a Project Management and a Rollout Assistant are planned, without dates (SAP News, October 2026). The announcement gives no pricing.

3. Explanation and guidance. Joule for developers offers ABAP Program Explain, which explains the business purpose of legacy code, and Issue Explain, which helps resolve S/4HANA-related code check findings (sap.com). Joule for Consultants answers questions grounded in SAP Notes and knowledge base articles and explains ABAP and CDS logic (sap.com).

Under all of these sit SAP's long-standing, non-AI tools: the ABAP Test Cockpit (ATC) with its S/4HANA readiness checks, the Simplification Database, and SAP's published list of released objects and their successors. The AI works on their findings. It doesn't replace them.

Licensing is unsettled. SAP's Joule for developers page describes a free access offer for developers using ABAP Development Tools. SAP News (August 2026) says developer AI billing is moving to usage-based AI units. Get the terms for your contract in writing.

Where AI helps across the migration

Workstream What AI does well SAP offering (November 2026) What a person still decides
Custom code scoping Summarises usage data and groups programs System Analysis Assistant (planned) Which programs to retire; the business owner signs
Custom code remediation Explains findings, drafts fixes, applies standard quick fixes Custom code migration agent (GA); Custom Code Assistant (early adopters) Every change that alters behaviour; anything writing to SAP tables
Data cleansing and mapping Finds duplicates and gaps, ranks issues, suggests value mappings Data Management Assistant (early adopters) Which records merge, which are excluded, mapping sign-off
Testing Drafts test scope, cases and test data from process descriptions Test Management Assistant (early adopters); Tricentis test generation Which tests prove the business works; test sign-off
Legacy documentation Explains what old programs do in business words ABAP Program Explain; Joule for Consultants Whether the explanation is right (ask the code's owner)
Configuration Compares source settings to the target, flags deviations Business Configuration Assistant (early adopters) Every configuration change, with an audit trail
Cutover planning Summarises status, flags risks in task lists Project Management Assistant (planned) The plan, the go/no-go call, the fallback deadline

Questions to ask

Ask your partner, SAP or your project team:

  1. Which of these tools are generally available for our release, and which are early adopter or planned? Get it in writing.
  2. What does the custom code agent change automatically? Which confidence threshold applies, and can we switch automatic changes off?
  3. Who reviews AI-made code changes before a transport is released? Where is that recorded?
  4. How is the saving measured? Days per object before and after, on comparable objects, not a vendor's percentage.
  5. What does it cost? AI units, partner fees, and the review time it adds.
  6. Where does our source code and data go? Which model provider sees it, in which region, under which terms?
  7. How do we test that nothing changed? Which comparison tests run on remediated programs?

Common misconceptions

  • "The agent fixed it, so it's done." A fix that passes the code checks can still change what the program does. Only a comparison test and a reviewer prove it didn't.
  • "Migrate everything, then clean up." Unused programs cost money to fix. Retire them first.
  • "AI-written tests cover us." Tests drafted from the old system's behaviour confirm the old behaviour. The business decides what correct looks like.
  • "Explanations of old code are facts." They are drafts. Check them with someone who knows the process.
  • "Early adopter means ready for our project." Early adopter programmes are for a limited group of customers. Plan without them unless you are in one.

Key terms

All course terms, A to Z, in the glossary

  • Custom code: programs and objects a company wrote itself, usually with names starting with Z or Y.
  • ABAP Test Cockpit (ATC): SAP's tool that runs code checks, including S/4HANA readiness checks.
  • Simplification item: a documented change in S/4HANA that can break old code or processes.
  • Released API: an SAP object that SAP promises to keep stable across upgrades.
  • Clean core levels: SAP's scale from A (released interfaces only) to D (modifications or writing to SAP tables).
  • Remediation: changing custom code so it works, and stays supportable, on the new system.
  • Cutover: the planned period when the old system stops and the new one takes over.
  • Early adopter: a limited SAP programme that gives selected customers access before general availability.

Check yourself

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

1What is the safest rule for AI in a migration project?

Answer: B. A migration is where confident mistakes are expensive, because they go live together. AI saves time on drafting, while people approve changes and the record shows who decided what.

2Calder finds a custom report that hasn't run in 14 months. What should happen first?

Answer: D. Unused code costs money to fix and test. Scoping comes before remediation, and a person who owns the process agrees to retire it.

3SAP's custom code migration agent applies high-confidence fixes automatically. What should a leader ask?

Answer: B. SAP says the agent records its changes in transport requests. The control point is the review and testing before those transports move on.

4Which SAP AI offering for migration was generally available as of November 2026?

Answer: B. SAP's Q2 2026 highlights list the custom code migration agent as generally available. The Project Management, System Analysis and Rollout Assistants were announced as planned, without dates.

5A partner shows AI-generated test cases for order-to-cash. What is the main risk?

Answer: C. Tests drafted from how the old system behaves prove that behaviour, not what the business needs. Functional consultants review each case against the process.

6How should a project prove that AI saved time on custom code?

Answer: A. A saving is a measured difference on comparable work. Vendor figures and impressions don't show what happened on your project, and a count of fixes ignores the review time they caused.

7A program writes directly into an SAP pricing table. What does SAP's clean core model say?

Answer: D. SAP's level D covers modifications and write access to SAP tables. Such code needs a redesign by a person, not an automatic fix.
Deep layer · 40 min read

Mental model

A migration is a funnel of decisions, and AI changes the cost of each one.

Every custom object, data record and test case passes the same three gates: keep or drop, change or leave, accept or reject. Before AI, people did the analysis and the drafting for each gate. Now an agent can draft most of it. What stays human is the decision at each gate and the evidence behind it.

So design your use of AI around two questions:

  1. How cheap is it to check the draft? A quick fix that swaps a table for its released successor is easy to check. A rewritten pricing routine is not.
  2. What happens if the draft is wrong and nobody notices? A wrong explanation wastes an hour. A wrong filter in a revenue report goes live.

When checking is cheap and the damage is small, let the tool act and review in batches. When either is high, the tool drafts and a named person approves each item. Everything in this topic follows from that.

How it works

flowchart LR
  U[Usage data] --> S[Scope:<br/>keep or retire]
  S --> A[Analyze:<br/>ATC + rule list]
  A --> R[Remediate:<br/>quick fix or AI draft]
  R --> V[Verify:<br/>re-check + tests]
  V --> H{Reviewer<br/>approves?}
  H -->|no| R
  H -->|yes| T[Transport to<br/>quality system]
  T --> C[Comparison test]

Scoping: usage first

SAP's conversion guidance starts with custom code scoping: find out which custom code is actually used and must move, and remove what isn't (SAP Learning). This is the cheapest saving in the whole project, and it needs no AI, only usage data and a business owner willing to say "drop it". CP20's report ZMM_OPEN_PO_AGING, with zero runs in 14 months, is the course example (Fit to standard).

Analysis: the checks AI builds on

The same SAP Learning page describes the analysis setup. A central check system (SAP_BASIS 7.52 or later, or S/4HANA 1809 or later) runs remote ATC against the old system with the check variant S4HANA_READINESS_REMOTE. The checks use the Simplification Database, downloaded through SAP Note 2241080 and imported with transaction SYCM. Findings point to SAP Notes that describe the fix.

A second source of rules is SAP's Cloudification Repository on GitHub. It lists objects with a state such as released, notToBeReleased or classicAPI, and a successor where one exists. The README says the ATC check that uses it can analyse custom code "on all ECC and S/4HANA releases" (GitHub). In the file for the latest Private Edition release, read on 1 November 2026, table VBAK is notToBeReleased with successor I_SALESDOCUMENT, and KNA1 points to I_CUSTOMER. The classifications file lists BAPI_SALESORDER_CHANGE as a classicAPI.

Map these states to SAP's clean core levels (SAP News, August 2025):

What the code does Example in CP20 Level Typical action
Uses a released API BAPI_TRANSACTION_COMMIT, I_SALESDOCUMENT A Keep
Uses a classic API BAPI_SALESORDER_CHANGE, POPUP_TO_CONFIRM B Keep in Private Edition; plan a successor
Reads an internal object SELECT from VBAK, KNA1, EKPO C Switch to the successor view
Writes to an SAP table UPDATE konv D Redesign; never auto-fix

Remediation: deterministic first, AI second

SAP's description of its custom code migration agent shows the right order. Deterministic quick fixes come first: rule-based changes that always produce the same result. AI changes come second, each with a confidence score; SAP's agent applies high-confidence changes and leaves the rest as comments for a developer (SAP News, July 2026).

The confidence score is the model's estimate, not proof. A fix can be confident, compile cleanly, pass every clean core check and still change the result. That's why verification has two parts:

  • Re-run the checks on the changed code. This proves the level, not the behaviour.
  • Compare behaviour. Run the old and new program on the same input in a test system and compare the output.

Review: a checklist, not a feeling

A reviewer facing fifty AI diffs a day needs a short, fixed checklist that targets what AI gets wrong in migrations: changed filters, invented field names, lost currency or unit, dropped authorization checks. A checklist also gives the audit trail something concrete to record.

Beyond code

The same pattern applies to the other workstreams:

  • Data: an assistant ranks issues and suggests merges or mappings; a data owner decides each one, and the mock load and reconciliation prove the result (Data migration strategy).
  • Testing: an assistant drafts scope and cases; a process owner accepts them; automated runs prove the system (Testing AI applications covers testing the AI itself, which is a separate job).
  • Documentation: an explanation tool drafts what a program does; someone who knows the process confirms it.

Build it yourself

Before you start: complete Set up your computer for this course and Set up for Unit 13. This walkthrough doesn't repeat those steps.

You will build a small custom code migration assistant for four of CP20's ECC programs. It scopes them by usage, classifies every object they use against SAP's rule list, lets an assistant draft a fix, checks the draft, and refuses to accept it until a person has worked through a review checklist. The point is to see where the human gate sits and why it catches what the checks miss.

flowchart LR
  P[4 legacy programs] --> S[scan]
  R[rules.json<br/>from SAP's repository] --> S
  S --> X[propose<br/>sample, model or file]
  X --> V[scan again + diff]
  V --> C[review checklist]
  C --> A[approve<br/>named reviewer]
  A --> L[decisions.jsonl]

What you need

  • The course folder orchestrate-course with its .venv, from the setup topics.
  • For the free path (--sample): nothing else. A scripted assistant makes the proposal.
  • For the model path (--llm, optional): the ANTHROPIC_API_KEY in your .env from Unit 1, and the anthropic and python-dotenv libraries already in your requirements.txt. One call per proposal, a small per-request charge.
  • Network only for the optional refresh-rules step, which downloads about 14 MB from GitHub.
  • Time: about 45 minutes. Cost: free on the sample path.

All programs, usage numbers and order types are made up for Calder, a fictional company. Nothing connects to an SAP system.

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

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

  2. Open a terminal: Terminal > New Terminal.

  3. Turn on the virtual environment:

    • Windows (PowerShell):

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

      source .venv/bin/activate
  4. You should see (.venv) at the start of the prompt. The unit13 folder exists from Set up for Unit 13. If it doesn't, create it with mkdir unit13 (the same command works in both terminals).

Run every command from the course folder, not from inside unit13.

Step 2: Create the script

  1. In VS Code's Explorer, right-click unit13 and choose New File. Name it ccm_assist.py.
  2. Paste in the code below and save.
"""Custom code migration assistant for Calder's CP20 move, with a human review gate.

It shows where AI helps a migration and where people must decide:
  scope    usage data decides what to migrate and what to retire
  scan     a rule list from SAP's Cloudification Repository classifies every object each program uses
  propose  an assistant (scripted or a real model) drafts a remediation; the scan runs again on it
  approve  a person ticks a review checklist; only then is the proposal accepted

Commands (run from your course folder):
  python unit13/ccm_assist.py init                      create the demo in unit13/ccm_demo
  python unit13/ccm_assist.py scan                      scope and clean core findings for every program
  python unit13/ccm_assist.py propose ZSD_OPEN_ORDER_VALUE --sample   scripted proposal (free)
  python unit13/ccm_assist.py propose ZSD_OPEN_ORDER_VALUE --llm      a real model proposes (small charge)
  python unit13/ccm_assist.py propose ZMM_STOCK_CHECK --file my_fix.abap      your own remediation
  python unit13/ccm_assist.py approve ZSD_OPEN_ORDER_VALUE --reviewer "your name"
  python unit13/ccm_assist.py refresh-rules             rebuild rules.json from SAP's files on GitHub

Nothing here connects to an SAP system. The original programs in src/ are never changed.
"""
import argparse
import difflib
import json
import os
import re
import sys
import urllib.request
from datetime import datetime, timezone
from pathlib import Path

HERE = Path(__file__).resolve().parent
DEMO = HERE / "ccm_demo"
SRC = DEMO / "src"
REVIEW = DEMO / "review"
DONE = DEMO / "remediated"
RULES = DEMO / "rules.json"
DECISIONS = DEMO / "decisions.jsonl"
REPO = "https://raw.githubusercontent.com/SAP/abap-atc-cr-cv-s4hc/main/src/"
RELEASE_FILE = "objectReleaseInfo_PCELatest.json"   # released and not-released objects, with successors
CLASSIC_FILE = "objectClassifications_SAP.json"     # classic APIs (clean core level B)

# ---------- Calder's sample: four CP20 programs from ECC (fictional) ----------

PROGRAMS = {
    "ZSD_OPEN_ORDER_VALUE": """REPORT zsd_open_order_value.
* Lists a customer's standard orders (type ZOR) with their net value.
PARAMETERS p_kunnr TYPE kunnr OBLIGATORY.
DATA: lt_vbak TYPE STANDARD TABLE OF vbak,
      ls_vbak TYPE vbak,
      ls_kna1 TYPE kna1,
      lv_sum  TYPE netwr.
SELECT SINGLE * FROM kna1 INTO ls_kna1 WHERE kunnr = p_kunnr.
SELECT * FROM vbak INTO TABLE lt_vbak
  WHERE kunnr = p_kunnr AND auart = 'ZOR'.
LOOP AT lt_vbak INTO ls_vbak.
  lv_sum = lv_sum + ls_vbak-netwr.
  WRITE: / ls_vbak-vbeln, ls_vbak-netwr, ls_vbak-waerk.
ENDLOOP.
WRITE: / 'Total for', ls_kna1-name1, lv_sum.
""",
    "ZMM_OPEN_PO_AGING": """REPORT zmm_open_po_aging.
* Ages open purchase order items by delivery date.
DATA lt_items TYPE STANDARD TABLE OF ekpo.
SELECT * FROM ekpo INTO TABLE lt_items WHERE elikz = space.
SELECT SINGLE * FROM ekko INTO @DATA(ls_ekko) WHERE ebeln = '4500001201'.
CALL FUNCTION 'REUSE_ALV_GRID_DISPLAY'
  TABLES t_outtab = lt_items.
""",
    "ZSD_PRICE_FIX": """REPORT zsd_price_fix.
* Corrects a condition amount directly in the pricing table.
PARAMETERS: p_knumv TYPE knumv OBLIGATORY,
            p_kbetr TYPE kbetr.
UPDATE konv SET kbetr = p_kbetr WHERE knumv = p_knumv AND kschl = 'ZPR0'.
COMMIT WORK.
""",
    "ZSD_ORDER_REASON_UPD": """REPORT zsd_order_reason_upd.
* Sets the order reason on a sales order after a phone call.
PARAMETERS: p_vbeln TYPE vbeln_va OBLIGATORY,
            p_augru TYPE augru.
DATA: ls_hdr  TYPE bapisdh1,
      ls_hdrx TYPE bapisdh1x,
      lt_ret  TYPE STANDARD TABLE OF bapiret2.
DATA lv_ok TYPE c LENGTH 1.
CALL FUNCTION 'POPUP_TO_CONFIRM'
  EXPORTING text_question = 'Change the order reason?'
  IMPORTING answer        = lv_ok.
CHECK lv_ok = '1'.
ls_hdr-ord_reason = p_augru.
ls_hdrx-updateflag = 'U'.
ls_hdrx-ord_reason = 'X'.
CALL FUNCTION 'BAPI_SALESORDER_CHANGE'
  EXPORTING salesdocument    = p_vbeln
            order_header_in  = ls_hdr
            order_header_inx = ls_hdrx
  TABLES    return           = lt_ret.
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'.
""",
}

# Runs in the last 14 months, as usage monitoring in ECC would report them (course values).
USAGE = {"ZSD_OPEN_ORDER_VALUE": 412, "ZMM_OPEN_PO_AGING": 0,
         "ZSD_PRICE_FIX": 9, "ZSD_ORDER_REASON_UPD": 57}

# The entries this demo needs, copied from SAP's Cloudification Repository on 1 November 2026
# (objectReleaseInfo_PCELatest.json and objectClassifications_SAP.json). refresh-rules rebuilds them.
BUNDLED_RULES = {
    "VBAK": {"state": "notToBeReleased", "successors": ["I_SALESDOCUMENT"]},
    "VBAP": {"state": "notToBeReleased", "successors": ["I_SALESDOCUMENTITEM"]},
    "KNA1": {"state": "notToBeReleased", "successors": ["I_CUSTOMER"]},
    "KONV": {"state": "notToBeReleased", "successors": ["I_SLSPRCGCONDITIONRECORD"]},
    "EKKO": {"state": "notToBeReleased", "successors": ["I_PURCHASEORDERAPI01"]},
    "EKPO": {"state": "notToBeReleased", "successors": ["I_PURCHASEORDERITEMAPI01"]},
    "MARD": {"state": "notToBeReleased", "successors": ["I_PRODUCTSTORAGELOCATIONBASIC"]},
    "I_SALESDOCUMENT": {"state": "released", "successors": []},
    "I_SALESDOCUMENTITEM": {"state": "released", "successors": []},
    "I_CUSTOMER": {"state": "released", "successors": []},
    "I_SLSPRCGCONDITIONRECORD": {"state": "released", "successors": []},
    "I_PURCHASEORDERAPI01": {"state": "released", "successors": []},
    "I_PURCHASEORDERITEMAPI01": {"state": "released", "successors": []},
    "I_PRODUCTSTORAGELOCATIONBASIC": {"state": "released", "successors": []},
    "BAPI_TRANSACTION_COMMIT": {"state": "released", "successors": []},
    "BAPI_SALESORDER_CHANGE": {"state": "classicAPI", "successors": []},
    "POPUP_TO_CONFIRM": {"state": "classicAPI", "successors": []},
    "CL_SALV_TABLE": {"state": "classicAPI", "successors": []},
}

CHECKLIST = [
    "Selection is the same as before: same filters, same values, same records",
    "Every field used on the successor view exists there (checked in ADT, not taken from the proposal)",
    "Amounts and quantities keep their currency and unit",
    "Authorization checks are still in place (the old code's checks, or the view's access control)",
    "Output matches the original on a test run with the same input in the quality system",
    "No write to an SAP table, no modification, no implicit enhancement",
]

# ---------- scanning ----------

READ = re.compile(r"\b(?:FROM|JOIN)\s+@?(\w+)", re.IGNORECASE)
WRITE = re.compile(r"^\s*(?:UPDATE|INSERT\s+INTO|INSERT|MODIFY|DELETE\s+FROM)\s+(\w+)", re.IGNORECASE | re.MULTILINE)
DECLARED = re.compile(r"(?:DATA|PARAMETERS)[:(]?\s*(\w+)|^\s*,?\s*(\w+)\s+TYPE\b", re.IGNORECASE | re.MULTILINE)
CALL = re.compile(r"CALL\s+FUNCTION\s+'(\w+)'", re.IGNORECASE)
CLASS = re.compile(r"\b(\w+)=>|NEW\s+(\w+)\(|TYPE\s+REF\s+TO\s+(\w+)", re.IGNORECASE)
LEVEL_TEXT = {"A": "released API", "B": "classic API", "C": "reads an internal object",
              "D": "writes to an SAP table", "?": "not in the rule list"}


def strip_comments(source: str) -> str:
    lines = [ln for ln in source.splitlines() if not ln.lstrip().startswith("*")]
    return "\n".join(re.sub(r'".*$', "", ln) for ln in lines)


def used_objects(source: str) -> list:
    """Objects a program uses, as (name, how). Simple patterns, not an ABAP parser."""
    code = strip_comments(source)
    found = []
    local = {next(g for g in m.groups() if g).upper() for m in DECLARED.finditer(code)}
    writes = {m.group(1).upper() for m in WRITE.finditer(code)} - local  # internal tables are not SAP tables
    for name in sorted(writes):
        found.append((name, "write"))
    for m in READ.finditer(code):
        name = m.group(1).upper()
        if name not in writes | local and (name, "read") not in found:
            found.append((name, "read"))
    for m in CALL.finditer(code):
        if (m.group(1).upper(), "call") not in found:
            found.append((m.group(1).upper(), "call"))
    for m in CLASS.finditer(code):
        name = next(g for g in m.groups() if g).upper()
        if name not in ("ME", "SUPER") and (name, "class") not in found:
            found.append((name, "class"))
    return found


def classify(name: str, how: str, rules: dict) -> dict:
    """Clean core level for one use of one object, following SAP's A-D model."""
    rule = rules.get(name, {})
    state, successors = rule.get("state"), rule.get("successors", [])
    custom = name.startswith(("Z", "Y"))
    if how == "write" and not custom:
        level, action = "D", "redesign with a person: never auto-fix"
    elif custom:
        level, action = "A", "own object: check it separately"
    elif state == "released":
        level, action = "A", "keep"
    elif state == "classicAPI":
        level, action = "B", "keep for now; allowed in Private Edition, plan a released successor"
    elif state in ("notToBeReleased", "notToBeReleasedStable", "deprecated"):
        level = "C"
        action = ("use " + ", ".join(successors)) if successors else "no successor listed: ask SAP"
    else:
        level, action = "?", "check ATC and the simplification items; a person decides"
    return {"object": name, "use": how, "level": level, "action": action}


def scan_source(source: str, rules: dict) -> list:
    return [classify(name, how, rules) for name, how in used_objects(source)]


def worst(findings: list) -> str:
    order = "AB?CD"
    return max((f["level"] for f in findings), key=order.index, default="A")


# ---------- helpers ----------

def need_demo() -> dict:
    if not RULES.exists():
        sys.exit("No demo yet. Run first: python unit13/ccm_assist.py init")
    return json.loads(RULES.read_text(encoding="utf-8"))["objects"]


def log_decision(**entry) -> None:
    entry = {"time": datetime.now(timezone.utc).isoformat(timespec="seconds"), **entry}
    with DECISIONS.open("a", encoding="utf-8") as f:
        f.write(json.dumps(entry) + "\n")


def scope(name: str) -> str:
    runs = USAGE.get(name)
    if runs is None:
        return "no usage data: find out before migrating"
    return "retire (not used)" if runs == 0 else f"migrate ({runs} runs)"


# ---------- the assistant ----------

SAMPLE_PROPOSAL = {
    "ZSD_OPEN_ORDER_VALUE": {
        "explanation": "Replaced KNA1 with I_Customer and VBAK with I_SalesDocument, used inline "
                       "declarations, and kept the list output.",
        "content": """REPORT zsd_open_order_value.
* Lists a customer's orders with their net value.
PARAMETERS p_kunnr TYPE kunnr OBLIGATORY.
SELECT SINGLE CustomerName FROM i_customer
  WHERE Customer = @p_kunnr INTO @DATA(lv_name).
SELECT SalesDocument, TotalNetAmount, TransactionCurrency FROM i_salesdocument
  WHERE SoldToParty = @p_kunnr
  INTO TABLE @DATA(lt_orders).
DATA lv_sum TYPE netwr.
LOOP AT lt_orders INTO DATA(ls_order).
  lv_sum = lv_sum + ls_order-TotalNetAmount.
  WRITE: / ls_order-SalesDocument, ls_order-TotalNetAmount, ls_order-TransactionCurrency.
ENDLOOP.
WRITE: / 'Total for', lv_name, lv_sum.
"""},
}

SYSTEM = """You remediate classic ABAP custom code for a move from SAP ECC to SAP S/4HANA Cloud Private Edition.
Rules:
- Replace reads of internal objects with the successor named in the findings. Keep every filter and value exactly.
- Keep classic APIs (level B) unless the findings name a successor.
- Never write to SAP tables. Never invent fields: if unsure of a field name, keep the old code and say so.
- Change nothing else. Reply with JSON only: {"explanation": "two sentences", "content": "the complete new program"}"""


def llm_proposal(model: str, name: str, source: str, findings: list) -> dict:
    try:
        from anthropic import Anthropic  # imported here so --sample works without the library
    except ImportError:
        sys.exit("The anthropic library is missing. Run: pip install -r requirements.txt")
    prompt = (f"Program {name}:\n<code>\n{source}</code>\n\nFindings:\n"
              + "\n".join(f"{f['level']} {f['object']} ({f['use']}): {f['action']}" for f in findings))
    try:
        reply = Anthropic().messages.create(model=model, max_tokens=3000, system=SYSTEM,
                                            messages=[{"role": "user", "content": prompt}])
    except Exception as err:  # wrong key, wrong model name, network blocked
        sys.exit(f"The model call failed: {type(err).__name__}: {err}")
    text = "".join(b.text for b in reply.content if b.type == "text")
    try:
        data = json.loads(text[text.index("{"):text.rindex("}") + 1])
        return {"explanation": str(data["explanation"]), "content": str(data["content"])}
    except (ValueError, KeyError) as err:
        sys.exit(f"The model's reply could not be read ({err}). Run propose again.")


# ---------- commands ----------

def cmd_init(args) -> None:
    if DEMO.exists() and not args.force:
        sys.exit(f"{DEMO.name} already exists. Add --force to start over.")
    for folder in (SRC, REVIEW, DONE):
        folder.mkdir(parents=True, exist_ok=True)
        for old in folder.glob("*"):
            old.unlink()
    for name, code in PROGRAMS.items():
        (SRC / f"{name.lower()}.prog.abap").write_text(code, encoding="utf-8")
    RULES.write_text(json.dumps({"source": "bundled copy, 2026-11-01", "objects": BUNDLED_RULES},
                                indent=1) + "\n", encoding="utf-8")
    DECISIONS.unlink(missing_ok=True)
    print(f"Wrote {len(PROGRAMS)} programs to unit13/ccm_demo/src and the rule list to unit13/ccm_demo/rules.json")
    print("Next: python unit13/ccm_assist.py scan")


def cmd_scan(args) -> None:
    rules = need_demo()
    for path in sorted(SRC.glob("*.abap")):
        name = path.name.split(".")[0].upper()
        findings = scan_source(path.read_text(encoding="utf-8"), rules)
        print(f"\n{name}  scope: {scope(name)}  worst level: {worst(findings)}")
        for f in findings:
            print(f"  {f['level']}  {f['use']:<5} {f['object']:<24} {f['action']}")
    print("\nLevels: A released API, B classic API, C reads an internal object, "
          "D writes to an SAP table, ? not in the rule list")


def cmd_propose(args) -> None:
    rules = need_demo()
    name = args.program.upper()
    path = SRC / f"{name.lower()}.prog.abap"
    if not path.exists():
        sys.exit(f"No program {name} in unit13/ccm_demo/src.")
    old = path.read_text(encoding="utf-8")
    findings = scan_source(old, rules)
    if USAGE.get(name) == 0:
        print(f"{name} has not run in 14 months. Retire it instead of migrating it.")
        log_decision(program=name, event="retire_suggested", runs=0)
        return
    if worst(findings) == "D":
        print(f"{name} writes to an SAP table (level D). The assistant does not propose a fix.")
        print("A person redesigns it, for example through a released API or a standard app.")
        log_decision(program=name, event="refused_level_d")
        return
    if worst(findings) in ("A", "B"):
        print(f"{name} is at level {worst(findings)}. No remediation needed; record the decision.")
        log_decision(program=name, event="no_change", level=worst(findings))
        return
    if args.llm:
        from dotenv import load_dotenv
        load_dotenv()
        if not os.getenv("ANTHROPIC_API_KEY"):
            sys.exit("ANTHROPIC_API_KEY is not set in .env (see Set up your computer). Or use --sample.")
        proposal = llm_proposal(args.model, name, old, findings)
    elif args.file:
        try:
            content = Path(args.file).read_text(encoding="utf-8")
        except OSError as err:
            sys.exit(f"Could not read {args.file}: {err}")
        proposal = {"explanation": f"Written by hand in {args.file}.", "content": content}
    elif args.sample and name in SAMPLE_PROPOSAL:
        proposal = SAMPLE_PROPOSAL[name]
    else:
        sys.exit(f"No scripted proposal for {name}. Use --llm, or --file with your own remediation.")
    after = scan_source(proposal["content"], rules)
    remaining = [f for f in after if f["level"] in ("C", "D", "?")]
    diff = "\n".join(difflib.unified_diff(old.splitlines(), proposal["content"].splitlines(),
                                          "before", "after", lineterm="", n=0))
    lines = [f"# Review: {name}", "", f"Assistant: {proposal['explanation']}", "",
             f"Scan after the change: worst level {worst(after)}"
             + ("" if not remaining else "; still open: " + ", ".join(f["object"] for f in remaining)),
             "", "```diff", diff, "```", "", "## Checklist: [x] checked, [-] deferred to the quality system test, [ ] open", ""]
    lines += [f"- [ ] {item}" for item in CHECKLIST]
    review = REVIEW / f"{name.lower()}.md"
    review.write_text("\n".join(lines) + "\n", encoding="utf-8")
    (REVIEW / f"{name.lower()}.prog.abap").write_text(proposal["content"], encoding="utf-8")
    log_decision(program=name, event="proposed", model=args.model if args.llm else ("file" if args.file else "sample"),
                 level_before=worst(findings), level_after=worst(after))
    print(diff)
    print(f"\nScan after the change: worst level {worst(after)} "
          f"({len(remaining)} open finding(s)). Field names are not checked by this scan.")
    print(f"Review file: unit13/ccm_demo/review/{review.name}")


def cmd_approve(args) -> None:
    rules = need_demo()
    name = args.program.upper()
    review = REVIEW / f"{name.lower()}.md"
    proposal = REVIEW / f"{name.lower()}.prog.abap"
    if not review.exists():
        sys.exit(f"No review file for {name}. Run propose first.")
    after = scan_source(proposal.read_text(encoding="utf-8"), rules)   # scan again: you may have edited it
    if worst(after) in ("C", "D", "?"):
        sys.exit(f"Not approved: the proposal is at level {worst(after)}. Fix it, then approve again.")
    text = review.read_text(encoding="utf-8")
    open_items = re.findall(r"^- \[ \] (.+)$", text, re.MULTILINE)
    deferred = re.findall(r"^- \[-\] (.+)$", text, re.MULTILINE)
    if open_items:
        print(f"Not approved: {len(open_items)} checklist item(s) still open:")
        for item in open_items:
            print("  -", item)
        log_decision(program=name, event="approval_refused", open_items=len(open_items), reviewer=args.reviewer)
        sys.exit(1)
    target = DONE / f"{name.lower()}.prog.abap"
    target.write_text(proposal.read_text(encoding="utf-8"), encoding="utf-8")
    log_decision(program=name, event="approved", reviewer=args.reviewer, level=worst(after), deferred=deferred)
    print(f"Approved by {args.reviewer} at level {worst(after)}. Saved to unit13/ccm_demo/remediated/{target.name}")
    if deferred:
        print(f"{len(deferred)} check(s) deferred to the quality system test:")
        for item in deferred:
            print("  -", item)


def cmd_refresh(args) -> None:
    rules = need_demo()
    names = set(rules)
    for path in SRC.glob("*.abap"):
        names |= {n for n, _ in used_objects(path.read_text(encoding="utf-8"))}
    fresh = {}
    for file in (RELEASE_FILE, CLASSIC_FILE):
        print("Downloading", file, "...")
        try:
            with urllib.request.urlopen(REPO + file, timeout=60) as resp:
                data = json.load(resp)
        except OSError as err:
            sys.exit(f"Download failed: {err}. The bundled rule list stays in place.")
        for item in data.get("objectReleaseInfo") or data.get("objectClassifications") or []:
            key = item.get("objectKey", "").upper()
            if key in names and key not in fresh:
                fresh[key] = {"state": item.get("state"),
                              "successors": [s["objectKey"] for s in item.get("successors", [])]}
    stamp = datetime.now(timezone.utc).date().isoformat()
    RULES.write_text(json.dumps({"source": f"SAP Cloudification Repository, {stamp}", "objects": fresh},
                                indent=1) + "\n", encoding="utf-8")
    changed = sorted(k for k in names if fresh.get(k) != rules.get(k))
    print(f"rules.json now holds {len(fresh)} objects. Changed since the bundled copy: "
          + (", ".join(changed) if changed else "none"))


def main() -> None:
    parser = argparse.ArgumentParser(description="Custom code migration assistant with a review gate.")
    sub = parser.add_subparsers(dest="command", required=True)
    p = sub.add_parser("init", help="create the demo")
    p.add_argument("--force", action="store_true", help="start over")
    sub.add_parser("scan", help="scope and clean core findings")
    p = sub.add_parser("propose", help="draft a remediation for one program")
    p.add_argument("program")
    mode = p.add_mutually_exclusive_group(required=True)
    mode.add_argument("--sample", action="store_true", help="scripted proposal, no key needed")
    mode.add_argument("--llm", action="store_true", help="a real model proposes (small charge)")
    mode.add_argument("--file", help="use a remediation you wrote yourself")
    p.add_argument("--model", default="claude-haiku-5-5", help="Anthropic model ID for --llm")
    p = sub.add_parser("approve", help="accept a reviewed proposal")
    p.add_argument("program")
    p.add_argument("--reviewer", required=True)
    sub.add_parser("refresh-rules", help="rebuild rules.json from SAP's files on GitHub")
    args = parser.parse_args()
    {"init": cmd_init, "scan": cmd_scan, "propose": cmd_propose, "approve": cmd_approve,
     "refresh-rules": cmd_refresh}[args.command](args)


if __name__ == "__main__":
    main()

Step 3: Create the demo

python unit13/ccm_assist.py init

You should see:

Wrote 4 programs to unit13/ccm_demo/src and the rule list to unit13/ccm_demo/rules.json
Next: python unit13/ccm_assist.py scan

Open unit13/ccm_demo/src/zsd_open_order_value.prog.abap. It is a classic ECC report: it reads customer master KNA1 and sales order headers VBAK directly, and sums the net value of a customer's orders of type ZOR (a Calder course label). Note the line AND auart = 'ZOR'. You'll need it in Step 6.

Step 4: Scan the programs

python unit13/ccm_assist.py scan

You should see:

ZMM_OPEN_PO_AGING  scope: retire (not used)  worst level: C
  C  read  EKPO                     use I_PURCHASEORDERITEMAPI01
  C  read  EKKO                     use I_PURCHASEORDERAPI01
  ?  call  REUSE_ALV_GRID_DISPLAY   check ATC and the simplification items; a person decides

ZSD_OPEN_ORDER_VALUE  scope: migrate (412 runs)  worst level: C
  C  read  KNA1                     use I_CUSTOMER
  C  read  VBAK                     use I_SALESDOCUMENT

ZSD_ORDER_REASON_UPD  scope: migrate (57 runs)  worst level: B
  B  call  POPUP_TO_CONFIRM         keep for now; allowed in Private Edition, plan a released successor
  B  call  BAPI_SALESORDER_CHANGE   keep for now; allowed in Private Edition, plan a released successor
  A  call  BAPI_TRANSACTION_COMMIT  keep

ZSD_PRICE_FIX  scope: migrate (9 runs)  worst level: D
  D  write KONV                     redesign with a person: never auto-fix

Levels: A released API, B classic API, C reads an internal object, D writes to an SAP table, ? not in the rule list

Read it as a project lead would:

  • ZMM_OPEN_PO_AGING hasn't run in 14 months. Retire it; don't spend a day fixing it.
  • ZSD_OPEN_ORDER_VALUE is used and reads two internal tables. That is a remediation candidate.
  • ZSD_ORDER_REASON_UPD uses classic APIs. Level B is allowed in Private Edition, so record the decision and move on.
  • ZSD_PRICE_FIX writes straight into the pricing table. No tool should fix that alone.
  • REUSE_ALV_GRID_DISPLAY is in neither of SAP's files used here, so the tool says ?. Unknown is not the same as fine: ATC and the simplification items decide.

Step 5: Let the assistant propose

  1. Try the two programs the assistant should not touch:

    python unit13/ccm_assist.py propose ZSD_PRICE_FIX --sample
    python unit13/ccm_assist.py propose ZMM_OPEN_PO_AGING --sample

    The first says the program writes to an SAP table and that a person redesigns it. The second says to retire it. Neither produces code.

  2. Now the real candidate:

    python unit13/ccm_assist.py propose ZSD_OPEN_ORDER_VALUE --sample

    You see a diff, then:

    Scan after the change: worst level A (0 open finding(s)). Field names are not checked by this scan.
    Review file: unit13/ccm_demo/review/zsd_open_order_value.md

By the checks, this is a perfect result: level C became level A. Don't stop there.

Step 6: Review it like a migration lead

  1. Open unit13/ccm_demo/review/zsd_open_order_value.md. It holds the assistant's explanation, the diff and a six-item checklist.

  2. Compare the selection. The old code had AND auart = 'ZOR'. The proposal's WHERE has only SoldToParty = @p_kunnr. The order type filter is gone, and so is "(type ZOR)" from the comment. The new report would sum every sales document for the customer, not just standard orders. The explanation didn't mention it, and every check passed.

  3. Try to approve anyway:

    python unit13/ccm_assist.py approve ZSD_OPEN_ORDER_VALUE --reviewer "Your Name"

    You should see Not approved: 6 checklist item(s) still open: and the six items. Nothing is accepted until you have worked through the list.

  4. Fix the proposal. Open unit13/ccm_demo/review/zsd_open_order_value.prog.abap and change the line WHERE SoldToParty = @p_kunnr to:

      WHERE SoldToParty = @p_kunnr AND SalesDocumentType = 'ZOR'

    Save. The field name SalesDocumentType is the kind of name an assistant suggests. You can't confirm it without an SAP system, which is exactly what checklist item 2 is for.

  5. Work through the checklist in the review file. Change [ ] to [x] for items you checked here: selection (item 1, now fixed), currency (item 3: the output still shows TransactionCurrency), and no table writes (item 6). Change [ ] to [-] for items 2, 4 and 5, which need a system: field names in ADT, authorization, and the comparison run in the quality system. Save.

  6. Approve:

    python unit13/ccm_assist.py approve ZSD_OPEN_ORDER_VALUE --reviewer "Your Name"

    You should see:

    Approved by Your Name at level A. Saved to unit13/ccm_demo/remediated/zsd_open_order_value.prog.abap
    3 check(s) deferred to the quality system test:
      - Every field used on the successor view exists there (checked in ADT, not taken from the proposal)
      - Authorization checks are still in place (the old code's checks, or the view's access control)
      - Output matches the original on a test run with the same input in the quality system

The approval scans your edited file again before accepting it, so an edit that reintroduced VBAK would be refused.

Step 7: Read the decision log

Open unit13/ccm_demo/decisions.jsonl. Each line is one event: retire_suggested, refused_level_d, proposed, approval_refused, approved. The approved line names you, the level and the deferred checks. That is the record an auditor or the business owner signing the migration asks for: what the tool proposed, what it refused, who accepted what, and what is still to be proven.

Step 8: Try a real model (optional, small charge)

  1. Start again: python unit13/ccm_assist.py init --force

  2. Run:

    python unit13/ccm_assist.py propose ZSD_OPEN_ORDER_VALUE --llm

The default model is claude-haiku-5-5; add --model <id> for another model your key supports. The system prompt tells the model to keep every filter. Read the diff anyway: answers vary from run to run, and some runs will keep the filter while others won't. If the model leaves an internal table in place, the scan after the change shows level C and approve refuses.

Step 9: Refresh the rule list from SAP (optional)

python unit13/ccm_assist.py refresh-rules

The script downloads objectReleaseInfo_PCELatest.json and objectClassifications_SAP.json from SAP's repository and keeps the entries your programs use. On 1 November 2026 it printed:

rules.json now holds 18 objects. Changed since the bundled copy: none

If SAP has changed an entry since then, the script names it. The repository also keeps files for specific Private Edition feature pack releases; a real project uses the one for its target release.

Step 10: Save your work in Git

git add unit13/ccm_assist.py unit13/ccm_demo
git commit -m "Unit 13: custom code migration assistant with review gate"

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

How the code works

Part of the script What it does
PROGRAMS, USAGE Four made-up CP20 programs and their run counts, as usage monitoring would report them
BUNDLED_RULES The entries from SAP's Cloudification Repository that the demo needs, copied on 1 November 2026, so it runs offline
used_objects() Finds tables read (FROM, JOIN), tables written (UPDATE, INSERT, MODIFY, DELETE FROM), function calls and classes, skipping the program's own variables. Patterns, not an ABAP parser
classify() Maps each use to SAP's clean core levels: released A, classic API B, internal read C, SAP table write D, unknown ?
cmd_propose() Refuses unused and level D programs, skips A and B, gets a draft from the sample, a model or your file, scans it again and writes the review file
CHECKLIST The six things AI gets wrong in migrations: filters, invented fields, currency, authorization, output, table writes
cmd_approve() Scans the reviewed file again, refuses while any item is [ ], records the reviewer and the deferred [-] items
cmd_refresh() Downloads SAP's two files and rebuilds rules.json for the objects your programs use

Two design choices to notice. The tool refuses level D and unused code before any model sees it, so the assistant spends effort only where a draft helps. And approval is a separate command with a named reviewer, so "the model proposed it" and "a person accepted it" are never the same event.

If something goes wrong

What you see What it means What to do
python is not recognized, or command not found Python isn't installed, or the terminal can't find it Windows: repeat Unit 1, Step 1, then open a new terminal. macOS/Linux: use python3 until .venv is active
No demo yet. Run first: ...init You skipped Step 3 Run python unit13/ccm_assist.py init
ccm_demo already exists. Add --force to start over. The demo is there from an earlier run Add --force; it deletes your review files and log
Not approved: the proposal is at level C Your edit or the model left an internal table in the code Fix the .prog.abap file in review/, then approve again
No scripted proposal for ... --sample has a draft only for ZSD_OPEN_ORDER_VALUE Use --llm, or --file with your own remediation
ANTHROPIC_API_KEY is not set in .env The key line is missing Add it as in Set up your computer, or use --sample
The anthropic library is missing The library isn't installed in .venv Run pip install -r requirements.txt with .venv active
The model call failed: AuthenticationError The key is wrong or revoked Create a new key in the Claude Console and update .env
The model call failed: APIConnectionError A network or proxy blocks the call Try another network, or ask IT to allow api.anthropic.com
Download failed: ... in refresh-rules GitHub can't be reached, often a company proxy Keep the bundled rules; ask IT to allow raw.githubusercontent.com

The SAP way

As of November 2026, here is how SAP covers each step of the funnel. Check each item for your contract and release; several changed status during 2026.

Scoping and analysis

  • ATC with S/4HANA readiness checks. Remote ATC from a central check system with S4HANA_READINESS_REMOTE, using the Simplification Database from SAP Note 2241080 (SAP Learning).
  • Cloudification Repository. The released and classic API lists behind the ATC checks "Usage of Released APIs" and "Usage of APIs", with release-specific files for Private Edition (GitHub).
  • System Analysis Assistant. Planned; SAP describes it as building a customer-specific understanding of systems, configuration and custom developments (SAP News, October 2026).

Remediation

  • SAP S/4HANA custom code migration agent. Generally available per SAP's Q2 2026 highlights, in Joule for developers (ABAP AI). Readiness checks across packages, deterministic quick fixes, AI changes with confidence scores, transport recording (SAP News, July 2026).
  • Custom Code Assistant. Early adopters as of 28 October 2026. SAP says it classifies findings by complexity, applies automated fixes, recommends corrections for the rest, and helps decide between standard processes and custom extensions. SAP News (August 2026) says it depends on the ABAP MCP server, which Coding agents for SAP developers covers.
  • Joule for developers. Documentation Chat, ABAP Program Explain and Issue Explain for custom code migration (sap.com). SAP News (August 2026) says ABAP AI extends to S/4HANA Cloud Private Edition releases from 2021, with a side-by-side service for other releases.
  • Clean core transformation agent. Announced as planned for Q3 2026; no availability found by 1 November 2026.

Data, testing and configuration

  • Data Management Assistant and Business Configuration Assistant: early adopters as of 28 October 2026. SAP says the configuration assistant extracts source configuration, flags deviations from the target and keeps an audit trail (SAP News).
  • Test Management Assistant: early adopters. Test cases go to SAP Test Automation by Tricentis for execution.
  • Tricentis. SAP's support site says Cloud ALM has no SAP-built automation engine: customers provision a Tricentis tenant from Cloud ALM, and Enterprise Support customers are entitled to Tricentis Test Automation for SAP as a term license (SAP Support Portal). Tricentis's own page, modified 21 October 2026, describes generating test cases from natural language descriptions "using SAP AI units" (Tricentis).

Licensing notes

  • Joule for developers: SAP's product page describes a free access offer for ADT users after accepting the SAP AI Terms, and a separate SKU for the ABAP environment. SAP News (August 2026) describes a move to usage-based AI units. Ask which applies to your contract.
  • The migration and modernization assistants: the October 2026 announcement gives no pricing.
  • ATC, the Simplification Database and the Cloudification Repository come with the platform or are public.

Build vs. SAP

Situation Use Why
ECC to S/4HANA conversion or rollout with ABAP AI licensed SAP's custom code migration agent on top of ATC Knows SAP's checks and simplification items; records changes in transports
An early adopter of SAP's assistants The Custom Code, Data Management and Test Management Assistants Covers several workstreams with one vendor; check what each changes automatically
No ABAP AI licence, or an independent second opinion Your own pipeline: ATC results plus the Cloudification Repository plus a model, like the script Free rules, any model, full control of the gate
Explaining legacy programs for documentation ABAP Program Explain or Joule for Consultants; or a model with the source A draft for the code's owner to confirm
Test automation for migrated processes Tricentis through Cloud ALM SAP's supported route; Enterprise Support entitlement
Code that writes to SAP tables or modifies SAP objects Neither tool alone Level D needs a redesign decided by people

Architecture view

Where it sits. For Calder, the source is CP20's ECC system (ECD, ECQ, ECP). The target is CP10's S/4HANA Cloud Private Edition, release 2023, with the route S4D to S4Q to S4P (Landscape and deployment options). Analysis reads the source; remediation writes only in S4D.

flowchart LR
  E[ECC CP20<br/>source code + usage] -->|remote ATC, RFC| C[Central check system]
  C --> F[Findings]
  F --> D[S4D development<br/>ADT + agent]
  M[Model: Joule ABAP AI<br/>or generative AI hub] <--> D
  D -->|transport after review| Q[S4Q quality<br/>comparison tests]
  Q --> P[S4P production]
  A[Cloud ALM<br/>+ Tricentis] --> Q

Non-functional requirements that decide the design:

Requirement Typical target What breaks if missed
Source code confidentiality Code goes only to model providers under the customer's AI terms and region Contract or data protection breach
Auditability Every AI change linked to a transport, a reviewer and a test result Migration sign-off and audits stall
Volume Thousands of objects analysed in batches; review throughput planned per developer Review queue becomes the bottleneck
Reversibility Remediation in its own transports, separate from functional changes A bad fix can't be backed out alone
Cost AI units or tokens per object tracked against days saved Savings claimed but never shown

Integration and identity. Remote ATC reads the old system over RFC with a technical connection set up by Basis. In S4D, the agent acts through the developer's own ADT session, so every change carries a person's user (Agent permissions and SAP authorizations). Transports carry code to S4Q; Cloud ALM and Tricentis run tests there.

The decision to record (short ADR form, as in Writing a solution design doc):

  • Context: CP20's custom code moves into CP10's system by July 2027. SAP's agent can apply high-confidence fixes automatically.
  • Options: A, allow automatic fixes in S4D and review at transport release. B, allow only deterministic quick fixes automatically; every AI change waits for a named reviewer. C, no AI changes; AI only explains.
  • Decision: B. Deterministic fixes are checkable in bulk; AI changes are reviewed one by one with the checklist.
  • Consequences: slower than A on simple objects; every AI change has a reviewer on record; review capacity must be planned.

What a design review will ask:

  1. Which objects did usage data retire, and who signed that?
  2. What can the agent change without a person, and where is that configured?
  3. How is behaviour compared before and after, for which objects?
  4. Where does source code go, and under which terms?
  5. How are level D objects redesigned, and who owns each?

Production concerns

Security and SAP authorizations

  • Development only. Agents and assistants that change code work in the development system with the developer's own user. Transport release stays with people, under the project's normal change process.
  • Source code leaves the building. Custom code can contain business rules, names and sometimes credentials. Scan for secrets before sending code to any model, and check the provider terms as in Data security and PII.
  • Prompt injection through code. Comments in legacy code reach the model. A comment can carry instructions, as Prompt injection and tool poisoning explains. The review checklist and the re-scan are the defence.
  • Authorization gaps. Legacy reports often have no authorization checks, like the demo. Released CDS views can come with access controls; confirm what applies to each successor rather than assume.

Evaluation

Measure the AI on your own objects, as LLM evaluation fundamentals describes:

  • Acceptance rate: share of AI proposals approved without edits, with edits, or rejected.
  • Escaped behaviour changes: comparison test failures in the quality system on AI-remediated objects.
  • Effort: days per object before and after, on comparable objects.
  • Level movement: count of objects at each clean core level before and after.

The decision log from the build gives the first two directly.

Cost

Three meters: SAP AI units or licences for ABAP AI and the assistants, tokens if you call models yourself, and review time. Review time is the one projects forget. Plan it per developer per day, and stop sending objects to the AI when the review queue grows faster than it shrinks.

Operations

  • Keep remediation transports separate from functional changes.
  • Freeze the rule list version per mock cycle, so findings are comparable between runs.
  • Keep the decision log with the project's test evidence until after go-live and the first audit.

Clean core

A migration is the cheapest moment to raise clean core levels: the code is being touched anyway. Aim for level A or B on everything that moves, and track level D objects by name until each one is redesigned. Don't let "the agent made it compile" become the definition of done.

Pitfalls

  • Remediating dead code. Fixing and testing programs nobody runs. Scope with usage data first.
  • Trusting the score. A high confidence score is the model's estimate. Behaviour is proven by comparison tests.
  • Dropped filters and invented fields. The two most common confident errors in remediated reports. The checklist names both.
  • Unknown treated as clean. An object missing from the rule list is unknown, not released. Route it to ATC and a person.
  • Review fatigue. After forty good diffs, the forty-first gets waved through. Keep batches small and the checklist short.
  • Early adopter in the plan. Building the project plan on assistants your company can't yet use.
  • No log. Approvals given in chat or email can't be audited. Record them where the transport and test evidence live.

Exercise

Add a fifth CP20 program, remediate it with your own fix, and record the decision. The decision log you produce feeds the portfolio work in Unit 14.

Before you start: finish Build it yourself above.

  1. Open unit13/ccm_assist.py. In PROGRAMS, after the ZSD_ORDER_REASON_UPD entry and before the closing }, add:

        "ZMM_STOCK_CHECK": """REPORT zmm_stock_check.
    * Shows Augsburg's unrestricted stock of one material.
    PARAMETERS p_matnr TYPE matnr DEFAULT 'CP-IMP200'.
    SELECT * FROM mard INTO TABLE @DATA(lt_stock)
      WHERE matnr = @p_matnr AND werks = 'CP21'.
    cl_salv_table=>factory( IMPORTING r_salv_table = DATA(lo_alv)
                            CHANGING  t_table      = lt_stock ).
    lo_alv->display( ).
    """,
  2. In USAGE, add "ZMM_STOCK_CHECK": 120 (put a comma after the entry before it). Save.

  3. Run python unit13/ccm_assist.py init --force, then python unit13/ccm_assist.py scan. Find ZMM_STOCK_CHECK: it should show worst level: C, with MARD (successor I_PRODUCTSTORAGELOCATIONBASIC) at C and CL_SALV_TABLE at B.

  4. Create unit13/zmm_stock_check_fix.abap with the same program, but replace the SELECT lines with:

    SELECT * FROM i_productstoragelocationbasic INTO TABLE @DATA(lt_stock)
      WHERE Product = @p_matnr AND Plant = 'CP21'.
  5. Run python unit13/ccm_assist.py propose ZMM_STOCK_CHECK --file unit13/zmm_stock_check_fix.abap. The scan after the change should show worst level B.

  6. Open unit13/ccm_demo/review/zmm_stock_check.md. Tick [x] for items 1, 3 and 6 after checking them; mark [-] for items 2, 4 and 5.

  7. Run python unit13/ccm_assist.py approve ZMM_STOCK_CHECK --reviewer "Your Name".

  8. Write three sentences in unit13/ccm_demo/NOTES.md: why CL_SALV_TABLE may stay for now, which field names you must confirm in ADT, and what the comparison test in the quality system should show.

  9. Commit: git add unit13 and git commit -m "Unit 13: remediate ZMM_STOCK_CHECK".

Done when decisions.jsonl has an approved line for ZMM_STOCK_CHECK at level B with three deferred checks, and NOTES.md is committed.

Check yourself

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

1In the decision funnel, when may a tool apply a change without a named reviewer for each item?

Answer: A. The mental model weighs how cheap a draft is to check against what happens if it's wrong. Confidence scores and passing checks prove neither behaviour nor harmlessness.

2In the script, why does propose refuse ZSD_PRICE_FIX before any model sees it?

Answer: B. The program updates KONV directly. SAP's level D covers write access to SAP tables, so the tool leaves the redesign to a person instead of drafting a fix.

3The sample proposal for ZSD_OPEN_ORDER_VALUE scans at level A. What did the review still catch?

Answer: C. The proposal removed AND auart = 'ZOR', so the report would sum every sales document. Clean core checks prove the level, not the behaviour; the checklist's first item exists for this.

4REUSE_ALV_GRID_DISPLAY shows ? in the scan. How should you treat it?

Answer: D. The object is in neither SAP file the demo uses. Unknown is not the same as clean, so the tool routes it to ATC, the simplification items and a person.

5How does SAP's custom code migration agent handle its changes, according to SAP's Q2 2026 highlights?

Answer: B. SAP describes deterministic quick fixes plus AI changes with confidence scores; high-confidence ones are applied, lower-confidence ones become comments, and updates are recorded in transport requests.

6Why does approve scan the proposal file again?

Answer: A. The reviewer fixes proposals by hand, as in Step 6. Re-scanning at approval means an edit that reintroduced an internal table can't slip through.

7Your project wants to use AI-remediated code but has no comparison tests. What do you do?

Answer: D. Checks prove the clean core level; only running old and new code on the same input proves behaviour. That evidence belongs in the quality system before transports move on.

8In the ADR for Calder, why was option B chosen over automatic AI fixes?

Answer: B. Rule-based quick fixes always produce the same result, so they can be reviewed in batches. AI changes vary and can alter behaviour, so each one gets a named reviewer, at the cost of planning review time.

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