Build a Department Trend Monitor
Use Claude AI to analyze Office docs and send monthly trend reports via email.
Introduction
30 Second Summary
Every department generates mountains of documents. Spreadsheets, presentations, and reports pile up in shared folders with no one connecting the dots between them.
In this project, you will build a Python application that uses Claude AI to automatically scan department documents, identify trends and patterns, and email a monthly summary report to your team.
What You'll Build
A modular Python system that extracts text from Office documents, analyzes them with Claude AI, sends reports via Microsoft Graph API, and cleans up old files on a monthly schedule.
By the end of this project, you'll have:
- A document scanner that extracts text from Excel, PowerPoint, and Word files in a shared folder.
- An AI-powered analyzer that sends extracted text to Claude and identifies key trends, recurring themes, and notable patterns.
- An automated email pipeline that delivers formatted monthly reports to your team via Microsoft Graph API.
- A scheduled automation using Windows Task Scheduler that runs the entire pipeline unattended on the 1st of every month.
- Secret Mission: Add a month-over-month trend comparison that highlights what's new and what's declining.
Are there any prerequisites?
No prior experience is required. This guide includes full setup instructions for every tool you need, including VS Code, Python, the Claude API, and Microsoft Graph.
Install Python Libraries and Get Your Claude API Key
Your environment is ready, and now it needs the building blocks that make the trend monitor work.
This project relies on six Python packages that each handle a specific job. Some extract text from Office documents, one talks to the Claude AI, and others handle authentication and email sending. In this step, you will install those packages, get your API key from Anthropic, and create a configuration file that holds all the settings your project needs.
In this step, get ready to:
- Install the Python packages the project depends on.
- Get a Claude API key from the Anthropic Console.
- Create the configuration file with placeholder values for all settings.
Install the required Python packages
Each package handles one piece of the pipeline. You need libraries for reading Excel, PowerPoint, and Word files, plus libraries for AI analysis, authentication, and sending HTTP requests.
- Install all six packages at once by running this command in your terminal (make sure your virtual environment is still active):
pip install anthropic openpyxl python-pptx python-docx msal requests
What does each package do?
- anthropic is the official SDK for talking to the Claude AI API.
- openpyxl reads Excel .xlsx files and extracts cell data.
- python-pptx reads PowerPoint .pptx files and extracts text from slides.
- python-docx reads Word .docx files and extracts paragraph text.
- msal is Microsoft's authentication library, used to get tokens for sending email.
- requests sends HTTP requests to the Microsoft Graph API.
- Wait for the installation to finish. You should see a Successfully installed message at the end listing all the packages and their versions.
Seeing a permission error or install failure?
- Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt. If not, activate it again with venv\Scripts\activate.
- If you see a network timeout, check your internet connection and try running the command again.
Why won't my packages install?
Get your Claude API key
To use Claude's AI in your project, you need an API key from Anthropic. This key authenticates your application so Claude knows who is making the request.
- Open your browser and go to console.anthropic.com.
- Create an account if you do not have one already. You can sign up with your email address.
- Once signed in, navigate to Settings in the left sidebar.
- Click Billing and add credit to your account (minimum $5).
- Navigate to API Keys in the left sidebar.
- Click Create Key.
- Give your key a name like department-monitor and click Create Key.
- Copy the key immediately and paste it somewhere safe (like a text file on your Desktop). You will not be able to see it again after you close this dialog.
Why do I need to add credit?
Claude's API charges based on usage. Each time your script sends document text to Claude and receives an analysis back, it costs a tiny amount. For this project, a single run typically costs less than $0.10 depending on how many documents are in your folder.
Claude Sonnet 4.6 costs $3 per million input tokens and $15 per million output tokens. The $5 minimum credit will cover hundreds of runs.
Keep your API key private
Never share your API key publicly, commit it to a Git repository, or paste it into a chat. Anyone with your key can make API calls billed to your account.
Create the configuration file
The configuration file keeps all your settings in one place. Instead of scattering API keys, folder paths, and email addresses throughout your code, you store them in a single file that every other module imports from.
- In VS Code, create a new file called config.py in the root of your project folder.
- Paste the following code into config.py:
"""Configuration for the Department Trend Monitor."""
# === Claude API Settings ===
ANTHROPIC_API_KEY = "your-api-key-here"
CLAUDE_MODEL = "claude-sonnet-4-6"
# === Folder Settings ===
WATCH_FOLDER = r"C:\Shared\DepartmentDocs"
ARCHIVE_FOLDER = r"C:\Shared\DepartmentDocs\archive"
# === Cleanup Settings ===
MAX_FILE_AGE_DAYS = 730 # 2 years
# === Microsoft Graph Email Settings ===
TENANT_ID = "your-tenant-id-here"
CLIENT_ID = "your-client-id-here"
CLIENT_SECRET = "your-client-secret-here"
SENDER_EMAIL = "sender@yourcompany.com"
RECIPIENTS = [
"colleague1@yourcompany.com",
"colleague2@yourcompany.com",
]
# === Email Content ===
EMAIL_SUBJECT = "Monthly Department Trend Report"
What do these settings control?
- ANTHROPIC_API_KEY authenticates your app with Claude's API. You will replace this placeholder with your real key shortly.
- CLAUDE_MODEL specifies which Claude model to use. claude-sonnet-4-6 balances speed and intelligence at a low cost.
- WATCH_FOLDER is the shared folder your script will scan for documents. You can change this path later to match your department's shared drive.
- MAX_FILE_AGE_DAYS controls how old files can get before the cleanup utility removes them (730 days = 2 years).
- The Microsoft Graph settings (TENANT_ID, CLIENT_ID, CLIENT_SECRET) will be filled in during Step 5 when you register your app with Microsoft Entra.
- Replace "your-api-key-here" with the real API key you copied from the Anthropic Console. Keep the surrounding quotes.
- Save config.py.
You should see config.py listed in the VS Code Explorer sidebar on the left.
Don't see config.py in the sidebar?
- Make sure you saved the file (Ctrl+S on Windows or Cmd+S on macOS). Unsaved files show a dot on their tab.
- Confirm the file is saved inside your project folder, not in a parent directory.
Help me find where my config.py file was saved.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file.
ⓧ I'd like to double check the full code
Here is the complete config.py for comparison:
"""Configuration for the Department Trend Monitor."""
# === Claude API Settings ===
ANTHROPIC_API_KEY = "your-api-key-here"
CLAUDE_MODEL = "claude-sonnet-4-6"
# === Folder Settings ===
WATCH_FOLDER = r"C:\Shared\DepartmentDocs"
ARCHIVE_FOLDER = r"C:\Shared\DepartmentDocs\archive"
# === Cleanup Settings ===
MAX_FILE_AGE_DAYS = 730 # 2 years
# === Microsoft Graph Email Settings ===
TENANT_ID = "your-tenant-id-here"
CLIENT_ID = "your-client-id-here"
CLIENT_SECRET = "your-client-secret-here"
SENDER_EMAIL = "sender@yourcompany.com"
RECIPIENTS = [
"colleague1@yourcompany.com",
"colleague2@yourcompany.com",
]
# === Email Content ===
EMAIL_SUBJECT = "Monthly Department Trend Report"
You have all your dependencies installed and your project's central settings file ready. Next up, you will build the document scanner that walks through a folder and extracts text from Excel, PowerPoint, and Word files.
Build the Document Scanner
With your configuration in place and libraries installed, it's time to build the first real module of your trend monitor. The scanner is the foundation of the entire pipeline.
Claude AI cannot read Office files directly. It only accepts plain text. Your scanner will walk through the shared folder, find Excel, PowerPoint, and Word documents, and extract their text content into a format Claude can analyze.
In this step, get ready to:
- Create scanner.py with a function that discovers Office files in a folder.
- Implement text extraction for .xlsx, .pptx, and .docx files.
- Test the scanner on sample documents to verify extraction works.
Create scanner.py with file discovery
The scanner needs to know which files to process. The first function will walk through the watch folder and collect paths to all supported Office files.
- In VS Code, create a new file called scanner.py in your project folder (the same folder where config.py lives).
- Add the module docstring, imports, and the file discovery function by pasting this code:
"""Document scanner that extracts text from Office files in the watch folder."""
import os
from openpyxl import load_workbook
from pptx import Presentation
from docx import Document
def get_files(folder_path):
"""Get all supported Office files from the folder."""
supported_extensions = ('.xlsx', '.pptx', '.docx')
files = []
for root, dirs, filenames in os.walk(folder_path):
# Skip the archive folder
if 'archive' in root:
continue
for filename in filenames:
if filename.lower().endswith(supported_extensions):
files.append(os.path.join(root, filename))
return files
What does this code do?
os.walk() recursively traverses the folder and all subfolders, returning each directory's files.
The function filters for files ending in .xlsx, .pptx, or .docx and skips anything inside an archive subfolder. This prevents the system from re-scanning old archived data later.
- Save scanner.py.
You won't see output yet. The next chunk adds the extraction functions that make this useful.
Implement text extraction for Excel, PowerPoint, and Word
Each file format stores text differently. Excel has cells organized in sheets, PowerPoint has text frames inside shapes on slides, and Word stores content in paragraphs.
- Add the three extraction functions below the get_files function in scanner.py:
def extract_text_from_xlsx(file_path):
"""Extract text from an Excel workbook."""
text_parts = []
wb = load_workbook(file_path, read_only=True, data_only=True)
for sheet in wb.worksheets:
text_parts.append(f"[Sheet: {sheet.title}]")
for row in sheet.iter_rows(values_only=True):
row_text = [str(cell) for cell in row if cell is not None]
if row_text:
text_parts.append(" | ".join(row_text))
wb.close()
return "\n".join(text_parts)
def extract_text_from_pptx(file_path):
"""Extract text from a PowerPoint presentation."""
text_parts = []
prs = Presentation(file_path)
for slide_num, slide in enumerate(prs.slides, 1):
text_parts.append(f"[Slide {slide_num}]")
for shape in slide.shapes:
if shape.has_text_frame:
text_parts.append(shape.text_frame.text)
return "\n".join(text_parts)
def extract_text_from_docx(file_path):
"""Extract text from a Word document."""
doc = Document(file_path)
text_parts = []
for paragraph in doc.paragraphs:
if paragraph.text.strip():
text_parts.append(paragraph.text)
return "\n".join(text_parts)
What does each function do?
- extract_text_from_xlsx opens the workbook in read-only mode and iterates through every sheet's rows. It joins cell values with | separators so the text stays readable.
- extract_text_from_pptx loads the presentation and checks each shape on each slide for a text frame. Shapes without text (like images) are skipped.
- extract_text_from_docx reads all paragraphs from the Word document and skips empty ones.
Now add the orchestrator function that ties everything together. This function calls get_files and then routes each file to the correct extraction function.
- Add the scan_folder function and the test block below the extraction functions in scanner.py:
def scan_folder(folder_path):
"""Scan the folder and extract text from all supported files."""
files = get_files(folder_path)
results = []
for file_path in files:
filename = os.path.basename(file_path)
ext = os.path.splitext(filename)[1].lower()
try:
if ext == '.xlsx':
text = extract_text_from_xlsx(file_path)
elif ext == '.pptx':
text = extract_text_from_pptx(file_path)
elif ext == '.docx':
text = extract_text_from_docx(file_path)
else:
continue
if text.strip():
results.append({
"filename": filename,
"text": text
})
print(f" Extracted: {filename}")
except Exception as e:
print(f" Error reading {filename}: {e}")
return results
if __name__ == "__main__":
from config import WATCH_FOLDER
print(f"Scanning: {WATCH_FOLDER}")
documents = scan_folder(WATCH_FOLDER)
print(f"\nExtracted text from {len(documents)} documents.")
What does this code do?
scan_folder loops through every file returned by get_files, determines its type by extension, and calls the matching extraction function. If extraction fails (corrupted file, password-protected, etc.), it catches the exception and prints an error instead of crashing the whole pipeline.
The if __name__ == "__main__" block lets you run scanner.py directly from the terminal for testing.
- Save scanner.py.
- Run the scanner by entering this command in your terminal:
python scanner.py
You should see output like:
Scanning: C:\Shared\DepartmentDocs
Extracted text from 0 documents.
This confirms the script runs without errors. It reports 0 documents because the watch folder is empty or doesn't exist yet.
Seeing a FileNotFoundError or ModuleNotFoundError?
- If you see a module error, make sure your virtual environment is still activated. You should see (venv) at the start of your terminal prompt.
- A FileNotFoundError about the folder path is expected if C:\Shared\DepartmentDocs doesn't exist yet. The next substep creates test documents in that location.
Help me debug this error
Test the scanner on sample documents
A scanner that reports 0 documents only proves it doesn't crash. To confirm extraction actually works, you need real test files.
- Create the watch folder by running this command:
mkdir C:\Shared\DepartmentDocs
- Create a quick test Word document by running this Python script in your terminal:
python -c "from docx import Document; doc = Document(); doc.add_paragraph('Q1 revenue up 15 percent. Customer satisfaction at all-time high.'); doc.save(r'C:\Shared\DepartmentDocs\test_report.docx')"
- Create a quick test Excel file by running:
python -c "from openpyxl import Workbook; wb = Workbook(); ws = wb.active; ws.append(['Project', 'Status']); ws.append(['Website Redesign', 'In Progress']); ws.append(['API Migration', 'Complete']); wb.save(r'C:\Shared\DepartmentDocs\projects.xlsx')"
- Now run the scanner again:
python scanner.py
You should see output like this:
Scanning: C:\Shared\DepartmentDocs
Extracted: test_report.docx
Extracted: projects.xlsx
Extracted text from 2 documents.
What should I see?
The scanner found both test files and extracted their text successfully. Each file name appears with "Extracted:" in front of it. The final line confirms 2 documents were processed.
Still seeing 0 documents?
- Make sure the WATCH_FOLDER path in config.py matches the folder you created. Check for typos or mismatched drive letters.
- Verify the test files exist by navigating to C:\Shared\DepartmentDocs in File Explorer. You should see test_report.docx and projects.xlsx.
Help me troubleshoot my scanner
✔️ Awesome, I've got everything!
Great. Double check you've saved scanner.py and that your test run shows extracted documents.
ⓧ I'd like to double check the full code
Here is the complete scanner.py file:
"""Document scanner that extracts text from Office files in the watch folder."""
import os
from openpyxl import load_workbook
from pptx import Presentation
from docx import Document
def get_files(folder_path):
"""Get all supported Office files from the folder."""
supported_extensions = ('.xlsx', '.pptx', '.docx')
files = []
for root, dirs, filenames in os.walk(folder_path):
# Skip the archive folder
if 'archive' in root:
continue
for filename in filenames:
if filename.lower().endswith(supported_extensions):
files.append(os.path.join(root, filename))
return files
def extract_text_from_xlsx(file_path):
"""Extract text from an Excel workbook."""
text_parts = []
wb = load_workbook(file_path, read_only=True, data_only=True)
for sheet in wb.worksheets:
text_parts.append(f"[Sheet: {sheet.title}]")
for row in sheet.iter_rows(values_only=True):
row_text = [str(cell) for cell in row if cell is not None]
if row_text:
text_parts.append(" | ".join(row_text))
wb.close()
return "\n".join(text_parts)
def extract_text_from_pptx(file_path):
"""Extract text from a PowerPoint presentation."""
text_parts = []
prs = Presentation(file_path)
for slide_num, slide in enumerate(prs.slides, 1):
text_parts.append(f"[Slide {slide_num}]")
for shape in slide.shapes:
if shape.has_text_frame:
text_parts.append(shape.text_frame.text)
return "\n".join(text_parts)
def extract_text_from_docx(file_path):
"""Extract text from a Word document."""
doc = Document(file_path)
text_parts = []
for paragraph in doc.paragraphs:
if paragraph.text.strip():
text_parts.append(paragraph.text)
return "\n".join(text_parts)
def scan_folder(folder_path):
"""Scan the folder and extract text from all supported files."""
files = get_files(folder_path)
results = []
for file_path in files:
filename = os.path.basename(file_path)
ext = os.path.splitext(filename)[1].lower()
try:
if ext == '.xlsx':
text = extract_text_from_xlsx(file_path)
elif ext == '.pptx':
text = extract_text_from_pptx(file_path)
elif ext == '.docx':
text = extract_text_from_docx(file_path)
else:
continue
if text.strip():
results.append({
"filename": filename,
"text": text
})
print(f" Extracted: {filename}")
except Exception as e:
print(f" Error reading {filename}: {e}")
return results
if __name__ == "__main__":
from config import WATCH_FOLDER
print(f"Scanning: {WATCH_FOLDER}")
documents = scan_folder(WATCH_FOLDER)
print(f"\nExtracted text from {len(documents)} documents.")
Your document scanner is working. Next up, you'll send this extracted text to Claude AI and get back trend analysis.
Build the Claude AI Trend Analyzer
Your document scanner can now extract text from Excel, PowerPoint, and Word files. The next step is to turn that raw text into something meaningful.
In this step, you will send the extracted document text to Claude's API and ask it to identify trends, recurring themes, and action items across the entire collection. This is the intelligence layer that transforms scattered files into a focused analysis.
In this step, get ready to:
- Create analyzer.py with logic to combine document texts and manage token usage.
- Design a prompt that asks Claude to identify trends, patterns, and action items.
- Test the analyzer with sample text and verify the AI output.
Create the analyzer file with text combination logic
Claude's API accepts plain text, not raw Office files. Your analyzer needs to bundle all the extracted document text into one payload, while keeping the total size manageable so you don't burn through tokens unnecessarily.
- Create a new file called analyzer.py in your project folder.
- Add the imports and the text combination logic by pasting this code:
"""Claude AI trend analyzer that identifies patterns in extracted documents."""
from anthropic import Anthropic
from config import ANTHROPIC_API_KEY, CLAUDE_MODEL
def analyze_trends(documents):
"""Send extracted document text to Claude for trend analysis."""
if not documents:
return "No documents found to analyze."
# Combine document texts with file names for context
combined_text = ""
for doc in documents:
combined_text += f"\n\n--- Document: {doc['filename']} ---\n"
# Limit each document to 2000 characters to manage token usage
combined_text += doc['text'][:2000]
# Limit total input to avoid excessive token usage
if len(combined_text) > 30000:
combined_text = combined_text[:30000]
What does this code do?
The function starts with a guard clause that returns early if no documents were passed in.
It then loops through each document, prepending the filename as a header so Claude knows which file each block of text came from. Each document is capped at 2000 characters to keep costs low.
The total combined text is also capped at 30,000 characters. At Claude Sonnet 4.6's pricing of $3 per million input tokens, a typical run costs well under $0.10.
- Save analyzer.py.
Design the Claude API prompt and send it
The quality of Claude's analysis depends entirely on how you structure your prompt. A clear, numbered prompt gives you consistent, structured output every time.
- In analyzer.py, add the following code directly below the line combined_text = combined_text[:30000] (still inside the analyze_trends function):
client = Anthropic(api_key=ANTHROPIC_API_KEY)
message = client.messages.create(
model=CLAUDE_MODEL,
max_tokens=2000,
messages=[
{
"role": "user",
"content": f"""You are a department analyst. Analyze the following documents from our department's shared folder and identify:
1. **Key Trends**: What topics or themes appear repeatedly across documents?
2. **Notable Patterns**: Are there any shifts, growth areas, or declining topics?
3. **Action Items**: What should the department pay attention to based on these documents?
4. **Summary**: A 2-3 sentence executive summary of the overall direction.
Format your response in clear sections with bullet points.
Documents:
{combined_text}"""
}
]
)
return message.content[0].text
What does this code do?
The Anthropic client is initialized with your API key from config.py. It calls client.messages.create() to send a single user message containing the full prompt and all your document text.
The prompt gives Claude a specific role ("department analyst") and asks for four clearly numbered sections. This structure ensures the output is easy to read in the email report you will build later.
The response text is accessed via message.content[0].text and returned directly to the caller.
- Save analyzer.py.
Seeing a red underline under Anthropic?
Make sure your virtual environment is activated (you should see (venv) at the start of your terminal prompt). The anthropic package was installed into your virtual environment in Step 2.
Help me troubleshoot this import error.
Test the analyzer with sample data
Before connecting the analyzer to real documents, you can test it with hardcoded sample data to confirm the API call works and Claude returns structured output.
- Add the following test block at the bottom of analyzer.py, outside the function:
if __name__ == "__main__":
# Test with sample data
sample_docs = [
{"filename": "test.docx", "text": "Q1 revenue increased by 15%. Customer satisfaction scores are at an all-time high. New product launch planned for Q3."}
]
result = analyze_trends(sample_docs)
print(result)
What does this code do?
The if __name__ == "__main__" block only runs when you execute the file directly. It creates a fake document list with sample business text, passes it to analyze_trends, and prints Claude's response.
- Save analyzer.py.
- Run the analyzer test by entering this command in your terminal:
python analyzer.py
Wait a few seconds for the API response. You should see a structured analysis with headings like Key Trends, Notable Patterns, Action Items, and Summary printed in your terminal.
Seeing an authentication error?
Open config.py and confirm that ANTHROPIC_API_KEY contains your actual API key (not the placeholder text). Also verify your account has at least $5 in credit at console.anthropic.com.
Help me fix this API authentication error.
✔️ Awesome, I've got everything!
Great. Double check you have saved analyzer.py and that your test returned a structured response.
ⓧ I'd like to double check the full code
Here is the complete analyzer.py file:
"""Claude AI trend analyzer that identifies patterns in extracted documents."""
from anthropic import Anthropic
from config import ANTHROPIC_API_KEY, CLAUDE_MODEL
def analyze_trends(documents):
"""Send extracted document text to Claude for trend analysis."""
if not documents:
return "No documents found to analyze."
# Combine document texts with file names for context
combined_text = ""
for doc in documents:
combined_text += f"\n\n--- Document: {doc['filename']} ---\n"
# Limit each document to 2000 characters to manage token usage
combined_text += doc['text'][:2000]
# Limit total input to avoid excessive token usage
if len(combined_text) > 30000:
combined_text = combined_text[:30000]
client = Anthropic(api_key=ANTHROPIC_API_KEY)
message = client.messages.create(
model=CLAUDE_MODEL,
max_tokens=2000,
messages=[
{
"role": "user",
"content": f"""You are a department analyst. Analyze the following documents from our department's shared folder and identify:
1. **Key Trends**: What topics or themes appear repeatedly across documents?
2. **Notable Patterns**: Are there any shifts, growth areas, or declining topics?
3. **Action Items**: What should the department pay attention to based on these documents?
4. **Summary**: A 2-3 sentence executive summary of the overall direction.
Format your response in clear sections with bullet points.
Documents:
{combined_text}"""
}
]
)
return message.content[0].text
if __name__ == "__main__":
# Test with sample data
sample_docs = [
{"filename": "test.docx", "text": "Q1 revenue increased by 15%. Customer satisfaction scores are at an all-time high. New product launch planned for Q3."}
]
result = analyze_trends(sample_docs)
print(result)
Your trend analyzer is working and Claude is returning structured insights from document text. Next up, you will build the email sender that delivers these reports directly to your team's inboxes via Microsoft Graph API.
Register a Microsoft Entra App and Build the Email Sender
Your trend analyzer can now identify patterns in documents using Claude AI. The next piece of the puzzle is delivering those insights to your team automatically.
In this step, you will register an application in Microsoft Entra (Microsoft's identity platform) and build an email sender that uses the Microsoft Graph API to deliver reports directly to your colleagues' inboxes.
In this step, get ready to:
- Register an app in Microsoft Entra with Mail.Send permission.
- Create the email sender module using MSAL and Microsoft Graph.
- Send a test email to verify everything works.
Register an application in Microsoft Entra admin center
Microsoft deprecated basic SMTP authentication for Exchange Online. The modern approach for sending email from a background script (without a user signing in) is to use the Microsoft Graph API with app-only authentication.
Why Microsoft Graph instead of SMTP?
Microsoft permanently disabled basic authentication for Exchange Online. SMTP AUTH basic auth for client submission is scheduled for permanent removal in March 2026.
Microsoft Graph API with client credentials is the recommended path for daemon apps that send email without user interaction. It also gives you fine-grained permissions and audit logging.
To use Microsoft Graph, you need to register an application in the Microsoft Entra admin center. This gives your script its own identity so it can authenticate and send email on behalf of a mailbox.
What does your IT admin need to do?
Registering an app with application-level permissions (like Mail.Send) requires admin consent. If you don't have admin access yourself, share these steps with your IT department and ask them to provide you with the Tenant ID, Client ID, and Client Secret.
- Sign in to the Microsoft Entra admin center at https://entra.microsoft.com.
- In the left sidebar, browse to Identity > Applications > App registrations.
- Click New registration.
- Enter a name for the app, such as Department Trend Monitor.
- Under Supported account types, select Accounts in this organizational directory only.
- Leave Redirect URI blank (not needed for this use case).
- Click Register.
After registration, the Overview page displays two values you need. Copy them now.
- Copy the Application (client) ID value.
- Copy the Directory (tenant) ID value.
Next, create a client secret so your script can authenticate.
- In the left menu of your app registration, click Certificates & secrets.
- Select the Client secrets tab, then click New client secret.
- Enter a description (e.g. Trend Monitor Secret) and select an expiration (e.g. 180 days).
- Click Add.
- Immediately copy the Value of the new secret and store it somewhere safe. This value is only shown once.
Copy the secret value immediately
Microsoft Entra only displays the client secret value at creation time. If you navigate away from the page without copying it, you will need to delete the secret and create a new one.
Now grant the app permission to send email via Microsoft Graph.
- In the left menu, click API permissions.
- Click Add a permission.
- Select Microsoft Graph.
- Select Application permissions (not Delegated permissions).
- Search for Mail.Send and check the box next to it.
- Click Add permissions.
- Back on the API permissions page, click Grant admin consent and confirm. This requires an admin account.
Now update config.py with your real values.
- In VS Code, open config.py.
- Replace your-tenant-id-here with the Directory (tenant) ID you copied.
- Replace your-client-id-here with the Application (client) ID you copied.
- Replace your-client-secret-here with the client secret value you copied.
- Replace sender@yourcompany.com with the email address the app will send from (must be a real mailbox in your tenant).
- Update the RECIPIENTS list with your own email address (for testing).
- Save config.py.
Don't have admin access?
If your IT department needs to perform the registration, share the steps above and ask them to provide the Tenant ID, Client ID, and Client Secret. They will also need to grant admin consent for the Mail.Send permission.
Help me understand the Microsoft Entra app registration process for sending emails via Graph API.
Create the email sender module
With the app registered, you can now build the Python module that authenticates and sends email. The module uses MSAL (Microsoft Authentication Library) to acquire a token, then calls the Microsoft Graph sendMail endpoint.
- Create a new file called emailer.py in your project folder.
- Add the imports and the get_access_token() function by pasting the following code:
"""Email sender using Microsoft Graph API with app-only authentication."""
import json
import requests
from msal import ConfidentialClientApplication
from config import TENANT_ID, CLIENT_ID, CLIENT_SECRET, SENDER_EMAIL, RECIPIENTS, EMAIL_SUBJECT
def get_access_token():
"""Acquire an access token using client credentials flow."""
authority = f"https://login.microsoftonline.com/{TENANT_ID}"
scopes = ["https://graph.microsoft.com/.default"]
app = ConfidentialClientApplication(
client_id=CLIENT_ID,
authority=authority,
client_credential=CLIENT_SECRET
)
result = app.acquire_token_silent(scopes, account=None)
if not result:
result = app.acquire_token_for_client(scopes=scopes)
if "access_token" in result:
return result["access_token"]
else:
raise Exception(f"Failed to get token: {result.get('error_description')}")
What does this code do?
The get_access_token() function uses the client credentials flow to authenticate your app without a user present. It first tries acquire_token_silent (in case a cached token exists), then falls back to acquire_token_for_client to get a fresh token.
The scope https://graph.microsoft.com/.default requests all permissions assigned to the app in the portal. The authority URL https://login.microsoftonline.com/{TENANT_ID} targets your specific organization's tenant.
- Now add the send_report() function below get_access_token() in emailer.py:
def send_report(analysis_text, subject=None):
"""Send the trend analysis report via email."""
access_token = get_access_token()
html_body = f"""
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6;">
<h2>Monthly Department Trend Report</h2>
<hr>
<pre style="white-space: pre-wrap; font-family: Arial, sans-serif;">{analysis_text}</pre>
<hr>
<p style="color: #666; font-size: 12px;">
This report was automatically generated by the Department Trend Monitor.
</p>
</body>
</html>
"""
to_recipients = [
{"emailAddress": {"address": addr}} for addr in RECIPIENTS
]
email_data = {
"message": {
"subject": subject or EMAIL_SUBJECT,
"body": {
"contentType": "HTML",
"content": html_body
},
"toRecipients": to_recipients
},
"saveToSentItems": "true"
}
endpoint = f"https://graph.microsoft.com/v1.0/users/{SENDER_EMAIL}/sendMail"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
response = requests.post(endpoint, headers=headers, data=json.dumps(email_data))
if response.status_code == 202:
print(" Email sent successfully!")
return True
else:
print(f" Failed to send email: {response.status_code} - {response.text}")
return False
What does this code do?
The send_report() function builds an HTML-formatted email containing the trend analysis, then sends it via the Microsoft Graph sendMail endpoint.
The endpoint format is https://graph.microsoft.com/v1.0/users/{SENDER_EMAIL}/sendMail. A successful send returns HTTP status 202. The saveToSentItems flag keeps a copy in the sender's Sent folder for auditing.
- Finally, add the test block at the bottom of emailer.py:
if __name__ == "__main__":
send_report("This is a test report.\n\nTrend 1: Testing works!")
- Save emailer.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved emailer.py.
ⓧ I'd like to double check the full code
"""Email sender using Microsoft Graph API with app-only authentication."""
import json
import requests
from msal import ConfidentialClientApplication
from config import TENANT_ID, CLIENT_ID, CLIENT_SECRET, SENDER_EMAIL, RECIPIENTS, EMAIL_SUBJECT
def get_access_token():
"""Acquire an access token using client credentials flow."""
authority = f"https://login.microsoftonline.com/{TENANT_ID}"
scopes = ["https://graph.microsoft.com/.default"]
app = ConfidentialClientApplication(
client_id=CLIENT_ID,
authority=authority,
client_credential=CLIENT_SECRET
)
result = app.acquire_token_silent(scopes, account=None)
if not result:
result = app.acquire_token_for_client(scopes=scopes)
if "access_token" in result:
return result["access_token"]
else:
raise Exception(f"Failed to get token: {result.get('error_description')}")
def send_report(analysis_text, subject=None):
"""Send the trend analysis report via email."""
access_token = get_access_token()
html_body = f"""
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6;">
<h2>Monthly Department Trend Report</h2>
<hr>
<pre style="white-space: pre-wrap; font-family: Arial, sans-serif;">{analysis_text}</pre>
<hr>
<p style="color: #666; font-size: 12px;">
This report was automatically generated by the Department Trend Monitor.
</p>
</body>
</html>
"""
to_recipients = [
{"emailAddress": {"address": addr}} for addr in RECIPIENTS
]
email_data = {
"message": {
"subject": subject or EMAIL_SUBJECT,
"body": {
"contentType": "HTML",
"content": html_body
},
"toRecipients": to_recipients
},
"saveToSentItems": "true"
}
endpoint = f"https://graph.microsoft.com/v1.0/users/{SENDER_EMAIL}/sendMail"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
response = requests.post(endpoint, headers=headers, data=json.dumps(email_data))
if response.status_code == 202:
print(" Email sent successfully!")
return True
else:
print(f" Failed to send email: {response.status_code} - {response.text}")
return False
if __name__ == "__main__":
send_report("This is a test report.\n\nTrend 1: Testing works!")
Test the email sender
Before wiring this into the full pipeline, confirm that authentication and email delivery work end-to-end.
- Make sure your RECIPIENTS list in config.py includes your own email address.
- Run the email sender test by executing this command in your terminal:
python emailer.py
- Check your inbox for the test email. You should see a message with the subject Monthly Department Trend Report and the body text "This is a test report."
Email not arriving?
If you see a "Failed to get token" error, double check your TENANT_ID, CLIENT_ID, and CLIENT_SECRET values in config.py. Make sure there are no extra spaces or quotes around the values.
If you see a 403 error, the Mail.Send permission may not have admin consent granted yet. Ask your IT admin to click Grant admin consent on the API permissions page.
If you see a 404 error, verify that SENDER_EMAIL matches a real mailbox in your Microsoft 365 tenant.
Help me troubleshoot my Microsoft Graph email sender that isn't working.
Your email pipeline is live. Next up, you will build the cleanup utility and the main orchestrator that ties all these modules together into one automated workflow.
Build the Cleanup Utility and Main Orchestrator
You've built the individual modules: a scanner that extracts text, an analyzer that finds trends, and an emailer that delivers reports. Now you need two final pieces to make this a complete system.
First, a cleanup utility that prevents the shared folder from growing forever by removing old files. Second, a main orchestrator that calls each module in the right order so the entire pipeline runs with a single command.
In this step, get ready to:
- Create cleanup.py to remove files older than 2 years.
- Create main.py to orchestrate the full pipeline.
- Run the pipeline end-to-end and verify everything works together.
Create the cleanup utility
Over months and years, department members will keep dropping files into the shared folder. Without maintenance, the folder grows indefinitely and the scanner takes longer each run.
The cleanup utility checks each file's last-modified date and deletes anything older than 2 years. This keeps the folder focused on recent, relevant content.
- Create a new file called cleanup.py in your project folder and paste the following code:
"""Cleanup utility that removes files older than the configured age."""
import os
import time
from config import WATCH_FOLDER, MAX_FILE_AGE_DAYS
def cleanup_old_files(folder_path, max_age_days):
"""Delete files older than max_age_days based on modification time."""
cutoff_time = time.time() - (max_age_days * 86400)
deleted_count = 0
supported_extensions = ('.xlsx', '.pptx', '.docx')
for root, dirs, filenames in os.walk(folder_path):
if 'archive' in root:
continue
for filename in filenames:
if not filename.lower().endswith(supported_extensions):
continue
file_path = os.path.join(root, filename)
try:
file_mtime = os.path.getmtime(file_path)
if file_mtime < cutoff_time:
os.remove(file_path)
print(f" Deleted (older than {max_age_days} days): {filename}")
deleted_count += 1
except Exception as e:
print(f" Error deleting {filename}: {e}")
return deleted_count
if __name__ == "__main__":
print(f"Cleaning files older than {MAX_FILE_AGE_DAYS} days in: {WATCH_FOLDER}")
count = cleanup_old_files(WATCH_FOLDER, MAX_FILE_AGE_DAYS)
print(f"Deleted {count} old files.")
What does this code do?
- cutoff_time calculates the timestamp for 730 days ago (2 years). Any file modified before this timestamp is considered old.
- The function walks the folder just like the scanner, but skips the archive folder and only targets .xlsx, .pptx, and .docx files.
- os.path.getmtime() reads each file's last-modified timestamp. If it's older than the cutoff, os.remove() deletes it.
- The function returns the count of deleted files so the orchestrator can report it.
- Save cleanup.py.
- Test it by running the following command in your terminal:
python cleanup.py
You should see output showing the folder path and a count of deleted files. If your test folder has no files older than 2 years, the count will be 0.
Seeing a ModuleNotFoundError?
- Make sure your virtual environment is still activated. You should see (venv) at the start of your terminal prompt.
- Confirm that cleanup.py is in the same folder as config.py. The import won't work if they're in different directories.
Why is my cleanup script failing to import config?
✔️ Awesome, I've got everything!
Great. Double check you've saved your file before moving on.
ⓧ I'd like to double check the full code
"""Cleanup utility that removes files older than the configured age."""
import os
import time
from config import WATCH_FOLDER, MAX_FILE_AGE_DAYS
def cleanup_old_files(folder_path, max_age_days):
"""Delete files older than max_age_days based on modification time."""
cutoff_time = time.time() - (max_age_days * 86400)
deleted_count = 0
supported_extensions = ('.xlsx', '.pptx', '.docx')
for root, dirs, filenames in os.walk(folder_path):
if 'archive' in root:
continue
for filename in filenames:
if not filename.lower().endswith(supported_extensions):
continue
file_path = os.path.join(root, filename)
try:
file_mtime = os.path.getmtime(file_path)
if file_mtime < cutoff_time:
os.remove(file_path)
print(f" Deleted (older than {max_age_days} days): {filename}")
deleted_count += 1
except Exception as e:
print(f" Error deleting {filename}: {e}")
return deleted_count
if __name__ == "__main__":
print(f"Cleaning files older than {MAX_FILE_AGE_DAYS} days in: {WATCH_FOLDER}")
count = cleanup_old_files(WATCH_FOLDER, MAX_FILE_AGE_DAYS)
print(f"Deleted {count} old files.")
Create the main orchestrator
Each module you've built handles one job. The orchestrator ties them together in the right sequence: scan documents, analyze trends, email the report, then clean up old files.
- Create a new file called main.py in your project folder and paste the following code:
"""Main orchestrator for the Department Trend Monitor."""
import os
from datetime import datetime
from config import WATCH_FOLDER, ARCHIVE_FOLDER, MAX_FILE_AGE_DAYS
from scanner import scan_folder
from analyzer import analyze_trends
from emailer import send_report
from cleanup import cleanup_old_files
def run():
"""Run the full trend monitoring pipeline."""
print("=" * 50)
print("DEPARTMENT TREND MONITOR")
print(f"Run date: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 50)
# Step 1: Scan documents
print("\n[1/4] Scanning documents...")
documents = scan_folder(WATCH_FOLDER)
if not documents:
print(" No documents found. Exiting.")
return
print(f" Found {len(documents)} documents.")
# Step 2: Analyze trends with Claude
print("\n[2/4] Analyzing trends with Claude AI...")
analysis = analyze_trends(documents)
print(" Analysis complete.")
# Step 3: Send email report
print("\n[3/4] Sending email report...")
month_label = datetime.now().strftime("%B %Y")
subject = f"Department Trend Report - {month_label}"
send_report(analysis, subject=subject)
# Step 4: Cleanup old files
print("\n[4/4] Cleaning up old files...")
deleted = cleanup_old_files(WATCH_FOLDER, MAX_FILE_AGE_DAYS)
print(f" Cleaned up {deleted} files.")
print("\n" + "=" * 50)
print("COMPLETE - Pipeline finished successfully.")
print("=" * 50)
if __name__ == "__main__":
run()
What does this code do?
- The imports pull in each module's key function: scan_folder, analyze_trends, send_report, and cleanup_old_files.
- The run() function executes four steps in sequence. If the scanner finds no documents, it exits early to avoid wasting an API call.
- Progress messages ([1/4], [2/4], etc.) help you track which stage the pipeline is in when it runs unattended.
- The email subject includes the current month and year (e.g. "Department Trend Report - May 2026") so recipients can identify each report at a glance.
- Save main.py.
Seeing import errors?
- Confirm all six files (config.py, scanner.py, analyzer.py, emailer.py, cleanup.py, main.py) are in the same folder.
- Check for typos in file names. Python imports are case-sensitive.
Help me fix import errors in my main.py orchestrator.
✔️ Awesome, I've got everything!
Great. Make sure you've saved the file before running the pipeline.
ⓧ I'd like to double check the full code
"""Main orchestrator for the Department Trend Monitor."""
import os
from datetime import datetime
from config import WATCH_FOLDER, ARCHIVE_FOLDER, MAX_FILE_AGE_DAYS
from scanner import scan_folder
from analyzer import analyze_trends
from emailer import send_report
from cleanup import cleanup_old_files
def run():
"""Run the full trend monitoring pipeline."""
print("=" * 50)
print("DEPARTMENT TREND MONITOR")
print(f"Run date: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 50)
# Step 1: Scan documents
print("\n[1/4] Scanning documents...")
documents = scan_folder(WATCH_FOLDER)
if not documents:
print(" No documents found. Exiting.")
return
print(f" Found {len(documents)} documents.")
# Step 2: Analyze trends with Claude
print("\n[2/4] Analyzing trends with Claude AI...")
analysis = analyze_trends(documents)
print(" Analysis complete.")
# Step 3: Send email report
print("\n[3/4] Sending email report...")
month_label = datetime.now().strftime("%B %Y")
subject = f"Department Trend Report - {month_label}"
send_report(analysis, subject=subject)
# Step 4: Cleanup old files
print("\n[4/4] Cleaning up old files...")
deleted = cleanup_old_files(WATCH_FOLDER, MAX_FILE_AGE_DAYS)
print(f" Cleaned up {deleted} files.")
print("\n" + "=" * 50)
print("COMPLETE - Pipeline finished successfully.")
print("=" * 50)
if __name__ == "__main__":
run()
Run the full pipeline end-to-end
Time to run the entire system and verify that scanning, analyzing, emailing, and cleanup all execute in sequence without errors.
- Make sure you have at least one .xlsx, .pptx, or .docx file in the folder specified by WATCH_FOLDER in config.py.
- Run the full pipeline by executing this command:
python main.py
You should see output showing all four stages completing in sequence. The pipeline prints progress for each step, ending with "COMPLETE - Pipeline finished successfully."
What should I see?
The terminal output shows a header with the run date, followed by four numbered stages. Stage [1/4] lists extracted file names. Stage [2/4] confirms the Claude analysis is complete. Stage [3/4] reports whether the email sent successfully. Stage [4/4] shows how many old files were cleaned up (likely 0 for a new folder).
Pipeline stops at "No documents found"?
- The scanner found no supported files. Confirm that your WATCH_FOLDER path in config.py points to a folder that actually contains .xlsx, .pptx, or .docx files.
- Check that the folder path uses a raw string (r"...") in config. Backslashes in Windows paths need the r prefix to avoid being treated as escape characters.
- If you're testing and don't have real department documents, create a quick test .docx file in Word with a few sentences of content.
Help me troubleshoot why my pipeline can't find documents.
Your entire pipeline now runs with a single command. Next up, you'll schedule it to run automatically every month so the department gets trend reports without anyone lifting a finger.
Schedule Monthly Automation with Windows Task Scheduler
Your full pipeline works end-to-end. Documents get scanned, trends get analyzed, emails get sent, and old files get cleaned up.
Running the script manually every month is not sustainable. You need the system to run automatically on a fixed schedule without any human intervention. Windows Task Scheduler is a built-in tool that triggers programs at specified times.
In this step, get ready to:
- Create a batch file that launches the pipeline.
- Schedule the batch file to run on the 1st of every month.
- Verify and test the scheduled task.
Create the batch file launcher
Task Scheduler needs to run a single file. Your Python script depends on the virtual environment being active first. A batch file (.bat) solves this by activating the virtual environment, running the script, and deactivating it in sequence.
- In VS Code, create a new file called run_monitor.bat in the root of your project folder (the same folder where main.py lives).
- Paste the following code into run_monitor.bat:
@echo off
cd /d "%~dp0"
call venv\Scripts\activate
python main.py
deactivate
What does this batch file do?
- @echo off suppresses command echoing so the output stays clean.
- cd /d "%~dp0" changes the working directory to wherever the batch file is located. This ensures the script finds all its modules regardless of where Task Scheduler launches from.
- call venv\Scripts\activate activates the virtual environment so all installed packages are available.
- python main.py runs the full pipeline orchestrator.
- deactivate cleanly exits the virtual environment after the script finishes.
- Save run_monitor.bat.
- Test the batch file by double-clicking it in File Explorer. You should see a terminal window appear, run through the four pipeline steps, and close.
Batch file closes immediately with an error?
- Make sure run_monitor.bat is in the same folder as main.py and your venv folder. The cd /d "%~dp0" command relies on the batch file being co-located with the project.
- If the window closes too fast to read the error, open a Command Prompt manually, cd to your project folder, and type run_monitor.bat to see the full output.
Help me debug my batch file not running correctly.
✔️ Awesome, I've got everything!
Great. Double check you have saved run_monitor.bat in the same folder as main.py.
ⓧ I'd like to double check the full code
@echo off
cd /d "%~dp0"
call venv\Scripts\activate
python main.py
deactivate
Set up the scheduled task in Windows Task Scheduler
Now that the batch file works on its own, you need to tell Windows to run it automatically on the 1st of every month.
- Press the Windows key to open the Start menu search bar.
- Type Task Scheduler and press Enter to open it.
- In the right-hand Actions panel, click Create Basic Task... to launch the wizard.
What is Windows Task Scheduler?
Task Scheduler is a built-in Windows utility that runs programs, scripts, or batch files on a schedule you define. It works even when you are not logged in, making it ideal for background automation like monthly reports.
- In the Name field, enter Department Trend Monitor.
- Optionally add a description like Runs the monthly trend analysis pipeline.
- Click Next.
- Select Monthly as the trigger frequency.
- Click Next.
- Set the start time to 6:00:00 AM (or any time your machine will be on).
- Under Days, check the box for day 1 to run on the first of each month.
- Make sure all months are selected (they should be by default).
- Click Next.
- Select Start a program as the action.
- Click Next.
- Click Browse... and navigate to your project folder.
- Select run_monitor.bat and click Open.
- Click Next.
- Review the summary and click Finish.
Run whether user is logged on or not
By default, basic tasks only run when you are logged in. If you need the task to run unattended (e.g. on a server), right-click the task after creation, select Properties, and change the radio button to Run whether user is logged on or not. You will be prompted for your Windows password.
Verify the scheduled task and test it
The task is now registered, but you should confirm it exists and test it before waiting until next month.
- In Task Scheduler, click Task Scheduler Library in the left panel.
- Look for Department Trend Monitor in the task list. You should see its status as Ready and the next run time set to the 1st of next month.
- Right-click the task and select Run to trigger it manually now.
- Watch for the pipeline output. The status should briefly change to Running and then return to Ready once complete.
How do I know it worked?
After the manual run, check your email inbox. You should receive the trend report email just as you did when running main.py directly. If the task's Last Run Result column shows 0x0, it completed successfully.
Task shows a non-zero result code?
- A result of 0x1 usually means the batch file ran but the Python script hit an error. Open a Command Prompt, navigate to your project folder, and run run_monitor.bat manually to see the full error output.
- Make sure the Start in field in the task properties points to your project folder path. If it is blank, the batch file may not find the venv folder.
Help me troubleshoot my scheduled task that is failing.
Secret mission
Add Month-over-Month Trend Comparison
Your trend monitor identifies what's happening right now. But what about what's changing? Add a comparison feature that tracks trends over time, highlighting what's new this month and what's fading away.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
The Claude API charges per use, so the scheduled task will incur a small cost each time it runs (typically $0.01-$0.50 per monthly run depending on document volume).
Cost warning
If you leave the Windows Task Scheduler task enabled, it will call the Claude API on the 1st of every month. Each run costs approximately $0.01-$0.50 depending on how many documents are in your watch folder.
If you are not actively using this system, disable or delete the scheduled task to avoid unexpected charges.
Resources you used:
- Windows Task Scheduler task (runs the pipeline monthly).
- Claude API credit on your Anthropic account (pay-per-use).
- Microsoft Entra app registration (free, used for email sending).
- Project folder on your Desktop (Python scripts, virtual environment, archive data).
Keep everything running
No action needed. Choose this if you are still actively using the trend monitor and want monthly reports to continue.
- The Task Scheduler task will run on the 1st of every month.
- Claude API charges will apply each run (typically pennies for small folders).
- The Microsoft Entra app registration remains active in your tenant at no cost.
- Your project folder, virtual environment, and archive data remain on your Desktop.
Pause - I'll come back to this later
Disable the scheduled task to stop monthly runs. No Claude API charges will occur while paused. Your files and app registration remain intact for when you return.
- Open Task Scheduler (press the Windows key, type Task Scheduler, and press Enter).
- In the left pane, click Task Scheduler Library.
- Find your Department Trend Monitor task in the list.
- Right-click the task and select Disable.
To resume later, right-click the same task and select Enable.
Delete - I don't want to use this again
Remove all project resources completely. Follow each step below.
Delete the scheduled task
- Open Task Scheduler (press the Windows key, type Task Scheduler, and press Enter).
- In the left pane, click Task Scheduler Library.
- Find your Department Trend Monitor task in the list.
- Right-click the task and select Delete.
- Click Yes to confirm.
Delete the project folder
- Open File Explorer and navigate to your Desktop.
- Right-click your project folder (the one containing main.py, config.py, and the venv folder).
- Select Delete to move it to the Recycle Bin, then empty the Recycle Bin.
Alternatively, delete the folder from the terminal by running this command (replace the folder name with your actual project folder name):
rmdir /s /q "%USERPROFILE%\Desktop\[[PROJECT_FOLDER=\"your-project-folder-name\"]]"
Delete the Microsoft Entra app registration
- Sign in to the Microsoft Entra admin center at entra.microsoft.com.
- Browse to App registrations.
- Select the app registration you created for this project.
- On the app's Overview page, click Delete.
- Check the confirmation box and click Delete to confirm.
What about my Anthropic account?
Your Anthropic account and any remaining API credit stay on your account. No charges will occur unless you make API calls.
You can leave the account as-is or delete your API key from the Anthropic console if you prefer.
Nice Work!
Nice Work!
You just built a fully automated, AI-powered department intelligence system. Scattered documents are now transformed into actionable monthly trend reports, delivered straight to your team's inboxes without anyone lifting a finger.
Here's what you accomplished:
- Built a document scanner that extracts text from Excel, PowerPoint, and Word files using openpyxl, python-pptx, and python-docx.
- Integrated Claude AI to identify trends, recurring themes, and notable patterns across department documents.
- Automated monthly email reports via Microsoft Graph API and scheduled the full pipeline to run unattended with Windows Task Scheduler.
- Added month-over-month trend comparison that highlights new and declining trends by archiving each month's analysis as JSON.
Ready to quiz yourself?