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.
git clone https://github.com/majiayu000/claude-skill-registry
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"
skills/data/cashflow-scheduler/SKILL.mdCashflow 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
: This function handles mid-month adjustments automatically.adjust_from_day()
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:
- Solves baseline schedule for the full month
- Locks days 1-20 to baseline schedule
- Adds adjustment to match your actual $230 balance on day 20
- 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):
- Day 1 must be "Spark" (work day)
- Daily closing balances must be ≥ 0
- Day 30 pre-rent balance must be ≥ rent_guard
- Final balance must be in [target - band, target + band]
Soft constraints (optimized lexicographically):
- Minimize total workdays
- Minimize back-to-back work pairs (prefer alternating work/rest)
- 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:
- Create Plan with bills, deposits, targets
- Solve with
solve(plan) - Validate result
- 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:
- Load JSON into Plan object
- Solve
- 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:
- Create two Plan variants
- Solve both
- 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:
- Use
for guided promptsexamples/interactive_create.py - Build Plan incrementally
- 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
or lowerband_centstarget_end_cents - Different work days? Lock specific days with
orplan.actions[day-1] = "Spark""O" - Move bills? Modify
list and re-solveplan.bills - Add income? Append to
and re-solveplan.deposits - 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:
- Increase band:
plan.band_cents = to_cents(50.0) - Lower target:
plan.target_end_cents = to_cents(450.0) - Add deposit:
plan.deposits.append(Deposit(day=15, amount_cents=to_cents(200.0))) - 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/:
- Basic example with minimal billssimple_plan.json
Example Scripts
Located in
examples/:
- Basic solve workflowsolve_basic.py
- Load and solve JSON planssolve_from_json.py
- Benchmark DP vs CP-SATcompare_solvers.py
- Interactive plan builderinteractive_create.py
Reference Documentation
Located in
references/:
- Complete JSON schema documentationplan_schema.md
- Constraint system detailsconstraints.md
- Common issues and solutionstroubleshooting.md
API Reference
Core Functions
solve(plan, solver="auto", **kwargs)
solve(plan, solver="auto", **kwargs)Main solver entry point for full-month scheduling.
Args:
(Plan): Problem definitionplan
(str): "auto", "dp", or "cpsat"solver
: Solver-specific options**kwargs
Returns: Schedule object
Raises: RuntimeError if no solution exists
adjust_from_day(original_plan, current_day, current_eod_balance, solver="auto", **kwargs)
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:
(Plan): The original full-month planoriginal_plan
(int): Current day number (1-30)current_day
(float): Your actual end-of-day balance in dollarscurrent_eod_balance
(str): "auto", "dp", or "cpsat"solver
: Solver-specific options**kwargs
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(plan, schedule)Validate a schedule against constraints.
Returns: ValidationReport with ok status and check details
to_cents(amount)
to_cents(amount)Convert dollars to integer cents.
Args: amount (float | int | str | Decimal)
Returns: int (cents)
cents_to_str(cents)
cents_to_str(cents)Convert integer cents to dollar string.
Args: cents (int)
Returns: str (e.g., "123.45")
Data Classes
Plan
PlanProblem definition.
Fields: start_balance_cents, target_end_cents, band_cents, rent_guard_cents, deposits, bills, actions, manual_adjustments, locks, metadata
Schedule
ScheduleSolution.
Fields: actions, objective, final_closing_cents, ledger
Bill
BillScheduled outflow.
Fields: day (1-30), name (str), amount_cents (int)
Deposit
DepositScheduled 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
- Start simple: Use example plans as templates
- Validate always: Check
before trusting resultsreport.ok - Use auto-fallback: Default
ensures robustnesssolver="auto" - Increase band if infeasible: Try $50+ instead of $25
- Check total funds: Ensure start + deposits >= bills + target
- Prefer fewer locks: Only lock days if absolutely necessary
See Also
- plan_schema.md - Complete JSON format
- constraints.md - Constraint system deep dive
- troubleshooting.md - Debugging guide
- Example scripts in
for working codeexamples/