Orchestrate

Report an issue

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

0 / 2000

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

An AI roadmap and operating model for an SAP customer

Sequence assessed AI use cases into an 18-month roadmap around data and migration dates, and set up the teams, funding and reporting that keep it honest.

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

The 60-second version

After a few opportunity assessments, a company running SAP has a list: ten or twenty AI ideas, each with a value, a cost and a risk. The steering committee asks two questions. In what order do we do them? And who does the work?

The first answer is the roadmap: the ideas placed in quarters over the next 12 to 24 months. Three things decide the order, more than the size of the prize:

  1. What each idea needs first. Shared platform, clean data, a finished migration, a recorded history to learn from.
  2. Dates you don't control. A migration go-live, a contract renewal, a release that delivers a feature in the standard.
  3. How much the team can actually build in each quarter.

The second answer is the operating model: the teams, roles, funding and reporting that run the roadmap. Most companies start with a small central team, often called a center of excellence (CoE). As use cases spread, they move delivery into the business areas and keep the central team for the platform, the rules and the register.

A roadmap is not a promise. It is a plan you re-run every quarter, with measured results replacing the estimates.

Why it matters to the business

Without a roadmap, AI work starts where the loudest sponsor sits. Three things then go wrong.

  • The platform gets paid twice, or never. The first use case carries the whole platform cost and fails its business case. Or every team builds its own model access, logging and approvals.
  • Work collides with the SAP program. A pilot goes live on a system that is migrated three months later, and has to be rebuilt.
  • Nobody reports the value. Projects end, the team moves on, and a year later no one can say what the money bought.

Take the course's fictional company, Calder Pumpworks. Its German company code CP20 moves from ECC into the US company's S/4HANA system on 5 July 2027. After the assessments it has 21 backlog items. In this topic's made-up plan, 19 fit into the six quarters from January 2027 to June 2028. One is dropped because it costs more to run than it returns. One, an agent that screens job applicants, is parked until legal reviews it: recruitment is a high-risk area under the EU AI Act.

The plan needs about 413,000 USD of build work. Shared platform work is 11% of it. The expected value reaches about 354,000 USD a year by mid-2028, but the portfolio as a whole only pays back around mid-2029. That is normal for a first portfolio, and it is the number a sponsor should see before approving it. If the data layer from the reporting program slips one quarter, two use cases go live a quarter later, a quarter of team time sits idle, and payback moves out by a quarter too.

How SAP does it

SAP doesn't sell an AI roadmap. It gives you catalogs to plan from, tools to record the plan, and commercial rules that shape it. As of November 2026:

  • SAP's own readiness checklist. An SAP Learning lesson for enterprise architects says AI adoption usually slows because foundations are missing. It lists five areas to prepare: value and business case, release scope, commercial and legal terms (such as Joule Base vs. Premium and AI Units), identity (SAP Identity Authentication as the single entry point), and governance. Under governance it recommends clear AI ownership and an AI Center of Excellence or equivalent. SAP Learning
  • What SAP will deliver, and when. The AI Feature Catalog lists AI features across SAP products. The SAP Road Map Explorer at roadmaps.sap.com shows planned capabilities by quarter. SAP Learning points to both for planning adoption. Check them before you build something the standard will soon deliver. SAP Learning
  • Where the plan and the register live. SAP LeanIX offers roadmaps for AI initiatives and an inventory of AI technologies, from AI-enabled applications to agents. SAP AI Agent Hub, part of SAP LeanIX, is described by SAP Learning as a system of record for SAP, in-house and third-party agents, mapped to the process steps they support. sap.com · SAP Learning
  • How AI is paid for. SAP's pricing page says base AI is included in standard cloud subscriptions. Premium AI runs on AI Units, bought annually; unused units expire after 12 months. Joule Studio and SAP Document AI are paid through SAP BTP credits. So the roadmap and the AI Unit purchase must agree on timing. SAP pricing
  • A time-limited offer to plan around. At SAP TechEd on 27 October 2026, SAP said running custom agents on the Joule Studio runtime is free through December 2026, as a promotion for select customers. Don't let a free period decide the order of a two-year plan. SAP News
  • An existing home for the team. Many SAP customers already run a Customer Center of Expertise, which SAP Support describes as a hub across IT and the business for operating SAP. An AI CoE can grow out of it rather than beside it. SAP Support

A roadmap on one page

This is the shape a steering committee should see. The numbers are Calder's made-up plan.

Quarter Built this quarter Why then
2027-Q1 Platform foundation, use-case register, invoice and intercompany quick wins, planners start recording outcomes Cheap, no platform needed, or needed by everything after
2027-Q2 Maintenance triage, email to draft sales order, Joule for CP10 users; blocked-orders assistant starts First users of the platform
2027-Q3 Blocked-orders assistant finishes, invoice extraction moves to S/4HANA, Joule for Augsburg users Capacity cut for the CP20 cutover; Augsburg's users arrive
2027-Q4 Late-delivery risk, MRP exception ranking, spare-parts forecast Need the data layer, or a recorded history to learn from
2028-Q1 Variance explanations finish; most capacity is free The backlog runs out: discovery must refill it
Not now Applicant screening (parked), a chat over an unused report (dropped) Legal review first; no demand

Choosing an operating model

Model How it works Fits when Watch for
Central CoE One team builds and runs everything The first 12 to 18 months; few use cases; skills are scarce A queue: approvals slow, the team becomes the bottleneck
Hub and spoke A central team owns platform, rules and register; FDEs sit in process areas Several areas have live use cases Spokes drift from the standards; the hub needs authority
Federated Each area builds its own; the center only advises AI is routine and areas have their own engineers Duplicate platforms, uneven governance

Microsoft's Cloud Adoption Framework gives the same advice: centralize at the start, then move the CoE to an advisory role as adoption matures. Its warning signs for the change are approval delays and knowledge bottlenecks. Microsoft CAF

Whatever the model, five roles must be named:

Role Owns At Calder (fictional)
Executive sponsor Budget and priorities; chairs the steering committee The CFO
Use-case owner The business result of one use case, and its quarterly number The credit manager for blocked orders
FDE Discovery, building and adoption, close to the users Two FDEs, one per region
Platform team Model access, identity, logging, the shared runtime One platform engineer and the BTP admin
Governance lead The register, risk tiers and approvals From risk and compliance, part time

Questions to ask

Your team:

  • Which items need something we don't have yet: platform, clean data, labeled history, a finished migration?
  • Which dates are outside our control, and which items move if one slips?
  • How many build days does the team really have each quarter, after support and the SAP program?
  • Who owns each use case's result, and who reports its number every quarter?

Your partner or SAP:

  • Which items on our list will SAP deliver in the standard, and in which release? Show us the Road Map Explorer entry.
  • How does the AI Unit purchase line up with our go-live dates, given that unused units expire?
  • What happens to agents we build during a free or promotional period when it ends?

Your steering committee:

  • Do we fund the shared platform as its own line, or inside the first use case?
  • When do we move from a central team to hub and spoke, and what tells us it's time?

Common misconceptions

  • "The highest-value idea goes first." Order is decided as much by dependencies, dates and effort. A large idea that needs a data layer waits for it.
  • "The first use case should pay for the platform." That makes the first case look bad and the second look free. Fund the shared platform once, justified by what it unlocks.
  • "A roadmap is a commitment." It is a plan re-run every quarter. Items move when facts change, and that is the point.
  • "A center of excellence is permanent." It is a starting shape. Kept central too long, it becomes a queue.
  • "Value is reported when the project closes." Value appears after go-live, slowly. Each owner reports it every quarter for as long as the use case runs.

Key terms

All course terms, A to Z, in the glossary

  • AI roadmap: the AI backlog placed in quarters, under capacity, dependency and date constraints.
  • Backlog: the list of assessed AI ideas, each with value, effort, run cost, risk tier and dependencies.
  • Shared platform: work many use cases need, such as model access, identity, logging and a use-case register.
  • Dependency: something an item needs before it can start, such as a platform item or another program's date.
  • Value window: the period in which an item has value, for example only until a migration replaces the process.
  • Expected value: the estimated value multiplied by the confidence that it will be reached.
  • Operating model: the teams, roles, funding and reporting that run the roadmap.
  • Center of excellence (CoE): a central team that sets standards, runs intake and builds shared assets.
  • Hub and spoke: a central team for platform and rules, with builders placed in the business areas.
  • Use-case owner: the business person accountable for one use case's result.

Check yourself

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

1Calder's highest-value AI idea needs a data layer that another program delivers next year. Where does it go on the roadmap?

Answer: A. A dependency sets the earliest start, whatever the value. The roadmap places the idea after the data layer and makes the dependency visible, so a slip in the other program is seen as a slip in the AI plan.

2Why fund the shared AI platform as its own budget line?

Answer: A. The platform serves many use cases. Loading it onto the first one makes that case fail its business case and the later ones look free. Justify it once, by what it unlocks.

3A sponsor asks why the portfolio only pays back after the 18-month plan ends. What is the honest answer?

Answer: C. Build spend falls in the first quarters, while value starts only after each go-live and grows as people adopt it. Showing the real payback date lets the sponsor decide with the facts rather than discover them later.

4When should a central AI team move toward hub and spoke?

Answer: A. A central team is right at the start, when skills are scarce. When several areas have live use cases and wait in its queue, delivery should move into those areas while the center keeps the platform, the rules and the register.

5Who should report each use case's value every quarter?

Answer: B. The use-case owner is accountable for the business result, so they report the measured number each quarter. Usage logs show activity, not value, and a one-off report at project close comes before the value appears.

6SAP's Road Map Explorer shows a standard feature for one of your ideas in two quarters. What should the roadmap do?

Answer: D. Building what the standard will soon deliver wastes effort and adds code to maintain. Check what the planned feature covers; if it fits, plan around its release and spend the team's days elsewhere.

7Unused AI Units expire after 12 months. What does that mean for the roadmap?

Answer: D. Units bought long before the features that use them go live can expire unused. The roadmap tells you when consumption starts, so the purchase can follow it.
Deep layer · 40 min read

Mental model

A roadmap is the output of a small scheduling problem, and the operating model is the set of people who keep its inputs true.

The inputs are a scored backlog, the team's capacity per quarter, the dependencies between items, and dates you don't control. The output is a timeline and its money: build spend, run cost and expected value by quarter. Change an input, run it again, and the roadmap changes. That is why a roadmap built in a slide tool goes stale in a month, and one built from data can be re-run at every quarterly review.

The operating model answers "who keeps each input true?":

Input Kept true by
Value and confidence per item The use-case owner, then measured results
Effort and run cost FDEs and the platform team, narrowed at each gate
Dependencies on platform and data The platform team and the data program
Dates outside the AI team The SAP program, the migration lead, procurement
Risk tier and approvals The governance lead, through the register
Priorities and the budget The sponsor and the steering committee

If nobody owns an input, it stays at its first guess, and the roadmap quietly turns into fiction.

How it works

flowchart LR
  A[Assessments<br/>and estimates] --> B[(Backlog)]
  B --> T{Triage}
  T -->|parked or dropped| N[Not now list]
  T --> S[Score]
  S --> P[Schedule in quarters]
  C[Capacity, dependencies,<br/>windows, dates] --> P
  P --> O[One-page view]
  P --> M[Money by quarter]
  O --> R[Quarterly review]
  X[(Measured value)] --> R
  R -->|update inputs| B

The backlog

Each row is one item, taken from the assessment and estimate topics. The fields that matter:

Field Meaning Comes from
kind ai, automation, standard, platform, enabler or migration The assessment's decision
value_per_year, confidence Yearly value, and the chance of reaching it (0 to 1) Business case
effort_days, run_cost_per_year Build days left, and the yearly cost to run Engagement estimate (use the P80)
tier low, medium, high Governance register
depends, lag_quarters Items needed first, and quarters to wait after them Architecture and data review
earliest, window First possible quarter; the last quarter with value Program dates
funded_by, status Who pays the build; candidate, committed or parked The steering committee

Two kinds are special. Platform and enabler items have no value of their own. They are scheduled when something needs them, so they are never ranked against use cases. Items funded by another program (funded_by not ai), such as Calder's data layer built by the reporting program, are fixed dates the AI team waits for, not work it does.

Triage before scoring

Some items should never reach the scoring step:

  • Parked: the status says so, or the tier is high without a legal and risk assessment. Calder's applicant-screening agent is parked: the EU AI Act topic explains why recruitment is a high-risk area.
  • Dropped: expected value (value × confidence) doesn't cover the run cost. A chat over a report nobody runs is the usual example.

Writing these down matters as much as the plan. A dropped item that isn't recorded comes back next quarter as a side project.

Scoring: value per day of effort

The ranking number is net expected value per year, divided by effort days:

net   = value_per_year x confidence - run_cost_per_year
score = net / effort_days

This is a simple form of the idea behind SAFe's weighted shortest job first (WSJF): divide the cost of delay by the job's duration, so small valuable items go first. SAFe's version scores value, time criticality and risk reduction separately, on a relative scale. This topic keeps money and days, because the earlier topics already produced them, and handles time criticality with windows and dates instead.

Three effects of this rule are worth knowing:

  1. Small items win. Calder's intercompany check scores 3,606 per day; the blocked-orders assistant, with the largest value, scores 520. That is intended: a quick win frees value early. It is not a judgment on which idea matters more.
  2. Confidence counts. The MRP ranking idea is worth 70,000 a year, but at 0.5 confidence it counts as 35,000 before run cost.
  3. Sunk cost is ignored. Only the days left count. A half-built item is ranked on what remains, as SAFe's WSJF also does.

Scheduling under constraints

Items are placed one by one, committed items first, then items with a value window, then by score. For each item:

  1. Every dependency is placed first. Platform items are pulled in by the first item that needs them. The item can start the quarter after its last dependency finishes, plus lag_quarters. The MRP ranking item waits two quarters after planners start recording outcomes, so there is history to learn from.
  2. earliest pushes the start later. Joule for Augsburg's users can't start before the migration quarter.
  3. Effort fills free capacity from the start quarter onward and may spill into the next quarter.
  4. If the item finishes in or after its window, it has missed its value and is left out. Calder's intercompany check only has value until CP20 joins CP10's system.

The result is a plan where every placement can be explained by a rule. That is the property a steering committee needs: "why is my item in 2028?" has an answer.

Money by quarter

For each quarter, the plan totals:

  • Build: days used × the day rate (course value 900 USD).
  • Run: a quarter of each live item's yearly run cost.
  • Value: a quarter of value × confidence, from the quarter after the build finishes. Items people must adopt (ai and standard) count half in their first live quarter, as a simple adoption ramp.

The cumulative line, value minus build minus run, gives the portfolio payback. This topic's tool counts value only in whole quarters after go-live, so it is more cautious than the month-by-month figures in the ECC topic.

Reporting: plan against measured value

Every quarter, each owner reports a measured value for each live item, with its source (system, measured or estimate, as in the discovery topic). The report compares it with the plan for that quarter:

Status Rule What happens
GREEN 90% of plan or more Nothing; very high numbers get their measurement checked
AMBER 60% to 90% The owner explains it at the review
RED Below 60% The steering committee decides: fix, re-scope or retire

The thresholds are course values. Choose your own, write them down, and keep them stable, or the colors stop meaning anything. The adoption topic shows how to measure the decision behind each number rather than logins.

The operating model as team types

Team Topologies names four team types and three ways teams interact. They map neatly onto an AI operating model:

Team Topologies type In an AI operating model Interaction
Stream-aligned FDEs with a process area (order-to-cash, procure-to-pay, plant) Collaborate with users while discovering; then own the use case
Platform Model access, identity, logging, register, shared runtime X-as-a-Service: use cases consume it without tickets
Enabling The CoE's coaches, early on Facilitation: teach a spoke, then step back
Complicated subsystem Specialists, such as a forecasting model team Used when the math needs deep experts

Team Topologies also warns that overloaded teams make poor decisions. A central AI team that both builds every use case and runs the platform is the classic overloaded team. Moving to hub and spoke is a way to cut its load.

Funding follows the same split:

What Funded how Why
Shared platform Yearly budget, as a product It serves every use case; it needs a stable team
Use cases By phase and gate Each phase can stop, as in the scoping topic
AI Units and BTP credits Central pool, timed to go-lives One contract, consumption follows the roadmap
Benefits Owned by the use-case owner, checked by finance The person who changes the process owns its result

Build it yourself: a roadmap builder

You will build a command-line tool that reads Calder's AI backlog, ranks it, places it in six quarters under capacity and dependency rules, writes a one-page executive view, and compares measured value with the plan.

Before you start: complete Set up your computer for this course and Set up for Unit 14. They create your orchestrate-course folder, its .venv and the unit14 folder. The script uses only Python's built-in modules, so there is nothing new to install and no requirements.txt change.

flowchart LR
  S[sample] --> B[(backlog.csv<br/>settings.json)]
  B --> SC[score]
  B --> PL[plan]
  PL --> MD[roadmap.md]
  A[(actuals.csv)] --> RE[report]
  B --> RE

What you need

  • Your course folder from the setup topics.
  • About 45 minutes.
  • No accounts and no cost. Calder Pumpworks is fictional and every number is made up.
  • Optional: your files from the opportunity assessment and scoping exercises, to add your own items later.

Step 1: Open your course folder

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

  2. Open a terminal: Terminal > New Terminal.

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

    • Windows (PowerShell):

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

      source .venv/bin/activate
  4. Check that the unit14 folder exists:

    • Windows (PowerShell):

      dir unit14
    • macOS / Linux:

      ls unit14

    If you see an error saying the folder doesn't exist, create it with mkdir unit14.

Step 2: Create the script

  1. In VS Code, right-click the unit14 folder, choose New File, name it roadmap.py, paste the code below and save.
"""AI roadmap builder: turn a backlog of assessed AI ideas into a quarterly roadmap.

Commands:
  sample   write a made-up backlog and settings for Calder Pumpworks (fictional)
  score    check the backlog and rank it: expected value per day of effort
  plan     place items in quarters (capacity, dependencies, windows); --md writes a one-page view
  report   compare measured value with the plan for live items (traffic lights)

Uses only Python's built-in modules. No account, no network. Every number is made up.
"""
import argparse
import csv
import json
import sys
from pathlib import Path

HERE = Path(__file__).resolve().parent
DEFAULT_DIR = HERE / "roadmap"

# ---------------------------------------------------------------------------
# 1. Columns of backlog.csv
# ---------------------------------------------------------------------------
COLUMNS = ["id", "name", "kind", "owner", "value_per_year", "confidence", "effort_days",
           "run_cost_per_year", "tier", "depends", "earliest", "lag_quarters", "window",
           "funded_by", "status", "note"]
KINDS = ["ai", "automation", "standard", "platform", "enabler", "migration"]
TIERS = ["low", "medium", "high"]
STATUSES = ["candidate", "committed", "parked"]
NUMBERS = ["value_per_year", "confidence", "effort_days", "run_cost_per_year", "lag_quarters"]
SHARED = ["platform", "enabler"]          # scheduled when something needs them, not ranked
RAMP_KINDS = ["ai", "standard"]           # people must adopt them: half value in the first live quarter

# ---------------------------------------------------------------------------
# 2. Made-up sample: Calder's AI backlog after the assessments (course values, USD)
# ---------------------------------------------------------------------------
SAMPLE_SETTINGS = {
    "company": "Calder Pumpworks (fictional)",
    "quarters": ["2027-Q1", "2027-Q2", "2027-Q3", "2027-Q4", "2028-Q1", "2028-Q2"],
    "capacity_days": {"2027-Q1": 90, "2027-Q2": 140, "2027-Q3": 70, "2027-Q4": 160,
                      "2028-Q1": 160, "2028-Q2": 160},
    "capacity_note": "AI team: product owner, 2 FDEs, 1 platform engineer; 2027-Q3 cut for the CP20 cutover and hypercare",
    "day_rate": 900,
    "funded_here": "ai",
    "milestones": {"2027-Q3": "CP20 goes live in CP10's S/4HANA system (5 July 2027)"},
}

SAMPLE_BACKLOG = [
    # id, name, kind, owner, value, conf, effort, run, tier, depends, earliest, lag, window, funded_by, status, note
    ["AI01", "Flag intercompany price mismatches (CP20/CP10)", "automation", "Intercompany team",
     20700, 0.9, 5, 600, "low", "", "", 0, "2027-Q2", "ai", "candidate",
     "E03 from the pre-migration register; value ends when CP20 joins CP10's system"],
    ["AI02", "Extract Augsburg supplier invoices with document AI", "ai", "Accounts payable CP20",
     43200, 0.8, 10, 6000, "medium", "", "", 0, "", "ai", "committed",
     "E01; in build; the last 10 days fall in 2027-Q1"],
    ["AI03", "Move invoice extraction to the S/4HANA adapter", "enabler", "Accounts payable CP20",
     0, 1.0, 3, 0, "medium", "AI02", "2027-Q3", 0, "", "ai", "committed",
     "the adapter rewrite from E01; must follow the CP20 go-live"],
    ["AI04", "Find duplicate suppliers and customers before the mock loads", "migration", "Migration lead",
     0, 1.0, 8, 0, "low", "", "2027-Q1", 0, "", "migration", "committed",
     "E05; the value is in the migration, so the migration budget pays"],
    ["AI05", "Blocked sales orders assistant for credit analysts", "ai", "Credit management",
     84000, 0.6, 70, 14000, "medium", "PL1;PL2", "", 0, "", "ai", "candidate",
     "assessed PROTOTYPE; P80 from the engagement estimate"],
    ["AI06", "Invoice exception decision table (after the price fix)", "automation", "Accounts payable CP10",
     52000, 0.85, 15, 1500, "low", "", "", 0, "", "ai", "candidate",
     "assessed AUTOMATE; no AI needed"],
    ["AI07", "Late-delivery risk for purchased components", "ai", "Purchasing",
     60000, 0.5, 40, 8000, "low", "PL3", "", 0, "", "ai", "candidate",
     "needs history from the data layer"],
    ["AI08", "Month-end variance explanations for controllers", "ai", "Controlling",
     30000, 0.6, 45, 9000, "medium", "PL1;PL3", "", 0, "", "ai", "candidate", ""],
    ["AI09", "Rank MRP exceptions for planners", "ai", "Production planning",
     70000, 0.5, 35, 6000, "low", "PL4", "", 2, "", "ai", "candidate",
     "assessed PARK until outcomes are recorded; 2 quarters of labels after PL4"],
    ["AI10", "Maintenance notification triage at Rockford", "ai", "Maintenance planning",
     36000, 0.7, 30, 5000, "low", "PL1", "", 0, "", "ai", "candidate", ""],
    ["AI11", "Customer email to draft sales order", "ai", "Customer service",
     75000, 0.55, 55, 12000, "medium", "PL1;PL2", "", 0, "", "ai", "candidate",
     "a person approves every order before it is saved"],
    ["AI12", "Joule for CP10 users (base AI)", "standard", "CIO office",
     40000, 0.6, 12, 0, "low", "PL1", "", 0, "", "ai", "candidate",
     "included in the subscription; effort is identity, roles and training"],
    ["AI13", "Joule for Augsburg users after the move", "standard", "CIO office",
     20000, 0.6, 6, 0, "low", "AI12", "2027-Q3", 0, "", "ai", "candidate",
     "CP20's users arrive in CP10's system in July 2027"],
    ["AI14", "Spare-parts demand forecast for the aftermarket", "ai", "Aftermarket sales",
     45000, 0.5, 50, 7000, "low", "PL3", "", 0, "", "ai", "candidate", ""],
    ["AI15", "Service billing release assistant", "ai", "Service management",
     28000, 0.6, 35, 5000, "medium", "PL1", "", 0, "", "ai", "candidate", ""],
    ["AI16", "Chat over Augsburg's open PO aging report", "ai", "Purchasing CP20",
     3000, 0.5, 12, 4000, "low", "PL1", "", 0, "", "ai", "candidate",
     "E02; zero runs of the report in 14 months"],
    ["AI17", "Screen Augsburg job applicants with an agent", "ai", "HR CP20",
     25000, 0.5, 40, 6000, "high", "PL1;PL2", "", 0, "", "ai", "parked",
     "recruitment is an Annex III high-risk area; needs a legal assessment first"],
    ["PL1", "AI platform foundation (BTP, generative AI hub, identity, logging)", "platform", "Platform team",
     0, 1.0, 30, 18000, "low", "", "", 0, "", "ai", "candidate", ""],
    ["PL2", "Use-case register and agent inventory", "platform", "Governance lead",
     0, 1.0, 10, 2000, "low", "", "", 0, "", "ai", "candidate", ""],
    ["PL3", "Data layer: CP10 history in Datasphere", "platform", "Data team",
     0, 1.0, 60, 0, "low", "", "2027-Q3", 0, "", "data", "committed",
     "the reporting program builds it; ready in 2027-Q3"],
    ["PL4", "Planners record what they did with each MRP exception", "enabler", "Production planning",
     0, 1.0, 8, 0, "low", "", "", 0, "", "ai", "candidate", "a process change, not a system"],
]

SAMPLE_ACTUALS = [
    # id, quarter, measured_value (USD in that quarter), note
    ["AI01", "2027-Q2", 4900, "system: mismatches caught before invoicing"],
    ["AI02", "2027-Q2", 8800, "system: invoices posted without manual keying"],
    ["AI06", "2027-Q2", 10100, "measured: clerk time sample, 40 invoices"],
    ["AI10", "2027-Q3", 2100, "measured: planner time sample; 1 of 3 shifts uses it"],
    ["AI12", "2027-Q3", 5200, "estimate: survey of 30 users"],
]

# ---------------------------------------------------------------------------
# 3. Reading and checking
# ---------------------------------------------------------------------------
def q_index(settings, q):
    """Position of a quarter in the horizon; None if empty, -1 if before it, len if after."""
    if not q:
        return None
    qs = settings["quarters"]
    if q in qs:
        return qs.index(q)
    return -1 if q < qs[0] else len(qs)


def read_backlog(path):
    items, errors = [], []
    with open(path, newline="", encoding="utf-8") as f:
        rows = list(csv.DictReader(f))
    if not rows:
        errors.append(f"{path} has no rows")
    for n, row in enumerate(rows, start=2):
        missing = [c for c in COLUMNS if c not in row]
        if missing:
            errors.append(f"line {n}: missing columns {', '.join(missing)}")
            continue
        it = {k: (row[k] or "").strip() for k in COLUMNS}
        where = f"line {n} ({it['id'] or 'no id'})"
        for k in NUMBERS:
            try:
                it[k] = float(it[k] or 0)
            except ValueError:
                errors.append(f"{where}: {k} must be a number, not '{row[k]}'")
                it[k] = 0.0
        for k, allowed in (("kind", KINDS), ("tier", TIERS), ("status", STATUSES)):
            if it[k] not in allowed:
                errors.append(f"{where}: {k} must be one of {', '.join(allowed)}")
        if not 0 <= it["confidence"] <= 1:
            errors.append(f"{where}: confidence must be between 0 and 1")
        it["depends"] = [d.strip() for d in it["depends"].split(";") if d.strip()]
        items.append(it)
    ids = [i["id"] for i in items]
    for i in items:
        if ids.count(i["id"]) > 1:
            errors.append(f"{i['id']}: id used more than once")
        for d in i["depends"]:
            if d not in ids:
                errors.append(f"{i['id']}: depends on {d}, which is not in the backlog")
    return items, sorted(set(errors))


def load(folder):
    folder = Path(folder)
    sp, bp = folder / "settings.json", folder / "backlog.csv"
    if not sp.exists() or not bp.exists():
        sys.exit(f"file not found: {bp} or {sp}. Run: python unit14/roadmap.py sample")
    settings = json.loads(sp.read_text(encoding="utf-8"))
    items, errors = read_backlog(bp)
    for e in errors:
        print("ERROR", e)
    if errors:
        sys.exit(1)
    return settings, {i["id"]: i for i in items}


def net_value(it):
    """Expected value per year (value x confidence) minus the yearly run cost."""
    return it["value_per_year"] * it["confidence"] - it["run_cost_per_year"]


def score(it):
    """Net expected value per year for each day of effort: the ranking number."""
    return net_value(it) / it["effort_days"] if it["effort_days"] else 0.0


def triage(it):
    """Why an item can't be ranked, or '' if it can."""
    if it["status"] == "parked":
        return "parked: " + (it["note"] or "see owner")
    if it["kind"] in ("ai", "automation", "standard") and net_value(it) <= 0:
        return "drop: expected value doesn't cover the run cost"
    if it["tier"] == "high" and it["status"] != "committed":
        return "parked: high tier needs a legal and risk assessment before planning"
    return ""

# ---------------------------------------------------------------------------
# 4. Scheduling
# ---------------------------------------------------------------------------
def plan(settings, items):
    qs = settings["quarters"]
    free = [settings["capacity_days"][q] for q in qs]
    here = settings.get("funded_here", "ai")
    placed, outcome = {}, {}     # placed[id] = (start, finish); outcome[id] = reason if not placed

    def place(iid, chain=()):
        if iid in placed:
            return True
        if iid in outcome:
            return False
        if iid in chain:
            outcome[iid] = "dependency loop: " + " > ".join(chain + (iid,))
            return False
        it = items[iid]
        why = triage(it)
        if why:
            outcome[iid] = why
            return False
        start = 0
        for d in it["depends"]:
            if not place(d, chain + (iid,)):
                outcome[iid] = f"waits for {d} ({outcome[d].split(':')[0]})"
                return False
            start = max(start, placed[d][1] + 1)
        start += int(it["lag_quarters"])
        e = q_index(settings, it["earliest"])
        if e is not None:
            if e >= len(qs):
                outcome[iid] = "later: earliest start is after the horizon"
                return False
            start = max(start, e)
        if it["funded_by"] != here:                 # another program delivers it on its own date
            if start >= len(qs):
                outcome[iid] = "later: delivered after the horizon"
                return False
            placed[iid] = (start, start)
            return True
        need, use, q = it["effort_days"], {}, start
        while need > 0 and q < len(qs):
            take = min(need, free[q])
            if take > 0:
                use[q] = use.get(q, 0) + take
                need -= take
            q += 1
        if need > 0:
            outcome[iid] = "later: no capacity left in the horizon"
            return False
        finish = max(use) if use else start
        w = q_index(settings, it["window"])
        if w is not None and finish >= w:
            outcome[iid] = f"missed window: must be live before {it['window']}"
            return False
        for q, d in use.items():
            free[q] -= d
        placed[iid] = (min(use) if use else start, finish)
        it["_use"] = use
        return True

    ranked = [i for i in items.values() if i["kind"] not in SHARED or i["status"] == "committed"]
    ranked.sort(key=lambda i: (i["status"] != "committed", not i["window"], -score(i)))
    for it in ranked:
        place(it["id"])
    for it in items.values():           # shared items nothing pulled in
        if it["id"] not in placed and it["id"] not in outcome:
            outcome[it["id"]] = "not needed: nothing in the plan depends on it"
    return placed, outcome, free


def money(settings, items, placed):
    """Quarterly build spend, run cost and expected value for the placed items."""
    qs, rate, here = settings["quarters"], settings["day_rate"], settings.get("funded_here", "ai")
    build, run, value = [0.0] * len(qs), [0.0] * len(qs), [0.0] * len(qs)
    per_item = {}
    for iid, (start, finish) in placed.items():
        it = items[iid]
        if it["funded_by"] == here:
            for q, d in it.get("_use", {}).items():
                build[q] += d * rate
        w = q_index(settings, it["window"])
        last = len(qs) - 1 if w is None else min(w, len(qs) - 1)
        v_total = 0.0
        for q in range(finish + 1, len(qs)):
            if it["funded_by"] == here:
                run[q] += it["run_cost_per_year"] / 4
            if q > last:
                continue
            share = 0.5 if (it["kind"] in RAMP_KINDS and q == finish + 1) else 1.0
            v = it["value_per_year"] * it["confidence"] / 4 * share
            value[q] += v
            v_total += v
        per_item[iid] = v_total
    return build, run, value, per_item


def fmt(x):
    return f"{x:,.0f}"

# ---------------------------------------------------------------------------
# 5. Commands
# ---------------------------------------------------------------------------
def cmd_sample(args):
    folder = Path(args.dir)
    folder.mkdir(parents=True, exist_ok=True)
    targets = [folder / "backlog.csv", folder / "settings.json", folder / "actuals.csv"]
    if any(t.exists() for t in targets) and not args.force:
        sys.exit(f"Sample files already exist in {rel(folder)}. Add --force to overwrite them.")
    with open(targets[0], "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        w.writerow(COLUMNS)
        w.writerows(SAMPLE_BACKLOG)
    targets[1].write_text(json.dumps(SAMPLE_SETTINGS, indent=2) + "\n", encoding="utf-8")
    with open(targets[2], "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        w.writerow(["id", "quarter", "measured_value", "note"])
        w.writerows(SAMPLE_ACTUALS)
    print(f"Wrote {len(SAMPLE_BACKLOG)} backlog items, settings and sample actuals to {rel(folder)}")
    print("All of it is made up (Calder Pumpworks is fictional). Next: python unit14/roadmap.py score")


def cmd_score(args):
    settings, items = load(args.dir)
    print(f"{'ID':5} {'Kind':10} {'Tier':6} {'Net/yr':>8} {'Days':>5} {'Score':>6}  Name")
    rows = [i for i in items.values() if i["kind"] not in SHARED]
    rows.sort(key=lambda i: (bool(triage(i)), -score(i)))
    for i in rows:
        why = triage(i)
        s = "-" if why else fmt(score(i))
        print(f"{i['id']:5} {i['kind']:10} {i['tier']:6} {fmt(net_value(i)):>8} {fmt(i['effort_days']):>5} {s:>6}  {i['name']}")
        if why:
            print(f"{'':37}{why}")
    print("\nShared items (scheduled when something needs them, not ranked):")
    for i in items.values():
        if i["kind"] in SHARED:
            users = [x["id"] for x in items.values() if i["id"] in x["depends"]]
            unlocked = sum(max(net_value(items[u]), 0) for u in users if not triage(items[u]))
            print(f"  {i['id']:5} {i['name']}: needed by {', '.join(users) or 'nothing'}; "
                  f"unlocks {fmt(unlocked)} net a year; runs at {fmt(i['run_cost_per_year'])} a year")


def timeline(settings, items, placed):
    qs = settings["quarters"]
    lines = [f"{'':5} " + " ".join(q[2:] for q in qs)]
    order = sorted(placed, key=lambda i: (placed[i][0], placed[i][1], i))
    for iid in order:
        start, finish = placed[iid]
        cells = []
        for q in range(len(qs)):
            if start <= q <= finish:
                cells.append("build  " if items[iid]["funded_by"] == settings.get("funded_here", "ai") else "ready  ")
            elif q > finish:
                cells.append("live   ")
            else:
                cells.append(".      ")
        lines.append(f"{iid:5} " + "".join(cells).rstrip() + f"  {items[iid]['name'][:44]}")
    return lines


def cmd_plan(args):
    settings, items = load(args.dir)
    qs = settings["quarters"]
    placed, outcome, free = plan(settings, items)
    build, run, value, per_item = money(settings, items, placed)
    for line in timeline(settings, items, placed):
        print(line)
    print("\nNot in this plan:")
    for iid in sorted(outcome):
        print(f"  {iid:5} {outcome[iid]}")
    print("\nQuarter   Capacity  Used  Build    Run      Value    Cumulative")
    cum = 0.0
    payback = None
    for q in range(len(qs)):
        cap = settings["capacity_days"][qs[q]]
        cum += value[q] - build[q] - run[q]
        if payback is None and cum >= 0 and q > 0:
            payback = qs[q]
        print(f"{qs[q]:9} {cap:>8} {fmt(cap - free[q]):>5}  {fmt(build[q]):>7}  {fmt(run[q]):>7}  {fmt(value[q]):>7}  {fmt(cum):>9}")
    plat = sum(d * settings["day_rate"] for iid in placed if items[iid]["kind"] in SHARED
               for d in items[iid].get("_use", {}).values())
    run_rate = sum(items[i]["value_per_year"] * items[i]["confidence"] for i in placed
                   if items[i]["kind"] not in SHARED and not items[i]["window"])
    print(f"\nBuild {fmt(sum(build))}, of which shared platform and enablers {fmt(plat)} "
          f"({plat / sum(build):.0%}). Expected value run-rate at the end: {fmt(run_rate)} a year.")
    idle = [qs[q] for q in range(len(qs)) if free[q] > settings["capacity_days"][qs[q]] / 2]
    if idle:
        print(f"Capacity more than half unused in {', '.join(idle)}: pull work forward or run discovery to refill the backlog.")
    if payback:
        print(f"Portfolio pays back in {payback} (expected values, cumulative).")
    else:
        last_net = value[-1] - run[-1]
        if last_net > 0:
            more = int(-cum // last_net) + 1
            payback = f"about {more} quarters after {qs[-1]}, if the last quarter's net value holds"
        else:
            payback = "not in sight: the last quarter's value doesn't cover its run cost"
        print(f"Portfolio pays back {payback} (expected values, cumulative).")
    if args.md:
        write_onepage(Path(args.md), settings, items, placed, outcome, build, run, value, per_item,
                      plat, run_rate, payback)
        print(f"Wrote {rel(Path(args.md))}")


def write_onepage(path, settings, items, placed, outcome, build, run, value, per_item, plat, run_rate, payback):
    qs = settings["quarters"]
    out = [f"# AI roadmap {qs[0]} to {qs[-1]}: {settings['company']}", "",
           "Made-up numbers from the course sample. Values are expected values (value x confidence).", "",
           "## The ask", "",
           f"- Build spend over the horizon: **{fmt(sum(build))}**, of which shared platform "
           f"**{fmt(plat)}** ({plat / sum(build):.0%}).",
           f"- Run cost in the horizon: **{fmt(sum(run))}**.",
           f"- Expected value in the horizon: **{fmt(sum(value))}**; run-rate at the end **{fmt(run_rate)} a year**.",
           f"- Cumulative payback: **{payback}**.", "",
           "## By quarter", "", "| Quarter | Goes live | Milestone |", "| --- | --- | --- |"]
    for q in range(len(qs)):
        live = [f"{i} {items[i]['name']}" for i in sorted(placed) if placed[i][1] == q - 1 and q > 0]
        out.append(f"| {qs[q]} | {'; '.join(live) or '-'} | {settings.get('milestones', {}).get(qs[q], '')} |")
    out += ["", "## Use cases, owners and gates", "",
            "| ID | Use case | Owner | Tier | Live from | Value in horizon |", "| --- | --- | --- | --- | --- | --- |"]
    for i in sorted(placed, key=lambda x: (placed[x][1], x)):
        it = items[i]
        if it["kind"] in SHARED:
            continue
        live = qs[placed[i][1] + 1] if placed[i][1] + 1 < len(qs) else "after horizon"
        out.append(f"| {i} | {it['name']} | {it['owner']} | {it['tier']} | {live} | {fmt(per_item[i])} |")
    out += ["", "## Not now, and why", ""]
    out += [f"- **{i}** {items[i]['name']}: {outcome[i]}" for i in sorted(outcome)]
    out += ["", "## Decisions for the steering committee", "",
            "- Approve the shared platform items as one budget line, not inside the first use case.",
            "- Confirm the owner of each use case, who reports its measured value each quarter.",
            "- Agree the parked and dropped items above, so they don't come back as side projects.", "",
            "Approved by: ____________________  Date: __________", ""]
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text("\n".join(out), encoding="utf-8")


def cmd_report(args):
    settings, items = load(args.dir)
    qs = settings["quarters"]
    placed, _, _ = plan(settings, items)
    ap = Path(args.actuals or Path(args.dir) / "actuals.csv")
    if not ap.exists():
        sys.exit(f"file not found: {ap}")
    with open(ap, newline="", encoding="utf-8") as f:
        rows = list(csv.DictReader(f))
    print(f"{'ID':5} {'Quarter':8} {'Planned':>8} {'Measured':>9} {'Share':>6}  Status")
    for r in rows:
        iid, q = r["id"], r["quarter"]
        if iid not in placed:
            print(f"{iid:5} {q:8} not in the plan; add it to the backlog first")
            continue
        it, finish = items[iid], placed[iid][1]
        qi = q_index(settings, q)
        if qi is None or qi < 0 or qi >= len(qs) or qi <= finish:
            print(f"{iid:5} {q:8} {'':>8} {'':>9} {'':>6}  not live in {q} in the plan")
            continue
        share = 0.5 if (it["kind"] in RAMP_KINDS and qi == finish + 1) else 1.0
        planned = it["value_per_year"] * it["confidence"] / 4 * share
        measured = float(r["measured_value"])
        ratio = measured / planned if planned else 0
        status = "GREEN" if ratio >= 0.9 else "AMBER" if ratio >= 0.6 else "RED: fix, re-scope or retire"
        if ratio > 1.5:
            status += " (far above plan: check how it was measured)"
        print(f"{iid:5} {q:8} {fmt(planned):>8} {fmt(measured):>9} {ratio:>6.0%}  {status}")
    print("\nGREEN at 90% of plan or more, AMBER from 60%, RED below. The owner explains every AMBER and RED.")


def rel(p):
    try:
        return str(Path(p).resolve().relative_to(Path.cwd())).replace("\\", "/")
    except ValueError:
        return str(p)


def main():
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--dir", default=str(DEFAULT_DIR), help="folder with backlog.csv and settings.json")
    sub = ap.add_subparsers(dest="cmd", required=True)
    s = sub.add_parser("sample")
    s.add_argument("--force", action="store_true")
    sub.add_parser("score")
    p = sub.add_parser("plan")
    p.add_argument("--md", help="write the one-page executive view to this Markdown file")
    r = sub.add_parser("report")
    r.add_argument("--actuals", help="CSV with id, quarter, measured_value, note")
    args = ap.parse_args()
    {"sample": cmd_sample, "score": cmd_score, "plan": cmd_plan, "report": cmd_report}[args.cmd](args)


if __name__ == "__main__":
    main()

Step 3: Write the sample backlog

  1. Run:

    python unit14/roadmap.py sample
  2. You should see:

    Wrote 21 backlog items, settings and sample actuals to unit14/roadmap
    All of it is made up (Calder Pumpworks is fictional). Next: python unit14/roadmap.py score
  3. Open unit14/roadmap/backlog.csv in VS Code. Each row is one item. The AI items are use cases; the PL items are platform and enabler work. Several come straight from earlier topics: AI01 to AI04 are the pre-migration register's E03, E01 and E05 plus the adapter rewrite, AI05 is the blocked-orders assistant, AI06 is the invoice decision table from the assessment, and AI09 is the parked MRP idea.

  4. Open unit14/roadmap/settings.json. It holds the six quarters, the build days the team has in each, the day rate and the migration milestone. Note that 2027-Q3 has only 70 days: the team supports the CP20 cutover.

If you see Sample files already exist, you ran this before. Add --force to overwrite them.

Step 4: Score the backlog

  1. Run:

    python unit14/roadmap.py score
  2. You should see:

    ID    Kind       Tier     Net/yr  Days  Score  Name
    AI01  automation low      18,030     5  3,606  Flag intercompany price mismatches (CP20/CP10)
    AI02  ai         medium   28,560    10  2,856  Extract Augsburg supplier invoices with document AI
    AI06  automation low      42,700    15  2,847  Invoice exception decision table (after the price fix)
    AI12  standard   low      24,000    12  2,000  Joule for CP10 users (base AI)
    AI13  standard   low      12,000     6  2,000  Joule for Augsburg users after the move
    AI09  ai         low      29,000    35    829  Rank MRP exceptions for planners
    AI10  ai         low      20,200    30    673  Maintenance notification triage at Rockford
    AI07  ai         low      22,000    40    550  Late-delivery risk for purchased components
    AI11  ai         medium   29,250    55    532  Customer email to draft sales order
    AI05  ai         medium   36,400    70    520  Blocked sales orders assistant for credit analysts
    AI15  ai         medium   11,800    35    337  Service billing release assistant
    AI14  ai         low      15,500    50    310  Spare-parts demand forecast for the aftermarket
    AI08  ai         medium    9,000    45    200  Month-end variance explanations for controllers
    AI04  migration  low           0     8      0  Find duplicate suppliers and customers before the mock loads
    AI17  ai         high      6,500    40      -  Screen Augsburg job applicants with an agent
                                         parked: recruitment is an Annex III high-risk area; needs a legal assessment first
    AI16  ai         low      -2,500    12      -  Chat over Augsburg's open PO aging report
                                         drop: expected value doesn't cover the run cost
    
    Shared items (scheduled when something needs them, not ranked):
      AI03  Move invoice extraction to the S/4HANA adapter: needed by nothing; unlocks 0 net a year; runs at 0 a year
      PL1   AI platform foundation (BTP, generative AI hub, identity, logging): needed by AI05, AI08, AI10, AI11, AI12, AI15, AI16, AI17; unlocks 130,650 net a year; runs at 18,000 a year
      PL2   Use-case register and agent inventory: needed by AI05, AI11, AI17; unlocks 65,650 net a year; runs at 2,000 a year
      PL3   Data layer: CP10 history in Datasphere: needed by AI07, AI08, AI14; unlocks 46,500 net a year; runs at 0 a year
      PL4   Planners record what they did with each MRP exception: needed by AI09; unlocks 29,000 net a year; runs at 0 a year
  3. Read it from the top. Small automation items lead, because their value per day is high. The two Joule items score well because the feature is included in the subscription; the effort is identity, roles and training. The blocked-orders assistant has the largest net value but sits low, because it takes 70 days.

  4. Read the last block. PL1 unlocks about 130,650 USD of net value a year and costs 18,000 a year to run. That is the sentence that justifies the platform, and it belongs on the steering committee's slide.

Step 5: Plan the quarters

  1. Run:

    python unit14/roadmap.py plan --md unit14/roadmap/roadmap.md
  2. You should see:

          27-Q1 27-Q2 27-Q3 27-Q4 28-Q1 28-Q2
    AI01  build  live   live   live   live   live  Flag intercompany price mismatches (CP20/CP1
    AI02  build  live   live   live   live   live  Extract Augsburg supplier invoices with docu
    AI04  ready  live   live   live   live   live  Find duplicate suppliers and customers befor
    AI06  build  live   live   live   live   live  Invoice exception decision table (after the
    PL1   build  live   live   live   live   live  AI platform foundation (BTP, generative AI h
    PL2   build  live   live   live   live   live  Use-case register and agent inventory
    PL4   build  live   live   live   live   live  Planners record what they did with each MRP
    AI10  .      build  live   live   live   live  Maintenance notification triage at Rockford
    AI11  .      build  live   live   live   live  Customer email to draft sales order
    AI12  .      build  live   live   live   live  Joule for CP10 users (base AI)
    AI05  .      build  build  live   live   live  Blocked sales orders assistant for credit an
    AI03  .      .      build  live   live   live  Move invoice extraction to the S/4HANA adapt
    AI13  .      .      build  live   live   live  Joule for Augsburg users after the move
    PL3   .      .      ready  live   live   live  Data layer: CP10 history in Datasphere
    AI15  .      .      build  build  live   live  Service billing release assistant
    AI07  .      .      .      build  live   live  Late-delivery risk for purchased components
    AI09  .      .      .      build  live   live  Rank MRP exceptions for planners
    AI14  .      .      .      build  live   live  Spare-parts demand forecast for the aftermar
    AI08  .      .      .      build  build  live  Month-end variance explanations for controll
    
    Not in this plan:
      AI16  drop: expected value doesn't cover the run cost
      AI17  parked: recruitment is an Annex III high-risk area; needs a legal assessment first
    
    Quarter   Capacity  Used  Build    Run      Value    Cumulative
    2027-Q1         90    78   70,200        0        0    -70,200
    2027-Q2        140   140  126,000    7,025   20,028   -183,198
    2027-Q3         70    70   63,000   11,275   30,996   -226,476
    2027-Q4        160   160  144,000   14,775   50,102   -335,149
    2028-Q1        160    11    9,900   21,275   70,940   -295,384
    2028-Q2        160     0        0   23,525   86,228   -232,681
    
    Build 413,100, of which shared platform and enablers 45,900 (11%). Expected value run-rate at the end: 353,910 a year.
    Capacity more than half unused in 2028-Q1, 2028-Q2: pull work forward or run discovery to refill the backlog.
    Portfolio pays back about 4 quarters after 2028-Q2, if the last quarter's net value holds (expected values, cumulative).
    Wrote unit14/roadmap/roadmap.md
  3. Read the timeline. build is a quarter with build work; ready marks an item another program delivers; live quarters carry value. Check three placements against the rules:

    • AI05, the blocked-orders assistant, starts in 2027-Q2 because it needs PL1 and PL2, which finish in 2027-Q1. It spills into 2027-Q3.
    • AI09, the MRP ranking, starts in 2027-Q4: PL4 finishes in 2027-Q1, plus one quarter, plus two quarters of recorded outcomes.
    • AI07, AI08 and AI14 wait for PL3, the data layer, which the reporting program delivers in 2027-Q3.
  4. Read the money table. Build spend is high in 2027 and drops in 2028, because the backlog runs out. The tool warns about it: unused capacity is a sign that discovery has to refill the backlog, not that the team is too big.

  5. Read the last line. The cumulative value is still negative at the end of the horizon. At the last quarter's net value, it pays back about four quarters later.

Step 6: Read the one-page view

  1. Open unit14/roadmap/roadmap.md. In VS Code, right-click the file tab and choose Open Preview to see it formatted.
  2. It has five parts: The ask (spend, run cost, value, payback), By quarter (what goes live, and the migration milestone), Use cases, owners and gates (one owner and one tier per row), Not now, and why, and Decisions for the steering committee.
  3. This is the page to send to a sponsor before the meeting. It names an owner for every number, and it shows what the plan leaves out. Rewrite the decisions to fit your audience; the storytelling topic covers how.

Step 7: Let a dependency slip

Suppose the reporting program tells you the data layer will be one quarter late.

  1. Open unit14/roadmap/backlog.csv.

  2. Find the line that starts with PL3,. Change 2027-Q3 to 2027-Q4 in that line (the first one, in the earliest column) and save.

  3. Run:

    python unit14/roadmap.py plan
  4. The end of the output should now be:

    AI15  .      .      build  build  live   live  Service billing release assistant
    AI09  .      .      .      build  live   live  Rank MRP exceptions for planners
    PL3   .      .      .      ready  live   live  Data layer: CP10 history in Datasphere
    AI07  .      .      .      .      build  live  Late-delivery risk for purchased components
    AI08  .      .      .      .      build  live  Month-end variance explanations for controll
    AI14  .      .      .      .      build  live  Spare-parts demand forecast for the aftermar
    
    Not in this plan:
      AI16  drop: expected value doesn't cover the run cost
      AI17  parked: recruitment is an Annex III high-risk area; needs a legal assessment first
    
    Quarter   Capacity  Used  Build    Run      Value    Cumulative
    2027-Q1         90    78   70,200        0        0    -70,200
    2027-Q2        140   140  126,000    7,025   20,028   -183,198
    2027-Q3         70    70   63,000   11,275   30,996   -226,476
    2027-Q4        160    36   32,400   14,775   50,102   -223,549
    2028-Q1        160   135  121,500   17,525   64,378   -298,196
    2028-Q2        160     0        0   23,525   79,665   -242,056
    
    Build 413,100, of which shared platform and enablers 45,900 (11%). Expected value run-rate at the end: 353,910 a year.
    Capacity more than half unused in 2027-Q4, 2028-Q2: pull work forward or run discovery to refill the backlog.
    Portfolio pays back about 5 quarters after 2028-Q2, if the last quarter's net value holds (expected values, cumulative).
  5. Compare it with Step 5. The three items that need PL3 start a quarter later, and two of them, AI07 and AI14, go live a quarter later. 2027-Q4 now has 124 days without work, and payback moved out by a quarter. That is the question to bring back to the steering committee: pull other work forward into 2027-Q4, or push the data program to keep its date?

  6. Undo the change: set PL3 back to 2027-Q3 and save, or run python unit14/roadmap.py sample --force.

Step 8: Report measured value

  1. Open unit14/roadmap/actuals.csv. It holds made-up measurements, as if it were October 2027, with a source note on each line.

  2. Run:

    python unit14/roadmap.py report
  3. You should see:

    ID    Quarter   Planned  Measured  Share  Status
    AI01  2027-Q2     4,658     4,900   105%  GREEN
    AI02  2027-Q2     4,320     8,800   204%  GREEN (far above plan: check how it was measured)
    AI06  2027-Q2    11,050    10,100    91%  GREEN
    AI10  2027-Q3     3,150     2,100    67%  AMBER
    AI12  2027-Q3     3,000     5,200   173%  GREEN (far above plan: check how it was measured)
    
    GREEN at 90% of plan or more, AMBER from 60%, RED below. The owner explains every AMBER and RED.
  4. Read the two flagged lines. Invoice extraction at 204% of plan and Joule at 173% look like good news, but a number far above plan usually means the measurement is wrong or the plan was. The Joule figure comes from a survey, the weakest source. Maintenance triage is AMBER: only one of three shifts uses it, which is an adoption problem, not a model problem.

Step 9: Save your work in Git

  1. Run:

    git add unit14/roadmap.py unit14/roadmap/
    git commit -m "Unit 14: AI roadmap builder and Calder sample"

How the code works

Part of the script What it does
COLUMNS, KINDS, TIERS, STATUSES Define the backlog's columns and the allowed words
SHARED, RAMP_KINDS Which kinds are pulled in by others, and which have an adoption ramp
SAMPLE_SETTINGS, SAMPLE_BACKLOG, SAMPLE_ACTUALS Calder's made-up horizon, backlog and measurements
read_backlog, load Read the CSV and JSON, and stop with clear errors for bad words, numbers or dependencies
net_value, score Expected value minus run cost, and that number per day of effort
triage Parks and drops items before scoring, with a reason
plan, inner place Places dependencies first, applies lag, earliest and window, fills capacity, records why items were left out
money Build, run and expected value by quarter, with the half-value first quarter
timeline, write_onepage The terminal timeline and the Markdown executive view
cmd_report Compares measured value with the plan and sets GREEN, AMBER or RED

The plan is deterministic: there is no randomness, so your output should match the text above exactly. If a number differs, compare your backlog.csv with the sample.

Use your own backlog

  1. Copy the sample folder to start your own:

    • Windows (PowerShell):

      Copy-Item -Recurse unit14/roadmap unit14/my_roadmap
    • macOS / Linux:

      cp -r unit14/roadmap unit14/my_roadmap
  2. Edit unit14/my_roadmap/backlog.csv and settings.json. Keep the header row.

  3. Run any command with --dir before the command name:

    python unit14/roadmap.py --dir unit14/my_roadmap plan

If something goes wrong

What you see What it means What to do
python: command not found or 'python' is not recognized Python isn't installed or isn't on the path Redo Set up your computer. On macOS or Linux, try python3
ModuleNotFoundError You ran another script, or a file named like a built-in module (such as csv.py or json.py) sits in unit14 This script needs no libraries. Rename any file called csv.py, json.py or argparse.py
A message about a missing API key You ran another unit's script This tool needs no key and no account
A proxy or network error You ran another unit's script This tool never uses the network
can't open file ... roadmap.py The terminal isn't in orchestrate-course, or the file is elsewhere Run cd to your course folder; check the file is in unit14
file not found: ... backlog.csv The sample wasn't written, or --dir points elsewhere Run sample first, or fix the --dir path
ERROR line 11 (AI10): tier must be one of low, medium, high A typo in that row Use exactly one of the listed words
ERROR AI10: depends on PL9, which is not in the backlog A dependency names a missing ID Fix the ID, or add the missing item
KeyError naming a quarter A quarter in quarters has no entry in capacity_days Add the quarter to capacity_days in settings.json
An item shows later: no capacity left in the horizon Everything before it used the capacity Add capacity, extend the horizon, or cut something above it

The SAP way

In a real program, the pieces of this tool live in SAP's tools and your own. As of November 2026:

Planning inputs from SAP

  • AI Feature Catalog and SAP Road Map Explorer. Before an item enters the backlog as ai, check whether the standard covers it now or soon. SAP Learning recommends both for planning adoption, and notes that Joule releases features every other week. A planned standard feature becomes a standard item with an earliest quarter, instead of a build. SAP Learning
  • SAP's readiness areas as backlog items. The five areas in SAP's enterprise architect lesson (value, release scope, commercial and legal, technical prerequisites, organization and governance) are a checklist for the PL rows. Identity through SAP Identity Authentication and a governance owner are on it; both are platform items in Calder's plan. SAP Learning

Recording the plan and the register

  • SAP LeanIX can hold the roadmap as initiatives with timelines, and the AI inventory with its approval and risk status, according to SAP's product page. If the customer already uses LeanIX for its application landscape, put the AI roadmap there rather than in a separate tool, so the migration and the AI plan share one view. sap.com
  • SAP AI Agent Hub records each agent's source, the process step it supports, its usage, benefits and costs, according to SAP Learning. That is the same information this tool's backlog and actuals hold for agents. SAP Learning's lesson doesn't state the release status; check it on the SAP Help Portal before relying on it. SAP Learning

Commercial timing

  • AI Units are bought annually and unused units expire after 12 months, per SAP's pricing page, with usage and balance shown in SAP for Me. Use the roadmap's go-live quarters for premium features to size and time the purchase. SAP pricing
  • SAP BTP credits pay for Joule Studio and SAP Document AI, on the same page. Your PL1 run cost and any Joule Studio agents belong in that budget.
  • Promotions end. SAP's TechEd release says running custom agents on the Joule Studio runtime is free through December 2026 for select customers, and that Joule Studio gains controls for model usage and spend. Give every agent built during a free period a real run_cost_per_year. SAP News

The team

  • Customer Center of Expertise. SAP Support's Customer COE program describes a hub across IT and the business for operating SAP. If one exists, the AI CoE is best started inside it, which matches Microsoft's advice to build on an existing CoE before creating a new team. SAP Support

Build vs. SAP

Need Own tool (this topic) SAP tool Choose SAP when
Backlog and scoring CSV and a script; transparent rules SAP LeanIX initiatives The landscape is already modeled in LeanIX
Roadmap view Markdown one-pager SAP LeanIX roadmaps Many stakeholders need a live, shared view
AI and agent register The governance register SAP AI Agent Hub in SAP LeanIX Agents come from SAP, partners and in-house builds
Standard feature timing Manual check AI Feature Catalog, Road Map Explorer Always: these are the source
AI Unit consumption Run cost column SAP for Me usage and balance Always, for premium SAP features
Measured value actuals.csv and owners AI Agent Hub's benefits and costs, for agents The hub covers your agents and owners use it

The script is not a replacement for LeanIX. It is a way to make the rules explicit before you configure any tool, and a quick check of the plan a tool produces.

Architecture view

Where it sits. The roadmap's platform items are real landscape components. In Calder's fictional landscape:

flowchart LR
  subgraph SAP[SAP systems]
    S4[CP10 S/4HANA<br/>CP20 from July 2027]
    ECC[CP20 ECC<br/>until July 2027]
  end
  subgraph BTP[SAP BTP]
    AI[PL1 AI platform<br/>generative AI hub]
    IS[Integration Suite]
  end
  IAS[Identity Authentication]
  DS[PL3 Datasphere]
  REG[PL2 register<br/>LeanIX / Agent Hub]
  S4 --> IS --> AI
  ECC -.SLT.-> DS
  S4 --> DS --> AI
  IAS --> AI
  AI -.records.-> REG

Non-functional requirements that decide the platform design:

Requirement Typical target (course value) What breaks if missed
Shared identity Every AI app signs in through one identity service Each use case builds its own login; agent rights can't be audited
Register coverage 100% of live AI uses registered before go-live Unknown AI in production; no review dates
Run cost visibility Cost per use case per month Platform cost can't be shared out; AI Units run out unseen
Platform availability Matches the business hours of the processes it serves A credit analyst's assistant down at month end
Change isolation One use case's change can't break another's Shared prompts or configs cause cross-team incidents

Integration and identity. SAP data reaches AI items through released APIs and events via the integration layer, as in Calder's integration platform design, and history through the data layer described in the reporting architecture. Users sign in through the shared identity service, and each agent gets its own technical user with narrow roles. Every use case on the roadmap inherits these; none builds its own.

The decision to record (short ADR form from the design-doc topic):

  • Context: 14 planned items over 18 months; six of them need model access, logging and identity; the first, alone, can't carry the platform cost.
  • Options: A) each use case builds its own; B) a shared AI platform funded as a product with a yearly budget; C) a shared platform charged back to the first use case.
  • Decision: B. The platform team owns PL1 and PL2 with a yearly budget; use cases consume them as a service.
  • Consequences: One place for model access, identity and logs. The platform team needs a product owner and its own roadmap. Use-case business cases show their own costs plus a share of platform run cost.

What a design review will ask:

  1. Which use cases depend on the data layer, and what is the plan if it slips?
  2. How is each AI item's identity and authorization handled after CP20 moves into CP10's system?
  3. Where is the register, and how is an unregistered AI use stopped before go-live?
  4. How is run cost split between use cases, and who sees it each month?
  5. Which items replace or extend standard SAP features, and how does each stay clean core?

Production concerns

  • Governance gates on the roadmap. No item goes live without a register entry and the approvals its tier requires. Put the gate in the plan: a high item is parked until legal review, as the tool does. Revisit the register at every quarterly review.
  • Security and authorizations. Platform items carry identity and technical users for every agent. When a migration moves users, as CP20's move does, the roles for AI items are part of the migration's role design, not an afterthought; see the roles topic.
  • Cost. Track run cost per use case per month. Premium features consume AI Units from a yearly pool, so time the purchase to the go-live quarters. Treat promotional periods as temporary.
  • Evaluation of the roadmap itself. Each quarter, compare measured value and actual effort with the plan. Feed the gaps back into confidence and effort. A team whose effort is always 30% over plan should raise its estimates.
  • Operations. Every live item has a run owner, an on-call path and a retirement rule. Thirty live items with no owner is a support problem waiting to happen.
  • Clean core. Roadmap items extend SAP side by side through released interfaces. An item that needs a modification of SAP code is a design problem to solve before it enters the plan, not during the build.

Pitfalls

  • Slide-only roadmaps. A plan drawn by hand can't be re-run when a date moves. Keep the backlog as data.
  • Point estimates in the backlog. Use the P80 from the estimate, not the hopeful number, or the plan overruns from the first quarter.
  • Ignoring the SAP program's calendar. Cutover, hypercare and release freezes take your people. Cut capacity in those quarters.
  • Ranking platform against use cases. The platform always loses on value per day, and then nothing that needs it can start.
  • No "not now" list. Parked and dropped ideas return as shadow projects if the decision isn't written down.
  • Value that nobody measures. If an owner can't name the source of their number, the item isn't ready to go live.
  • A CoE that never lets go. Central control is right at the start; kept too long, it becomes a queue.

Exercise

Add the delivery-blocks extension you estimated in the scoping exercise to Calder's roadmap, and write the steering committee's note.

  1. Make sure the sample is fresh:

    python unit14/roadmap.py sample --force
  2. Open unit14/roadmap/backlog.csv and add this line at the end, then save:

    AI18,Extend the blocked-orders assistant to delivery blocks,ai,Credit management,30000,0.6,30,3000,medium,AI05,,0,,ai,candidate,P80 from the delivery-blocks estimate

    If you did the scoping exercise, replace 30 with your P80 divided by the day rate, rounded up.

  3. Run:

    python unit14/roadmap.py plan --md unit14/roadmap/roadmap_ai18.md
  4. Check where AI18 landed and why. It should start the quarter after AI05 finishes.

  5. Now cut 2027-Q4 capacity from 160 to 100 in settings.json (a second program needs the FDEs), and run the plan again. Write down which items moved.

  6. Create unit14/roadmap/steering_note.md with four lines: what changed, which items moved and by how much, the effect on payback, and one decision you are asking the committee for.

  7. Commit the files:

    git add unit14/roadmap/
    git commit -m "Unit 14: roadmap with the delivery-blocks extension and a steering note"

Done when plan shows AI18 placed after AI05, the 2027-Q4 capacity change has been run, and steering_note.md names the moved items, the payback effect and one decision. Keep the files: your portfolio's one-page case study can reuse the executive view.

Going further

For readers who want the economics behind sequencing.

The score in this topic is a shortcut for cost of delay: what it costs, per unit of time, not to have an item live. Dividing it by duration and doing the highest ratio first minimizes the total cost of delay when items compete for one team. That is the logic SAFe packages as WSJF, using relative scores for value, time criticality and risk reduction or opportunity enablement. Two refinements are worth knowing.

  • Time criticality is not constant. A value window, such as Calder's intercompany check, makes cost of delay jump to zero after a date. This topic models it as a hard window. A fuller model gives each item a value curve over time.
  • Enablement value. Platform items have no value of their own, but they enable others. SAFe counts it as "opportunity enablement" inside cost of delay. This topic instead schedules them just in time, which is simpler but can start a platform late when two cheap items both need it.

On the operating model, two sources add depth:

Check yourself

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

1Why are platform items such as PL1 scheduled when something needs them instead of ranked with the use cases?

Answer: B. A platform item's value is what it unlocks. Scored on its own value per day, it would sit at the bottom and block every use case that needs it. Pulling it in with its first dependent places it just in time.

2The blocked-orders assistant has the largest net value but ranks tenth. Why?

Answer: A. The score divides net expected value by effort days. Small, valuable items come first because they free value early. A low rank means "later", not "less important", and dependencies on PL1 and PL2 would delay it anyway.

3Why does AI09, the MRP exception ranking, start in 2027-Q4 although PL4 finishes in 2027-Q1?

Answer: A. PL4 makes planners record what they did with each exception. The model needs that history, so the item waits the quarter after PL4 plus two quarters of lag. Without the lag it would start with no labels.

4The data layer slips one quarter. What does the plan show, and what should you bring to the steering committee?

Answer: C. Only items that depend on the data layer move; the freed capacity in 2027-Q4 is a decision to make. Re-running the plan shows the effect on dates and payback in minutes, which is why the backlog is kept as data.

5The quarterly report shows one item at 204% of plan. What should the owner do first?

Answer: B. A result far above plan usually means a measurement or planning error. Checking the source keeps the report trustworthy. Only after that should confidence or value in the backlog change.

6Calder's applicant-screening agent has a positive net value. Why is it not on the plan?

Answer: D. Triage comes before scoring. A high-tier item needs its legal and risk assessment first; recruitment is a high-risk area under the EU AI Act. Recording it on the "not now" list stops it from returning as a side project.

7Which way of funding the shared platform does the topic's ADR choose, and what follows from it?

Answer: B. Option B funds the platform once, as a product with its own team and roadmap. Use cases consume it as a service, and their business cases show their own costs plus a share of platform run cost, so neither the first case nor the later ones are misleading.

8You plan Joule Studio agents while the runtime is free through December 2026. How should they enter the backlog?

Answer: C. A promotional period is temporary, so planning with zero run cost overstates the net value for every year after it. Enter a realistic run cost, paid from BTP credits, and note the assumption for the next review.

Sources

Sign in to track your progress

We'll email you a one-time sign-in link. No password needed.

or

Tell us a little about you

Optional, every field. It helps us pitch answers to your questions at the right level and decide which topics to write next. It is never shown publicly, and you can change or clear it anytime from the account menu.

SAP areas you work in