By the end you'll know
- Why an AI inventory comes first, and what each framework asks for
- How to find AI use in code and configuration, and why that isn't enough
- How to turn files into systems, and what to record for each
- How to check an inventory automatically, so it stays honest
Before you start
- You need: Lessons 1 to 5 help, but aren't required. Comfort with a shell, and Python 3 for the checker (standard library only).
- The example: Fernhill is made up: a small company with six projects under
~/Apps, built for this lesson. The scripts and every line of output below are real; the company, its people and its projects aren't. - Maps to: the IAPP's AIGP (Domain III, governing AI development and deployment); NIST AI RMF GOVERN 1.6; ISO/IEC 42001 clause 4.3 and Annex A.4.
The short version
- You can't govern what you haven't listed. NIST, ISO/IEC 42001, the EU AI Act and OMB memo M-25-21 all start here.
- Find it three ways: search code and configuration for signs of AI, check what's actually running, and ask people, because code finds neither bought software nor plans.
- List systems, not files. Nine files that call a model became five systems; a conversation added a sixth.
- Record what decides risk: owner, model, where it runs, what data goes in, what the output is used for, whether it decides about people, and a review date.
- Check it with code, so gaps fail loudly. Fernhill's first check failed on an entry with no owner, last looked at a year ago.
- The riskiest system on the list doesn't exist yet. That's the cheapest time to govern it.
Why the inventory comes first
Every rule in the last five lessons assumes you know what AI you have:
| Source | What it asks |
|---|---|
| NIST AI RMF, GOVERN 1.6 | "Mechanisms are in place to inventory AI systems", resourced according to risk |
| ISO/IEC 42001 | Clause 4.3 sets the scope of the management system; Annex A.4 asks you to document the resources each AI system uses (data, tools, models, people) |
| EU AI Act | No list by name, but you can't classify a system's tier, or know if you're a provider or a deployer, without one |
| OMB M-25-21 | US federal agencies publish an AI use case inventory every year, and flag high-impact uses |
An inventory isn't paperwork for its own sake. It's the list every other control hangs from: which systems need an impact assessment, which need human oversight, which send data to someone else, which nobody owns any more.
Step 1: find it in code
The scan script searches every project under ~/Apps for seven kinds of signal. Each is a regular expression, checked against code and configuration files only:
# signal type <TAB> regex
SIGNALS='sdk-hosted ^[[:space:]]*(import|from)[[:space:]]+(anthropic|openai)
sdk-local ^[[:space:]]*(import|from)[[:space:]]+ollama
endpoint-local (localhost|127\.0\.0\.1):11434|OLLAMA_(HOST|URL)
endpoint-hosted api\.(anthropic|openai)\.com
api-key (ANTHROPIC|OPENAI)_API_KEY
model-name (qwen[0-9.]*:[0-9]+b|llama[0-9.]*:[0-9]+b|claude-[a-z]+-[0-9]|gpt-[0-9])
embeddings SentenceTransformer|chromadb'
Each line is a name, a tab, and a pattern: imports of hosted or local model libraries, the address of a local model server (Ollama, on port 11434) or a hosted provider's API, the name of an API key variable, a model name, or an embeddings library (the building block of retrieval). Add patterns for whatever providers your organization uses.
while IFS=$'\t' read -r sig rx; do
grep -rIlE "${EXCL[@]}" "${INCL[@]}" "$rx" "$ROOT" 2>/dev/null | while read -r f; do
rel="${f#"$ROOT"/}"
printf '%s\t%s\t%s\n' "${rel%%/*}" "$rel" "$sig" >> "$OUT"
done
done <<< "$SIGNALS"
For each signal, grep -rIlE lists the files that match: -r searches folders, -I skips binary files, -l prints only file names, -E allows the pattern syntax above. EXCL skips dependencies, virtual environments and build output (node_modules, .venv, .git and so on); INCL limits the search to code and config (.py, .js, .ts, .yaml, .toml, .sh). Each hit becomes a line of scan.tsv: project, file, signal. The rest of the script summarizes that file. Run on Fernhill:
(Your machine)
chmod +x scan_ai.sh
./scan_ai.sh
chmod +x makes the script runnable; the second line runs it. It reads files only, and writes nothing outside ~/Apps/ai-inventory-lab.
== 1. Scanning ~/Apps for AI signals (code and config only)
25 signal hits written to ~/Apps/ai-inventory-lab/scan.tsv
== 2. Signals by project
experiments 3 files: endpoint-local model-name
invoice-reader 4 files: sdk-hosted endpoint-local model-name
lead-scorer-poc 1 files: sdk-hosted model-name
marketing-site 1 files: endpoint-local model-name
support-bot 3 files: sdk-hosted api-key model-name embeddings
Five of Fernhill's six projects have signals; billing-api has none. Next, the files that actually call a model, leaving out tests and paths known not to be AI use (the script takes a list; here, the marketing site):
== 3. Files that call a model (not tests, not known non-uses)
experiments/ask.py
experiments/rag.py
experiments/summarize.py
invoice-reader/archive/compare_models.py
invoice-reader/archive/read_invoices_v1.py
invoice-reader/archive/read_invoices_v2.py
invoice-reader/read_invoices.py
lead-scorer-poc/score.py
support-bot/app.py
Which models the code depends on, and what's running on the machine doing the scan:
== 4. Model names written in code (unique)
4 qwen2.5:3b
3 qwen2.5:7b
3 gpt-4o
2 gpt-4o-mini
1 qwen2.5:14b
== 5. What's running on this machine
Ollama is not answering on localhost:11434
AI API keys set in this shell (names only, never values):
(none)
Section 5 prints the names of API key variables, never their values. And when each file last changed:
== 6. Last change to each model-calling file
2026-10-29 invoice-reader/read_invoices.py
2026-10-15 experiments/summarize.py
2026-10-15 experiments/rag.py
2026-10-15 experiments/ask.py
2026-06-11 invoice-reader/archive/read_invoices_v2.py
2026-06-11 invoice-reader/archive/read_invoices_v1.py
2026-06-11 invoice-reader/archive/compare_models.py
2026-04-02 support-bot/app.py
2025-11-20 lead-scorer-poc/score.py
Three things stand out:
- No model is running here. Fernhill's invoice reader runs nightly on an ops server, not on the machine that holds the code. A scan sees code, not where it runs.
- The marketing site's hit isn't AI use. It's a page that shows an example request, and makes none. A signal is a lead, not a finding.
- Dates separate the living from the dead. One invoice reader file changed in October; three sit in
archive/, untouched since June. The lead scorer hasn't changed in a year.
Step 2: list systems, not files
A system is something with one purpose, one owner and one life: it gets deployed, changed and retired as a unit. Nine files became five systems, and a conversation with HR added a sixth:
| ID | System | Files | Why it's one system |
|---|---|---|---|
| AI-01 | Invoice reader | 1 | The current version, the one that runs nightly |
| AI-02 | Invoice reader, earlier versions | 3 | Hosted-model versions and the comparison that chose the local model; none runs |
| AI-03 | Support bot | 1 | One app, with its test and Kubernetes manifest |
| AI-04 | Lead scorer (proof of concept) | 1 | A one-off experiment from 2025 |
| AI-05 | Experiments | 3 | Learning scripts with one purpose and no users |
| AI-06 | Applicant screening helper | 0 | Planned; HR asked for it. Found by asking, not scanning |
The last row is the important one. It's the only system that would make findings about people, the only one that's high-risk under the EU AI Act (Lesson 4), and the only one covered by hiring rules like Illinois's and New York City's (Lesson 5). A code scan would never find it, because it doesn't exist yet. Planned uses belong in the inventory, because that's when governance is cheapest.
Step 3: describe each system
The inventory is a CSV file, so it opens in any spreadsheet and is easy to check with code. Each system gets fifteen fields:
| Field | Why it's there |
|---|---|
id, name, purpose | What it is, in a sentence someone outside the team understands |
status | active, planned, inactive, retired or practice: what needs attention now |
owner | One accountable person. "The team" isn't an owner. |
where_it_runs | The machine or cluster, and how often |
model, hosting | What it depends on; local, hosted, or mixed |
data_in | What goes in, especially personal or financial data, and where it's sent |
output_use | What happens to the result; this, more than the model, sets the risk |
decides_about_people | The single most useful risk flag in every framework |
human_oversight | Who checks, and at what point |
eu_tier | Lesson 4's classification |
code_path | Where the code is, so retired systems can be checked |
last_reviewed | When someone last confirmed the entry is true |
Two of Fernhill's rows, for the system in production and the one that's planned:
AI-01,Invoice reader,active,Reads supplier invoices and proposes the total and due date,
Priya (finance ops),"Ops server, nightly at 02:15",qwen2.5:7b,local,
Supplier invoices (include bank details),Proposal a clerk approves before payment,no,
A clerk approves every invoice,minimal,invoice-reader/read_invoices.py,2026-11-07
AI-06,Applicant screening helper,planned,Summarizes each applicant's evidence against the
job's criteria for HR,Dana (HR),Not built,not chosen,hosted,
Résumés and job descriptions (personal data),Summaries HR uses to shortlist,yes,
HR reviews every summary; the hiring manager decides,high (Annex III employment),
not built,2026-11-07
(Each row is one line in the file; they're wrapped here to fit.)
The support bot's status was the hard one. The code and a Kubernetes manifest were there, but nobody was sure whether it had ever been deployed, and an inventory built from memory is how forgotten systems keep running. So the team checked instead of guessing:
| Where | Command | What it shows |
|---|---|---|
| Containers | docker ps -a | Containers running or stopped on a machine |
| Kubernetes | kubectl get deploy,cronjob -A | Deployments and scheduled jobs in every namespace |
| Scheduled jobs | crontab -l, systemctl list-timers | What runs on a schedule on a server |
| Model servers | curl localhost:11434/api/tags | Which models a local Ollama server has installed |
Nothing named support-bot was running anywhere. A manifest in a repository shows a system could be deployed, not that it is: status inactive. If it ever is deployed, it's a chatbot, so Article 50's transparency duty applies (Lesson 4), and its row says so.
Step 4: check the inventory
The checker reads the CSV and applies seven rules. It's standard-library Python, so it runs anywhere Python 3 does:
#!/usr/bin/env python3
"""Check an AI inventory (CSV) for gaps. Standard library only.
Usage: check_inventory.py ai-inventory.csv ~/Apps
Exit code 1 if any problem is found, so it can run in CI.
"""
import csv
import os
import sys
from datetime import date, datetime
REQUIRED = ["id", "name", "status", "purpose", "owner", "where_it_runs", "model",
"hosting", "data_in", "output_use", "decides_about_people",
"human_oversight", "eu_tier", "code_path", "last_reviewed"]
STATUSES = {"active", "planned", "inactive", "retired", "practice"}
REVIEW_DAYS = 90
def main(inv_path, root):
rows = list(csv.DictReader(open(inv_path, newline="", encoding="utf-8")))
problems, notes = [], []
print(f"{len(rows)} entries in {os.path.basename(inv_path)}\n")
print(f"{'id':6} {'status':9} {'hosting':8} {'people?':8} {'name':42} EU tier")
for r in rows:
print(f"{r['id']:6} {r['status']:9} {r['hosting']:8} {r['decides_about_people']:8} "
f"{r['name'][:42]:42} {r['eu_tier']}")
rid = r["id"]
# 1. every field filled
for f in REQUIRED:
if not (r.get(f) or "").strip() or r[f].strip().upper() == "CHECK":
problems.append(f"{rid}: '{f}' is empty or still CHECK")
# 2. status is one we know
if r["status"] not in STATUSES:
problems.append(f"{rid}: status '{r['status']}' is not one of {sorted(STATUSES)}")
# 3. reviewed recently
try:
age = (date.today() - datetime.strptime(r["last_reviewed"], "%Y-%m-%d").date()).days
if age > REVIEW_DAYS:
problems.append(f"{rid}: last reviewed {age} days ago (limit {REVIEW_DAYS})")
except ValueError:
problems.append(f"{rid}: last_reviewed '{r['last_reviewed']}' is not YYYY-MM-DD")
# 4. retired means the code is gone, or there's a reason it stays
path = os.path.join(root, r["code_path"].rstrip("/"))
if r["status"] == "retired" and os.path.exists(path):
notes.append(f"{rid}: retired, but its code is still at {r['code_path']}")
# 5. decisions about people need named oversight and an assessment first
if r["decides_about_people"] == "yes":
if r["status"] == "planned":
notes.append(f"{rid}: decides about people; do an impact assessment "
"and check hiring rules before it's built")
if r["human_oversight"] in ("", "n/a"):
problems.append(f"{rid}: decides about people with no human oversight named")
# 6. high-risk can't be quietly active
if r["eu_tier"].startswith("high") and r["status"] == "active":
problems.append(f"{rid}: high-risk and active; needs its assessment on file")
# 7. hosted models: where does the data go?
if r["hosting"] in ("hosted", "mixed") and r["status"] in ("active", "planned"):
notes.append(f"{rid}: sends '{r['data_in']}' to a hosted model; "
"check the provider's data terms")
print()
for n in notes:
print("NOTE " + n)
for p in problems:
print("PROBLEM " + p)
print(f"\n{len(problems)} problems, {len(notes)} notes")
return 1 if problems else 0
if __name__ == "__main__":
if len(sys.argv) != 3:
sys.exit(__doc__)
sys.exit(main(sys.argv[1], os.path.expanduser(sys.argv[2])))
How it works:
csv.DictReaderturns each row into a dictionary keyed by the header, sor["owner"]is that row's owner.- Rules 1 to 3 are about the entry itself: nothing blank or left as
CHECK, a known status, and a review in the last 90 days. - Rules 4 to 7 are about risk: retired systems whose code still exists (they can be run again by accident), anything that decides about people, high-risk systems running without an assessment, and data sent to a hosted provider.
- Problems fail the run; notes don't. Problems mean the inventory is wrong or stale. Notes are decisions for a person.
- The exit code is 1 on any problem, so the checker can run in CI on every change to the inventory, like a test.
Fernhill's first run:
(Your machine)
python3 check_inventory.py ai-inventory.csv ~/Apps
6 entries in ai-inventory.csv
id status hosting people? name EU tier
AI-01 active local no Invoice reader minimal
AI-02 retired mixed no Invoice reader (earlier versions) minimal
AI-03 inactive hosted no Support bot transparency (chatbot) if deployed
AI-04 inactive hosted no Lead scorer (proof of concept) minimal
AI-05 practice local no Experiments minimal
AI-06 planned hosted yes Applicant screening helper high (Annex III employment)
NOTE AI-02: retired, but its code is still at invoice-reader/archive/
NOTE AI-06: decides about people; do an impact assessment and check hiring rules before it's built
NOTE AI-06: sends 'Résumés and job descriptions (personal data)' to a hosted model; check the provider's data terms
PROBLEM AI-04: 'owner' is empty or still CHECK
PROBLEM AI-04: last reviewed 352 days ago (limit 90)
2 problems, 3 notes
The lead scorer had no owner (the person who wrote it had moved on), and nobody had looked at it in almost a year. That's the most common inventory finding there is. Sales ops took ownership, confirmed nobody used it, and marked it retired. The second run:
6 entries in ai-inventory.csv
id status hosting people? name EU tier
AI-01 active local no Invoice reader minimal
AI-02 retired mixed no Invoice reader (earlier versions) minimal
AI-03 inactive hosted no Support bot transparency (chatbot) if deployed
AI-04 retired hosted no Lead scorer (proof of concept) minimal
AI-05 practice local no Experiments minimal
AI-06 planned hosted yes Applicant screening helper high (Annex III employment)
NOTE AI-02: retired, but its code is still at invoice-reader/archive/
NOTE AI-04: retired, but its code is still at lead-scorer-poc/
NOTE AI-06: decides about people; do an impact assessment and check hiring rules before it's built
NOTE AI-06: sends 'Résumés and job descriptions (personal data)' to a hosted model; check the provider's data terms
0 problems, 4 notes
Zero problems. Four notes, each a decision for a person:
- AI-02 and AI-04: retired code that still exists can be run again by accident. Delete it, or keep it on purpose and write down why. The invoice reader's archive is a fair keep: it's the record of why the local model was chosen.
- AI-06: before any code is written, the screening helper needs an impact assessment, a decision on which hiring rules apply, and a look at what the hosted provider does with résumés. The later lessons on testing, oversight and vendors pick each of these up.
In real systems
- Code is one source of four. The others are procurement (AI inside bought software: a CRM's lead scoring, an HR system's screening), expense reports and single sign-on logs (people signing up for AI tools on their own, sometimes called shadow AI), and asking teams directly. Most organizations find more AI in what they bought than in what they built.
- Network and cloud logs help. Outbound calls to model providers' API domains, and model endpoints in cloud accounts, show use the code search missed.
- Own each entry. An inventory nobody reviews drifts within a quarter. A review date and a check that fails when it passes are the cheapest fix.
- Agencies publish theirs. US federal AI use case inventories are public, and a good model of the fields that matter: purpose, stage, whether it's high-impact, and what's been done about it.
- Keep it boring. A spreadsheet that people update beats a governance platform nobody opens. Start with the fifteen fields and a checker.
Spot the problem
Work out what's wrong before you read the explanation.
"We inventoried the repos." A company's AI governance lead runs a code scan like the one above across all 400 of its repositories, builds a tidy inventory of 31 systems, all minimal risk, and reports to the board that the company has no high-risk AI. Six months later, an applicant's complaint reveals that the HR team has been using a vendor's résumé screening feature, switched on in the applicant tracking system the company already paid for. What's wrong? The inventory only looked where AI is built, not where it's bought or switched on. The screening feature never appeared in any repository; it was a setting in purchased software, and it's the one system that was high-risk under the EU AI Act and covered by New York City's audit law. Three fixes: add procurement and contract review to the inventory process, asking every vendor whether their product uses AI and for what; ask the teams that make decisions about people (HR, lending, support) what tools they use, not just engineering; and don't report "no high-risk AI" without saying which sources were checked.
Keep this
| Step | Do |
|---|---|
| Find in code | Model libraries, endpoints, key names, model names, embeddings; skip dependencies |
| Find at runtime | Containers, clusters, scheduled jobs, model servers, network logs |
| Find what's bought | Vendor products with AI features; contracts; expense reports |
| Find what's planned | Ask: the cheapest time to govern a system is before it's built |
| Group | One system per purpose and owner; tests and demos aren't uses |
| Describe | The fifteen fields; above all, output use and whether it decides about people |
| Check | Rules that fail on gaps; notes for decisions; run in CI |
| Keep current | Review date on every entry; retire with the code, or say why it stays |
Where you are now
You can find AI use in code and at runtime, turn files into systems, describe each with the fields that set its risk, and check the inventory automatically. You've also seen what a scan misses. The next lesson takes the inventory's two riskiest fields, data_in and output_use, and looks at the data: what goes into prompts, what comes out, and what to keep out of both.
Check yourself
Why does an AI inventory come before the other controls?
Fernhill's marketing site matched the scan. Why isn't it an entry?
Which system did the code scan miss entirely?
Which field does most to set a system's risk?
The support bot has a Kubernetes manifest. Is it deployed?
A company scanned all its repositories and found no high-risk AI. What's the gap?
Key terms
- AI inventory: a list of an organization's AI systems, with what each does, who owns it and what risk it carries.
- AI system (in an inventory): something with one purpose and one owner, deployed and retired as a unit, however many files it has.
- Signal: evidence in code or configuration that AI may be used, such as an import, an endpoint or a key name. A lead, not a finding.
- Shadow AI: AI tools people use without the organization knowing, often signed up for individually.
- Output use: what happens to a system's result; the main driver of its risk.
- Use case inventory: the US federal term for an agency's public AI inventory.
Go deeper
- NIST AI RMF, GOVERN 1.6 (opens in a new tab): the inventory subcategory and its suggested actions.
- The US federal 2025 AI use case inventory (opens in a new tab), a real example of fields and scale: agencies reported over 3,000 uses (opens in a new tab).
- Try: run a scan like this on your own projects. How many signals become systems?
- Try: list three products your team bought in the last year. Does any have an AI feature switched on? Who decided?