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 governance for SAP programs

Set up the register, risk tiers, approvals, change control, monitoring and vendor checks that let a company run AI in SAP processes with evidence, not hope.

Updated Oct 22, 2026Foundational 9 minDeep 40 min
Foundational layer · 9 min read

The 60-second version

AI governance is how a company decides which AI it runs, who is accountable for each use, and how it notices and fixes problems. It is not a committee that meets once a year. It is a small set of habits with written evidence behind them.

In practice it comes down to five questions you can answer at any time for every AI use in your SAP landscape:

  1. What do we run? A register lists every AI use case, including AI features switched on inside SAP applications.
  2. How risky is it? Each use case has a risk tier, and the tier decides how much checking it needs.
  3. Who said yes? Named people approved it, and the approvals match the tier.
  4. What changed? Every change to a prompt, model or threshold is recorded and approved before it goes live.
  5. Is it still working? Each use case has a measure, a limit, and a person who acts when the limit is crossed.

The register comes first. You can't govern AI you don't know you have.

Why it matters to the business

Take the running example from this unit: an assistant that explains blocked sales orders to order-to-cash clerks. It went live with a careful prompt. Six weeks later a developer adds one sentence to make answers shorter. Nobody tests it. Clerks start ignoring the assistant's answers, and a few release orders they shouldn't. When the credit manager asks what changed, nobody can say.

Governance would have caught this at three points. The prompt change would have needed an approval and an evaluation report. A monitoring limit on how often clerks override the answer would have flagged the drop. And the change record would have said exactly what to roll back.

Leaders get four things from it:

  • Speed with fewer surprises. Low-risk uses get a light path, so teams don't wait months. High-risk uses get the scrutiny they need.
  • Answers for auditors and regulators. A register, approvals and change records are what an auditor asks for first. The EU AI Act topic shows which legal duties attach to which uses.
  • Control over vendors. You know which vendor processes which data, and whether your data may be used to train their models.
  • A way to stop. When something goes wrong, a named person can switch the use case off, and everyone knows who to tell.

The cost of not doing it shows up as stalled programs. When nobody can say what is safe, cautious people say no to everything.

How SAP does it

SAP governs its own AI and offers some tools you can use for yours. Treat these as building blocks, not as your governance.

SAP's own governance (as of October 2026). SAP's Responsible AI course on SAP Learning describes Guiding Principles for AI from 2018 and a Global AI Ethics Policy from 2022 that is binding for SAP employees. It says every SAP AI use case gets an ethics assessment, and high-risk ones are reviewed by an AI Ethics Committee and a Global AI Ethics Advisory Panel. SAP publishes an ISO/IEC 42001 certificate for its AI management system. ISO/IEC 42001 is the international standard for running such a system.

SAP's data commitments. SAP's course page on training data says customer data is not used by external model providers for training or retraining. It also says some optional learning features exist, and customers decide whether to enable them where they apply. The generative AI hub product page says customer data is never shared with third-party vendors to train their large language models. Record these statements in your register with the date you checked them, and confirm them against your own contract.

Tools for your own governance. Two SAP offerings map to parts of the job:

  • SAP AI Agent Hub is a registry for AI agents, models and MCP servers across vendors. SAP's product page describes a verification workflow that records a risk rating and compliance mappings. SAP LeanIX's May 2026 announcement says discovered items start as unverified and can be marked verified or rejected. The same announcement lists observability and identity and access control as future capabilities. Check what is generally available before you plan around it.
  • The prompt registry in the generative AI hub stores prompt templates with versions. SAP Learning's prompt registry lesson describes keeping templates in a Git repository that SAP AI Core syncs. That gives you a change history for prompts.

Neither tool decides your risk tiers, your approvers or your limits. That is your company's job.

A governance starter kit

You can start with six documents and one meeting rhythm. Most of the work is deciding who owns what.

Item What it holds Owner Updated when
Use-case register Every AI use: process, purpose, tier, owner, vendor, status, next review AI lead A use case is proposed, changed or retired
Risk tiers Three tiers with examples from your processes Risk or compliance Once a year, or when the law changes
Approval matrix Who must approve, by tier Steering committee Once a year
Change log Every approved change to prompts, models, thresholds and data sources AI lead Every change
Monitoring sheet One measure and one limit per live use case, and who watches it Business owner Each review
Incident runbook How to switch a use case off, who to tell, how to restart AI lead with IT operations After every incident

A simple tiering that works for many SAP programs:

Tier Example Approvals
Low MRP exception triage that only sorts a planner's worklist Business owner
Medium Blocked sales order explainer used by clerks every day Business owner and AI lead
High Ranking job candidates, or anything that scores people Business owner, AI lead and risk officer, plus legal review

The steering committee meets monthly at first. It reviews new high-tier proposals, incidents, overdue reviews and use cases with unknown vendor terms.

Questions to ask

A steering committee can start with these:

  • Can we list every AI use in our SAP landscape today, including features switched on in SAP applications and partner add-ons?
  • Which of them affect people's jobs, pay, credit or access to services? Those are our high tier.
  • For each vendor, do we know in writing whether our data may be used to train their models?
  • Who can change a prompt, a model or a threshold in production, and who approves it?
  • Can one person both make and approve a change?
  • What is the one number for each live use case that tells us it is working? What limit makes us act?
  • If an AI use case has to be switched off tonight, who decides, how is it done, and who is told?
  • When is each use case reviewed next, and which reviews are overdue?

Common misconceptions

  • "Our vendor is certified, so we're covered." A vendor's certificate covers the vendor's own management system. Your choices about which uses to run, and how, are yours to govern.
  • "Governance means a long approval for every idea." Tiers exist so low-risk uses move fast. One process for everything either blocks good work or waves through risky work.
  • "We only need to approve go-live." Most failures come from later changes. A prompt edit can change behavior as much as a new model.
  • "The register is a spreadsheet the auditors want." Done well, it is what lets you answer "where do we use this model?" in minutes when a vendor issue appears.
  • "Embedded SAP AI features don't need governing." Someone in your company switched them on and relies on them. They belong in the register with an owner.

Key terms

All course terms, A to Z, in the glossary

  • AI governance: the decisions, roles and records that control which AI a company runs and how.
  • Use-case register: the list of every AI use, with owner, tier, status and vendor details.
  • Risk tier: a level, such as low, medium or high, that sets how much checking a use case needs.
  • Approval matrix: the table of which roles must approve, by tier.
  • Change control: recording and approving every change before it reaches production.
  • Separation of duties: the rule that the person who makes a change can't approve it.
  • Monitoring limit: the value of a measure, such as how often users override answers, that triggers action.
  • Incident: an event where an AI use case causes, or could cause, harm or a wrong business result.
  • AI management system: the company-wide set of policies and processes for AI, as defined by ISO/IEC 42001.

Check yourself

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

1A steering committee wants to start AI governance this quarter. What should come first?

Answer: C. You can't tier, approve or monitor AI you don't know about. The register comes first, and it must include AI features switched on inside SAP applications, not only new projects.

2Why do governance programs use risk tiers instead of one approval process?

Answer: D. One process for everything either blocks useful low-risk work or under-checks risky work. Tiers match the effort to the risk, and live use cases are still monitored whatever their tier.

3The blocked-order assistant's prompt was edited after go-live and answers got worse. Which control would have stopped this?

Answer: B. The problem was an unreviewed change. Change control makes every prompt, model or threshold change pass an approval with evidence first, and records what to roll back.

4What does SAP's statement on training data mean for your register?

Answer: C. SAP states that external providers don't train on customer data, and that some optional learning features are the customer's choice. Statements change, and partners have their own terms, so the register records what was checked, when, and against which contract.

5Your company uses SAP, which holds an ISO/IEC 42001 certificate. What does that cover?

Answer: A. A vendor's certificate covers the vendor's own system. Your decisions about which uses to run, and how, sit in your own governance.

6Which question best tests whether a live AI use case can be stopped safely?

Answer: D. An incident runbook names who can switch a use case off, how, and who must be informed. Without it, a failing use case keeps running while people look for an owner.
Deep layer · 40 min read

Mental model: no record, no run

Governance for builders is a control loop with evidence at every step. The register says what may run. The change log says what changed and who agreed. Monitoring says whether it still works. An incident stops it.

The idea that makes it practical: the service checks the record before it runs. If the use case isn't live in the register, or its prompt isn't the approved version, the service refuses the request. That turns a policy document into a control that works at 2 a.m. without anyone remembering it.

You don't need a product to start. A register in a JSON file, an append-only change log and a ten-line gate in your service give you most of the value. Products such as SAP AI Agent Hub help when the landscape is too big to track by hand.

How it works

flowchart LR
  P[Proposal] --> T{Tier}
  T --> A[Approvals<br/>by tier]
  A --> R[(Register)]
  A --> L[(Change log)]
  R --> G{Gate}
  G -->|approved| S[AI service]
  G -->|refused| X[No model call]
  S --> U[(Usage log)]
  U --> M{Monitor}
  M -->|breach| I[Incident:<br/>suspend]
  I --> P

The register

One record per use case. The fields that matter most:

Field Why it's there
id, name, process Find it, and tie it to an SAP process such as order-to-cash
tier Sets approvals and checks
status proposed, live, suspended or retired; only live may run
owner, business_owner Two names: the person who runs it and the person who answers for the business result
model, prompt_file, prompt_sha256, version What exactly is approved; the hash pins the prompt text
vendor.training_use no, opt-in, yes or unknown, with a reference to the contract clause
approvals Who approved, by role
monitoring One measure and its limit
incident_contact Who is told when it goes wrong
legal_review For high tier: the reference to the legal assessment
next_review When someone must look at it again

The NIST AI Risk Management Framework names this practice in GOVERN 1.6: mechanisms to inventory AI systems. Its core also covers roles (GOVERN 2.1), third-party risk (GOVERN 6.1), decommissioning (GOVERN 1.7) and telling the right people about incidents (MANAGE 4.3). The fields above give you evidence for each.

Tiers and the approval matrix

The tier decides which roles must approve. A common rule set:

Tier Required approvals Extra condition
low business owner none
medium business owner, AI lead evidence for go-live, prompt and model changes
high business owner, AI lead, risk officer the same evidence, plus a recorded legal review before go-live

Two rules make approvals mean something. The person who proposed a change can't approve it. And one person can't approve in two roles. Both are separation of duties, the same idea SAP authorization design uses for creating and releasing a payment.

Change classes

Not every change is the same. Name the classes, so each one gets the right check:

  • Prompt: new prompt text. Needs an evaluation showing it is no worse.
  • Model: a different model or version. Same evidence, often more.
  • Threshold: a new monitoring limit or confidence cut-off.
  • Data source: a new table, document set or API the AI reads.
  • Go-live and retire: the start and end of the use case.

A change log you can trust

A change log is only evidence if nobody can quietly edit it. A simple technique is a hash chain. Each entry stores a fingerprint (a SHA-256 hash) of its own content and the fingerprint of the entry before it. Edit any old entry and its fingerprint no longer matches, and every later link breaks.

A hash chain makes tampering visible, not impossible. Someone with write access could rebuild the whole chain. So copy the latest fingerprint somewhere the log's editors can't change, such as a Git commit or a ticket.

The runtime gate

The gate is a function the AI service calls before every request. It reads the register and answers yes or no:

  • Is the use case in the register and live?
  • Does the prompt file on disk have the approved hash?

A refusal means nothing is sent to the model. This is the step that turns a document into a control.

Monitoring and incidents

Pick one measure per use case that a business person understands. For an assistant that clerks read, the override rate works well: the share of answers the clerk rejected or corrected. Set a limit, require a minimum number of answers before judging, and judge each approved version separately.

When the limit is crossed, open an incident. The use case is suspended, so the gate refuses requests. It comes back only after a fix has passed the same change control.

Build it yourself: governance as code for the blocked-order assistant

You will build two scripts. unit11/governance.py keeps the register, the change log and the monitoring checks. unit11/blocked_order_service.py is a small AI service that asks the governance gate before every answer. Then you will push one prompt change through approval, watch monitoring catch a bad result, suspend the service, roll back and restart it.

Before you start: complete Set up your computer for this course and Set up for Unit 11. This lab uses only modules that come with Python, so there is nothing new to install.

flowchart LR
  C[You: propose<br/>and approve] --> GOV[governance.py]
  GOV --> REG[(ai_register.json)]
  GOV --> LOG[(change_log.jsonl)]
  SVC[blocked_order_service.py] -->|gate| REG
  SVC --> USE[(ai_usage.jsonl)]
  USE --> GOV

What you need

  • Your course folder orchestrate-course with its .venv.
  • About 45 minutes.
  • Cost: free. No account, no key, no network call.

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

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

  2. Open a terminal: Terminal > New Terminal.

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

    • Windows (PowerShell):

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

      source .venv/bin/activate
  4. Check Python:

    python --version

    You should see Python 3.10 or newer. On macOS or Linux without .venv, use python3 in every command.

Run every command in this topic from the course folder, not from inside unit11.

Step 2: Create the governance script

  1. The unit11 folder already exists from the Unit 11 setup. If it doesn't, create it:

    • Windows (PowerShell):

      New-Item -ItemType Directory -Force unit11
    • macOS / Linux:

      mkdir -p unit11
  2. In VS Code, create the file unit11/governance.py, paste the whole block below, and save.

"""AI governance as code: a use-case register, a change log and a release gate.

Built-in modules only. Run from the orchestrate-course folder, for example:
    python unit11/governance.py init --sample
    python unit11/governance.py check --as-of 2026-10-22
"""
import argparse
import hashlib
import json
import sys
from datetime import date
from pathlib import Path

HERE = Path(__file__).resolve().parent
REGISTER = HERE / "ai_register.json"
CHANGE_LOG = HERE / "change_log.jsonl"
USAGE_LOG = HERE / "ai_usage.jsonl"
PROMPTS = HERE / "prompts"

TIERS = ("low", "medium", "high")
STATUSES = ("proposed", "live", "suspended", "retired")
# Who must approve, by risk tier. One person may not hold two roles on one change.
REQUIRED_ROLES = {
    "low": ["business_owner"],
    "medium": ["business_owner", "ai_lead"],
    "high": ["business_owner", "ai_lead", "risk_officer"],
}
TRAINING_USE = ("no", "opt-in", "yes", "unknown")
CHANGE_KINDS = ("go_live", "prompt", "model", "threshold", "data_source", "retire")
MIN_EVENTS = 20  # monitoring needs at least this many events before it judges a rate

PROMPT_V1 = """You explain why an SAP sales order is blocked.
Use only the facts in ORDER_FACTS. If a fact is missing, say so.
Never tell the user that a block has been released. Only credit control releases blocks.
Answer in at most three sentences, then name the role that can act.
"""


def sha256_file(path):
    return hashlib.sha256(Path(path).read_bytes()).hexdigest()


def load_register():
    if not REGISTER.exists():
        sys.exit("No register found. Run: python unit11/governance.py init --sample")
    return json.loads(REGISTER.read_text(encoding="utf-8"))


def save_register(reg):
    REGISTER.write_text(json.dumps(reg, indent=2) + "\n", encoding="utf-8")


def find(reg, uc_id):
    for uc in reg["use_cases"]:
        if uc["id"] == uc_id:
            return uc
    sys.exit(f"Unknown use case {uc_id}. Run: python unit11/governance.py status")


# ---------- change log: append-only, each entry chained to the one before ----------

def read_log():
    if not CHANGE_LOG.exists():
        return []
    return [json.loads(line) for line in CHANGE_LOG.read_text(encoding="utf-8").splitlines() if line.strip()]


def entry_hash(entry):
    body = {k: v for k, v in entry.items() if k != "hash"}
    return hashlib.sha256(json.dumps(body, sort_keys=True).encode("utf-8")).hexdigest()


def append_log(entry):
    log = read_log()
    entry["seq"] = len(log) + 1
    entry["prev"] = log[-1]["hash"] if log else "0" * 64
    entry["hash"] = entry_hash(entry)
    with CHANGE_LOG.open("a", encoding="utf-8") as f:
        f.write(json.dumps(entry, sort_keys=True) + "\n")
    return entry


def verify_log():
    prev = "0" * 64
    for i, e in enumerate(read_log(), start=1):
        if e.get("prev") != prev or e.get("seq") != i:
            return False, i, "link to the previous entry is broken"
        if entry_hash(e) != e.get("hash"):
            return False, i, "entry content does not match its hash"
        prev = e["hash"]
    return True, len(read_log()), prev


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

def cmd_init(args):
    if REGISTER.exists() and not args.force:
        sys.exit("ai_register.json already exists. Add --force to start again (this deletes your log).")
    if not args.sample:
        sys.exit("Only sample data is built in. Run: python unit11/governance.py init --sample")
    PROMPTS.mkdir(exist_ok=True)
    (PROMPTS / "blocked_order_v1.txt").write_text(PROMPT_V1, encoding="utf-8", newline="\n")
    for p in (CHANGE_LOG, USAGE_LOG):
        if p.exists():
            p.unlink()
    day = args.as_of
    v1 = sha256_file(PROMPTS / "blocked_order_v1.txt")
    reg = {"as_of": day, "use_cases": [
        {"id": "UC-01", "name": "Blocked sales order explainer", "process": "order-to-cash",
         "tier": "medium", "status": "live", "build": "own (BTP)",
         "owner": "A. Weber (AI lead)", "business_owner": "M. Rossi (O2C process owner)",
         "model": "gpt-4o-mini via generative AI hub", "prompt_file": "prompts/blocked_order_v1.txt",
         "prompt_sha256": v1, "version": 1,
         "vendor": {"name": "SAP (generative AI hub)", "training_use": "no", "terms_ref": "contract + SAP data-use statement, checked 2026-10-01"},
         "approvals": {"business_owner": "M. Rossi", "ai_lead": "A. Weber"},
         "monitoring": {"metric": "override_rate", "max": 0.25}, "incident_contact": "o2c-ai@example.com",
         "legal_review": "", "next_review": "2027-01-15"},
        {"id": "UC-02", "name": "Three-way match exception summary", "process": "procure-to-pay",
         "tier": "medium", "status": "live", "build": "partner add-on",
         "owner": "J. Kim (AP lead)", "business_owner": "J. Kim (AP lead)",
         "model": "partner-hosted LLM", "prompt_file": None, "prompt_sha256": None, "version": 1,
         "vendor": {"name": "Example Partner GmbH", "training_use": "unknown", "terms_ref": ""},
         "approvals": {"business_owner": "J. Kim"},
         "monitoring": {"metric": "override_rate", "max": 0.30}, "incident_contact": "p2p-ai@example.com",
         "legal_review": "", "next_review": "2026-09-30"},
        {"id": "UC-03", "name": "MRP exception triage", "process": "plan-to-produce",
         "tier": "low", "status": "proposed", "build": "own (BTP)",
         "owner": "A. Weber (AI lead)", "business_owner": "S. Lopez (planning lead)",
         "model": "not chosen", "prompt_file": None, "prompt_sha256": None, "version": 0,
         "vendor": {"name": "SAP (generative AI hub)", "training_use": "no", "terms_ref": "same as UC-01"},
         "approvals": {}, "monitoring": {}, "incident_contact": "",
         "legal_review": "", "next_review": "2027-03-31"},
        {"id": "UC-04", "name": "Candidate shortlist ranking", "process": "hire-to-retire",
         "tier": "high", "status": "proposed", "build": "own (BTP)",
         "owner": "A. Weber (AI lead)", "business_owner": "T. Novak (HR director)",
         "model": "not chosen", "prompt_file": None, "prompt_sha256": None, "version": 0,
         "vendor": {"name": "SAP (generative AI hub)", "training_use": "no", "terms_ref": "same as UC-01"},
         "approvals": {}, "monitoring": {}, "incident_contact": "",
         "legal_review": "", "next_review": "2026-12-31"},
        {"id": "UC-05", "name": "Embedded AI feature used as delivered", "process": "record-to-report",
         "tier": "low", "status": "live", "build": "vendor feature",
         "owner": "R. Singh (finance systems)", "business_owner": "R. Singh (finance systems)",
         "model": "vendor-managed", "prompt_file": None, "prompt_sha256": None, "version": 1,
         "vendor": {"name": "SAP", "training_use": "no", "terms_ref": "SAP feature documentation, checked 2026-10-01"},
         "approvals": {"business_owner": "R. Singh"},
         "monitoring": {}, "incident_contact": "fin-systems@example.com",
         "legal_review": "", "next_review": "2027-06-30"},
    ]}
    save_register(reg)
    for uc in reg["use_cases"]:
        append_log({"type": "registered", "at": day, "use_case": uc["id"], "tier": uc["tier"],
                    "status": uc["status"], "prompt_sha256": uc["prompt_sha256"], "by": "init --sample"})
    print(f"Created {REGISTER.name} with {len(reg['use_cases'])} use cases")
    print(f"Created prompts/blocked_order_v1.txt  sha256 {v1[:12]}")
    print(f"Created {CHANGE_LOG.name} with {len(read_log())} entries")


def cmd_status(args):
    reg = load_register()
    print(f"{'ID':6} {'TIER':7} {'STATUS':10} {'VER':>3}  NAME")
    for uc in reg["use_cases"]:
        print(f"{uc['id']:6} {uc['tier']:7} {uc['status']:10} {uc['version']:>3}  {uc['name']}")


def findings_for(uc, as_of):
    out = []
    if not uc.get("owner") or not uc.get("business_owner"):
        out.append("no named owner or business owner")
    if uc.get("tier") not in TIERS:
        out.append(f"tier must be one of {TIERS}")
        return out
    tu = uc.get("vendor", {}).get("training_use")
    if tu not in TRAINING_USE:
        out.append(f"vendor training_use must be one of {TRAINING_USE}")
    elif tu == "unknown":
        out.append("vendor data use for training is unknown: ask the vendor and record the contract clause")
    elif tu == "yes" and uc["tier"] != "low":
        out.append("vendor may train on your data: needs a documented decision by the risk officer")
    if uc["tier"] == "high" and not uc.get("legal_review"):
        out.append("high tier: legal review not recorded (see the EU AI Act topic)")
    if uc.get("next_review", "") < as_of:
        out.append(f"review overdue since {uc.get('next_review')}")
    if uc["status"] == "live":
        missing = [r for r in REQUIRED_ROLES[uc["tier"]] if not uc.get("approvals", {}).get(r)]
        if missing:
            out.append("live without approval from: " + ", ".join(missing))
        names = [n for n in uc.get("approvals", {}).values() if n]
        if len(names) != len(set(names)):
            out.append("one person approved in two roles")
        if not uc.get("monitoring", {}).get("metric"):
            out.append("live without a monitoring metric and threshold")
        if not uc.get("incident_contact"):
            out.append("live without an incident contact")
        if uc.get("prompt_file"):
            actual = sha256_file(HERE / uc["prompt_file"])
            if actual != uc.get("prompt_sha256"):
                out.append("prompt file changed without an approved change record")
    return out


def cmd_check(args):
    reg = load_register()
    total = 0
    for uc in reg["use_cases"]:
        f = findings_for(uc, args.as_of)
        total += len(f)
        if not f:
            print(f"OK       {uc['id']}  {uc['name']}")
        for line in f:
            print(f"FINDING  {uc['id']}  {line}")
    ok, n, info = verify_log()
    print(f"{'OK' if ok else 'FINDING':8} change log  {n} entries, chain {'intact' if ok else 'BROKEN at entry ' + str(n) + ': ' + info}")
    total += 0 if ok else 1
    print(f"\n{total} finding(s) as of {args.as_of}")
    return 1 if total else 0


def cmd_propose(args):
    reg = load_register()
    uc = find(reg, args.use_case)
    if uc["status"] == "retired":
        sys.exit(f"{uc['id']} is {uc['status']}; resolve that first.")
    entry = {"type": "proposed", "at": args.as_of, "use_case": uc["id"], "kind": args.kind,
             "summary": args.summary, "by": args.by, "tier": uc["tier"],
             "required": REQUIRED_ROLES[uc["tier"]], "evidence": args.evidence or ""}
    if args.kind == "prompt":
        if not args.prompt_file or not (HERE / args.prompt_file).exists():
            sys.exit("A prompt change needs --prompt-file, relative to unit11, e.g. prompts/blocked_order_v2.txt")
        entry["prompt_file"] = args.prompt_file
        entry["prompt_sha256"] = sha256_file(HERE / args.prompt_file)
    if args.kind == "model":
        entry["model"] = args.model or ""
    if args.kind == "threshold":
        if args.max is None:
            sys.exit("A threshold change needs --max, e.g. --max 0.20")
        entry["max"] = args.max
    if args.kind == "go_live":
        if uc["status"] != "proposed":
            sys.exit(f"{uc['id']} is already {uc['status']}.")
        if uc["tier"] == "high" and not uc.get("legal_review"):
            sys.exit("A high tier use case needs legal_review recorded in the register before go-live.")
    if args.kind in ("go_live", "prompt", "model") and uc["tier"] != "low" and not args.evidence:
        sys.exit("Go-live, prompt and model changes on a medium or high tier use case need --evidence "
                 "(the evaluation report that shows it is good enough).")
    log = read_log()
    entry["change_id"] = f"CHG-{sum(1 for e in log if e['type'] == 'proposed') + 1:03d}"
    append_log(entry)
    print(f"Proposed {entry['change_id']} for {uc['id']} ({args.kind}): needs " + ", ".join(entry["required"]))


def change_state(change_id):
    log = read_log()
    prop = next((e for e in log if e["type"] == "proposed" and e["change_id"] == change_id), None)
    if not prop:
        sys.exit(f"Unknown change {change_id}")
    approvals = {e["role"]: e["by"] for e in log if e["type"] == "approved" and e["change_id"] == change_id}
    applied = any(e["type"] == "applied" and e["change_id"] == change_id for e in log)
    return prop, approvals, applied


def cmd_approve(args):
    prop, approvals, applied = change_state(args.change_id)
    if applied:
        sys.exit(f"{args.change_id} is already applied.")
    if args.role not in prop["required"]:
        sys.exit(f"{args.role} is not a required role for {args.change_id}: " + ", ".join(prop["required"]))
    if args.by == prop["by"]:
        sys.exit("The person who proposed a change can't approve it.")
    if args.by in approvals.values():
        sys.exit(f"{args.by} already approved {args.change_id} in another role.")
    append_log({"type": "approved", "at": args.as_of, "change_id": args.change_id,
                "use_case": prop["use_case"], "role": args.role, "by": args.by})
    approvals[args.role] = args.by
    missing = [r for r in prop["required"] if r not in approvals]
    if missing:
        print(f"Approved by {args.role}. Still needed: " + ", ".join(missing))
        return
    reg = load_register()
    uc = find(reg, prop["use_case"])
    if prop["kind"] == "prompt":
        uc["prompt_file"] = prop["prompt_file"]
        uc["prompt_sha256"] = prop["prompt_sha256"]
    if prop["kind"] == "model":
        uc["model"] = prop.get("model") or uc["model"]
    if prop["kind"] == "threshold":
        uc.setdefault("monitoring", {"metric": "override_rate"})["max"] = prop["max"]
    if prop["kind"] == "retire":
        uc["status"] = "retired"
    if prop["kind"] != "retire" and uc["status"] == "proposed":
        uc["status"] = "live"
    uc["approvals"] = dict(approvals)
    uc["version"] = uc.get("version", 0) + 1
    save_register(reg)
    append_log({"type": "applied", "at": args.as_of, "change_id": args.change_id, "use_case": uc["id"],
                "version": uc["version"], "prompt_sha256": uc.get("prompt_sha256")})
    print(f"All approvals in. {args.change_id} applied: {uc['id']} is now version {uc['version']} ({uc['status']}).")


def cmd_incident(args):
    reg = load_register()
    uc = find(reg, args.use_case)
    uc["status"] = "suspended"
    save_register(reg)
    append_log({"type": "incident", "at": args.as_of, "use_case": uc["id"], "summary": args.summary, "by": args.by})
    print(f"{uc['id']} suspended. The service will refuse requests until it is resumed.")
    print(f"Tell: {uc.get('incident_contact') or 'NO CONTACT RECORDED'}")


def cmd_resume(args):
    reg = load_register()
    uc = find(reg, args.use_case)
    if uc["status"] != "suspended":
        sys.exit(f"{uc['id']} is {uc['status']}, not suspended.")
    log = read_log()
    last_incident = max(e["seq"] for e in log if e["type"] == "incident" and e["use_case"] == uc["id"])
    fixed = [e for e in log if e["type"] == "applied" and e["change_id"] == args.change
             and e["use_case"] == uc["id"] and e["seq"] > last_incident]
    if not fixed:
        sys.exit(f"{args.change} is not an applied change for {uc['id']} made after the incident. "
                 "Propose and approve the fix first.")
    uc["status"] = "live"
    save_register(reg)
    append_log({"type": "resumed", "at": args.as_of, "use_case": uc["id"], "change_id": args.change, "by": args.by})
    print(f"{uc['id']} live again.")


def cmd_monitor(args):
    reg = load_register()
    events = []
    if USAGE_LOG.exists():
        events = [json.loads(l) for l in USAGE_LOG.read_text(encoding="utf-8").splitlines() if l.strip()]
    breaches = 0
    for uc in reg["use_cases"]:
        mon = uc.get("monitoring") or {}
        if mon.get("metric") != "override_rate":
            continue
        # Judge only answers given by the version that is approved now.
        current = (uc.get("prompt_sha256") or "")[:12]
        mine = [e for e in events if e["use_case"] == uc["id"]
                and (not current or e.get("prompt_sha256") == current)][-args.window:]
        if len(mine) < MIN_EVENTS:
            print(f"WAIT     {uc['id']}  {len(mine)} events, need {MIN_EVENTS} before judging")
            continue
        rate = sum(1 for e in mine if e["outcome"] == "overridden") / len(mine)
        state = "BREACH" if rate > mon["max"] else "OK"
        breaches += state == "BREACH"
        print(f"{state:8} {uc['id']}  v{uc['version']} override rate {rate:.2f} over last {len(mine)} answers (limit {mon['max']:.2f})")
    if breaches:
        print("\nOpen an incident for each BREACH: python unit11/governance.py incident <ID> --summary \"...\" --by \"...\"")
    return 1 if breaches else 0


def cmd_verify(args):
    ok, n, info = verify_log()
    if ok:
        print(f"OK  {n} entries, chain intact. Latest hash: {info[:16]}")
        print("Keep this hash somewhere the log's editors can't change (a Git commit, a ticket).")
        return 0
    print(f"BROKEN at entry {n}: {info}")
    return 1


def gate(uc_id):
    """Called by the AI service before every request. Returns (allowed, reason)."""
    reg = load_register()
    uc = next((u for u in reg["use_cases"] if u["id"] == uc_id), None)
    if uc is None:
        return False, f"{uc_id} is not in the register"
    if uc["status"] != "live":
        return False, f"{uc_id} is {uc['status']}"
    if uc.get("prompt_file"):
        if sha256_file(HERE / uc["prompt_file"]) != uc.get("prompt_sha256"):
            return False, "prompt differs from the approved version"
    return True, f"{uc_id} v{uc['version']} approved"


def main(argv=None):
    common = argparse.ArgumentParser(add_help=False)
    common.add_argument("--as-of", default=date.today().isoformat(), help="date to record and check against, YYYY-MM-DD")
    p = argparse.ArgumentParser(description="AI governance as code")
    sub = p.add_subparsers(dest="cmd", required=True)

    def add(name):
        return sub.add_parser(name, parents=[common])

    s = add("init"); s.add_argument("--sample", action="store_true"); s.add_argument("--force", action="store_true")
    add("status")
    add("check")
    s = add("propose"); s.add_argument("use_case"); s.add_argument("--kind", choices=CHANGE_KINDS, required=True)
    s.add_argument("--summary", required=True); s.add_argument("--by", required=True)
    s.add_argument("--prompt-file"); s.add_argument("--model"); s.add_argument("--evidence")
    s.add_argument("--max", type=float, help="new monitoring limit for a threshold change")
    s = add("approve"); s.add_argument("change_id"); s.add_argument("--role", required=True); s.add_argument("--by", required=True)
    s = add("incident"); s.add_argument("use_case"); s.add_argument("--summary", required=True); s.add_argument("--by", required=True)
    s = add("resume"); s.add_argument("use_case"); s.add_argument("--change", required=True); s.add_argument("--by", required=True)
    s = add("monitor"); s.add_argument("--window", type=int, default=50)
    add("verify")
    args = p.parse_args(argv)
    handler = {"init": cmd_init, "status": cmd_status, "check": cmd_check, "propose": cmd_propose,
               "approve": cmd_approve, "incident": cmd_incident, "resume": cmd_resume,
               "monitor": cmd_monitor, "verify": cmd_verify}[args.cmd]
    return handler(args) or 0


if __name__ == "__main__":
    sys.exit(main())

Step 3: Create the AI service

  1. Create unit11/blocked_order_service.py, paste this block, and save.
"""A tiny AI service that asks the governance gate before every answer.

The "model" is a stand-in built from sample facts, so this runs with no account.
    python unit11/blocked_order_service.py 4711
    python unit11/blocked_order_service.py --simulate 40 --override 0.10 --seed 7
"""
import argparse
import json
import random
from datetime import date
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent))
import governance  # noqa: E402

USE_CASE = "UC-01"
SAMPLE_ORDERS = {  # made-up order-to-cash facts, not from a real system
    "4711": "credit limit exceeded by 12,400 EUR; customer has two overdue invoices",
    "4712": "delivery block set manually by sales; reason text: price check",
    "4713": "incomplete data: payment terms missing",
}


def answer(order_id):
    allowed, reason = governance.gate(USE_CASE)
    if not allowed:
        return None, f"REFUSED  {reason}. Nothing was sent to the model."
    facts = SAMPLE_ORDERS.get(order_id)
    if facts is None:
        return None, f"No sample facts for order {order_id}. Try 4711, 4712 or 4713."
    prompt_hash = governance.find(governance.load_register(), USE_CASE)["prompt_sha256"]
    text = f"Order {order_id} is blocked: {facts}. Credit control can release it."
    return {"order": order_id, "prompt_sha256": prompt_hash}, f"ANSWER   {text}  [{reason}]"


def log_usage(meta, outcome, as_of):
    rec = {"at": as_of, "use_case": USE_CASE, "order": meta["order"],
           "prompt_sha256": meta["prompt_sha256"][:12], "outcome": outcome}
    with governance.USAGE_LOG.open("a", encoding="utf-8") as f:
        f.write(json.dumps(rec) + "\n")


def main():
    p = argparse.ArgumentParser()
    p.add_argument("order", nargs="?", default="4711")
    p.add_argument("--simulate", type=int, default=0, help="simulate N clerk sessions")
    p.add_argument("--override", type=float, default=0.10, help="share of answers clerks override")
    p.add_argument("--seed", type=int, default=7)
    p.add_argument("--as-of", default=date.today().isoformat())
    args = p.parse_args()
    if not args.simulate:
        meta, line = answer(args.order)
        print(line)
        if meta:
            log_usage(meta, "accepted", args.as_of)
        return 0 if meta else 1
    rng = random.Random(args.seed)
    served = refused = 0
    for _ in range(args.simulate):
        meta, line = answer(rng.choice(list(SAMPLE_ORDERS)))
        if meta is None:
            refused += 1
            continue
        served += 1
        log_usage(meta, "overridden" if rng.random() < args.override else "accepted", args.as_of)
    print(f"Simulated {args.simulate} sessions: {served} answered, {refused} refused by the gate")
    return 0


if __name__ == "__main__":
    sys.exit(main())

Step 4: Build the sample register

  1. Run:

    python unit11/governance.py init --sample

    You should see:

    Created ai_register.json with 5 use cases
    Created prompts/blocked_order_v1.txt  sha256 63ff41a90d7e
    Created change_log.jsonl with 5 entries
  2. List the use cases:

    python unit11/governance.py status
    ID     TIER    STATUS     VER  NAME
    UC-01  medium  live         1  Blocked sales order explainer
    UC-02  medium  live         1  Three-way match exception summary
    UC-03  low     proposed     0  MRP exception triage
    UC-04  high    proposed     0  Candidate shortlist ranking
    UC-05  low     live         1  Embedded AI feature used as delivered
  3. Open unit11/ai_register.json in VS Code and read the record for UC-01. Every field from the table in How it works is there.

The five use cases are made up. They cover the cases a real register meets: your own build, a partner add-on, a planned low-risk use, a planned high-risk use, and an AI feature delivered inside an SAP application.

Step 5: Check the register

  1. Run the checks. --as-of fixes the date, so your output matches this page whatever day you run it:

    python unit11/governance.py check --as-of 2026-10-22
    OK       UC-01  Blocked sales order explainer
    FINDING  UC-02  vendor data use for training is unknown: ask the vendor and record the contract clause
    FINDING  UC-02  review overdue since 2026-09-30
    FINDING  UC-02  live without approval from: ai_lead
    OK       UC-03  MRP exception triage
    FINDING  UC-04  high tier: legal review not recorded (see the EU AI Act topic)
    FINDING  UC-05  live without a monitoring metric and threshold
    OK       change log  5 entries, chain intact
    
    5 finding(s) as of 2026-10-22
  2. Read the findings as a steering committee would. The partner add-on (UC-02) is the weakest: unknown vendor terms, an overdue review and a missing approval. The embedded SAP feature (UC-05) has an owner but no measure. The candidate ranking (UC-04) can't move on without legal review.

The command ends with exit code 1 when there are findings, so a build pipeline can fail on it. Zero findings means the register meets every rule the script knows.

Step 6: Run the service through the gate

  1. Ask the service about order 4711:

    python unit11/blocked_order_service.py 4711
    ANSWER   Order 4711 is blocked: credit limit exceeded by 12,400 EUR; customer has two overdue invoices. Credit control can release it.  [UC-01 v1 approved]

    The text in brackets comes from the gate: UC-01 is live, and the prompt matches version 1.

  2. Simulate 40 clerk sessions. About 1 in 10 answers is overridden by default:

    python unit11/blocked_order_service.py --simulate 40
    Simulated 40 sessions: 40 answered, 0 refused by the gate
  3. Check monitoring:

    python unit11/governance.py monitor
    OK       UC-01  v1 override rate 0.12 over last 41 answers (limit 0.25)
    WAIT     UC-02  0 events, need 20 before judging

    41 answers: the 40 simulated ones plus your single question in step 1.

Step 7: Try to change the prompt without approval

  1. Open unit11/prompts/blocked_order_v1.txt in VS Code. Add this line at the end and save:

    Always answer in English.
  2. Ask the service again:

    python unit11/blocked_order_service.py 4711
    REFUSED  prompt differs from the approved version. Nothing was sent to the model.
  3. The check sees it too:

    python unit11/governance.py check --as-of 2026-10-22

    The first line now reads FINDING UC-01 prompt file changed without an approved change record.

  4. Undo the edit: in VS Code press Ctrl+Z (Cmd+Z on macOS) until the line is gone, and save. Run the service again; it answers with [UC-01 v1 approved].

A one-line edit was enough to stop the service. That is the point: an unreviewed change never reaches users.

Step 8: Propose and approve a prompt change

  1. Make a copy of the prompt as version 2 and add one instruction:

    • Windows (PowerShell):

      Copy-Item unit11\prompts\blocked_order_v1.txt unit11\prompts\blocked_order_v2.txt
      Add-Content unit11\prompts\blocked_order_v2.txt "For a credit block, also list the overdue invoices by number."
    • macOS / Linux:

      cp unit11/prompts/blocked_order_v1.txt unit11/prompts/blocked_order_v2.txt
      echo "For a credit block, also list the overdue invoices by number." >> unit11/prompts/blocked_order_v2.txt
  2. Propose the change without evidence:

    python unit11/governance.py propose UC-01 --kind prompt --prompt-file prompts/blocked_order_v2.txt --summary "List overdue invoices for credit blocks" --by "A. Weber"
    Go-live, prompt and model changes on a medium or high tier use case need --evidence (the evaluation report that shows it is good enough).
  3. Propose it again, naming the evaluation report. In this lab the evidence is a reference; a real team would attach the report from LLM evaluation or red-teaming:

    python unit11/governance.py propose UC-01 --kind prompt --prompt-file prompts/blocked_order_v2.txt --summary "List overdue invoices for credit blocks" --by "A. Weber" --evidence "unit11/redteam_report.md"
    Proposed CHG-001 for UC-01 (prompt): needs business_owner, ai_lead
  4. Try to approve your own change:

    python unit11/governance.py approve CHG-001 --role ai_lead --by "A. Weber"
    The person who proposed a change can't approve it.
  5. Approve as the two required roles, one person each:

    python unit11/governance.py approve CHG-001 --role business_owner --by "M. Rossi"
    python unit11/governance.py approve CHG-001 --role ai_lead --by "L. Chen"
    Approved by business_owner. Still needed: ai_lead
    All approvals in. CHG-001 applied: UC-01 is now version 2 (live).
  6. Ask the service. The bracket now says [UC-01 v2 approved].

Step 9: Let monitoring catch a bad version

  1. Simulate 30 sessions where clerks override far more often:

    python unit11/blocked_order_service.py --simulate 30 --override 0.45
  2. Check monitoring:

    python unit11/governance.py monitor
    BREACH   UC-01  v2 override rate 0.42 over last 31 answers (limit 0.25)
    WAIT     UC-02  0 events, need 20 before judging
    
    Open an incident for each BREACH: python unit11/governance.py incident <ID> --summary "..." --by "..."

    Only version 2's answers count. The good results from version 1 don't hide the problem.

  1. Open an incident:

    python unit11/governance.py incident UC-01 --summary "Clerks override about 4 in 10 answers since v2" --by "M. Rossi"
    UC-01 suspended. The service will refuse requests until it is resumed.
    Tell: o2c-ai@example.com
  2. Ask the service. It answers REFUSED UC-01 is suspended. Nothing was sent to the model.

Step 10: Roll back and resume

  1. Try to resume without a fix:

    python unit11/governance.py resume UC-01 --change CHG-002 --by "A. Weber"
    CHG-002 is not an applied change for UC-01 made after the incident. Propose and approve the fix first.
  2. Propose the rollback to version 1. It goes through the same approvals:

    python unit11/governance.py propose UC-01 --kind prompt --prompt-file prompts/blocked_order_v1.txt --summary "Roll back to v1 after incident" --by "A. Weber" --evidence "v1 baseline in unit11/redteam_report.md"
    python unit11/governance.py approve CHG-002 --role business_owner --by "M. Rossi"
    python unit11/governance.py approve CHG-002 --role ai_lead --by "L. Chen"

    The last line reads All approvals in. CHG-002 applied: UC-01 is now version 3 (suspended). The fix is in, but the use case stays suspended until someone resumes it.

  3. Resume:

    python unit11/governance.py resume UC-01 --change CHG-002 --by "A. Weber"
    python unit11/blocked_order_service.py 4711
    python unit11/governance.py monitor
    UC-01 live again.
    ANSWER   Order 4711 is blocked: credit limit exceeded by 12,400 EUR; customer has two overdue invoices. Credit control can release it.  [UC-01 v3 approved]
    OK       UC-01  v3 override rate 0.12 over last 43 answers (limit 0.25)
    WAIT     UC-02  0 events, need 20 before judging

    Version 3 uses the same prompt text as version 1, so the earlier version 1 answers count again.

Step 11: Prove the log can't be quietly edited

  1. Verify the chain:

    python unit11/governance.py verify
    OK  15 entries, chain intact. Latest hash: 55161522eec3ba89
    Keep this hash somewhere the log's editors can't change (a Git commit, a ticket).

    Your hash will differ, because each entry records the date you ran it. The 15 entries must match.

  2. Open unit11/change_log.jsonl. Find about 4 in 10 and change it to about 1 in 10. Save.

  3. Verify again:

    python unit11/governance.py verify
    BROKEN at entry 10: entry content does not match its hash
  4. Undo the edit with Ctrl+Z (Cmd+Z), save, and verify once more. It reports OK again.

Step 12: Save your work

git add unit11/governance.py unit11/blocked_order_service.py unit11/ai_register.json unit11/change_log.jsonl unit11/prompts
git commit -m "Unit 11: AI governance register, change log and gate"

Committing the change log also stores its latest hash in Git history, which is the anchor Step 11 asked for. Leave unit11/ai_usage.jsonl out: usage logs grow quickly and can hold business data.

How the code works

Part What it does
REQUIRED_ROLES The approval matrix: which roles must approve, by tier
cmd_init Writes the sample register, prompt version 1 and the first log entries
findings_for The register rules: owners, vendor training use, legal review, overdue reviews, approvals, monitoring, prompt hash
append_log, entry_hash, verify_log The hash chain: each entry stores its own hash and the previous one
cmd_propose Records a change with its class, required roles and evidence; refuses missing evidence and high-tier go-live without legal review
cmd_approve Enforces separation of duties; when every role has approved, updates the register and logs applied
cmd_monitor Override rate per use case for the current approved version, with a minimum of 20 answers
cmd_incident, cmd_resume Suspend a use case; resume only after an approved change made after the incident
gate The runtime check the service calls before every request
blocked_order_service.answer Calls gate, refuses or answers, and logs usage with the prompt hash

If something goes wrong

What you see What it means What to do
python is not recognized, or command not found Python isn't on your path, or .venv isn't on Turn on .venv (Step 1). On macOS or Linux, try python3.
ModuleNotFoundError: No module named 'governance' The service can't find the governance script Make sure both files are in unit11 and you run from the course folder.
No register found init hasn't run in this folder Run python unit11/governance.py init --sample.
ai_register.json already exists You ran init twice Add --force to start again. It deletes your change log and usage log.
REFUSED prompt differs from the approved version when you didn't mean to edit An editor changed the file, for example line endings or a trailing space Undo the change, or run init --sample --force to start clean.
Your override rates differ from this page You ran simulations in a different order or count Run init --sample --force and repeat Steps 6 to 10 in order.
IndentationError or SyntaxError Part of the code was lost when pasting Delete the file contents, copy the whole block again and save.
Network or proxy errors Not expected: nothing here uses the network If you see one, you are running a different script.

The SAP way

SAP offers pieces of governance tooling. None of them sets your tiers, your approvers or your limits.

SAP AI Agent Hub for the register

As of October 2026, SAP's AI Agent Hub product page describes an AI registry of agents, LLMs and MCP servers from SAP and other vendors, each with a classification, status and fact sheet. A verification workflow captures a risk rating and compliance mappings. The page says only verified MCP servers can be called in production workflows, the same idea as this lab's gate.

SAP LeanIX's 12 May 2026 announcement adds detail. The hub, earlier called the AI agent hub in SAP LeanIX, is now offered to more SAP customers through Joule Studio. It can discover ServiceNow agents and active SAP AI Core model deployments. Discovered items start unverified and can be marked verified or rejected. Observability, identity and access control, and agent mining are listed as outlook, not as delivered. Check availability and licensing for your contract before you plan on them.

What this lab's register adds that a registry may not: your own tiers and approval matrix, the business owner, the monitoring limit, and the vendor training-use field with a contract reference. Many teams keep both: the registry for discovery, and a governance record per use case.

The prompt registry for change control

The generative AI hub in SAP AI Core has a prompt registry. SAP Learning's lesson on it describes two ways to manage templates:

  • Imperative: create and update templates through the API, with an edit history you can read back. The SDK reference names get_prompt_template_history for this.
  • Declarative: keep templates as YAML files in a Git repository that SAP AI Core syncs into the registry.

For governance, prefer the declarative path in production. The Git repository becomes your change log for prompts, and a pull request with required reviewers gives you separation of duties. The lesson also says fetching a template by its ID returns the exact stored version, which is the same pinning this lab does with a hash. Use the ID in production code, not "latest".

SAP's data commitments, recorded per use case

SAP's training-data lesson says external providers don't use customer data for training, that optional learning features are the customer's decision where they apply, and that retention depends on the service and its configuration. Each of those is a register field: training_use, the opt-in setting you chose, and the retention you configured. Record the source and date, because the wording can change.

Build vs. SAP

Need Build it yourself (this lab) SAP offering When to choose which
Inventory of AI uses JSON register in Git SAP AI Agent Hub registry Hub when you have many agents across vendors and want discovery; a file when you have a handful
Risk tiers and approvals REQUIRED_ROLES and approve Verification status and risk rating in AI Agent Hub Your matrix either way; the hub records the result
Prompt change control Hash-pinned prompt files and a change log Prompt registry, declarative via Git Prompt registry when you run on the generative AI hub
Runtime gate gate() in the service Verified-only MCP servers in production workflows, per the AI Agent Hub page Your own gate for your services; the hub's for agent tools it governs
Monitoring Override rate from a usage log AI Agent Hub observability, listed as outlook in May 2026 Your own measure until the product capability is available to you
Vendor terms vendor.training_use with a contract reference SAP's published data statements Always record your own; statements aren't contracts

Production concerns

  • Who can edit the register. The register and change log are controls, so access to them is an authorization question. Keep them in a repository where changes need a reviewed pull request, and limit write access to the AI lead's team.
  • Identity, not typed names. In this lab --by is free text. In production, take the approver from the signed-in identity, such as the user in your SAP Cloud Identity Services tenant or your Git host, never from a parameter.
  • Emergency changes. Sometimes a fix can't wait for three approvers. Define an emergency path in advance: one named approver now, the full approval within a set number of working days, and a log entry that marks it as an emergency.
  • Evaluation as evidence. --evidence should point to a stored report from your evaluation suite, run against the exact prompt and model being approved. The Exercise makes the script check that the file exists.
  • Monitoring that people read. One measure per use case, owned by the business owner, reviewed on a fixed cadence. Twenty measures nobody reads are worse than one that someone acts on.
  • Logs and personal data. Usage logs can hold order numbers, customer names and user IDs. Set retention with your data protection officer, and keep prompt and answer text out of governance logs where you can. Data security and PII covers masking.
  • Embedded and vendor AI. For AI features inside SAP applications, you can't gate the code, but you can gate the switch: record who may turn the feature on, in which system, and keep that setting under change control like other configuration.
  • Clean core. Keep your own AI services side by side on SAP BTP. That keeps a clear line between what SAP provides and what you build and must govern.
  • Regulation. Map each high-tier use case to the legal duties in the EU AI Act topic. The register's legal_review field holds the reference.

Pitfalls

  • A register nobody updates. If go-live doesn't require a register entry, the register drifts within months. Make the gate depend on it.
  • Approval by the same person twice. Small teams let one person hold every role. Name a deputy from another team instead.
  • Governing the model, not the use. The same model can be low tier in MRP triage and high tier in candidate ranking. Tier the use case.
  • "Latest" in production. Fetching the latest prompt or model version means unreviewed changes go live automatically. Pin versions.
  • Monitoring all versions together. A bad new version hides inside months of good history. Judge each approved version on its own answers.
  • Treating a vendor statement as a contract. Record the clause in your agreement, not only the web page.
  • No way back. If the previous prompt or model isn't kept, a rollback becomes a new, untested change.

Exercise

Add a high-tier use case, make the script demand real evidence, and take the use case live through the full process. Keep the register: when you build the capstone, add its agent here as a new use case and take it through the same steps.

Before you start: finish all of Build it yourself above, so that CHG-001 and CHG-002 already exist.

  1. Open unit11/governance.py. Find these two lines in cmd_propose:

        log = read_log()
        entry["change_id"] = f"CHG-{sum(1 for e in log if e['type'] == 'proposed') + 1:03d}"
  2. Just above them, add these two lines (four spaces at the start of each) and save:

        if args.evidence and not (HERE.parent / args.evidence).exists():
            sys.exit(f"Evidence file {args.evidence} not found. Give the path of a real report.")
  3. Open unit11/ai_register.json. Find the last } before the closing ] near the end of the file. After it, type a comma, then paste this record and save. It uses the shift-assignment case from the EU AI Act topic exercise:

    {
      "id": "UC-06", "name": "Shift assignment by reliability score", "process": "plan-to-produce",
      "tier": "high", "status": "proposed", "build": "own (BTP)",
      "owner": "A. Weber (AI lead)", "business_owner": "S. Lopez (planning lead)",
      "model": "trained classifier", "prompt_file": null, "prompt_sha256": null, "version": 0,
      "vendor": {"name": "none (own model)", "training_use": "no", "terms_ref": "n/a"},
      "approvals": {}, "monitoring": {"metric": "override_rate", "max": 0.20},
      "incident_contact": "plant-ai@example.com", "legal_review": "", "next_review": "2027-04-30"
    }
  4. Run python unit11/governance.py check --as-of 2026-10-22. You should see FINDING UC-06 high tier: legal review not recorded.

  5. Try to take it live:

    python unit11/governance.py propose UC-06 --kind go_live --summary "Pilot in plant 1010" --by "A. Weber" --evidence "unit11/evidence/uc06_eval.md"

    It refuses: A high tier use case needs legal_review recorded in the register before go-live.

  6. In ai_register.json, set UC-06's legal_review to a short reference, such as "Legal: high-risk, works council informed, 2026-10-20", and save. Run the same propose command. It now refuses with Evidence file unit11/evidence/uc06_eval.md not found.

  7. Create the folder unit11/evidence and a file uc06_eval.md in it. Write three lines: what was tested, the result, and who reviewed it. Save, and run propose again. You should see Proposed CHG-003 for UC-06 (go_live): needs business_owner, ai_lead, risk_officer.

  8. Approve it with three different people:

    python unit11/governance.py approve CHG-003 --role business_owner --by "S. Lopez"
    python unit11/governance.py approve CHG-003 --role ai_lead --by "L. Chen"
    python unit11/governance.py approve CHG-003 --role risk_officer --by "P. Dubois"
  9. Run check --as-of 2026-10-22 and verify, then commit:

    git add unit11/governance.py unit11/ai_register.json unit11/change_log.jsonl unit11/evidence
    git commit -m "Unit 11: evidence rule and high-tier go-live"

Done when check shows OK UC-06 Shift assignment by reliability score, status lists UC-06 as high and live at version 1, verify reports the chain intact, and a propose with a missing evidence file is refused.

Going further

For readers who want to connect this lab to the frameworks auditors and certification bodies use.

The lab covers the operational core of governance. Two frameworks describe the whole system around it, and most corporate AI policies borrow their structure from one or both.

  • NIST AI Risk Management Framework core. The AI RMF Core splits the work into GOVERN, MAP, MEASURE and MANAGE. GOVERN is cross-cutting: it covers policies, roles and accountability, and supports the other three. Reading its categories against this lab is a good gap analysis. GOVERN 1.6 (an inventory of AI systems) is the register. GOVERN 2.1 (documented roles and lines of communication) is the approval matrix and incident contact. GOVERN 6.1 (risks from third parties) is the vendor block. GOVERN 1.7 (decommissioning and phasing out AI systems safely) is the retire change class, which the lab records but doesn't exercise. MANAGE 4.3 (telling relevant actors about incidents and errors) is the incident contact. MAP and MEASURE are where tiering and evaluation evidence come from.
  • ISO/IEC 42001. The standard sets requirements for establishing, implementing, maintaining and continually improving an AI management system. It is certifiable, which is why vendors such as SAP publish a certificate. The "continually improving" part is what this lab lacks: a regular management review that looks at incidents, findings and overdue reviews and changes the rules, not only the use cases.
  • SAP's own model. SAP's Responsible AI lesson shows a vendor-side version: a binding policy, an ethics assessment for every use case, and escalation of high-risk cases to an ethics committee and an advisory panel. Compare its escalation path with your approval matrix. A customer usually needs a lighter version, but the shape is the same.

A useful next step is to write your tier definitions as test cases: ten example use cases with the tier your policy says they should get. When the policy changes, run the examples again and see which tiers move.

Check yourself

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

1What makes the runtime gate a control rather than a policy?

Answer: A. A policy depends on people remembering it. The gate runs before every request, and if the use case isn't live or the prompt hash differs from the approved one, nothing is sent to the model.

2Why does the register store a SHA-256 hash of the prompt file, not only its file name?

Answer: D. A file name stays the same when someone edits the file. The hash changes with any change to the content, which is how Step 7's one-line edit was refused.

3In cmd_approve, the proposer "A. Weber" tries to approve CHG-001 as AI lead. What happens and why?

Answer: D. Separation of duties means the person who made a change can't approve it, and one person can't approve in two roles. UC-01 is medium tier, so it needs a business owner and an AI lead, from two other people.

4Why does cmd_monitor judge only answers given with the current prompt hash?

Answer: C. Version 2's high override rate would be diluted by many good version 1 answers if all were mixed. Judging each approved version on its own answers makes a regression visible quickly.

5The change log's hash chain is intact. What does that prove?

Answer: D. Editing one entry breaks its hash and every later link. Someone with write access could rebuild the whole chain, which is why you anchor the latest hash in Git or a ticket. Who approved is only as trustworthy as the identity check behind --by.

6A team runs on the generative AI hub and wants change control for prompts. What fits best?

Answer: D. The declarative path keeps templates in Git, so pull requests give you history and reviewers. Fetching by ID returns the exact stored version, so unreviewed edits don't reach production as they would with "latest".

7The override rate for a live assistant breaches its limit at night. What should happen, in order?

Answer: C. The incident suspends the use case so the gate refuses requests, and the incident contact is told. It resumes only after a fix passes the same change control, which in the lab was an approved rollback.

8Your company switched on an AI feature delivered inside an SAP application. What belongs in your governance?

Answer: D. SAP governs how it builds the feature, but your company chose to switch it on and relies on it. You can't gate SAP's code, so you register the use, give it an owner and a measure, and control who may turn it on.

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