name: cashflow-scheduler description: 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.
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:
- 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
examples/interactive_create.pyfor guided prompts - 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
band_centsor lowertarget_end_cents - Different work days? Lock specific days with
plan.actions[day-1] = "Spark"or"O" - Move bills? Modify
plan.billslist and re-solve - Add income? Append to
plan.depositsand 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:
- 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/:
simple_plan.json- Basic example with minimal bills
Example Scripts
Located in examples/:
solve_basic.py- Basic solve workflowsolve_from_json.py- Load and solve JSON planscompare_solvers.py- Benchmark DP vs CP-SATinteractive_create.py- Interactive plan builder
Reference Documentation
Located in references/:
plan_schema.md- Complete JSON schema documentationconstraints.md- Constraint system detailstroubleshooting.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 definitionsolver(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 plancurrent_day(int): Current day number (1-30)current_eod_balance(float): Your actual end-of-day balance in dollarssolver(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
- Start simple: Use example plans as templates
- Validate always: Check
report.okbefore trusting results - Use auto-fallback: Default
solver="auto"ensures robustness - 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
examples/for working code