Claude-skill-registry cashflow-scheduler

Optimize 30-day work schedules for cashflow management using CP-SAT and DP solvers. Use this skill when users need to plan work schedules around bills, deposits, and target balances while minimizing workdays and maximizing rest distribution.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/cashflow-scheduler" ~/.claude/skills/majiayu000-claude-skill-registry-cashflow-scheduler && rm -rf "$T"
manifest: skills/data/cashflow-scheduler/SKILL.md
source content

Cashflow Scheduler

Overview

This skill provides constraint-based work schedule optimization for 30-day cashflow planning. It solves the problem: "Given my bills, deposits, and target ending balance, what's the optimal work schedule that minimizes workdays while maintaining positive daily balances?"

Key Features:

  • Dual solvers: CP-SAT (OR-Tools) primary with automatic DP fallback
  • Lexicographic optimization: Minimize workdays → back-to-back work → distance from target
  • Constraint validation: Ensures non-negative balances, rent guard, and final band requirements
  • Self-contained: Works immediately, no installation required (OR-Tools optional)

When to use this skill:

  • User needs to plan gig work schedule (e.g., DoorDash, Uber, freelance)
  • Optimizing work-life balance while meeting financial obligations
  • Ensuring enough cash for rent while saving towards target
  • Comparing different financial scenarios
  • Validating existing schedules against constraints

Quick Start

Generate a Default Schedule (Fastest)

Want to see it work immediately? Run this:

from core import solve, Plan, Bill, Deposit, to_cents, cents_to_str

# Default plan from plan.json (real-world example)
plan = Plan(
    start_balance_cents=to_cents(90.50),
    target_end_cents=to_cents(90.50),
    band_cents=to_cents(100.0),
    rent_guard_cents=to_cents(1636.0),
    deposits=[
        Deposit(day=10, amount_cents=to_cents(1021.0)),
        Deposit(day=24, amount_cents=to_cents(1021.0))
    ],
    bills=[
        Bill(day=1, name="Auto Insurance", amount_cents=to_cents(108.0)),
        Bill(day=2, name="YouTube Premium", amount_cents=to_cents(8.0)),
        Bill(day=5, name="Groceries", amount_cents=to_cents(112.5)),
        Bill(day=5, name="Weed", amount_cents=to_cents(20.0)),
        Bill(day=6, name="Electric", amount_cents=to_cents(139.0)),
        Bill(day=8, name="Paramount Plus", amount_cents=to_cents(12.0)),
        Bill(day=8, name="iPad AppleCare", amount_cents=to_cents(8.49)),
        Bill(day=10, name="Streaming Svcs", amount_cents=to_cents(230.0)),
        Bill(day=10, name="AI Subscription", amount_cents=to_cents(220.0)),
        Bill(day=11, name="Cat Food", amount_cents=to_cents(40.0)),
        Bill(day=12, name="Groceries", amount_cents=to_cents(112.5)),
        Bill(day=12, name="Weed", amount_cents=to_cents(20.0)),
        Bill(day=14, name="iPad AppleCare", amount_cents=to_cents(8.49)),
        Bill(day=16, name="Cat Food", amount_cents=to_cents(40.0)),
        Bill(day=19, name="Groceries", amount_cents=to_cents(112.5)),
        Bill(day=19, name="Weed", amount_cents=to_cents(20.0)),
        Bill(day=22, name="Cell Phone", amount_cents=to_cents(177.0)),
        Bill(day=23, name="Cat Food", amount_cents=to_cents(40.0)),
        Bill(day=25, name="Ring Subscription", amount_cents=to_cents(10.0)),
        Bill(day=26, name="Groceries", amount_cents=to_cents(112.5)),
        Bill(day=26, name="Weed", amount_cents=to_cents(20.0)),
        Bill(day=28, name="iPhone AppleCare", amount_cents=to_cents(13.49)),
        Bill(day=29, name="Internet", amount_cents=to_cents(30.0)),
        Bill(day=29, name="Cat Food", amount_cents=to_cents(40.0)),
        Bill(day=30, name="Rent", amount_cents=to_cents(1636.0))
    ],
    actions=[None] * 30,
    manual_adjustments=[],
    locks=[],
    metadata={}
)

schedule = solve(plan)

# Show results
w, b2b, diff = schedule.objective
print(f"✅ Schedule created!")
print(f"Workdays: {w}")
print(f"Schedule: {' '.join(schedule.actions)}")
print(f"Work on days: {[i+1 for i, a in enumerate(schedule.actions) if a == 'Spark']}")

Or use the helper script:

python examples/create_default.py

Then customize it! Adjust bills, deposits, or target balance and re-solve.


Adjust Mid-Month (Primary Use Case)

Scenario: You're on day 20 and have $230 actual balance. What should you do for the rest of the month?

Use

adjust_from_day()
: This function handles mid-month adjustments automatically.

from core import adjust_from_day

# Your original plan (from above, or load from JSON)
# plan = Plan(...)

# Adjust from current day with actual balance
new_schedule = adjust_from_day(
    original_plan=plan,
    current_day=20,
    current_eod_balance=230.00  # Your actual balance today
)

# See what to do for days 21-30
print(f"Days 21-30: {' '.join(new_schedule.actions[20:])}")
work_days_remaining = [i+1 for i, a in enumerate(new_schedule.actions[20:], start=20) if a == 'Spark']
print(f"Work on days: {work_days_remaining}")

How it works:

  1. Solves baseline schedule for the full month
  2. Locks days 1-20 to baseline schedule
  3. Adds adjustment to match your actual $230 balance on day 20
  4. Re-solves days 21-30 optimally

Result: You get an updated schedule that accounts for your actual financial position!


Basic Usage

from core import solve, Plan, Bill, Deposit, to_cents, cents_to_str

# Create a plan
plan = Plan(
    start_balance_cents=to_cents(100.00),
    target_end_cents=to_cents(500.00),
    band_cents=to_cents(25.0),
    rent_guard_cents=to_cents(1600.0),
    deposits=[
        Deposit(day=11, amount_cents=to_cents(1021.0)),
        Deposit(day=25, amount_cents=to_cents(1021.0))
    ],
    bills=[
        Bill(day=1, name="Insurance", amount_cents=to_cents(177.0)),
        Bill(day=30, name="Rent", amount_cents=to_cents(1636.0))
    ],
    actions=[None] * 30,  # Let solver decide all days
    manual_adjustments=[],
    locks=[],
    metadata={}
)

# Solve (auto-selects CP-SAT, falls back to DP if OR-Tools unavailable)
schedule = solve(plan)

# Display results
print(f"Workdays: {schedule.objective[0]}")
print(f"Back-to-back pairs: {schedule.objective[1]}")
print(f"Distance from target: ${cents_to_str(schedule.objective[2])}")
print(f"Schedule: {' '.join(schedule.actions)}")

Loading from JSON

import json
from pathlib import Path
from core import Plan, Bill, Deposit, to_cents, solve

def load_plan(path: Path) -> Plan:
    with open(path) as f:
        data = json.load(f)

    return Plan(
        start_balance_cents=to_cents(data['start_balance']),
        target_end_cents=to_cents(data['target_end']),
        band_cents=to_cents(data['band']),
        rent_guard_cents=to_cents(data['rent_guard']),
        deposits=[Deposit(day=d['day'], amount_cents=to_cents(d['amount']))
                  for d in data['deposits']],
        bills=[Bill(day=b['day'], name=b['name'],
                    amount_cents=to_cents(b['amount']))
               for b in data['bills']],
        actions=data.get('actions', [None] * 30),
        manual_adjustments=[],
        locks=[],
        metadata=data.get('metadata', {})
    )

# Usage
plan = load_plan(Path('plan.json'))
schedule = solve(plan)

Core Concepts

Plan Object

Defines the problem constraints:

  • start_balance_cents: Starting cash on Day 1
  • target_end_cents: Desired ending balance on Day 30
  • band_cents: Tolerance around target (final must be in [target-band, target+band])
  • rent_guard_cents: Minimum balance required before paying rent on Day 30
  • deposits: List of scheduled cash inflows (paychecks, deposits)
  • bills: List of scheduled cash outflows (rent, utilities, etc.)
  • actions: Optional pre-filled days (None = solver decides, "O" = off, "Spark" = work)
  • manual_adjustments: One-time corrections (e.g., refunds, found money)

Schedule Object

Contains the solution:

  • actions: 30-element list of "O" (off) or "Spark" (work at $100/day)
  • objective: Tuple of (workdays, back_to_back_pairs, abs_diff_from_target)
  • final_closing_cents: Final balance on Day 30
  • ledger: Daily ledger entries with opening/closing balances

Constraints

Hard constraints (must satisfy):

  1. Day 1 must be "Spark" (work day)
  2. Daily closing balances must be ≥ 0
  3. Day 30 pre-rent balance must be ≥ rent_guard
  4. Final balance must be in [target - band, target + band]

Soft constraints (optimized lexicographically):

  1. Minimize total workdays
  2. Minimize back-to-back work pairs (prefer alternating work/rest)
  3. Minimize absolute difference from exact target

Solver Selection

The skill includes two solvers with automatic selection:

# Auto-select (default): Try CP-SAT, fall back to DP
schedule = solve(plan)  # Recommended

# Force DP only
schedule = solve(plan, solver="dp")

# Force CP-SAT (raises error if OR-Tools unavailable)
schedule = solve(plan, solver="cpsat")

Solver comparison:

  • CP-SAT: Faster on complex plans, requires OR-Tools (
    pip install ortools
    )
  • DP: Pure Python, always available, slightly slower on large problems
  • Both produce identical optimal results (proven equivalent)

Validation

Always validate schedules before trusting them:

from core import validate

report = validate(plan, schedule)

if report.ok:
    print("✅ Schedule is valid")
else:
    print("❌ Validation failed:")
    for name, ok, detail in report.checks:
        if not ok:
            print(f"  ✗ {name}: {detail}")

Validation checks:

  • Day 1 is "Spark"
  • All daily balances non-negative
  • Final balance within band
  • Day 30 pre-rent guard satisfied

Example Workflows

Workflow 1: Basic Schedule Optimization

User request: "I need to plan my October work schedule. I have rent on the 30th and want to save $500."

Steps:

  1. Create Plan with bills, deposits, targets
  2. Solve with
    solve(plan)
  3. Validate result
  4. Display work schedule

See:

examples/solve_basic.py

Workflow 2: Load Existing Plan

User request: "Here's my plan.json file, can you optimize it?"

Steps:

  1. Load JSON into Plan object
  2. Solve
  3. Show ledger with daily balances

See:

examples/solve_from_json.py

Workflow 3: Compare Scenarios

User request: "What if I delay my internet bill to the 15th instead of the 5th?"

Steps:

  1. Create two Plan variants
  2. Solve both
  3. Compare objectives and schedules

See:

examples/compare_solvers.py
(adapt for scenario comparison)

Workflow 4: Interactive Plan Creation

User request: "Help me create a plan from scratch."

Steps:

  1. Use
    examples/interactive_create.py
    for guided prompts
  2. Build Plan incrementally
  3. Solve and save to JSON

See:

examples/interactive_create.py

Workflow 5: Iterative Adjustments

User request: "Can you make me work less?" or "What if I move my internet bill to the 15th?"

The skill is designed for iterative refinement:

# 1. Start with a schedule
schedule = solve(plan)
print(f"Current: {schedule.objective[0]} workdays")

# 2. User wants fewer workdays? Increase target tolerance
plan.band_cents = to_cents(150.0)  # Was 100.0
schedule = solve(plan)
print(f"Adjusted: {schedule.objective[0]} workdays")

# 3. User wants to move a bill? Update and re-solve
# Find and update bill
for bill in plan.bills:
    if bill.name == "Internet":
        plan.bills.remove(bill)
        plan.bills.append(Bill(day=15, name="Internet", amount_cents=bill.amount_cents))
        break

schedule = solve(plan)
print(f"After moving bill: {schedule.objective[0]} workdays")

# 4. User wants specific days off? Lock them
plan.actions[5:8] = ["O", "O", "O"]  # Lock days 6-8 as off
schedule = solve(plan)
print(f"With days 6-8 off: {schedule.objective[0]} workdays")

Common adjustment patterns:

  • Fewer workdays? Increase
    band_cents
    or lower
    target_end_cents
  • Different work days? Lock specific days with
    plan.actions[day-1] = "Spark"
    or
    "O"
  • Move bills? Modify
    plan.bills
    list and re-solve
  • Add income? Append to
    plan.deposits
    and re-solve
  • Account for one-time cash? Use
    plan.manual_adjustments

Common Issues

"No feasible schedule found"

Causes:

  • Bills exceed available income (start + deposits + max work income)
  • target_end too high relative to available funds
  • band too tight (try increasing from $25 to $50)
  • rent_guard too high (reduce to just cover rent)

Solutions:

  1. Increase band:
    plan.band_cents = to_cents(50.0)
  2. Lower target:
    plan.target_end_cents = to_cents(450.0)
  3. Add deposit:
    plan.deposits.append(Deposit(day=15, amount_cents=to_cents(200.0)))
  4. Reduce bills or remove locked actions

See: references/troubleshooting.md

"OR-Tools CP-SAT not installed"

Cause: OR-Tools library missing and

dp_fallback=False

Solution:

pip install ortools

Or use auto-fallback:

schedule = solve(plan)  # Auto-falls back to DP

Schedule has negative balance

Cause: Validation bug (should be impossible with correct solver)

Solution: Re-solve with fresh Plan, report as bug if persistent

Available Resources

Example Plans

Located in

assets/example_plans/
:

  • simple_plan.json
    - Basic example with minimal bills

Example Scripts

Located in

examples/
:

  • solve_basic.py
    - Basic solve workflow
  • solve_from_json.py
    - Load and solve JSON plans
  • compare_solvers.py
    - Benchmark DP vs CP-SAT
  • interactive_create.py
    - Interactive plan builder

Reference Documentation

Located in

references/
:

  • plan_schema.md
    - Complete JSON schema documentation
  • constraints.md
    - Constraint system details
  • troubleshooting.md
    - Common issues and solutions

API Reference

Core Functions

solve(plan, solver="auto", **kwargs)

Main solver entry point for full-month scheduling.

Args:

  • plan
    (Plan): Problem definition
  • solver
    (str): "auto", "dp", or "cpsat"
  • **kwargs
    : Solver-specific options

Returns: Schedule object

Raises: RuntimeError if no solution exists

adjust_from_day(original_plan, current_day, current_eod_balance, solver="auto", **kwargs)

PRIMARY FUNCTION for mid-month adjustments. Use when you know your actual balance on a specific day.

Args:

  • original_plan
    (Plan): The original full-month plan
  • current_day
    (int): Current day number (1-30)
  • current_eod_balance
    (float): Your actual end-of-day balance in dollars
  • solver
    (str): "auto", "dp", or "cpsat"
  • **kwargs
    : Solver-specific options

Returns: Schedule object with days 1-current_day locked and days current_day+1 to 30 re-optimized

Raises:

  • ValueError if current_day not in 1-30
  • RuntimeError if no feasible schedule exists

Example:

# You're on day 20 with $230 actual balance
new_schedule = adjust_from_day(plan, current_day=20, current_eod_balance=230.0)
print(f"Days 21-30: {' '.join(new_schedule.actions[20:])}")

validate(plan, schedule)

Validate a schedule against constraints.

Returns: ValidationReport with ok status and check details

to_cents(amount)

Convert dollars to integer cents.

Args: amount (float | int | str | Decimal)

Returns: int (cents)

cents_to_str(cents)

Convert integer cents to dollar string.

Args: cents (int)

Returns: str (e.g., "123.45")

Data Classes

Plan

Problem definition.

Fields: start_balance_cents, target_end_cents, band_cents, rent_guard_cents, deposits, bills, actions, manual_adjustments, locks, metadata

Schedule

Solution.

Fields: actions, objective, final_closing_cents, ledger

Bill

Scheduled outflow.

Fields: day (1-30), name (str), amount_cents (int)

Deposit

Scheduled inflow.

Fields: day (1-30), amount_cents (int)

Advanced Usage

Using CP-SAT Options

from core.cpsat_solver import CPSATSolveOptions

options = CPSATSolveOptions(
    max_time_seconds=30.0,
    num_search_workers=4,
    log_search_progress=True
)

schedule = solve(plan, solver="cpsat", options=options)

Locking Specific Days

# Force Day 1 to work, Day 2 to off, let solver decide rest
plan.actions = ["Spark", "O"] + [None] * 28

schedule = solve(plan)

Manual Adjustments

from core import Adjustment

# One-time correction on Day 15
plan.manual_adjustments = [
    Adjustment(day=15, amount_cents=to_cents(-50.0), note="Venmo refund")
]

schedule = solve(plan)

Tips for Success

  1. Start simple: Use example plans as templates
  2. Validate always: Check
    report.ok
    before trusting results
  3. Use auto-fallback: Default
    solver="auto"
    ensures robustness
  4. Increase band if infeasible: Try $50+ instead of $25
  5. Check total funds: Ensure start + deposits >= bills + target
  6. Prefer fewer locks: Only lock days if absolutely necessary

See Also