Build a Daily Agenda Engine with Claude

Automate a time-blocked daily plan using Claude Sonnet 5 and Pydantic.

Introduction

30 Second Summary

You sit down in the morning with a vague sense of everything you need to do. Some tasks are urgent, some can wait, and figuring out which is which eats up half your productive energy before you even start.

In this project, you will build a Python command-line tool that reads your tasks and deadlines. It calls the Claude API to reason about your priorities, then writes a time-blocked daily agenda to your desktop each morning.

What You'll Build

You run a single command, and a structured Markdown agenda appears on your desktop with time blocks, priority reasoning, and a plan that reshuffles itself when yesterday went sideways.

By the end of this project, you'll have:

  • A command-line tool that turns your messy task list into a prioritized, time-blocked daily plan using Claude Sonnet 5.
  • Low-capacity day handling that reshuffles undone work compassionately instead of guilt-stacking it into tomorrow's backlog.
  • An automated morning schedule powered by Windows Task Scheduler that delivers a fresh agenda to your desktop before you sit down.
  • An optional Secret Mission to push your skills further.

Are there any prerequisites?

This project assumes moderate Python experience and a Windows computer. The guide walks you through setting up your Claude Console account, API key, and all Python dependencies.

Before We Start

Before you touch any code or create any accounts, take a moment to lock in what you're about to build and why it matters to you.

Set Up Your Claude API Key and Python Environment

Before your agenda engine can call Claude and generate daily plans, it needs a way to authenticate with the Anthropic API. That means creating an account, getting an API key, and installing the Python SDK that talks to Claude.

In this step, you'll set up everything your script needs to make its first successful call to Claude Sonnet 5.

In this step, get ready to:
  • Create a Claude Console account and generate an API key.
  • Set up a Python project folder with a virtual environment and the Anthropic SDK.
  • Run a test script that sends a message to Claude and prints the response.
Create your Claude Console account and API key

The Claude Console is where you manage your API access to Claude. You need an account and an API key before your Python script can send requests.

  • Open your browser and navigate to platform.claude.com.
  • Sign up for a new account (or sign in if you already have one).
  • Click Settings in the left sidebar.
  • Click Keys.
  • Click Create Key to generate a new API key.
  • Copy the key immediately and paste it somewhere safe (like a text file on your desktop). You will not be able to see the full key again after closing this dialog.

What is an API key?

An API key is a unique string that identifies your account when your code calls a service. The Anthropic SDK reads the key from an environment variable called ANTHROPIC_API_KEY so you never have to paste it directly into your code.

Now you need to make this key available to your Python scripts. You'll set it in two places: once for the current terminal session, and once permanently so it survives restarts.

  • Press the Windows key to open your search bar.
  • Type PowerShell and press Enter to open it.
  • Set the API key for your current session by running this command (replace your-api-key-here with the key you just copied):
$env:ANTHROPIC_API_KEY = "your-api-key-here"
  • Verify the variable is set by running:
echo $env:ANTHROPIC_API_KEY

You should see your API key printed back to you. This confirms it is available in your current PowerShell session.

Why set the variable twice?

The $env: command only sets the variable for the current terminal window. If you close PowerShell and reopen it, the variable is gone. Persisting it through System Properties makes it available in every future session.

Now persist the variable so it survives restarts.

  • Press the Windows key to open your search bar.
  • Type Edit the system environment variables and select it from the results.
  • In the System Properties window that opens, click Environment Variables... at the bottom.
  • Under User variables, click New....
  • In the Variable name field, type ANTHROPIC_API_KEY.
  • In the Variable value field, paste your API key.
  • Click OK on each open dialog to save and close.

Can't find the Environment Variables menu?

Some Windows versions have a known issue with the search result. If it does not appear, try typing View advanced system settings instead. In the resulting window, click Environment Variables... at the bottom.

Help me set up my ANTHROPIC_API_KEY environment variable on Windows.

Create your project folder and install the SDK

With your API key in place, you need a project folder and a Python virtual environment to keep your dependencies isolated.

  • Move to your Desktop by running this command in the same PowerShell window:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir agenda-engine
cd agenda-engine
  • Confirm the folder was created by running:
ls

You should see an empty listing for the agenda-engine directory. The folder is ready.

  • Create a virtual environment and activate it by running these commands:
python -m venv .venv
.venv\Scripts\activate

Your terminal prompt now shows (.venv) at the start. This means your virtual environment is active and any packages you install will stay inside this project.

What is a virtual environment?

A virtual environment is an isolated Python installation inside your project folder. Packages you install here do not affect other projects on your machine. It keeps your system clean and your dependencies reproducible.

  • Install the Anthropic Python SDK by running:
pip install anthropic

The SDK includes Pydantic as a dependency, so you do not need to install it separately.

  • Verify the SDK installed correctly by running:
python -c "import anthropic; print(anthropic.__version__)"

You should see a version number like 1.0.0 printed with no errors.

Seeing an import error?

Make sure you see (.venv) in your terminal prompt. If you do not, run .venv\Scripts\activate again. The SDK is only installed inside the virtual environment, so it will not be found if the environment is not active.

Help me troubleshoot my anthropic SDK installation on Windows.

Test your connection to Claude

The SDK is installed and your API key is set. Time to make your first API call and confirm everything is wired up correctly.

  • Open VS Code. Press the Windows key, type VS Code, and press Enter.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the agenda-engine folder, and click Select Folder.
  • Create a new file called test_connection.py by right-clicking in the Explorer sidebar and selecting New File.
  • Paste the following code into test_connection.py:
import anthropic


def main():
    # Create a client (uses ANTHROPIC_API_KEY env var automatically)
    client = anthropic.Anthropic()

    # Send a simple test message to Claude Sonnet 5
    message = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=256,
        messages=[
            {
                "role": "user",
                "content": "Say hello and confirm you're working. One sentence.",
            }
        ],
    )

    # Print the text response from Claude
    for block in message.content:
        if block.type == "text":
            print(block.text)


if __name__ == "__main__":
    main()

What does this code do?

anthropic.Anthropic() creates a client that automatically reads your ANTHROPIC_API_KEY environment variable. No need to paste the key into your code.

client.messages.create() sends a message to the claude-sonnet-5 model with a 256-token response limit. The script then loops through the response blocks and prints any text content.

  • Save test_connection.py.

Before you run this, think about what you expect to see. If your API key is set correctly and the SDK is installed, what should Claude send back?

  • Switch back to your PowerShell window (make sure you still see (.venv) in the prompt) and run the script:
python test_connection.py

You should see a one-sentence greeting from Claude printed in your terminal. Something like: "Hello! I'm working and ready to assist you."

Seeing an authentication error?

If you see an error mentioning authentication or a missing API key, your ANTHROPIC_API_KEY environment variable is not set in this terminal session. Run the set command again in the same PowerShell window:

$env:ANTHROPIC_API_KEY = "your-api-key-here"

Then try python test_connection.py again. If you opened a new terminal window after setting the variable, the session-level value is lost. This is why the persistence step through System Properties matters.

Help me fix my Claude API authentication error.

✔️ Awesome, I've got everything!

Great. Double check you have saved test_connection.py and that Claude responded with a greeting in your terminal.

ⓧ I'd like to double check the full code

Here is the complete test_connection.py file. Make sure yours matches exactly:

import anthropic


def main():
    client = anthropic.Anthropic()

    message = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=256,
        messages=[
            {
                "role": "user",
                "content": "Say hello and confirm you're working. One sentence.",
            }
        ],
    )

    for block in message.content:
        if block.type == "text":
            print(block.text)


if __name__ == "__main__":
    main()

Your environment is ready and Claude is responding. Next up, you'll build your first agenda generator and see what Claude can do with your task data.

Build a Naive Agenda Generator

Your environment is set up and you proved Claude responds to your code. Now it's time to feed it real task data and get something useful back.

In this step, you'll create sample data files, send them to Claude in a plain-text prompt, and get a daily agenda printed in your terminal. The result will look great. But you'll also discover a critical flaw that makes this approach unreliable for daily use.

In this step, get ready to:
  • Create sample task and log data in JSON files.
  • Build a script that sends your tasks to Claude and prints a daily agenda.
  • Run the script twice and spot the consistency problem.
Create your task data files

The agenda engine needs two inputs: a list of your current tasks, and a log of what you did yesterday. Both live in JSON files that Claude will read as part of each prompt.

  • In VS Code, create a new file called tasks.json inside your agenda-engine folder.
  • Paste the first section of sample tasks into the file:
{
  "tasks": [
    {
      "name": "Finish OS assignment on memory management",
      "deadline": "2026-08-25",
      "effort_hours": 4,
      "priority": 1,
      "tags": ["coursework", "cs"],
      "status": "in_progress"
    },
    {
      "name": "Review linked list implementations for interview prep",
      "deadline": "2026-08-28",
      "effort_hours": 2,
      "priority": 2,
      "tags": ["career", "dsa"],
      "status": "not_started"
    },
    {
      "name": "Read Chapter 5 of distributed systems textbook",
      "deadline": "2026-08-27",
      "effort_hours": 3,
      "priority": 2,
      "tags": ["coursework", "cs"],
      "status": "not_started"
    },
  • Paste these three remaining tasks directly below, closing out the file:
    {
      "name": "Draft resume for fall internship applications",
      "deadline": "2026-09-01",
      "effort_hours": 2,
      "priority": 3,
      "tags": ["career"],
      "status": "not_started"
    },
    {
      "name": "Set up portfolio website with project demos",
      "deadline": "2026-09-05",
      "effort_hours": 5,
      "priority": 4,
      "tags": ["career", "personal"],
      "status": "not_started"
    },
    {
      "name": "Gym session",
      "deadline": "2026-08-20",
      "effort_hours": 1,
      "priority": 3,
      "tags": ["personal", "health"],
      "status": "not_started"
    }
  ]
}

What does each field mean?

  • priority is a 1-5 scale where 1 is the most urgent.
  • effort_hours tells Claude how long the task takes, so it can build realistic time blocks.
  • status tracks whether a task is not_started, in_progress, or done so the script can filter out completed work.
  • Save tasks.json.
  • Confirm the file exists by running:
ls

You should see tasks.json alongside test_connection.py and your .venv folder in the output.

Now create the daily log file. This records what you did yesterday so Claude can account for recent progress.

  • Create a new file called daily_log.json in your agenda-engine folder and paste this content:
{
  "logs": [
    {
      "date": "2026-08-19",
      "entries": [
        {
          "task_name": "Finish OS assignment on memory management",
          "hours_spent": 2,
          "completed": false,
          "notes": "Got through paging section, still need to do virtual memory part"
        },
        {
          "task_name": "Gym session",
          "hours_spent": 1,
          "completed": true,
          "notes": ""
        }
      ],
      "capacity_note": "Normal day, focused in the afternoon"
    }
  ]
}

What does this log represent?

This sample entry shows a normal day where you made progress on the OS assignment (2 hours, not finished) and completed a gym session. Claude uses this context to build a smarter plan for today.

Adjust the date if needed

The date field must be yesterday's date in YYYY-MM-DD format for the script to find it. If you're doing this project on a different day, update the date to match your actual yesterday.

  • Save daily_log.json.
  • Run ls again. You should now see both tasks.json and daily_log.json listed.

Don't see both files?

  • Make sure you saved both files in the agenda-engine folder, not in a subfolder or on the desktop.
  • Check that your terminal's working directory is agenda-engine. Run pwd to confirm.
  • help me find where my JSON files were saved
Build the agenda script

With your data files ready, the script needs to read both files, assemble a prompt that gives Claude all the context, and print whatever Claude sends back.

  • Create a new file called agenda.py in your agenda-engine folder.
  • Add the imports and file-loading functions by pasting this code:
import json
from datetime import date, timedelta
from pathlib import Path

import anthropic


# Load the task list from the JSON file
def load_tasks(path: str = "tasks.json") -> dict:
    with open(path, "r") as f:
        return json.load(f)


# Load the daily log from the JSON file
def load_log(path: str = "daily_log.json") -> dict:
    with open(path, "r") as f:
        return json.load(f)


# Find yesterday's log entry so the agenda can account for recent work
def get_yesterday_log(log_data: dict) -> dict | None:
    yesterday = (date.today() - timedelta(days=1)).isoformat()
    for entry in log_data.get("logs", []):
        if entry["date"] == yesterday:
            return entry
    return None

What does this code do?

  • load_tasks() and load_log() read your two JSON files into Python dictionaries.
  • get_yesterday_log() calculates yesterday's date and searches the log for a matching entry. If you didn't log anything yesterday, it returns None so the script can handle that gracefully.
  • Add the main function and entry point below the code you just pasted:
def main():
    tasks = load_tasks()
    log_data = load_log()
    yesterday_log = get_yesterday_log(log_data)
    client = anthropic.Anthropic()

    # Build a prompt with today's date, pending tasks, and yesterday's log
    today_str = date.today().isoformat()
    pending_tasks = [t for t in tasks["tasks"] if t["status"] != "done"]
    prompt = f"Today is {today_str}.\n\n"
    prompt += "Here are my current tasks:\n"
    prompt += json.dumps(pending_tasks, indent=2)
    prompt += "\n\n"
    if yesterday_log:
        prompt += "Here is what I did yesterday:\n"
        prompt += json.dumps(yesterday_log, indent=2)
    else:
        prompt += "No log from yesterday (either first day or log missing)."
    prompt += "\n\nGenerate my daily agenda for today with time blocks."

    # Send the prompt to Claude and print the raw response
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=4096,
        messages=[{"role": "user", "content": prompt}],
    )
    for block in response.content:
        if block.type == "text":
            print(block.text)


if __name__ == "__main__":
    main()

What does the main function do?

  • It loads both JSON files and filters out any tasks with a done status so Claude only plans around unfinished work.
  • It builds a plain-text prompt by concatenating today's date, the pending task list as JSON, and yesterday's log. This gives Claude all the context it needs.
  • client.messages.create() sends the prompt to claude-sonnet-5 with a max_tokens limit of 4096. The response is printed as raw text.
  • Save agenda.py.
  • Run the agenda generator by running this command:
python agenda.py

After a few seconds, a full daily agenda with time blocks prints in your terminal. Claude reasons about your deadlines, priorities, and yesterday's progress to build the plan.

Seeing an error instead of an agenda?

  • If you see a FileNotFoundError, make sure tasks.json and daily_log.json are in the same folder as agenda.py, and that your terminal is in the agenda-engine directory.
  • If you see a JSONDecodeError, double check your JSON files for missing commas, extra trailing commas, or mismatched brackets. The two code blocks for tasks.json must be pasted together as one continuous file.
  • If you see an authentication error, confirm your ANTHROPIC_API_KEY environment variable is still set in this terminal session.
  • help me debug my agenda.py script
Run it again and spot the problem

The agenda looks great. But can you rely on it looking the same way every morning? Before you run the script a second time, think about this: do you expect the format of the output to be identical, or different?

  • Run the script again by running this command:
python agenda.py

Compare this output to your first run. The tasks and time blocks are similar, but the formatting is different. You might see different heading styles, inconsistent time formats, bullet points where the first run had numbered lists, or a completely different layout.

Why does the format keep changing?

An LLM generates text one token at a time, and the specific formatting it chooses varies between calls. Sometimes it uses markdown headers, sometimes plain text. Sometimes it writes times as "9:00 AM" and other times as "09:00".

This inconsistency makes it impossible to reliably save the output to a file and expect a clean, uniform experience every morning. If you wanted to parse the agenda programmatically, you'd have no stable structure to grab onto.

This is the gap. The engine produces useful content, but the shape of that content is unpredictable. The next step fixes this with structured output.

✔️ Awesome, I've got everything!

Great. Double check you've saved all three files: tasks.json, daily_log.json, and agenda.py.

ⓧ I'd like to double check the full code

Scroll up and confirm each section is present in your files, in order:

  • tasks.json: Both code blocks pasted together as one file. Six tasks total, each with name, deadline, effort_hours, priority, tags, and status fields.
  • daily_log.json: One log entry with yesterday's date, two task entries, and a capacity_note.
  • agenda.py: Chunk 1 has imports, load_tasks(), load_log(), and get_yesterday_log(). Chunk 2 has the main() function and the if __name__ entry point.

You've proven that Claude can generate a useful daily agenda from your task data. The content is solid. But the unpredictable format makes this approach a dead end for automation. Next up, you'll force Claude to return a consistent, parseable structure every single time.

Add Structured Output with Pydantic

Your agenda engine works. You ran it twice in the last step and got a daily plan both times. The problem is that the two outputs looked different. Different heading styles, different time formats, sometimes bullets, sometimes numbered lists.

That inconsistency makes it impossible to save the output to a reliable file. Pydantic solves this by defining an exact data shape that Claude must follow every time. In this step, you will define that shape, switch to a structured API call, and write the result as a clean markdown file to your desktop.

In this step, get ready to:
  • Define Pydantic models that describe the exact structure of your agenda.
  • Replace the raw API call with a structured one that returns validated Python objects.
  • Write a markdown formatter that saves a consistent agenda.md to your desktop.
Define Pydantic models for your agenda

Right now, Claude returns free-form text. You need to tell it exactly what shape the response should be. Pydantic models define that shape as Python classes with typed fields.

  • In agenda.py, find the import section at the top of the file. Add these two new imports below the existing from pathlib import Path line:
from typing import List

from pydantic import BaseModel
  • Now add the following two model classes below the import anthropic line, before the load_tasks() function:
# Define the exact shape Claude must return
class TimeBlock(BaseModel):
    start_time: str
    end_time: str
    task_name: str
    priority_reason: str


class DailyAgenda(BaseModel):
    date: str
    summary: str
    time_blocks: List[TimeBlock]
    rollover_notes: str

What do these models do?

Each class defines a set of required fields with types. TimeBlock represents one chunk of your day: a start time, end time, which task fills it, and why that task earned that slot.

DailyAgenda is the top-level container. It holds the date, a plain-English summary, a list of TimeBlock items, and notes about any tasks that got bumped. When you pass this model to the API, Claude is forced to return data matching this exact shape.

  • Save agenda.py. The models are not used yet, so hold tight for the next chunk where you wire them into the API call.
Switch to structured output with parse()

The Anthropic SDK has a method called client.messages.parse() that accepts a Pydantic model and guarantees the response matches it. You will swap out the old client.messages.create() call and replace the print loop.

There are several small changes scattered across your main() function, so you will replace the entire function.

  • In agenda.py, find the line that says prompt = f"Today is {today_str}.\n\n". This is inside your main() function.
  • Select everything from that line down to the end of the function (including the for block in response.content: loop).
  • Replace it with the following code:
    # Renamed to user_prompt for clarity in later steps
    user_prompt = f"Today is {today_str}.\n\n"
    user_prompt += "Here are my current tasks:\n"
    user_prompt += json.dumps(pending_tasks, indent=2)
    user_prompt += "\n\n"

    if yesterday_log:
        user_prompt += "Here is what I did yesterday:\n"
        user_prompt += json.dumps(yesterday_log, indent=2)
    else:
        user_prompt += "No log from yesterday (either first day or log missing)."

    user_prompt += "\n\nGenerate my daily agenda for today."

    # parse() validates the response against your Pydantic model
    response = client.messages.parse(
        model="claude-sonnet-5",
        max_tokens=4096,
        messages=[{"role": "user", "content": user_prompt}],
        output_format=DailyAgenda,
    )

    # Access the validated Pydantic object directly
    agenda = response.parsed_output
    print(agenda)

What changed here?

Three key changes. First, the variable name changed from prompt to user_prompt to keep things clear as the code grows.

Second, client.messages.create() became client.messages.parse() with a new output_format=DailyAgenda parameter. This tells Claude to return JSON that matches your Pydantic schema.

Third, instead of looping through raw text blocks, you access response.parsed_output to get a validated DailyAgenda object. The print(agenda) is temporary so you can confirm the structure before writing a markdown file in the next chunk.

  • Save agenda.py.
  • Run the script to see structured output for the first time:
python agenda.py

You should see a single line of output that looks like a Pydantic model representation. It starts with date= and contains time_blocks= with a list of TimeBlock objects inside. The exact task names and times vary, but the structure is always the same.

Seeing an error about output_format?

Make sure your DailyAgenda class is defined above the main() function. Python reads top to bottom, so the model must exist before output_format=DailyAgenda references it.

Also confirm you are calling client.messages.parse(), not client.messages.create(). The output_format parameter only works with parse().

help me debug an output_format error when calling client.messages.parse() with my DailyAgenda Pydantic model

Write the agenda to a markdown file

The structured data is flowing. Now you need a function that turns your DailyAgenda object into a clean, readable markdown file on your desktop.

  • In agenda.py, add the following function between the get_yesterday_log() function and the main() function:
def write_agenda_markdown(agenda: DailyAgenda, output_path: Path) -> None:
    lines = []
    lines.append(f"# Daily Agenda - {agenda.date}\n")
    lines.append(f"{agenda.summary}\n")
    lines.append("## Time Blocks\n")

    # Format each time block with its priority reasoning
    for block in agenda.time_blocks:
        lines.append(f"### {block.start_time} - {block.end_time}: {block.task_name}\n")
        lines.append(f"Priority reasoning: {block.priority_reason}\n")

    # Add rollover notes if any tasks were bumped
    if agenda.rollover_notes:
        lines.append("## Rollover Notes\n")
        lines.append(f"{agenda.rollover_notes}\n")

    output_path.write_text("\n".join(lines), encoding="utf-8")
    print(f"Agenda written to {output_path}")

What does this function do?

It takes your validated DailyAgenda object and builds a list of markdown-formatted strings. Each TimeBlock becomes a level-3 heading with its time range and task name, followed by the priority reasoning.

Because the Pydantic model guarantees the same fields every time, this function always produces an identically-structured file. No more surprises in formatting.

Now update main() to call this function instead of printing the raw object.

  • In main(), find this line at the bottom:
    print(agenda)
  • Replace it with these two lines:
    output_path = Path.home() / "Desktop" / "agenda.md"
    write_agenda_markdown(agenda, output_path)
  • Save agenda.py.

Before you run this, what do you think will happen? Will the output still print to your terminal, or will it go somewhere else?

  • Run the script:
python agenda.py

You should see a message like Agenda written to C:\Users\YourName\Desktop\agenda.md printed in your terminal. The full agenda now lives in a file instead of scrolling past in the console.

  • Open agenda.md on your desktop. You should see a markdown file with a date heading, a summary paragraph, time blocks formatted as level-3 headings with priority reasoning underneath, and a rollover notes section.

No file on your desktop?

Check that Path.home() resolves to the right user directory. Run python -c "from pathlib import Path; print(Path.home())" in your terminal and confirm it points to your Windows user folder.

If your Desktop is in a different location (like OneDrive), update the output_path line to match the actual path.

help me figure out why agenda.md is not appearing on my desktop when I run agenda.py

Now for the real test. The whole point of structured output is consistency.

Before you run the next command, predict: if you run the script three times, will the content of agenda.md be identical each time, or just the structure?

  • Run the script two more times:
python agenda.py
python agenda.py

Open agenda.md after each run. The exact task assignments and times may shift (Claude still reasons fresh each call), but the structure is identical every time: date heading, summary, time blocks with start/end and priority reasoning, rollover notes. That is the win. Your downstream code can rely on this shape.

✔️ Awesome, I've got everything!

Great. Double check you have saved agenda.py and that agenda.md is sitting on your desktop.

ⓧ I'd like to double check the full code

Scroll up through your agenda.py and confirm the following sections appear in this order:

  • Imports: json, date, timedelta, Path, List, BaseModel, anthropic.
  • Two Pydantic model classes: TimeBlock (4 string fields) and DailyAgenda (date, summary, time_blocks as List[TimeBlock], rollover_notes).
  • Three helper functions: load_tasks(), load_log(), get_yesterday_log().
  • The write_agenda_markdown() function that writes to a Path and prints a confirmation.
  • The main() function using client.messages.parse() with output_format=DailyAgenda, and ending with write_agenda_markdown(agenda, output_path).
  • The if __name__ == "__main__": guard at the bottom.

Your agenda engine now produces a reliable, identically-structured file every single time. Next up, you will teach it to handle the days where life gets in the way and you complete nothing on your list.

Handle Low-Capacity Days

Your agenda engine now generates a clean, consistently-structured markdown plan every time you run it. Pydantic structured output solved the formatting problem. But there is a bigger gap.

Right now, the engine does not know what you actually did yesterday. If you have a bad day and complete nothing, tomorrow's plan will just stack yesterday's backlog on top of today's tasks. That is a recipe for guilt-stacking, not productive planning. In this step, you will add an end-of-day logging script, teach Claude how to handle low-capacity days, and test that the system responds with compassion instead of pressure.

In this step, get ready to:
  • Create an end-of-day logging script to record what you accomplished.
  • Add a system prompt that handles low-capacity days gracefully.
  • Test the low-capacity path with a zero-completion day.
Create the end-of-day logging script

The agenda engine reads daily_log.json each morning, but right now you have to edit that file by hand. A dedicated logging script makes it easy to record your day in seconds.

  • Create a new file called log_day.py in your agenda-engine folder.
  • Add the imports, main function setup, and log file loading by copying the following code:
import json
from datetime import date


def main():
    log_path = "daily_log.json"

    # Load the existing log, or start fresh if the file doesn't exist
    try:
        with open(log_path, "r") as f:
            log_data = json.load(f)
    except FileNotFoundError:
        log_data = {"logs": []}

    today_str = date.today().isoformat()
    print(f"Logging your day for {today_str}")
    print("Enter tasks you worked on. Type 'done' when finished, or 'skip' for a zero day.\n")

What does this code do?

The script tries to open the existing daily_log.json file. If it does not exist yet, it starts with an empty log structure instead of crashing.

The date is captured automatically so you never have to type it manually.

  • Add the interactive input loop below the print statements. This collects each task you worked on:
    # Collect task entries one at a time
    entries = []
    while True:
        task_name = input("Task name (or 'done'/'skip'): ").strip()
        if task_name.lower() == "done":
            break
        if task_name.lower() == "skip":
            print("Zero day logged. No worries, tomorrow is a fresh start.")
            entries = []
            break

        hours = input("  Hours spent: ").strip()
        try:
            hours = float(hours)
        except ValueError:
            hours = 0

        completed = input("  Completed? (y/n): ").strip().lower() == "y"
        notes = input("  Notes (optional): ").strip()

        entries.append(
            {
                "task_name": task_name,
                "hours_spent": hours,
                "completed": completed,
                "notes": notes,
            }
        )

What does the input loop do?

The loop keeps asking for tasks until you type done. Typing skip logs a zero day with an empty entries list. This is the signal your agenda engine will use tomorrow to detect a low-capacity day.

If the learner types something that is not a valid number for hours, the except ValueError block defaults to 0 instead of crashing.

  • Add the save section and entry point below the input loop. This writes the log entry to the JSON file:
    # Ask for an overall capacity note
    capacity_note = input("How was your capacity today? (brief note): ").strip()

    # Build the log entry for today
    log_entry = {
        "date": today_str,
        "entries": entries,
        "capacity_note": capacity_note,
    }

    log_data["logs"].append(log_entry)

    with open(log_path, "w") as f:
        json.dump(log_data, f, indent=2)

    print(f"\nDay logged to {log_path}")


if __name__ == "__main__":
    main()

What does the save section do?

It appends your new log entry to the existing list of logs, then writes the whole structure back to daily_log.json. The capacity_note field gives Claude extra context about why your day went the way it did.

  • Save log_day.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved log_day.py.

ⓧ I'd like to double check the full code

Scroll up and confirm your log_day.py contains these three sections in order:

  • Imports, main() function start, log file loading with try/except, and the header prints.
  • The while True input loop collecting task name, hours, completion, and notes.
  • The capacity note prompt, log entry construction, JSON write, and the if __name__ entry point.
  • Test the logging script by running this command:
python log_day.py

The script will prompt you interactively. Here is what to expect and what to enter:

  • At Task name (or 'done'/'skip'): type any task name, such as Reading chapter 3.
  • At Hours spent: type a number like 1.5.
  • At Completed? (y/n): type y or n.
  • At Notes (optional): type anything or press Enter to skip.
  • Type done at the next task prompt to finish entering tasks.
  • At How was your capacity today? type a brief note like Good focus today.

You should see Day logged to daily_log.json printed in the terminal.

Script crashes or log file looks wrong?

  • Make sure your indentation inside main() is consistent. All lines inside the function should be indented one level.
  • If you see a json.decoder.JSONDecodeError, your daily_log.json may have been corrupted. Replace its contents with {"logs": []} and try again.
  • help me debug my log_day.py script
Add a system prompt and refactor prompt building

Right now, agenda.py sends tasks and log data to Claude without any instructions about how to handle a bad day. Claude will just plan everything, regardless of your capacity. A system prompt gives Claude a persistent set of rules for every request, including explicit instructions for low-capacity recovery.

  • In agenda.py, add the build_system_prompt() function directly below the get_yesterday_log() function:
def build_system_prompt() -> str:
    # Define Claude's persona and rules for daily planning
    return """You are a daily agenda planner for a final-year CS student. Your job is to create a realistic, prioritized, time-blocked daily plan.

Rules:
- Prioritize tasks by deadline proximity first, then by priority number (1 is highest).
- Create time blocks between 8:00 AM and 10:00 PM. Include breaks.
- Each block should be 30 minutes to 2 hours. No block longer than 2 hours without a break.
- Explain your priority reasoning for each block in one sentence.
- If yesterday's log shows zero or very few completions, this is a low-capacity day recovery. Do NOT stack all undone work onto today. Instead:
  - Reduce today's planned productive hours to 60-80% of a normal day.
  - Pick only the most urgent 2-3 tasks.
  - Include a brief encouraging note in the summary acknowledging that low days happen and the plan has been adjusted.
  - Move non-urgent tasks to the rollover_notes with suggested future dates.
- Always include at least one non-academic block (exercise, rest, personal).
- The rollover_notes field should list any tasks that were deprioritized and why."""

What does the system prompt do?

The system prompt defines Claude's persona and a ruleset it follows for every agenda generation. The key section is the low-capacity recovery block: when yesterday's log shows zero or minimal completions, Claude reduces the planned hours to 60-80%, picks only the 2-3 most urgent tasks, and moves everything else to rollover notes.

This is prompt engineering in action. Instead of writing code to detect low-capacity days, you are teaching the LLM to recognize the pattern and respond appropriately.

  • Now add the build_user_prompt() function directly below build_system_prompt(). This extracts the prompt-building logic that currently lives inline in main() into its own function:
def build_user_prompt(tasks: dict, yesterday_log: dict | None) -> str:
    today_str = date.today().isoformat()
    # Filter out completed tasks
    pending_tasks = [t for t in tasks["tasks"] if t["status"] != "done"]

    # Build the prompt with today's date and task data
    prompt = f"Today is {today_str}.\n\n"
    prompt += "Here are my current tasks:\n"
    prompt += json.dumps(pending_tasks, indent=2)
    prompt += "\n\n"

    if yesterday_log:
        prompt += "Here is what I did yesterday:\n"
        prompt += json.dumps(yesterday_log, indent=2)
    else:
        prompt += "No log from yesterday (either first day or log missing)."

    prompt += "\n\nGenerate my daily agenda for today."
    return prompt

Why extract this into a function?

This is the same prompt-building logic that was previously inline in main(). Pulling it into its own function makes main() cleaner and makes the prompt easier to modify later.

  • Save agenda.py.
  • Confirm the new functions are syntactically correct by running this command:
python -c "import agenda; print('Functions loaded OK')"

You should see Functions loaded OK printed. This confirms the new functions are valid Python without running the full script yet.

Seeing a SyntaxError?

  • Check that the triple-quoted string in build_system_prompt() opens and closes with exactly three double quotes (""").
  • Make sure both new functions are placed between get_yesterday_log() and write_agenda_markdown(), not inside another function.
  • help me fix the syntax error in my agenda.py

Now you need to update main() to use these new functions instead of the inline prompt-building code.

  • In agenda.py, find the entire main() function and the if __name__ block at the bottom of the file.
  • Replace the entire main() function and if __name__ block with the following code:
def main():
    # Load task data and yesterday's log
    tasks = load_tasks()
    log_data = load_log()
    yesterday_log = get_yesterday_log(log_data)

    client = anthropic.Anthropic()

    # Build prompts using dedicated functions
    system_prompt = build_system_prompt()
    user_prompt = build_user_prompt(tasks, yesterday_log)

    # Call Claude with the system prompt for low-capacity awareness
    response = client.messages.parse(
        model="claude-sonnet-5",
        max_tokens=4096,
        system=system_prompt,
        messages=[{"role": "user", "content": user_prompt}],
        output_format=DailyAgenda,
    )

    agenda = response.parsed_output

    output_path = Path.home() / "Desktop" / "agenda.md"
    write_agenda_markdown(agenda, output_path)


if __name__ == "__main__":
    main()

What changed in main()?

The inline prompt-building code (about 15 lines) is now two clean function calls: build_system_prompt() and build_user_prompt().

The key addition is system=system_prompt in the client.messages.parse() call. This passes the system prompt as a separate parameter so Claude treats it as persistent instructions, not part of the user's message.

  • Save agenda.py.
  • Run the updated script to confirm the system prompt is working:
python agenda.py

You should see Agenda written to followed by the path to agenda.md on your desktop. Open the file and confirm it still has the same structured format: date, summary, time blocks with priority reasoning, and rollover notes.

Script crashes with an error?

  • Check that you removed ALL of the old inline prompt-building code from main(). Leftover lines like today_str = date.today().isoformat() inside main() will conflict with the new structure.
  • Make sure build_system_prompt() and build_user_prompt() are defined ABOVE main(), not inside it.
  • help me fix the error in my updated agenda.py

✔️ Awesome, I've got everything!

Great. Double check you have saved agenda.py.

ⓧ I'd like to double check the full code

Scroll up and confirm your agenda.py contains these sections in order:

  • Imports (json, date, timedelta, Path, List, BaseModel, anthropic).
  • TimeBlock and DailyAgenda Pydantic models.
  • load_tasks(), load_log(), and get_yesterday_log() helper functions.
  • build_system_prompt() with the low-capacity handling rules.
  • build_user_prompt() that builds the user message from tasks and log data.
  • write_agenda_markdown() for writing the output file.
  • main() using system=system_prompt in the client.messages.parse() call, plus the if __name__ entry point.
Test the low-capacity path

The system prompt has the rules, but you have not seen them in action yet. To test the low-capacity path, you need to give the engine a log that shows a zero-completion day for yesterday.

  • Open daily_log.json in VS Code.
  • Replace the entire file contents with the following. This creates a log entry for yesterday with zero task entries and a low-capacity note:
{
  "logs": [
    {
      "date": "2026-08-19",
      "entries": [],
      "capacity_note": "Rough day, couldn't focus at all"
    }
  ]
}

Why is the entries list empty?

An empty entries array signals a zero day. This is the exact structure the skip option in log_day.py would produce. Your system prompt tells Claude to treat this as a low-capacity recovery scenario.

  • Save daily_log.json.

Before you run the script, think about what you expect to see. Will the generated agenda pack in all six tasks, or will it look different from the last run?

  • Run the agenda generator by running this command:
python agenda.py
  • Open the generated agenda.md on your desktop.

The agenda should look noticeably different from previous runs. You should see fewer time blocks (only the 2-3 most urgent tasks), an encouraging note in the summary acknowledging the low day, and a rollover notes section listing the deprioritized tasks with suggested future dates. The tone should feel gentler, not punishing.

What should I see?

Compare this to your previous agenda. The normal-day agenda likely had 5-6 time blocks filling most of the day. The low-capacity agenda should have roughly 3-4 blocks (60-80% load), focus on tasks with the nearest deadlines, and explicitly mention redistributed work in the rollover notes.

Agenda still looks fully loaded?

  • Verify that the date in your daily_log.json matches yesterday's actual date. The get_yesterday_log() function looks for an exact date match, so if the date is wrong, Claude will not see the low-capacity log.
  • Check that system=system_prompt appears in your client.messages.parse() call. Without it, Claude never receives the low-capacity handling rules.
  • Make sure the entries array in your log is empty ([]), not missing entirely. The key must exist with an empty list value.
  • help me debug why my low-capacity day handling is not working
  • Restore daily_log.json to its original sample data so future runs use a realistic log. Replace the file contents with the following:
{
  "logs": [
    {
      "date": "2026-08-19",
      "entries": [
        {
          "task_name": "Finish OS assignment on memory management",
          "hours_spent": 2,
          "completed": false,
          "notes": "Got through paging section, still need to do virtual memory part"
        },
        {
          "task_name": "Gym session",
          "hours_spent": 1,
          "completed": true,
          "notes": ""
        }
      ],
      "capacity_note": "Normal day, focused in the afternoon"
    }
  ]
}
  • Save daily_log.json.

Your agenda engine now handles real life. It logs what you did, reads your capacity, and adjusts tomorrow's plan accordingly. Next up, you will automate the whole thing so a fresh agenda is waiting on your desktop every morning without you lifting a finger.

Schedule It to Run Every Morning

Your agenda engine now reads your tasks, reasons about your capacity, and writes a clean daily plan. One problem remains: you have to remember to run it.

The whole point of a daily planner is that it works without you thinking about it. In this step, you will create a batch file that launches the agenda script with the correct environment, then schedule it with Windows Task Scheduler so a fresh agenda.md is waiting on your desktop every morning at 7:00 AM.

In this step, get ready to:
  • Create a batch file wrapper that activates your virtual environment and runs the script.
  • Schedule the batch file to run daily using Windows Task Scheduler.
  • Test the scheduled task to confirm end-to-end automation works.
Create the batch file wrapper

Running a Python script directly from Task Scheduler is fragile. Task Scheduler launches processes without an interactive shell, so it does not know about your virtual environment or your project folder.

A batch file solves this by setting the working directory, activating the virtual environment, and then running the script. This way Task Scheduler only needs to know one thing: where the batch file lives.

Find your project folder path

You need the full path to your agenda-engine folder. In your terminal, make sure you are inside the folder, then run:

cd

PowerShell prints the full path (for example, C:\Users\YourName\Desktop\agenda-engine). Copy this path for the next step.

  • Create a file called run_agenda.bat in your agenda-engine folder by pasting the following code into a new file in VS Code:
@echo off
cd /d "[[AGENDA_ENGINE_PATH="C:\Users\YourName\Desktop\agenda-engine"]]"
call .venv\Scripts\activate.bat
python agenda.py

What does this batch file do?

  • @echo off suppresses command echoing so the output stays clean.
  • cd /d changes to your project folder. The /d flag also switches drive letters if needed.
  • call .venv\Scripts\activate.bat activates the virtual environment so Python uses your installed packages.
  • python agenda.py runs the agenda engine.
  • Save run_agenda.bat.
  • Confirm the file exists by running:
dir run_agenda.bat

You should see run_agenda.bat listed in the output.

File not listed?

  • Make sure you saved the file inside your agenda-engine folder, not somewhere else.
  • Check that the file extension is .bat, not .bat.txt. In VS Code, the file extension shown in the tab should end at .bat.
  • help me figure out why my batch file isn't showing up

✔️ Awesome, I've got everything!

Great. Double check you have saved run_agenda.bat and it is the only file you created in this substep.

ⓧ I'd like to double check the full code

Here is the complete run_agenda.bat file. Make sure yours matches:

@echo off
cd /d "[[AGENDA_ENGINE_PATH="C:\Users\YourName\Desktop\agenda-engine"]]"
call .venv\Scripts\activate.bat
python agenda.py
Schedule the script with Task Scheduler

Windows Task Scheduler can trigger any program on a daily schedule. You will use the schtasks command to create a task called DailyAgenda that runs your batch file every morning at 7:00 AM.

  • Press the Windows key to open your search bar.
  • Type PowerShell.
  • Right-click Windows PowerShell in the results.
  • Select Run as administrator.
  • Create the scheduled task by running this command:
schtasks /create /tn "DailyAgenda" /tr "[[AGENDA_ENGINE_PATH="C:\Users\YourName\Desktop\agenda-engine"]]\run_agenda.bat" /sc daily /st 07:00

What does this command do?

  • /tn "DailyAgenda" names the task so you can find it later.
  • /tr specifies the program to run (your batch file).
  • /sc daily sets the schedule to repeat every day.
  • /st 07:00 sets the start time to 7:00 AM.

You should see: SUCCESS: The scheduled task "DailyAgenda" has successfully been created.

Seeing an access denied error?

  • Make sure you opened PowerShell with Run as administrator. Without admin privileges, schtasks cannot create tasks.
  • Double-check that the path to run_agenda.bat is correct. The file must exist at that exact path for the task to work.
  • help me fix an access denied error when creating a scheduled task with schtasks
Test the scheduled task

The task is registered, but you should verify it works before relying on it tomorrow morning. You can trigger the task manually from the Task Scheduler GUI.

  • Press the Windows key to open your search bar.
  • Type Task Scheduler and press Enter to open it.
  • Click Task Scheduler Library in the left pane.
  • Find DailyAgenda in the task list.

Before you run it, what do you expect to happen? Think about what the batch file does and where its output goes.

  • Right-click DailyAgenda and select Run.

The task launches your batch file in the background. Within a few seconds, a fresh agenda.md should appear on your desktop. Open it to confirm it contains today's date, time blocks, and rollover notes.

No agenda.md on your desktop?

  • Check the Last Run Result column in Task Scheduler. A result of 0x0 means the task completed successfully. Any other code means something went wrong.
  • A result of 0x1 usually means the batch file path is wrong. Double-check that the path in run_agenda.bat matches your actual agenda-engine folder.
  • Try running run_agenda.bat directly from your terminal to see if any Python errors appear. Errors inside the script will not be visible when Task Scheduler runs it in the background.
  • Make sure your ANTHROPIC_API_KEY is set as a persistent environment variable (via System Properties), not just a session variable. Task Scheduler starts a new session that does not inherit session-only variables.
  • help me troubleshoot why my scheduled task is not producing agenda.md

Why use a batch file instead of running Python directly?

Task Scheduler does not activate virtual environments or set working directories on its own. Without the batch file, Python would fail to find the anthropic package (because the virtual environment is not active) or fail to find tasks.json (because the working directory is wrong).

The batch file wraps all of that setup into one step so the scheduled task stays simple.

Secret mission

Add Depth-Check Mode

Your agenda engine plans your day, but it does not check whether you actually learned anything. Marking a task "done" is easy. Explaining what you learned from it is harder. Can you add a mode that generates self-test questions for completed tasks, testing real understanding instead of just recall?

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. This project calls the Claude API on a daily schedule, so leaving the task active incurs a small ongoing cost.

Cost warning

The DailyAgenda scheduled task calls Claude Sonnet 5 once per day. At $2 per million input tokens and $10 per million output tokens, each call costs fractions of a penny.

Over a month of daily use, this adds up to roughly $1 or less. If you do not plan to keep using the engine, disable or delete the task to stop all charges.

Resources you used:

  • Claude Console API key (at platform.claude.ai).
  • ANTHROPIC_API_KEY user environment variable.
  • Windows scheduled task DailyAgenda.
  • agenda-engine project folder on your Desktop (includes virtual environment, Python scripts, and JSON data files).
  • agenda.md file on your Desktop.

Keep everything running

No action needed. Choose this if you are still actively using the agenda engine and want a fresh plan every morning.

  • The DailyAgenda scheduled task continues to run daily at 7:00 AM and costs fractions of a penny per call.
  • Your API key stays active in the Claude Console.
  • Update tasks.json with new tasks and run log_day.py each evening to keep the engine accurate.

Pause - I'll come back to this later

Disable the scheduled task to stop daily API calls. Your project files and API key stay in place so you can re-enable anytime.

  • Press the Windows key, type Task Scheduler, and press Enter.
  • In the task list, right-click DailyAgenda 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, revoke API access, and clean up your environment.

  • Delete the scheduled task by opening PowerShell as Administrator and running:
schtasks /delete /tn "DailyAgenda" /f
  • Delete your API key from the Claude Console. Go to platform.claude.ai, open Settings, select Keys, and delete the key you created for this project.
  • Remove the ANTHROPIC_API_KEY environment variable. Open System Properties, click Environment Variables, find ANTHROPIC_API_KEY under User variables, and click Delete.
  • Also remove it from your current PowerShell session by running:
Remove-Item Env:\ANTHROPIC_API_KEY
  • Delete the project folder and the generated agenda file from your Desktop by running:
Remove-Item -Recurse -Force "$HOME\Desktop\agenda-engine"
Remove-Item -Force "$HOME\Desktop\agenda.md"

Nice Work!

Nice Work!

Nice work! You just built a personal daily planning engine powered by Claude Sonnet 5. Every morning, a fresh agenda.md lands on your desktop with a prioritized, time-blocked plan tailored to your real workload and yesterday's progress.

You've learned how to:

  • Built a Python CLI tool that reads your tasks, calls the Claude API, and generates a structured, time-blocked daily agenda with priority reasoning.
  • Used Pydantic structured output with client.messages.parse() and output_format to guarantee consistent, parseable responses from the LLM every single run.
  • Automated the engine with Windows Task Scheduler and a batch file so a fresh plan is waiting on your desktop every morning, with low-capacity day handling that reshuffles work compassionately instead of guilt-stacking.
  • Added a depth-check mode (Secret Mission) that generates self-test questions per completed task to verify you actually understood the material, not just opened it.

Ready to quiz yourself?