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.
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:
What do we run? A register lists every AI use case, including AI features switched on inside SAP applications.
How risky is it? Each use case has a risk tier, and the tier decides how much checking it needs.
Who said yes? Named people approved it, and the approvals match the tier.
What changed? Every change to a prompt, model or threshold is recorded and approved before it goes live.
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.
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.
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.
"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.
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.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
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.
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
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.
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.
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.
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.
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
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())
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())
Created ai_register.json with 5 use cases
Created prompts/blocked_order_v1.txt sha256 63ff41a90d7e
Created change_log.jsonl with 5 entries
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
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.
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
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.
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.
Simulate 40 clerk sessions. About 1 in 10 answers is overridden by default:
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
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).
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:
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.
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
Ask the service. It answers REFUSED UC-01 is suspended. Nothing was sent to the model.
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.
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.
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.
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 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 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.
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.
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.
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}"
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.")
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:
It refuses: A high tier use case needs legal_review recorded in the register before go-live.
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.
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.
Done whencheck 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.
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.
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.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Related topics
Picked from the terms, links and tags this topic shares with others.
Introducing Responsible AI at SAP (SAP Learning, Positioning SAP Business AI)— Guiding Principles for AI (2018); Global AI Ethics Policy (2022), binding for all SAP employees, aligned with the UNESCO Recommendation in 2024; AI Ethics Committee and Global AI Ethics Advisory Panel; all AI use cases get an ethics assessment, high-risk ones reviewed by the committee and panel
What is generative AI hub? (SAP product page)— customer data never shared with third-party vendors for training their LLMs; built-in governance; access management through SAP Cloud Identity Services; audit logging; prompt lifecycle management
SAP SE ISO 42001 Certificate (SAP document)— certificate scope covers governing the use of AI in SAP and SAP products and providing AI through SAP products; Statement of Applicability dated 28 July 2025
Managing Prompts with the Prompt Registry and Templates (SAP Learning)— version-controlled prompt templates by scenario, name and version; imperative API with edit history; declarative YAML in a Git repository synced by SAP AI Core; retrieval by ID returns the exact stored version
SAP AI Agent Hub (SAP product page)— vendor-agnostic AI registry of agents, LLMs and MCP servers with classification, status and fact sheets; verification captures risk rating and compliance mappings; only verified MCP servers in production workflows
SAP AI Agent Hub announcement (SAP LeanIX product updates, 12 May 2026)— AI agent hub renamed SAP AI Agent Hub and offered through Joule Studio; discovery of ServiceNow agents and SAP AI Core model deployments; verified or rejected badge; architecture decisions; observability, identity and access control and agent mining listed as outlook
AI RMF Core (NIST AI Resource Center)— GOVERN, MAP, MEASURE, MANAGE; GOVERN 1.6 inventory of AI systems; GOVERN 1.7 decommissioning; GOVERN 2.1 roles and responsibilities; GOVERN 6.1 third-party risk; MANAGE 4.3 incident communication
ISO/IEC 42001:2023 AI management systems (ISO)— requirements for establishing, implementing, maintaining and continually improving an AI management system; first edition published December 2023