Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Link schedule activities to cost items. Create cost-loaded schedules, generate cash flow curves, and track earned value.
.claude/skills/datadrivenconstruction-schedule-cost-link/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-06 | ✗→✓ | ▲ Improved | 131% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 198% | 0% |
| case-16 | ✗→✓ | ▲ Improved | 146% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 82% | 0% |
| case-22 | ✗→✓ | ▲ Improved | 173% | 0% |
Integrating schedule and cost requires:
Systematic linkage between schedule activities and cost data to enable integrated project control.
pythonimport pandas as pd import numpy as np from typing import Dict, Any, List, Optional, Tuple from dataclasses import dataclass, field from datetime import date, timedelta from enum import Enum from collections import defaultdict class LoadingMethod(Enum): UNIFORM = "uniform" # Even distribution FRONT_LOADED = "front_loaded" BACK_LOADED = "back_loaded" BELL_CURVE = "bell_curve" @dataclass class ScheduleActivity: activity_id: str name: str start_date: date finish_date: date duration: int percent_complete: float = 0 @dataclass class CostItem: cost_code: str description: str budgeted_cost: float labor_cost: float material_cost: float equipment_cost: float @dataclass class ActivityCostLink: activity_id: str cost_code: str budgeted_cost: float loading_method: LoadingMethod @dataclass class EarnedValueMetrics: data_date: date bcws: float # Budgeted Cost of Work Scheduled (PV) bcwp: float # Budgeted Cost of Work Performed (EV) acwp: float # Actual Cost of Work Performed (AC) sv: float # Schedule Variance cv: float # Cost Variance spi: float # Schedule Performance Index cpi: float # Cost Performance Index eac: float # Estimate at Completion etc: float # Estimate to Complete vac: float # Variance at Completion class ScheduleCostLinker: """Link schedule activities to cost items.""" def __init__(self, project_name: str, budget_at_completion: float): self.project_name = project_name self.bac = budget_at_completion self.activities: Dict[str, ScheduleActivity] = {} self.cost_items: Dict[str, CostItem] = {} self.links: List[ActivityCostLink] = [] self.actual_costs: Dict[str, float] = {} # activity_id -> actual cost def add_activity(self, activity_id: str, name: str, start_date: date, finish_date: date, percent_complete: float = 0): """Add schedule activity.""" duration = (finish_date - start_date).days + 1 self.activities[activity_id] = ScheduleActivity( activity_id=activity_id, name=name, start_date=start_date, finish_date=finish_date, duration=duration, percent_complete=percent_complete ) def add_cost_item(self, cost_code: str, description: str, budgeted_cost: float, labor_pct: float = 0.4, material_pct: float = 0.5, equipment_pct: float = 0.1): """Add cost item.""" self.cost_items[cost_code] = CostItem( cost_code=cost_code, description=description, budgeted_cost=budgeted_cost, labor_cost=budgeted_cost * labor_pct, material_cost=budgeted_cost * material_pct, equipment_cost=budgeted_cost * equipment_pct ) def link_activity_cost(self, activity_id: str, cost_code: str, loading_method: LoadingMethod = LoadingMethod.UNIFORM): """Link activity to cost item.""" if activity_id not in self.activities: return cost_item = self.cost_items.get(cost_code) budgeted = cost_item.budgeted_cost if cost_item else 0 self.links.append(ActivityCostLink( activity_id=activity_id, cost_code=cost_code, budgeted_cost=budgeted, loading_method=loading_method )) def record_actual_cost(self, activity_id: str, actual_cost: float): """Record actual cost for activity.""" self.actual_costs[activity_id] = actual_cost def _distribute_cost(self, cost: float, start_date: date, duration: int, method: LoadingMethod) -> Dict[date, float]: """Distribute cost over activity duration.""" daily_costs = {} if duration <= 0: return {start_date: cost} if method == LoadingMethod.UNIFORM: daily = cost / duration for i in range(duration): daily_costs[start_date + timedelta(days=i)] = daily elif method == LoadingMethod.FRONT_LOADED: total_weight = sum(range(duration, 0, -1)) for i in range(duration): weight = (duration - i) / total_weight daily_costs[start_date + timedelta(days=i)] = cost * weight elif method == LoadingMethod.BACK_LOADED: total_weight = sum(range(1, duration + 1)) for i in range(duration): weight = (i + 1) / total_weight daily_costs[start_date + timedelta(days=i)] = cost * weight elif method == LoadingMethod.BELL_CURVE: # Simplified bell curve mid = duration / 2 for i in range(duration): distance = abs(i - mid) weight = 1 - (distance / mid) * 0.5 daily_costs[start_date + timedelta(days=i)] = cost * weight / duration return daily_costs def generate_cost_loaded_schedule(self) -> pd.DataFrame: """Generate cost-loaded schedule.""" data = [] for link in self.links: activity = self.activities.get(link.activity_id) cost_item = self.cost_items.get(link.cost_code) if activity and cost_item: data.append({ 'Activity ID': activity.activity_id, 'Activity Name': activity.name, 'Cost Code': link.cost_code, 'Description': cost_item.description, 'Start': activity.start_date, 'Finish': activity.finish_date, 'Duration': activity.duration, 'Budget': link.budgeted_cost, '% Complete': activity.percent_complete, 'Earned Value': link.budgeted_cost * activity.percent_complete / 100, 'Loading': link.loading_method.value }) return pd.DataFrame(data) def generate_cash_flow(self, project_start: date = None, project_end: date = None) -> pd.DataFrame: """Generate cash flow curve.""" if not self.links: return pd.DataFrame() # Get date range if project_start is None: project_start = min(self.activities[l.activity_id].start_date for l in self.links) if project_end is None: project_end = max(self.activities[l.activity_id].finish_date for l in self.links) # Aggregate daily costs daily_totals = defaultdict(float) for link in self.links: activity = self.activities.get(link.activity_id) if not activity: continue daily_costs = self._distribute_cost( link.budgeted_cost, activity.start_date, activity.duration, link.loading_method ) for day, cost in daily_costs.items(): daily_totals[day] += cost # Build cash flow data data = [] cumulative = 0 current = project_start while current <= project_end: daily = daily_totals.get(current, 0) cumulative += daily data.append({ 'Date': current, 'Daily': round(daily, 2), 'Cumulative': round(cumulative, 2), 'Cumulative %': round(cumulative / self.bac * 100, 1) if self.bac > 0 else 0 }) current += timedelta(days=1) return pd.DataFrame(data) def calculate_earned_value(self, data_date: date) -> EarnedValueMetrics: """Calculate earned value metrics at data date.""" # BCWS - Planned Value through data date bcws = 0 for link in self.links: activity = self.activities.get(link.activity_id) if not activity: continue daily_costs = self._distribute_cost( link.budgeted_cost, activity.start_date, activity.duration, link.loading_method ) for day, cost in daily_costs.items(): if day <= data_date: bcws += cost # BCWP - Earned Value (budget * % complete) bcwp = 0 for link in self.links: activity = self.activities.get(link.activity_id) if activity: bcwp += link.budgeted_cost * activity.percent_complete / 100 # ACWP - Actual Cost acwp = sum(self.actual_costs.values()) # Variances sv = bcwp - bcws cv = bcwp - acwp # Indices spi = bcwp / bcws if bcws > 0 else 0 cpi = bcwp / acwp if acwp > 0 else 0 # Forecasts eac = self.bac / cpi if cpi > 0 else self.bac etc = eac - acwp vac = self.bac - eac return EarnedValueMetrics( data_date=data_date, bcws=round(bcws, 2), bcwp=round(bcwp, 2), acwp=round(acwp, 2), sv=round(sv, 2), cv=round(cv, 2), spi=round(spi, 2), cpi=round(cpi, 2), eac=round(eac, 2), etc=round(etc, 2), vac=round(vac, 2) ) def get_monthly_cash_flow(self) -> pd.DataFrame: """Aggregate cash flow by month.""" daily = self.generate_cash_flow() if daily.empty: return pd.DataFrame() daily['Month'] = pd.to_datetime(daily['Date']).dt.to_period('M') monthly = daily.groupby('Month').agg({ 'Daily': 'sum', 'Cumulative': 'last' }).reset_index() monthly.columns = ['Month', 'Monthly Cost', 'Cumulative'] return monthly def export_to_excel(self, output_path: str) -> str: """Export integrated data to Excel.""" with pd.ExcelWriter(output_path, engine='openpyxl') as writer: # Cost-loaded schedule schedule = self.generate_cost_loaded_schedule() schedule.to_excel(writer, sheet_name='Cost-Loaded Schedule', index=False) # Cash flow cash_flow = self.generate_cash_flow() if not cash_flow.empty: cash_flow.to_excel(writer, sheet_name='Cash Flow', index=False) # Monthly monthly = self.get_monthly_cash_flow() if not monthly.empty: monthly.to_excel(writer, sheet_name='Monthly', index=False) # Earned Value evm = self.calculate_earned_value(date.today()) evm_df = pd.DataFrame([{ 'Data Date': evm.data_date, 'BCWS (PV)': evm.bcws, 'BCWP (EV)': evm.bcwp, 'ACWP (AC)': evm.acwp, 'SV': evm.sv, 'CV': evm.cv, 'SPI': evm.spi, 'CPI': evm.cpi, 'EAC': evm.eac, 'ETC': evm.etc, 'VAC': evm.vac }]) evm_df.to_excel(writer, sheet_name='Earned Value', index=False) return output_path
pythonfrom datetime import date, timedelta # Initialize linker linker = ScheduleCostLinker("Office Building", budget_at_completion=5000000) # Add activities linker.add_activity("A-001", "Foundation", date(2024, 6, 1), date(2024, 6, 30), percent_complete=100) linker.add_activity("A-002", "Structure", date(2024, 7, 1), date(2024, 9, 30), percent_complete=60) linker.add_activity("A-003", "MEP", date(2024, 8, 1), date(2024, 11, 30), percent_complete=30) # Add cost items linker.add_cost_item("01-FOUND", "Foundation Work", 500000) linker.add_cost_item("02-STRUCT", "Structural Work", 2000000) linker.add_cost_item("03-MEP", "MEP Systems", 1500000) # Link linker.link_activity_cost("A-001", "01-FOUND") linker.link_activity_cost("A-002", "02-STRUCT", LoadingMethod.BELL_CURVE) linker.link_activity_cost("A-003", "03-MEP", LoadingMethod.BACK_LOADED) # Record actuals linker.record_actual_cost("A-001", 520000) linker.record_actual_cost("A-002", 1300000)
pythonevm = linker.calculate_earned_value(date.today()) print(f"CPI: {evm.cpi}") print(f"SPI: {evm.spi}") print(f"EAC: ${evm.eac:,.2f}")
pythoncash_flow = linker.generate_cash_flow() print(cash_flow.tail(10))
pythonmonthly = linker.get_monthly_cash_flow() print(monthly)
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 18,383 | 28,574 | +55% | 1 | 1 | 0% | 3,986 | 9,455 | +137% | 0 | 0 | — |
case-02 | fail→fail | 29,606 | 27,976 | -6% | 1 | 1 | 0% | 6,217 | 9,955 | +60% | 0 | 0 | — |
case-03 | pass→pass | 8,562 | 7,187 | -16% | 1 | 1 | 0% | 1,808 | 5,200 | +188% | 0 | 0 | — |
case-04 | pass→pass | 6,777 | 5,006 | -26% | 1 | 1 | 0% | 1,513 | 4,888 | +223% | 0 | 0 | — |
case-05 | pass→pass | 7,501 | 5,407 | -28% | 1 | 1 | 0% | 1,539 | 4,898 | +218% | 0 | 0 | — |
case-06 | fail→pass | 12,246 | 3,406 | -72% | 1 | 1 | 0% | 1,946 | 4,488 | +131% | 0 | 0 | — |
case-07 | pass→pass | 7,224 | 8,327 | +15% | 1 | 1 | 0% | 1,329 | 5,419 | +308% | 0 | 0 | — |
case-08 | fail→fail | 13,410 | 16,622 | +24% | 1 | 1 | 0% | 2,387 | 6,789 | +184% | 0 | 0 | — |
case-09 | fail→fail | 10,171 | 11,101 | +9% | 1 | 1 | 0% | 1,702 | 5,511 | +224% | 0 | 0 | — |
case-18 | pass→pass | 10,556 | 9,403 | -11% | 1 | 1 | 0% | 2,067 | 5,741 | +178% | 0 | 0 | — |
case-10 | pass→pass | 4,785 | 3,875 | -19% | 1 | 1 | 0% | 956 | 4,511 | +372% | 0 | 0 | — |
case-11 | pass→pass | 5,649 | 4,882 | -14% | 1 | 1 | 0% | 1,091 | 4,769 | +337% | 0 | 0 | — |
case-12 | pass→pass | 2,790 | 2,815 | +1% | 1 | 1 | 0% | 534 | 4,215 | +689% | 0 | 0 | — |
case-13 | pass→pass | 12,466 | 6,883 | -45% | 1 | 1 | 0% | 2,264 | 4,945 | +118% | 0 | 0 | — |
case-14 | pass→pass | 8,059 | 3,581 | -56% | 1 | 1 | 0% | 1,460 | 4,410 | +202% | 0 | 0 | — |
case-15 | fail→pass | 9,221 | 3,918 | -58% | 1 | 1 | 0% | 1,520 | 4,535 | +198% | 0 | 0 | — |
case-16 | fail→pass | 12,093 | 6,427 | -47% | 1 | 1 | 0% | 2,014 | 4,962 | +146% | 0 | 0 | — |
case-17 | pass→pass | 3,904 | 2,807 | -28% | 1 | 1 | 0% | 779 | 4,196 | +439% | 0 | 0 | — |
case-19 | pass→pass | 10,249 | 6,627 | -35% | 1 | 1 | 0% | 1,789 | 4,804 | +169% | 0 | 0 | — |
case-20 | fail→pass | 14,981 | 4,433 | -70% | 1 | 1 | 0% | 2,485 | 4,530 | +82% | 0 | 0 | — |
case-21 | pass→pass | 17,243 | 25,454 | +48% | 1 | 1 | 0% | 3,074 | 8,700 | +183% | 0 | 0 | — |
case-22 | fail→pass | 11,071 | 9,629 | -13% | 1 | 1 | 0% | 2,050 | 5,587 | +173% | 0 | 0 | — |
case-23 | pass→pass | 17,293 | 28,519 | +65% | 1 | 1 | 0% | 2,779 | 9,644 | +247% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +22 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.