A forward deployed engineer (FDE) is hired for one claim: "I can move a business number in a real process, safely." A CV can only say that. A portfolio can show it.
By now a learner on this course has working projects: a blocked sales orders triage, an invoice exception copilot, a capstone exception platform. Each one runs on made-up, SAP-shaped data and has an evaluation, a human approval step and a KPI. This topic is about turning them into evidence that a stranger can check in ten minutes.
Three things do most of the work:
A one-page case study per project: the problem, who feels it, what the system does, where a person approves, the numbers, and what you decided next.
A 3-minute demo that shows the decision, not the code.
Honest metrics: baseline, target, result, and a clear label when the result comes from synthetic data.
The second half of the topic is about the path itself: who hires FDEs around SAP as of November 2026, what their interviews test, and how a functional or technical SAP consultant gets there.
For a hiring manager or a client buying an FDE engagement, the risk is paying for someone who demos well and delivers nothing. FDE work is expensive because the person sits inside the business and owns a number. A wrong hire costs months of a process team's goodwill, not just a salary.
A good portfolio lowers that risk. It shows how the candidate thinks about the parts that usually sink AI on SAP: authorizations, a human approval step, an evaluation set, a baseline measured before go-live.
For the candidate, a portfolio replaces claims with checks. "I know agents" is a claim. A page that says "the procurement copilot cut the median time to decision from 25 to under 6 minutes on synthetic data, but buyers marked only 88% of explanations correct, below the 90% guardrail, so I fixed the explanations before claiming the speed" is a check. The reader can run it.
The honest version is the stronger one. A reviewer who sees a missed guardrail reported plainly learns that the candidate will tell a sponsor bad news early. That is the behavior an FDE is paid for.
SAP and its partners now hire for the role by name. As of November 2026:
SAP's own FDE organization. A Forward Deployed AI Engineer Expert posting in Garching, posted 1 September 2026, sits in SAP's Forward Deployed Engineering organization. It asks for 10+ years including significant customer-facing delivery, leading discovery workshops and executive presentations, and deep experience with agents, RAG, MCP, evaluation and AI observability on SAP BTP, SAP AI Core and Joule. A Forward Deployed Application/ML Engineering Expert posting in Dubai, posted 2 October 2026, asks for production LLM, RAG and agent applications and names SAP AI Core, Joule Studio, RPT-1 and Business Data Cloud as a strong advantage.
How SAP describes the work. An SAP News article of 13 October 2026 by SAP's Industry AI lead describes FDE as co-development with customers inside live operations, starting from the business value, with examples from Takeda and Doehler.
Partners. Accenture launched an FDE program with SAP in June 2026 (covered in What an SAP FDE does). Lemongrass, an SAP cloud migration firm, added FDE teams to its modernization method in August 2026.
The same model elsewhere. Salesforce launched an FDE partner network in April 2026 and ServiceNow and Accenture an FDE program in May 2026. Several of the firms in these programs are also large SAP partners.
Postings open and close quickly, and levels vary. Read the live posting before you tailor anything to it.
SAP certifications are a separate signal. They prove product knowledge under exam conditions; a portfolio proves you can find and solve a problem. Preparing for SAP's AI certifications compares the current exams.
"More projects make a stronger portfolio." Three well-framed projects beat ten demos. Reviewers read one case study closely, not ten.
"Synthetic data makes a project worthless." Reviewers know you can't publish client data. What they check is whether you label it and whether the method would hold on real data.
"A certification replaces a portfolio." It proves product knowledge. It doesn't prove you can find the right problem and ship a solution that people adopt.
"FDE interviews are coding tests." Coding is one round. Breaking a vague business problem into parts, and talking to the user, weigh as much.
"I can show my client project if I remove the name." Client work usually stays confidential under your contract. Rebuild the idea on made-up data instead.
Pick one answer for each question. The explanation appears after you choose.
1A candidate's portfolio shows a 70% time saving with no baseline source and no data label. What should a hiring manager conclude?
Answer: D. A result without a baseline source or a data label can't be checked. Asking how it was measured tests exactly what an FDE does on a real engagement. Synthetic results are fine when labeled.
2Why is reporting a missed guardrail a strength in a portfolio?
Answer: B. An FDE owns a number and must report problems before a sponsor finds them. A plainly reported miss, with the next step, shows that behavior. A page where everything succeeded shows nothing about it.
3An ABAP developer wants to move into FDE work. Which gap should they close first?
Answer: B. Developers bring the data model and clean core already. FDE work adds finding and framing the right problem and building with LLMs, which is where their gap usually is.
4What does an SAP certification prove that a portfolio doesn't?
Answer: C. Certifications test product knowledge under exam conditions. Finding problems and getting solutions adopted are what a portfolio and references show, so the two signals complement each other.
5A consultant wants to show a strong client project in their portfolio. What is the safe approach?
Answer: C. Client work usually stays confidential under the contract, even without names. Rebuilding the idea on made-up data shows the same thinking with nothing to leak.
6Which part of an FDE interview loop usually tests discovery rather than code?
Answer: D. In a decomposition round you break a vague business problem into parts, usually with little or no code. Interviewers score structure, user empathy and scoping, which is discovery work.
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.
A portfolio is an evidence file for one claim: "I can move metric M in SAP process P, safely." Every piece answers a question a reviewer would otherwise have to ask you.
Reviewer's question
Evidence that answers it
What problem did you solve, for whom?
The case study's problem and users
Did it work? Compared with what?
KPI table: baseline, target, result, source
Would it be safe in SAP?
Human-in-the-loop line, authorizations, the approval in the code
Can I trust your numbers?
Synthetic label, evaluation report, a guardrail metric
Can I reproduce it?
A run command with a sample path, a fresh-clone test
Can you explain it to my CFO?
The 3-minute demo
What do you do when it fails?
The decision line and what you learned
Treat the portfolio like the systems you built in this course: a manifest of facts, a check that refuses bad evidence, and a generated page. Then the page can't drift from the facts, and a careless claim fails the check before anyone sees it.
flowchart LR
P[Course projects<br/>orchestrate-course] -->|allowlist copy| R[sap-ai-portfolio<br/>public repo]
M[portfolio.json<br/>facts per project] --> C{check}
C -->|FAIL| F[fix the facts]
C -->|OK| B[build pages]
B --> D[docs/index.html<br/>+ case studies]
D -->|GitHub Pages| W[owner.github.io/<br/>sap-ai-portfolio]
R --> M
Set up for Unit 14 created sap-ai-portfolio as a separate public repository, filled through an allowlist. This topic adds two things to it: a facts file and a generated site.
sap-ai-portfolio/
README.md what this is, how to run it, the rules
LICENSE
requirements.txt only what the published projects need
portfolio.json the facts behind the landing page (new)
docs/ generated landing page and case studies (new)
index.html
invoice-copilot.html
unit01/triage.py
unit14/exception_hub.py
projects/
invoice-exception-copilot/
README.md problem, KPI, how to run each stage
reports/eval_offline.md
reports/kpi_report_simulated.md
Each project folder follows the Portfolio checklist in its own topic: README, architecture diagram, run instructions, evaluation report, a one-page design summary and a demo script. The landing page sits above them and links to each.
A case study is not a README. The README tells someone how to run the project. The case study tells someone why it matters and whether to believe it. Keep it to the sections the generated page uses:
The problem, in the process owner's words, with a number: "Buyers spend about 25 minutes per late PO line finding the cause."
Who feels it: the role, not "the business".
What it does: one or two sentences on what code does and what the model does.
Human in the loop: who approves what, and that the system never changes SAP data alone.
Results: a table with baseline, target, result and source for each KPI, including one guardrail.
Decision: scale, iterate, fix first or stop, and why.
Run it yourself: the folder and a command with a sample path.
3-minute demo: the steps, with seconds.
What I learned: one sentence a reviewer can ask you about.
Technical storytelling gives the structure for briefs. A demo follows the same order in four beats:
Beat
Time
Show
Problem
~30 s
One real-looking case and what the user does today
The system at work
~60 s
The output with its evidence, in the user's terms
The control
~45 s
A person approves; the system can't act alone; a bad input is refused
The number
~45 s
The KPI against baseline and target, and the decision
Record it once as a screen recording if you can, and link it from the README. The written steps on the case study page let a reviewer follow without sound.
Reviewers look for four properties in every number:
A baseline with a source. "17 minutes, shadowing of 8 orders" beats "17 minutes".
A target agreed before the result. A target set afterwards proves nothing.
A guardrail. Speed without accuracy is a liability. Every project in this course has one: explanation accuracy, acceptance rate or flags per week.
A source label.synthetic or measured. A measured number needs evidence a reviewer could ask about, such as "pilot log, 2 weeks, 40 cases".
Cost per case and the share of proposals people overrule are good second-line numbers. Model accuracy alone is not a business metric; put it in the evaluation report, not the headline.
FDE hiring loops differ by company. An interview-preparation site's analysis, updated August 2026, describes these rounds at Palantir, OpenAI, Anthropic and Google. It is a third party's summary, not the companies' own pages, so treat it as a guide to the shape:
Round
What it tests
Portfolio piece that prepares you
Course topic
Decomposition
Breaking a vague real-world problem into parts; user empathy; scoping; handling pushback
The same source says behavioral questions run through most rounds rather than sitting in one. SAP's own postings point the same way: the Garching expert role names discovery workshops and executive presentations next to the technical stack.
An SAP-shaped decomposition prompt to practise with: "Calder Pumpworks, a fictional pump maker, says its credit team is slow. Where would you start?" A strong answer asks who is slow at what, how it is measured, which orders block and why, what SAP standard already does, what data and authorizations exist, and what a first measurable target would be. It doesn't name a model in the first ten minutes.
The foundational layer's table lists the gap for each starting role. Two practical points:
Make your old role visible in the work. A functional consultant's case study should read like someone who has configured credit management: the right block reasons, the right document flow, the right roles. That is the rare half of the FDE profile, and SAP's postings ask for it next to the AI stack.
Build one thing end to end yourself. Reviewers can tell a project assembled from a tutorial from one where you made the decisions. The decision line and "what I learned" are where that shows.
You will write a small script that reads a facts file, portfolio.json, checks it for the mistakes reviewers punish, and builds a landing page plus one case study page per project. Then you publish the pages for free with GitHub Pages.
Before you start: complete Set up your computer for this course and Set up for Unit 14. They create your orchestrate-course folder with its .venv, and the public sap-ai-portfolio repository next to it. This walkthrough doesn't repeat those steps. The script uses built-in Python only, so there is nothing to install.
In VS Code, right-click the unit14 folder, choose New File, name it build_portfolio_page.py, paste the code below and save.
"""Check a portfolio manifest and build a static landing page from it.
Built-in Python only. Examples:
python unit14/build_portfolio_page.py --sample
python unit14/build_portfolio_page.py --manifest ../sap-ai-portfolio/portfolio.json --repo ../sap-ai-portfolio
python unit14/build_portfolio_page.py --manifest ../sap-ai-portfolio/portfolio.json --check-only
"""
import argparse
import html
import json
import os
import re
import sys
from pathlib import Path
DISCLAIMER = ("Independent learning work on made-up, SAP-shaped data. "
"Not affiliated with or endorsed by SAP.")
SYNTHETIC_NOTE = "Synthetic data: shows the method, not a result from a real SAP system."
SAMPLE = {
"profile": {
"name": "Your Name",
"headline": "SAP order-to-cash consultant moving into forward deployed engineering",
"about": ("Eight years configuring sales and credit management. I now build small AI "
"services around SAP processes, each with an evaluation, a human approval "
"step and a business KPI."),
"repo_url": "https://github.com/YOUR-NAME/sap-ai-portfolio",
"links": [{"label": "GitHub", "url": "https://github.com/YOUR-NAME"}],
"disclaimer": DISCLAIMER,
},
"projects": [
{
"id": "exception-hub",
"title": "Exception management platform (capstone)",
"process": "Order-to-cash, procure-to-pay, plan-to-produce",
"folder": "unit14",
"problem": "Six teams work SAP exceptions in six worklists; nobody sees the total or the trend.",
"users": "Credit analysts, AP clerks, planners, buyers",
"approach": "One case record per exception, playbooks that propose an action, one approval gate.",
"human_in_loop": "Every proposal waits for a named person; the proposer cannot approve it.",
"status": "sample data",
"kpis": [
{"name": "Credit: median minutes per blocked order", "baseline": "17",
"target": "10", "result": "9.4", "source": "synthetic"},
{"name": "AP: share of proposals accepted", "baseline": "none",
"target": "below 97%", "result": "100%", "source": "synthetic", "guardrail": True},
],
"next_step": "Fix first: AP may be rubber-stamping, and planners reject too many proposals.",
"run": "python unit14/exception_hub.py sample",
"demo": [
{"seconds": 30, "show": "The business problem and the 17-minute baseline"},
{"seconds": 60, "show": "Triage the sample cases into one inbox"},
{"seconds": 45, "show": "Approve as a second person; show the proposer cannot"},
{"seconds": 45, "show": "The KPI table and why the report says fix first"},
],
"lessons": "The flag I nearly ignored (AP accepting everything) was the most important finding.",
},
{
"id": "invoice-copilot",
"title": "Invoice exception copilot (three-way match)",
"process": "Procure-to-pay",
"folder": "projects/invoice-exception-copilot",
"problem": "Blocked supplier invoices take about 30 minutes each to explain and resolve.",
"users": "Accounts payable clerks",
"approach": "Code finds the match differences; the model writes the explanation; the clerk decides.",
"human_in_loop": "Releasing or changing an invoice needs the clerk's approval; the copilot only drafts.",
"status": "sample data",
"kpis": [
{"name": "Median minutes per exception", "baseline": "30", "target": "10",
"result": "about 8", "source": "synthetic"},
{"name": "First-pass match rate", "baseline": "68%", "target": "80%",
"result": "73%", "source": "synthetic"},
{"name": "Explanations marked correct", "baseline": "none", "target": "90%",
"result": "96%", "source": "synthetic", "guardrail": True},
],
"next_step": "Talk to purchasing about PO prices: the largest cause is upstream.",
"run": "python projects/invoice-exception-copilot/sap_data.py --sample",
"demo": [
{"seconds": 30, "show": "One blocked invoice and what the clerk does today"},
{"seconds": 60, "show": "The copilot's explanation with the evidence"},
{"seconds": 45, "show": "The clerk approves; the log records the decision"},
{"seconds": 45, "show": "The three KPIs and why match rate barely moved"},
],
"lessons": "Speed came from the copilot; the match rate only moves when the causes are fixed.",
},
{
"id": "procurement-copilot",
"title": "Procurement copilot for late PO lines",
"process": "Procure-to-pay",
"folder": "projects/procurement-copilot",
"problem": "Buyers spend about 25 minutes per late purchase order line finding the cause.",
"users": "Buyers",
"approach": "Code computes the delay facts; the model explains them and proposes an action.",
"human_in_loop": "The buyer approves any change before it reaches SAP.",
"status": "sample data",
"kpis": [
{"name": "Median minutes to decision", "baseline": "25", "target": "10",
"result": "under 6", "source": "synthetic"},
{"name": "Explanations marked correct", "baseline": "none", "target": "90%",
"result": "88%", "source": "synthetic", "guardrail": True},
],
"next_step": "Fix the explanations before claiming the speed: the guardrail is missed.",
"run": "python projects/procurement-copilot/sap_data.py --sample",
"demo": [
{"seconds": 30, "show": "A late PO line and the buyer's question"},
{"seconds": 60, "show": "The explanation and the proposed action"},
{"seconds": 30, "show": "The hidden-instructions test line being refused"},
{"seconds": 40, "show": "The KPI and the missed guardrail"},
],
"lessons": "A fast wrong answer is worse than a slow right one; the guardrail says which.",
},
],
}
REQUIRED_PROFILE = ["name", "headline", "disclaimer"]
REQUIRED_PROJECT = ["id", "title", "process", "folder", "problem", "users", "approach",
"human_in_loop", "status", "kpis", "next_step", "run", "demo"]
STATUSES = ["sample data", "pilot", "production"]
HYPE = ["revolutionary", "game-changing", "game changing", "cutting-edge", "world-class",
"seamless", "10x", "magic"]
SECRET_PATTERNS = [
(re.compile(r"sk-[A-Za-z0-9_-]{16,}"), "looks like a model API key"),
(re.compile(r"AKIA[0-9A-Z]{16}"), "looks like a cloud access key"),
(re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"), "looks like a private key"),
(re.compile(r"(?i)(api[_-]?key|client[_-]?secret|password)\s*[:=]\s*\S{6,}"),
"looks like a key or password"),
]
EMAIL = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
SLUG = re.compile(r"^[a-z0-9][a-z0-9-]{1,40}$")
def all_text(value, path="manifest"):
"""Yield (path, text) for every string in the manifest."""
if isinstance(value, dict):
for k, v in value.items():
yield from all_text(v, f"{path}.{k}")
elif isinstance(value, list):
for i, v in enumerate(value):
yield from all_text(v, f"{path}[{i}]")
elif isinstance(value, str):
yield path, value
def check(manifest, repo=None):
"""Return a list of (level, code, message). Level is OK, WARN or FAIL."""
out = []
prof = manifest.get("profile", {})
missing = [k for k in REQUIRED_PROFILE if not str(prof.get(k, "")).strip()]
out.append(("FAIL" if missing else "OK", "P01",
f"profile is missing {', '.join(missing)}" if missing else "profile"))
projects = manifest.get("projects", [])
if not projects:
out.append(("FAIL", "P02", "no projects"))
ids = [p.get("id", "") for p in projects]
for p in projects:
pid = p.get("id") or "(no id)"
miss = [k for k in REQUIRED_PROJECT if not p.get(k)]
if miss:
out.append(("FAIL", "P02", f"{pid}: missing {', '.join(miss)}"))
if not SLUG.match(pid) or ids.count(pid) > 1:
out.append(("FAIL", "P10", f"{pid}: id must be unique, lowercase letters, digits and dashes"))
if p.get("status") and p["status"] not in STATUSES:
out.append(("FAIL", "P02", f"{pid}: status must be one of {', '.join(STATUSES)}"))
kpis = p.get("kpis", [])
for k in kpis:
name = k.get("name", "(no name)")
gaps = [f for f in ("name", "baseline", "target", "result") if not str(k.get(f, "")).strip()]
if gaps:
out.append(("FAIL", "P03", f"{pid}: KPI '{name}' is missing {', '.join(gaps)}"))
if k.get("source") not in ("synthetic", "measured"):
out.append(("FAIL", "P03", f"{pid}: KPI '{name}' needs source synthetic or measured"))
if k.get("source") == "measured" and not str(k.get("evidence", "")).strip():
out.append(("FAIL", "P04", f"{pid}: KPI '{name}' says measured but gives no evidence"))
measured = [k for k in kpis if k.get("source") == "measured" and k.get("evidence")]
if p.get("status") in ("pilot", "production") and not measured:
out.append(("FAIL", "P04", f"{pid}: status '{p['status']}' needs at least one measured KPI"))
if kpis and not any(k.get("guardrail") for k in kpis):
out.append(("WARN", "P03", f"{pid}: no guardrail KPI (what must not get worse?)"))
if p.get("run") and "--sample" not in p["run"] and " sample" not in p["run"]:
out.append(("FAIL", "P05", f"{pid}: run command has no sample path a stranger can try"))
demo = p.get("demo", [])
total = sum(int(s.get("seconds", 0)) for s in demo)
if demo and not 60 <= total <= 180:
out.append(("FAIL", "P06", f"{pid}: demo runs {total} s; keep it between 60 and 180"))
if repo is not None and p.get("folder") and not (repo / p["folder"]).exists():
out.append(("FAIL", "P09", f"{pid}: folder {p['folder']} not found in {repo}"))
for path, text in all_text(manifest):
for rx, why in SECRET_PATTERNS:
if rx.search(text):
out.append(("FAIL", "P07", f"{path}: {why}"))
for m in EMAIL.finditer(text):
if not text.startswith("mailto:"):
out.append(("FAIL", "P07", f"{path}: email address {m.group(0)} (use a mailto: link in profile.links)"))
low = text.lower()
for word in HYPE:
if re.search(r"(?<![a-z])" + re.escape(word) + r"(?![a-z])", low):
out.append(("WARN", "P08", f"{path}: hype word '{word}'"))
if repo is None:
out.append(("WARN", "P09", "folders not checked (no --repo given)"))
if not any(level == "FAIL" and code != "P01" for level, code, _ in out):
out.append(("OK", "P02", f"{len(projects)} projects"))
return out
CSS = """
:root{--bg:#fbfaf7;--fg:#1d1f21;--muted:#5b6066;--card:#ffffff;--line:#e2ded6;--accent:#0b6b5d;--warn:#8a5a00;--warnbg:#fff4dc}
@media (prefers-color-scheme:dark){:root{--bg:#15171a;--fg:#e9e7e2;--muted:#a4a8ad;--card:#1d2024;--line:#33373c;--accent:#5cc8b5;--warn:#f2c46b;--warnbg:#3a2f17}}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--fg);font:16px/1.55 system-ui,-apple-system,Segoe UI,sans-serif}
main{max-width:960px;margin:0 auto;padding:32px 16px 64px}h1{font-size:1.9rem;margin:0 0 4px}h2{margin-top:2rem}
.muted{color:var(--muted)}a{color:var(--accent)}.grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fill,minmax(280px,1fr))}
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:16px}.card h3{margin:0 0 6px;font-size:1.1rem}
.tag{display:inline-block;font-size:.8rem;border:1px solid var(--line);border-radius:999px;padding:1px 8px;margin-right:4px}
.synthetic{background:var(--warnbg);color:var(--warn);border-radius:6px;padding:6px 10px;font-size:.9rem}
table{border-collapse:collapse;width:100%;font-size:.95rem}th,td{border-bottom:1px solid var(--line);padding:6px 8px;text-align:left;vertical-align:top}
code{background:var(--card);border:1px solid var(--line);border-radius:4px;padding:1px 4px;overflow-wrap:anywhere}
footer{margin-top:48px;font-size:.85rem;color:var(--muted)}
"""
def esc(value):
return html.escape(str(value), quote=True)
def page(title, body, disclaimer):
return (f'<!doctype html>\n<html lang="en"><head><meta charset="utf-8">'
f'<meta name="viewport" content="width=device-width,initial-scale=1">'
f"<title>{esc(title)}</title><style>{CSS}</style></head>"
f"<body><main>{body}<footer>{esc(disclaimer)}</footer></main></body></html>\n")
def has_synthetic(p):
return any(k.get("source") == "synthetic" for k in p.get("kpis", []))
def headline_kpi(p):
k = p["kpis"][0]
label = " (synthetic)" if k.get("source") == "synthetic" else ""
return f"{k['name']}: {k['baseline']} to {k['result']}, target {k['target']}{label}"
def build_index(m):
prof = m["profile"]
links = " · ".join(f'<a href="{esc(l["url"])}">{esc(l["label"])}</a>' for l in prof.get("links", []))
cards = []
for p in m["projects"]:
cards.append(
f'<div class="card"><h3><a href="{esc(p["id"])}.html">{esc(p["title"])}</a></h3>'
f'<p><span class="tag">{esc(p["process"])}</span><span class="tag">{esc(p["status"])}</span></p>'
f'<p>{esc(p["problem"])}</p><p class="muted">{esc(headline_kpi(p))}</p>'
f'<p><strong>Next:</strong> {esc(p["next_step"])}</p></div>')
note = (f'<p class="synthetic">{esc(SYNTHETIC_NOTE)} Results marked synthetic are labelled on every page.</p>'
if any(has_synthetic(p) for p in m["projects"]) else "")
body = (f'<h1>{esc(prof["name"])}</h1><p class="muted">{esc(prof["headline"])}</p>'
f'<p>{esc(prof.get("about", ""))}</p><p>{links}</p>{note}'
f'<h2>Projects</h2><div class="grid">{"".join(cards)}</div>')
return page(f'{prof["name"]}: portfolio', body, prof["disclaimer"])
def build_case(m, p):
prof = m["profile"]
repo = prof.get("repo_url", "").rstrip("/")
folder = (f'<a href="{esc(repo)}/tree/main/{esc(p["folder"])}"><code>{esc(p["folder"])}</code></a>'
if repo else f'<code>{esc(p["folder"])}</code>')
rows = "".join(
f'<tr><td>{esc(k["name"])}{" (guardrail)" if k.get("guardrail") else ""}</td><td>{esc(k["baseline"])}</td>'
f'<td>{esc(k["target"])}</td><td>{esc(k["result"])}</td>'
f'<td>{esc(k["source"])}{": " + esc(k["evidence"]) if k.get("evidence") else ""}</td></tr>'
for k in p["kpis"])
note = f'<p class="synthetic">{esc(SYNTHETIC_NOTE)}</p>' if has_synthetic(p) else ""
demo = "".join(f'<li>{esc(s["show"])} <span class="muted">({int(s["seconds"])} s)</span></li>' for s in p["demo"])
total = sum(int(s["seconds"]) for s in p["demo"])
lessons = f'<h2>What I learned</h2><p>{esc(p["lessons"])}</p>' if p.get("lessons") else ""
body = (f'<p><a href="index.html">All projects</a></p><h1>{esc(p["title"])}</h1>'
f'<p><span class="tag">{esc(p["process"])}</span><span class="tag">{esc(p["status"])}</span></p>'
f'<h2>The problem</h2><p>{esc(p["problem"])}</p><p><strong>Who feels it:</strong> {esc(p["users"])}</p>'
f'<h2>What it does</h2><p>{esc(p["approach"])}</p>'
f'<h2>Human in the loop</h2><p>{esc(p["human_in_loop"])}</p>'
f'<h2>Results</h2>{note}<table><tr><th>KPI</th><th>Baseline</th><th>Target</th><th>Result</th>'
f'<th>Source</th></tr>{rows}</table>'
f'<h2>Decision</h2><p>{esc(p["next_step"])}</p>'
f'<h2>Run it yourself</h2><p>Code: {folder}</p><p><code>{esc(p["run"])}</code></p>'
f'<h2>3-minute demo ({total} s)</h2><ol>{demo}</ol>{lessons}')
return page(f'{p["title"]}: {prof["name"]}', body, prof["disclaimer"])
def rel(path):
"""Show a path relative to the current folder when possible."""
try:
return os.path.relpath(path)
except ValueError:
return str(path)
def main():
ap = argparse.ArgumentParser(description="Check a portfolio manifest and build its landing page.")
ap.add_argument("--sample", action="store_true", help="write and build the sample manifest")
ap.add_argument("--manifest", type=Path, help="path to your portfolio.json")
ap.add_argument("--repo", type=Path, help="portfolio repository folder, to check project folders exist")
ap.add_argument("--out", type=Path, help="output folder (default: docs next to the manifest)")
ap.add_argument("--check-only", action="store_true", help="run the checks, write nothing")
args = ap.parse_args()
if args.sample:
base = Path(__file__).resolve().parent / "portfolio_sample"
base.mkdir(parents=True, exist_ok=True)
args.manifest = base / "portfolio.json"
args.manifest.write_text(json.dumps(SAMPLE, indent=2) + "\n", encoding="utf-8")
print(f"Wrote sample manifest {rel(args.manifest)}")
if not args.manifest:
ap.error("give --manifest PATH, or --sample")
manifest_path = args.manifest.expanduser()
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except FileNotFoundError:
sys.exit(f"Manifest not found: {manifest_path}")
except json.JSONDecodeError as e:
sys.exit(f"Manifest is not valid JSON: line {e.lineno}, column {e.colno}: {e.msg}")
repo = args.repo.expanduser() if args.repo else None
results = check(manifest, repo)
for level, code, msg in results:
print(f"{level:<4} {code} {msg}")
fails = sum(1 for r in results if r[0] == "FAIL")
warns = sum(1 for r in results if r[0] == "WARN")
print(f"{fails} FAIL, {warns} WARN")
if fails:
sys.exit("Nothing written. Fix the FAIL lines and run again.")
if args.check_only:
return
out = (args.out or manifest_path.parent / "docs").expanduser()
out.mkdir(parents=True, exist_ok=True)
(out / "index.html").write_text(build_index(manifest), encoding="utf-8")
for p in manifest["projects"]:
(out / f"{p['id']}.html").write_text(build_case(manifest, p), encoding="utf-8")
print(f"Wrote {rel(out / 'index.html')} and {len(manifest['projects'])} case study pages")
if __name__ == "__main__":
main()
Wrote sample manifest unit14/portfolio_sample/portfolio.json
OK P01 profile
WARN P09 folders not checked (no --repo given)
OK P02 3 projects
0 FAIL, 1 WARN
Wrote unit14/portfolio_sample/docs/index.html and 3 case study pages
On Windows the paths show with backslashes. The WARN is expected: the sample has no repository to check folders against.
Open the page in your browser. In VS Code's Explorer, right-click unit14/portfolio_sample/docs/index.html and choose Reveal in File Explorer (Windows) or Reveal in Finder (macOS), then double-click the file. On Linux, open the file from your file manager.
You should see your name placeholder, a one-line headline, a yellow note saying the results are synthetic, and three project cards. Click Procurement copilot for late PO lines. The case study shows the missed guardrail (88% against a 90% target) and the decision to fix the explanations first.
OK P01 profile
FAIL P04 exception-hub: status 'production' needs at least one measured KPI
FAIL P04 procurement-copilot: KPI 'Median minutes to decision' says measured but gives no evidence
WARN P09 folders not checked (no --repo given)
2 FAIL, 1 WARN
Nothing written. Fix the FAIL lines and run again.
Nothing was written: the page you built in Step 3 is unchanged. This is the point. A sample result can't quietly become a "measured" claim, and a project on made-up data can't call itself production.
Run python unit14/build_portfolio_page.py --sample again to restore the sample.
In VS Code, open sap-ai-portfolio/portfolio.json (File > Open File). Edit it:
profile.name, headline and about: you, in plain words. Say where you come from ("SAP MM consultant", "ABAP developer").
repo_url and the GitHub link: replace YOUR-NAME with your GitHub user name.
projects: keep only projects you have actually copied into sap-ai-portfolio. Replace the sample numbers with the ones your own runs printed, from each project's reports/ files or the capstone's capstone_report.md.
run: the exact command a stranger should run first. It must include a sample path (--sample, or sample for the capstone).
demo: four steps whose seconds add up to between 60 and 180.
Keep "source": "synthetic" on every number from the course. Use "measured" only for numbers from a real pilot you are allowed to share, and add "evidence" saying how it was measured.
With two projects whose folders exist, you should see something like:
OK P01 profile
OK P02 2 projects
0 FAIL, 0 WARN
Wrote ../sap-ai-portfolio/docs/index.html and 2 case study pages
A line such as FAIL P09 procurement-copilot: folder projects/procurement-copilot not found means the project isn't in the portfolio yet. Add it to portfolio.txt and run python publish_portfolio.py --copy as in Set up for Unit 14, or remove it from portfolio.json.
Open sap-ai-portfolio/docs/index.html in your browser as in Step 3 and read it as a stranger would. Fix wording in portfolio.json, never in the HTML: the next build overwrites the HTML.
In your browser, open https://github.com/YOUR-NAME/sap-ai-portfolio.
Click Settings (under the repository name).
In the left sidebar, in the Code, planning, and automation section, click Pages.
Under Build and deployment, under Source, choose Deploy from a branch.
In the branch menu (it shows None), choose main. In the folder menu next to it (it shows /(root)), choose /docs.
Click Save.
Wait a minute or two, then open https://YOUR-NAME.github.io/sap-ai-portfolio/. Refresh the Settings > Pages screen: when the site is live, GitHub shows its address there.
What success looks like: your landing page at that address, with your projects and the synthetic-data note.
Significant customer-facing delivery, 10+ years (Garching expert level)
Not something a portfolio can replace; client references do
The Garching posting is a senior architect-level role; the Dubai posting is an engineering expert role. Both list 0 to 10% expected travel, and both describe customer-facing work: the Dubai role embeds in customer projects, the Garching role is the main technical contact for customer executives. Entry and associate roles have also been posted; What an SAP FDE does summarizes the associate profile from earlier in 2026. Check the live posting: they change.
Where SAP's own FDE work starts. SAP's 13 October article describes starting from the customer's desired business value and working backwards, inside live operations. That is the metric-first framing of problem framing, and a portfolio built on it speaks the same language.
Partners. Large SAP partners now run FDE practices on several platforms (Salesforce's partner network names Accenture, Deloitte, Capgemini, IBM Consulting, KPMG and PwC among others), and Accenture has a program with SAP. A candidate aiming at a partner should expect the client-facing half to weigh heavily: scoping, estimating and adoption, all in Unit 14.
Certifications. SAP's AI-related certifications test product skills in practical exams; Preparing for SAP's AI certifications lists them by role. Link the certification badge from your portfolio's profile; don't let it stand in for a project.
Where it sits. The portfolio is outside any SAP landscape on purpose. It runs on made-up data and reaches no SAP system:
flowchart LR
subgraph Laptop
C[orchestrate-course<br/>private: keys, logs]
B[build_portfolio_page.py]
end
subgraph GitHub
R[sap-ai-portfolio<br/>public repo]
PG[GitHub Pages<br/>static site]
end
C -->|allowlist copy| R
R -->|portfolio.json| B
B -->|docs/| R
R --> PG
V[Reviewer] --> PG
V -->|clone + --sample| R
Non-functional requirements that decide the design:
Requirement
Target
What breaks if missed
Confidentiality
No keys, client names or real data in the public repo
A security incident or a breached contract, in public
Honesty of numbers
Every result has a source; synthetic is labeled on the page
Credibility, the first time a reviewer asks
Reproducibility
A fresh clone runs each project with --sample, no account
"Works on my machine" in front of a hiring manager
Time to read
Landing page understood in one minute, case study in three
The reviewer moves on
Cost and upkeep
Free hosting; rebuild in seconds after a change
Stale pages, or a bill for something that should be free
Integration and identity. No SAP identity or API is involved. The only identity is your GitHub account, which pushes to the repository; visitors are anonymous. GitHub's documentation states a Pages site is public even if the repository is private, so the repository's privacy is no protection. Keys stay in the course folder's .env, behind the allowlist and the secret scan from Set up for Unit 14.
Context: a public page must stay consistent with each project's results, contain nothing private, and be cheap to keep current.
Options: A) write the HTML by hand; B) a static site generator or hosted website builder; C) a facts file plus a small checked build script.
Decision: C. The facts live in portfolio.json; build_portfolio_page.py refuses to publish unlabeled or unsupported claims and writes plain HTML that GitHub Pages serves from /docs.
Consequences: Pages can't drift from the facts, and the checks run every time. The design is plain; anyone wanting more styling edits the CSS in the script. New sections mean changing the script, not the HTML.
What a design review will ask:
What stops a key or client name from reaching the public site?
How does a reviewer know which numbers are synthetic?
Can a stranger run every project without your accounts?
What happens to the page when a project's results change?
What would you show instead for work you are not allowed to publish?
Confidentiality and IP. Client work, your employer's code and anything under NDA stay out. Check your employment contract on side projects and public code; some employers require approval. Rebuild ideas on Calder's fictional data instead.
Secrets. The script's P07 rule and GitHub's push protection catch common key formats; they don't catch everything. The allowlist from Set up for Unit 14 stays your main control.
Trademarks and endorsement. Name SAP products factually. Don't use SAP logos, and keep the disclaimer that the work is independent and not endorsed by SAP. The generated footer carries it.
Accuracy over time. Product names and statuses change. Date anything time-sensitive in a README ("as of November 2026, Joule Studio…"), and rebuild after each change.
Privacy. Commits carry an email address. Keep using the no-reply address from Set up for Unit 14. Put only contact details you are happy to make public on the page.
Accessibility. The generated page uses real headings, tables and links, and works at phone width. Add alt text to any image you link from READMEs.
Upkeep. Run the fresh-clone test from Set up for Unit 14 before you send the link to anyone. A broken run command costs more than a missing project.
Leading with the tech stack. "LangGraph, MCP, RAG" says nothing about the business. Lead with the problem and the number.
Rounding a synthetic number into a claim. "Cut handling time by 45%" on a CV, from a simulated month, is a misrepresentation. Keep the label wherever the number goes.
Ten half-finished projects. Reviewers open one. Make sure every one is finished, or remove it.
Hiding the failures. A missed guardrail, honestly reported, is your best evidence of judgment.
A demo that is a code tour. Show the user's decision and the number; keep the code for the technical interview.
Copying the course word for word. Reviewers will meet other learners with the same projects. Your framing, your module knowledge, your extension in each Exercise and your lessons are what make it yours.
Ignoring the SAP half. A portfolio that could be about any company misses the rare part of the profile. Show SAP documents, roles, approvals and clean core choices.
Make the capstone your lead project, and practise the interview round that tests discovery.
Run the capstone's report again in the course folder, so your numbers are current:
python unit14/exception_hub.py report
Open unit14/capstone/capstone_report.md. In sap-ai-portfolio/portfolio.json, update the exception-hub project with your own numbers: the credit queue's median, baseline and target, one guardrail (AP acceptance) and the decision line the report printed. Keep "source": "synthetic".
Make it the first project in the projects list, so it is the first card.
Write the demo for it in four beats that add up to 150 to 180 seconds, then rehearse it once with a timer.
Commit and push from sap-ai-portfolio as in Step 7, then open your Pages address.
Practise a decomposition round. Ask a friend or colleague to read you this prompt and play the customer for 30 minutes: "Calder Pumpworks, a fictional pump maker, says its credit team is slow. Where would you start?" Don't name a model or tool in the first ten minutes.
In the course folder, create unit14/career/interview_notes.md with three headings: Questions I asked, Where I jumped to a solution, What I would ask first next time. Fill each with two or three lines.
Optional: create a GitHub profile README. GitHub's docs say it needs a public repository named exactly like your user name with a README.md in its root. Put two lines and your Pages link in it.
Commit the notes in the course folder:
git add unit14/career/interview_notes.md
git commit -m "Unit 14: portfolio lead project and decomposition practice"
Done when your Pages address shows the capstone as the first card with your own numbers labeled synthetic, the build prints 0 FAIL, the capstone's demo adds up to 150 to 180 seconds, and interview_notes.md is committed. The portfolio link is what you send with every application from now on.
Pick one answer for each question. The explanation appears after you choose.
1Why does the build script refuse to write any page when one check fails?
Answer: B. The page is public the moment it is pushed. Refusing to write on any FAIL means a mislabeled number or a key can't slip through while the rest of the page looks fine.
2A learner changes a sample KPI's source from synthetic to measured without adding evidence. What happens, and why?
Answer: B. P04 requires an evidence string for any measured KPI, and a measured KPI before a project may claim pilot or production. That stops a sample number from being promoted into a business claim.
3Why does the script escape every manifest value with html.escape?
Answer: B. The manifest is free text that ends up in HTML. Escaping turns characters such as < and > into text, so nothing typed into a field can add tags or scripts to the page.
4Your portfolio repository is private and you publish docs/ with GitHub Pages on a paid plan. What is true?
Answer: B. GitHub's docs state that Pages sites are public on the internet even when the repository is private. Repository privacy doesn't protect what is in docs/, which is why the checks run before publishing.
5In an FDE interview's decomposition round, you are told "the credit team is slow". What should your first ten minutes focus on?
Answer: D. Decomposition tests breaking a vague problem into parts and scoping it with the user. Naming a model or writing code first skips discovery, which is the skill the round is scoring.
6SAP's Garching expert posting asks for evaluation and AI observability. Which portfolio evidence answers that best?
Answer: C. Evaluation reports and decision logs show you measured quality and watched it over time. A badge or a list of tools names the skill without showing it in use.
7A reviewer asks for a client project you delivered last year under NDA. What is the best response?
Answer: C. Confidential work stays private even without names. You can describe it as far as your contract allows, ideally backed by a reference, and point to the public project that shows the same skills on made-up data.
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.
SAP job posting: Forward Deployed AI Engineer Expert (f/m/d), Garching (requisition 457547)— Opened 2026-11-03. Posted 1 Sep 2026 in SAP's Forward Deployed Engineering organization; 10+ years with significant customer-facing delivery; LLMs, agentic AI, RAG, GraphRAG, knowledge graphs, vector databases, MCP, tool calling, evaluation and AI observability; SAP BTP, SAP AI Core, Joule and SAP integration; discovery workshops and executive presentations; travel 0-10%
Configuring a publishing source for your GitHub Pages site (GitHub Docs)— Opened 2026-11-03. Settings > Pages > Build and deployment > Source: Deploy from a branch; branch and /(root) or /docs folder; Save. Free plan: public repositories only. Pages sites are public even if the repository is private