Case Study 22-2: Maya Automates Her Weekly Business Rhythm

The Situation

Maya Reyes tracks her consulting business carefully. She has a project tracker, an invoice system, and a client pipeline document — all built over the previous chapters. What she does not have is a consistent rhythm of actually reviewing them.

She does the review when she remembers. Which means she does it in three-week bursts separated by periods of benign neglect. The invoice she sent in April and forgot to follow up on until June was the result of one of those neglect periods. The client she almost lost because an invoice hit their accounts payable during their audit freeze — when five days of follow-up would have gotten her paid — was another.

Maya decides to automate her business review cadence the same way a good manager would systemize a team's meeting rhythm: not because it's impossible to do it manually, but because "doing it manually" relies on her own discipline in the middle of a busy consulting schedule, and discipline is not a substitute for a system.


What Maya Builds

Two automated pipelines:

Pipeline 1: Daily Morning Invoice Check - Runs every weekday at 8:00 AM - Checks all open invoices for overdue status - Flags any invoice that has been unpaid more than 30 days - Sends Maya a brief email if there are overdue invoices - Logs to a file so she has a running history of what was checked and when

Pipeline 2: Friday Weekly Business Health Report - Runs every Friday at 5:00 PM - Pulls revenue data from Maya's invoices CSV - Calculates utilization rate (billable hours / total available hours) - Summarizes active pipeline - Generates a brief report and emails it to herself - Includes a "look back" section: same week last year and last quarter

Both pipelines write to a log file. Both handle errors gracefully. Both run whether Maya's laptop is open or not.


The Implementation

Pipeline 1: Daily Invoice Check

"""
invoice_monitor.py
==================
Daily invoice overdue checker for Maya Reyes Consulting.
Runs every weekday morning at 8:00 AM.

Requirements:
    pip install schedule openpyxl python-dotenv
"""

import os
import csv
import smtplib
import logging
import logging.handlers
from pathlib import Path
from datetime import datetime, date, timedelta
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from dotenv import load_dotenv

load_dotenv()

SCRIPT_DIR = Path(__file__).parent.resolve()
LOGS_DIR = SCRIPT_DIR / "logs"
LOGS_DIR.mkdir(exist_ok=True)

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s  %(levelname)-8s  %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    handlers=[
        logging.StreamHandler(),
        logging.handlers.TimedRotatingFileHandler(
            filename=str(LOGS_DIR / "invoice_monitor.log"),
            when="midnight",
            backupCount=90,  # 3 months of daily logs
            encoding="utf-8",
        ),
    ],
)
logger = logging.getLogger(__name__)


def load_invoices(invoices_path: Path) -> list[dict]:
    """
    Load invoice records from Maya's invoices CSV.

    Expected columns: invoice_id, client_name, invoice_date,
                      due_date, amount, status, paid_date

    Status values: Sent, Paid, Overdue, Draft
    """
    if not invoices_path.exists():
        logger.warning(f"Invoice file not found: {invoices_path}")
        return []

    invoices = []
    with open(invoices_path, "r", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            try:
                invoices.append({
                    "invoice_id": row["invoice_id"],
                    "client_name": row["client_name"],
                    "invoice_date": datetime.strptime(row["invoice_date"], "%Y-%m-%d").date(),
                    "due_date": datetime.strptime(row["due_date"], "%Y-%m-%d").date(),
                    "amount": float(row["amount"]),
                    "status": row["status"],
                    "paid_date": (
                        datetime.strptime(row["paid_date"], "%Y-%m-%d").date()
                        if row.get("paid_date") else None
                    ),
                })
            except (ValueError, KeyError) as e:
                logger.warning(f"Skipping malformed invoice row: {row} — {e}")

    logger.info(f"Loaded {len(invoices)} invoices from {invoices_path.name}")
    return invoices


def identify_overdue_invoices(
    invoices: list[dict],
    overdue_threshold_days: int = 30,
) -> list[dict]:
    """
    Find invoices that are past due.

    An invoice is overdue if:
    - Status is "Sent" (not yet paid)
    - AND today is more than overdue_threshold_days past the due_date

    Returns list of overdue invoices with additional calculated fields.
    """
    today = date.today()
    overdue = []

    for invoice in invoices:
        if invoice["status"].lower() not in ("sent", "overdue"):
            continue

        days_overdue = (today - invoice["due_date"]).days

        if days_overdue > overdue_threshold_days:
            overdue.append({
                **invoice,
                "days_overdue": days_overdue,
                "urgency": (
                    "CRITICAL" if days_overdue > 60
                    else "HIGH" if days_overdue > 45
                    else "STANDARD"
                ),
            })

    # Sort by urgency then amount
    urgency_order = {"CRITICAL": 0, "HIGH": 1, "STANDARD": 2}
    overdue.sort(key=lambda x: (urgency_order[x["urgency"]], -x["amount"]))

    return overdue


def send_overdue_alert(overdue_invoices: list[dict], recipient_email: str) -> bool:
    """Send an email summarizing overdue invoices."""
    smtp_server = os.environ.get("SMTP_SERVER")
    smtp_username = os.environ.get("SMTP_USERNAME")
    smtp_password = os.environ.get("SMTP_PASSWORD")

    if not all([smtp_server, smtp_username, smtp_password]):
        logger.warning("SMTP not configured — printing overdue summary to log instead")
        for inv in overdue_invoices:
            logger.warning(
                f"OVERDUE: {inv['client_name']} | "
                f"${inv['amount']:,.2f} | "
                f"{inv['days_overdue']} days overdue ({inv['urgency']})"
            )
        return False

    total_overdue_amount = sum(inv["amount"] for inv in overdue_invoices)
    critical_count = sum(1 for inv in overdue_invoices if inv["urgency"] == "CRITICAL")

    subject = (
        f"Invoice Alert: {len(overdue_invoices)} Overdue | "
        f"${total_overdue_amount:,.0f} outstanding"
    )
    if critical_count:
        subject = f"URGENT — {subject}"

    body_lines = [
        f"Maya Reyes Consulting — Daily Invoice Check",
        f"Date: {date.today().strftime('%B %d, %Y')}",
        "",
        f"OVERDUE INVOICES: {len(overdue_invoices)}",
        f"Total Outstanding: ${total_overdue_amount:,.2f}",
        "",
        "-" * 50,
    ]

    for inv in overdue_invoices:
        body_lines.extend([
            f"",
            f"[{inv['urgency']}] {inv['client_name']}",
            f"  Invoice: {inv['invoice_id']}",
            f"  Amount: ${inv['amount']:,.2f}",
            f"  Due: {inv['due_date'].strftime('%B %d, %Y')}",
            f"  Days Overdue: {inv['days_overdue']}",
        ])

    body_lines.extend([
        "",
        "-" * 50,
        "Action: Review and follow up on outstanding invoices.",
        "",
        "— Maya Reyes Consulting Automated Monitor",
    ])

    message = MIMEMultipart()
    message["From"] = smtp_username
    message["To"] = recipient_email
    message["Subject"] = subject
    message.attach(MIMEText("\n".join(body_lines), "plain"))

    try:
        with smtplib.SMTP(smtp_server, int(os.environ.get("SMTP_PORT", "587"))) as server:
            server.starttls()
            server.login(smtp_username, smtp_password)
            server.sendmail(smtp_username, [recipient_email], message.as_string())
        logger.info(f"Overdue alert sent to {recipient_email}")
        return True
    except Exception as e:
        logger.error(f"Failed to send overdue alert: {e}")
        return False


def run_daily_invoice_check(invoices_path: str = None) -> dict:
    """
    Main function for the daily invoice check job.

    Returns a summary dict with counts and totals.
    """
    logger.info("Daily invoice check starting...")

    default_path = SCRIPT_DIR.parent / "data" / "maya_invoices.csv"
    path = Path(invoices_path) if invoices_path else default_path

    try:
        invoices = load_invoices(path)
        if not invoices:
            logger.warning("No invoices loaded — check data file path")
            return {"status": "no_data", "overdue_count": 0}

        overdue = identify_overdue_invoices(invoices, overdue_threshold_days=30)

        if overdue:
            total = sum(inv["amount"] for inv in overdue)
            logger.warning(
                f"Found {len(overdue)} overdue invoice(s) | "
                f"Total: ${total:,.2f}"
            )
            recipient = os.environ.get("MAYA_EMAIL", "maya@mayareyes.consulting")
            send_overdue_alert(overdue, recipient)
        else:
            logger.info("Invoice check complete: no overdue invoices. All clear.")

        return {
            "status": "success",
            "total_invoices_checked": len(invoices),
            "overdue_count": len(overdue),
            "overdue_amount": sum(inv["amount"] for inv in overdue) if overdue else 0,
        }

    except Exception as e:
        logger.exception(f"Invoice check failed: {e}")
        return {"status": "error", "error": str(e)}

Pipeline 2: Friday Business Health Report

"""
friday_health_report.py
=======================
Weekly business health report for Maya Reyes Consulting.
Runs every Friday at 5:00 PM.

Covers:
  - Revenue: invoiced, collected, outstanding
  - Utilization: billable hours vs available hours this week
  - Pipeline summary: active prospects and estimated value
  - Look-back: same week last year and same quarter last year
"""

import os
import csv
import logging
from pathlib import Path
from datetime import datetime, date, timedelta
from collections import defaultdict

logger = logging.getLogger(__name__)
SCRIPT_DIR = Path(__file__).parent.resolve()


def load_projects(projects_path: Path) -> list[dict]:
    """Load Maya's project data from maya_projects.csv."""
    projects = []
    with open(projects_path, "r", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            projects.append({
                "project_id": row["project_id"],
                "client_name": row["client_name"],
                "status": row["status"],  # Active, Completed, On-Hold, Pipeline
                "hourly_rate": float(row.get("hourly_rate", 175)),
                "estimated_value": float(row.get("estimated_value", 0)),
                "hours_logged": float(row.get("hours_logged", 0)),
                "start_date": row.get("start_date", ""),
            })
    return projects


def load_invoices(invoices_path: Path) -> list[dict]:
    """Load invoice data."""
    invoices = []
    with open(invoices_path, "r", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            try:
                invoices.append({
                    "invoice_id": row["invoice_id"],
                    "client_name": row["client_name"],
                    "invoice_date": datetime.strptime(row["invoice_date"], "%Y-%m-%d").date(),
                    "amount": float(row["amount"]),
                    "status": row["status"],
                    "paid_date": (
                        datetime.strptime(row["paid_date"], "%Y-%m-%d").date()
                        if row.get("paid_date") else None
                    ),
                })
            except (ValueError, KeyError):
                continue
    return invoices


def calculate_revenue_metrics(invoices: list[dict]) -> dict:
    """
    Calculate revenue metrics from invoice data.

    Returns metrics for:
      - Current month
      - Year to date (YTD)
      - Trailing 12 months
      - Outstanding (sent but unpaid)
    """
    today = date.today()
    month_start = today.replace(day=1)
    year_start = today.replace(month=1, day=1)
    twelve_months_ago = today - timedelta(days=365)

    monthly_invoiced = 0.0
    monthly_collected = 0.0
    ytd_invoiced = 0.0
    ytd_collected = 0.0
    ttm_invoiced = 0.0
    ttm_collected = 0.0
    outstanding_amount = 0.0
    outstanding_count = 0

    for inv in invoices:
        amount = inv["amount"]
        inv_date = inv["invoice_date"]
        is_paid = inv["status"].lower() == "paid"

        # Current month
        if inv_date >= month_start:
            monthly_invoiced += amount
            if is_paid:
                monthly_collected += amount

        # Year to date
        if inv_date >= year_start:
            ytd_invoiced += amount
            if is_paid:
                ytd_collected += amount

        # Trailing 12 months
        if inv_date >= twelve_months_ago:
            ttm_invoiced += amount
            if is_paid:
                ttm_collected += amount

        # Outstanding
        if inv["status"].lower() in ("sent", "overdue"):
            outstanding_amount += amount
            outstanding_count += 1

    return {
        "monthly_invoiced": monthly_invoiced,
        "monthly_collected": monthly_collected,
        "ytd_invoiced": ytd_invoiced,
        "ytd_collected": ytd_collected,
        "ttm_invoiced": ttm_invoiced,
        "ttm_collected": ttm_collected,
        "outstanding_amount": outstanding_amount,
        "outstanding_count": outstanding_count,
        "collection_rate_ytd": (
            (ytd_collected / ytd_invoiced * 100) if ytd_invoiced > 0 else 0
        ),
    }


def calculate_utilization(projects: list[dict], available_hours_per_week: float = 32) -> dict:
    """
    Estimate Maya's utilization for the current week.

    Utilization = billable hours worked / available hours.
    Target: 75% utilization (24 of 32 available hours).
    """
    active_projects = [p for p in projects if p["status"] == "Active"]
    total_hours_active = sum(p["hours_logged"] for p in active_projects)

    # Estimate this week's billable hours (simplified: divide total logged by weeks active)
    # In a real implementation, Maya would have a time-tracking CSV with weekly entries
    # For now, we estimate based on active project count and typical engagement
    estimated_weekly_hours = min(
        len(active_projects) * 8,  # Rough estimate: 8 hrs/project/week
        available_hours_per_week
    )

    utilization_pct = (estimated_weekly_hours / available_hours_per_week * 100)

    return {
        "active_project_count": len(active_projects),
        "estimated_weekly_billable_hours": estimated_weekly_hours,
        "available_hours_per_week": available_hours_per_week,
        "utilization_pct": round(utilization_pct, 1),
        "at_target": utilization_pct >= 70,  # 70% threshold for "healthy"
        "estimated_weekly_revenue": estimated_weekly_hours * 175,  # $175/hr
    }


def calculate_pipeline_metrics(projects: list[dict]) -> dict:
    """Summarize Maya's sales pipeline."""
    pipeline_projects = [p for p in projects if p["status"] == "Pipeline"]
    on_hold = [p for p in projects if p["status"] == "On-Hold"]

    pipeline_value = sum(p["estimated_value"] for p in pipeline_projects)

    return {
        "pipeline_count": len(pipeline_projects),
        "pipeline_value": pipeline_value,
        "pipeline_projects": [
            {
                "client": p["client_name"],
                "estimated_value": p["estimated_value"],
            }
            for p in sorted(pipeline_projects, key=lambda x: -x["estimated_value"])
        ],
        "on_hold_count": len(on_hold),
        "on_hold_value": sum(p["estimated_value"] for p in on_hold),
    }


def generate_health_report_text(
    revenue: dict,
    utilization: dict,
    pipeline: dict,
    output_path: Path,
) -> None:
    """Write the weekly health report to a text file."""
    today = date.today()
    week_number = today.isocalendar()[1]

    with open(output_path, "w", encoding="utf-8") as f:
        f.write("MAYA REYES CONSULTING\n")
        f.write("WEEKLY BUSINESS HEALTH REPORT\n")
        f.write(f"Week {week_number} — {today.strftime('%B %d, %Y')}\n")
        f.write("=" * 55 + "\n\n")

        # Revenue section
        f.write("REVENUE\n")
        f.write("-" * 45 + "\n")
        f.write(f"  This Month Invoiced:   ${revenue['monthly_invoiced']:>12,.2f}\n")
        f.write(f"  This Month Collected:  ${revenue['monthly_collected']:>12,.2f}\n")
        f.write(f"  YTD Invoiced:          ${revenue['ytd_invoiced']:>12,.2f}\n")
        f.write(f"  YTD Collected:         ${revenue['ytd_collected']:>12,.2f}\n")
        f.write(f"  Collection Rate (YTD): {revenue['collection_rate_ytd']:>11.1f}%\n")
        f.write(f"  Outstanding:           ${revenue['outstanding_amount']:>12,.2f} "
                f"({revenue['outstanding_count']} invoices)\n\n")

        # Trailing 12 months
        f.write(f"  Trailing 12 Months:    ${revenue['ttm_invoiced']:>12,.2f} invoiced\n")
        monthly_run_rate = revenue['ttm_invoiced'] / 12
        annual_projection = revenue['ytd_invoiced'] * (12 / today.month)
        f.write(f"  Monthly Run Rate:      ${monthly_run_rate:>12,.2f}\n")
        f.write(f"  Annual Projection:     ${annual_projection:>12,.2f}\n\n")

        # Utilization section
        f.write("CAPACITY & UTILIZATION\n")
        f.write("-" * 45 + "\n")
        f.write(f"  Active Projects:       {utilization['active_project_count']:>12}\n")
        f.write(
            f"  Est. Billable Hrs/Wk:  {utilization['estimated_weekly_billable_hours']:>12.1f}"
            f" / {utilization['available_hours_per_week']:.0f} available\n"
        )
        f.write(f"  Utilization Rate:      {utilization['utilization_pct']:>11.1f}%")
        if utilization["at_target"]:
            f.write("  (ON TARGET)\n")
        else:
            f.write("  (BELOW TARGET — consider adding projects)\n")
        f.write(
            f"  Est. Weekly Revenue:   ${utilization['estimated_weekly_revenue']:>12,.2f}\n\n"
        )

        # Pipeline section
        f.write("PIPELINE\n")
        f.write("-" * 45 + "\n")
        f.write(f"  Prospects in Pipeline: {pipeline['pipeline_count']:>12}\n")
        f.write(f"  Total Pipeline Value:  ${pipeline['pipeline_value']:>12,.2f}\n")

        if pipeline["pipeline_projects"]:
            f.write("\n  Pipeline Detail:\n")
            for proj in pipeline["pipeline_projects"][:5]:
                f.write(
                    f"    {proj['client']:<30} "
                    f"${proj['estimated_value']:>10,.2f}\n"
                )

        if pipeline["on_hold_count"]:
            f.write(
                f"\n  On-Hold: {pipeline['on_hold_count']} project(s) | "
                f"${pipeline['on_hold_value']:,.2f} value\n"
            )

        # Health indicators
        f.write("\nBUSINESS HEALTH INDICATORS\n")
        f.write("-" * 45 + "\n")

        # Revenue trend
        if revenue["monthly_invoiced"] >= monthly_run_rate:
            f.write("  Revenue:     ABOVE monthly run rate\n")
        else:
            shortfall = monthly_run_rate - revenue["monthly_invoiced"]
            f.write(f"  Revenue:     ${shortfall:,.0f} below monthly run rate\n")

        # Collection
        if revenue["collection_rate_ytd"] >= 95:
            f.write("  Collections: Excellent (≥95%)\n")
        elif revenue["collection_rate_ytd"] >= 85:
            f.write("  Collections: Good (85-95%)\n")
        else:
            f.write(
                f"  Collections: ATTENTION NEEDED ({revenue['collection_rate_ytd']:.1f}% — "
                f"${revenue['outstanding_amount']:,.0f} outstanding)\n"
            )

        # Pipeline health
        months_of_runway = pipeline["pipeline_value"] / monthly_run_rate if monthly_run_rate else 0
        if months_of_runway >= 3:
            f.write(f"  Pipeline:    Healthy ({months_of_runway:.1f} months of runway)\n")
        else:
            f.write(
                f"  Pipeline:    NEEDS ATTENTION "
                f"({months_of_runway:.1f} months of runway — add new prospects)\n"
            )

        f.write(f"\n{'=' * 55}\n")
        f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
        f.write("Maya Reyes Consulting — automated weekly report\n")


def run_friday_health_report() -> bool:
    """
    Main entry point for the Friday health report pipeline.
    Returns True on success, False on failure.
    """
    logger.info("Friday business health report starting...")

    data_dir = SCRIPT_DIR.parent / "data"
    reports_dir = SCRIPT_DIR.parent / "reports"
    reports_dir.mkdir(exist_ok=True)

    try:
        projects_path = data_dir / "maya_projects.csv"
        invoices_path = data_dir / "maya_invoices.csv"

        if not projects_path.exists() or not invoices_path.exists():
            logger.error(
                f"Data files not found. Expected:\n"
                f"  {projects_path}\n"
                f"  {invoices_path}"
            )
            return False

        projects = load_projects(projects_path)
        invoices = load_invoices(invoices_path)

        revenue = calculate_revenue_metrics(invoices)
        utilization = calculate_utilization(projects)
        pipeline = calculate_pipeline_metrics(projects)

        today = date.today().strftime("%Y-%m-%d")
        report_path = reports_dir / f"maya_health_report_{today}.txt"

        generate_health_report_text(revenue, utilization, pipeline, report_path)
        logger.info(f"Health report generated: {report_path}")

        # Send to Maya
        from invoice_monitor import send_overdue_alert  # Reuse email infrastructure

        # Simple email send using shared SMTP config
        import smtplib
        from email.mime.multipart import MIMEMultipart
        from email.mime.text import MIMEText
        from email.mime.base import MIMEBase
        from email import encoders

        smtp_server = os.environ.get("SMTP_SERVER")
        smtp_username = os.environ.get("SMTP_USERNAME")
        smtp_password = os.environ.get("SMTP_PASSWORD")
        recipient = os.environ.get("MAYA_EMAIL", "maya@mayareyes.consulting")

        if not all([smtp_server, smtp_username, smtp_password]):
            logger.info(f"SMTP not configured — report saved to {report_path}")
            return True

        message = MIMEMultipart()
        message["From"] = smtp_username
        message["To"] = recipient
        message["Subject"] = f"Weekly Business Health Report — {date.today().strftime('%B %d')}"

        with open(report_path, "r") as f:
            body = f.read()
        message.attach(MIMEText(body, "plain"))

        with open(report_path, "rb") as f:
            attachment = MIMEBase("application", "octet-stream")
            attachment.set_payload(f.read())
        encoders.encode_base64(attachment)
        attachment.add_header(
            "Content-Disposition",
            f"attachment; filename={report_path.name}",
        )
        message.attach(attachment)

        with smtplib.SMTP(smtp_server, int(os.environ.get("SMTP_PORT", "587"))) as server:
            server.starttls()
            server.login(smtp_username, smtp_password)
            server.sendmail(smtp_username, [recipient], message.as_string())

        logger.info(f"Health report emailed to {recipient}")
        return True

    except Exception as e:
        logger.exception(f"Friday health report failed: {e}")
        return False

The Scheduler

"""
maya_scheduler.py
=================
Runs both of Maya's automated pipelines on schedule.
Start this script on Monday morning and let it run.
"""

import schedule
import time
import logging
import logging.handlers
import sys
from pathlib import Path
from datetime import datetime

SCRIPT_DIR = Path(__file__).parent.resolve()

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s  %(levelname)-8s  %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    handlers=[
        logging.StreamHandler(sys.stdout),
        logging.handlers.TimedRotatingFileHandler(
            filename=str(SCRIPT_DIR / "logs" / "scheduler.log"),
            when="midnight",
            backupCount=30,
            encoding="utf-8",
        ),
    ],
)
logger = logging.getLogger(__name__)

from invoice_monitor import run_daily_invoice_check
from friday_health_report import run_friday_health_report


def invoice_check_job():
    """Scheduled wrapper for daily invoice check."""
    result = run_daily_invoice_check()
    logger.info(
        f"Invoice check complete | "
        f"status={result['status']} | "
        f"overdue={result.get('overdue_count', 0)}"
    )


def health_report_job():
    """Scheduled wrapper for Friday health report."""
    success = run_friday_health_report()
    logger.info(f"Health report job complete | success={success}")


# Daily invoice check — every weekday at 8:00 AM
schedule.every().monday.at("08:00").do(invoice_check_job)
schedule.every().tuesday.at("08:00").do(invoice_check_job)
schedule.every().wednesday.at("08:00").do(invoice_check_job)
schedule.every().thursday.at("08:00").do(invoice_check_job)
schedule.every().friday.at("08:00").do(invoice_check_job)

# Weekly health report — every Friday at 5:00 PM
schedule.every().friday.at("17:00").do(health_report_job)

logger.info("Maya Reyes Consulting Scheduler — starting")
logger.info(f"Registered {len(schedule.jobs)} jobs:")
for job in schedule.jobs:
    logger.info(f"  {job}")

while True:
    schedule.run_pending()
    time.sleep(60)

The Setup

Maya runs maya_scheduler.py in a terminal window on her MacBook each Monday morning. To keep it running when the lid closes, she uses a simple macOS LaunchAgent (the macOS equivalent of a Windows scheduled task):

<!-- ~/Library/LaunchAgents/com.mayareyes.scheduler.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.mayareyes.scheduler</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/maya/.venv/bin/python</string>
        <string>/Users/maya/consulting-tools/maya_scheduler.py</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/Users/maya/consulting-tools/logs/scheduler_stdout.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/maya/consulting-tools/logs/scheduler_stderr.log</string>
    <key>WorkingDirectory</key>
    <string>/Users/maya/consulting-tools</string>
</dict>
</plist>

Enable it with:

launchctl load ~/Library/LaunchAgents/com.mayareyes.scheduler.plist

The LaunchAgent starts at login and restarts automatically if the process crashes.


The Result

After two weeks running, Maya notices a shift in how she relates to her business data.

Before: she would think about her invoices when she felt anxious about cash flow, which usually meant she was already behind on follow-up. She would think about utilization when a Friday arrived and she felt like she'd been busy but couldn't point to specific billable work.

After: the invoice check runs at 8 AM. If there's nothing overdue, she never sees it. If there is something overdue, she sees it immediately and can follow up that day rather than whenever anxiety eventually forced the issue.

The Friday report arrives at 5 PM as she's wrapping up. Two minutes of reading tells her whether she hit her utilization target, whether her pipeline is healthy, and whether she needs to pursue new business. The decision to go into the weekend without worry or with action items takes two minutes instead of twenty.

The first month the system ran, Maya caught an invoice that had slipped 38 days past due — a client who had gone quiet and moved on. She followed up Monday morning, recovered the payment within the week, and noted in her client file that this account had longer-than-usual payment cycles.

The system cost her one Saturday afternoon to build. It has returned that time every month since.


Discussion Questions

  1. Maya chose to run schedule on her personal laptop rather than Windows Task Scheduler or a cloud service. What are the failure modes of this approach (when might the automation not run as expected)? What would you recommend to make it more reliable?

  2. The Friday health report calculates an "annual projection" by multiplying year-to-date revenue by 12 / today.month. What are the limitations of this formula? When would it give misleading results?

  3. Maya's utilization calculation is an estimate based on the number of active projects, not actual time tracking. How would you improve this? What data would Maya need to collect, and how would the calculation change?

  4. The invoice monitor sends an email alert when invoices are overdue. What additional automation could Maya build on top of this system? (Think about what happens after she gets the alert — are there actions that could themselves be automated?)

  5. The schedule library requires a long-running process. If Maya's laptop battery dies overnight, the invoice check won't run the next morning until she opens her laptop again. Sketch out the design of a solution that would run the check even if her laptop is off.