Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Process construction payment applications. Validate schedule of values, calculate retainage, track billing status, and generate G702/G703 forms.
.claude/skills/datadrivenconstruction-payment-application-processor/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-11 | ✗→✓ | ▲ Improved | 256% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 270% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 212% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 278% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 213% | 0% |
Process construction payment applications from creation through approval. Validate against schedule of values, calculate retainage, generate AIA G702/G703 forms, and track payment status.
┌─────────────────────────────────────────────────────────────────┐
│ PAYMENT APPLICATION FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Schedule of → Draft → Review → Approve │
│ Values Pay App & Verify & Pay │
│ ────────── ─────── ──────── ──────── │
│ 📋 SOV items 📝 Enter % ✓ Verify ✅ Approve │
│ 💰 Line values 📊 Calculate 📸 Field 💵 Release │
│ 📅 Billing plan 📄 G702/703 📧 Submit 📄 Record │
│ │
└─────────────────────────────────────────────────────────────────┘pythonfrom dataclasses import dataclass, field from typing import List, Dict, Optional from datetime import datetime, timedelta from enum import Enum class PayAppStatus(Enum): DRAFT = "draft" SUBMITTED = "submitted" UNDER_REVIEW = "under_review" APPROVED = "approved" REJECTED = "rejected" PAID = "paid" PARTIAL_PAID = "partial_paid" class LineItemStatus(Enum): NOT_STARTED = "not_started" IN_PROGRESS = "in_progress" COMPLETE = "complete" @dataclass class SOVLineItem: number: str description: str scheduled_value: float previous_completed: float = 0.0 current_completed: float = 0.0 materials_stored: float = 0.0 percent_complete: float = 0.0 balance_to_finish: float = 0.0 retainage: float = 0.0 def calculate(self, retainage_rate: float = 0.10): """Calculate line item values.""" self.percent_complete = ( (self.previous_completed + self.current_completed + self.materials_stored) / self.scheduled_value * 100 if self.scheduled_value > 0 else 0 ) self.balance_to_finish = ( self.scheduled_value - self.previous_completed - self.current_completed - self.materials_stored ) total_completed = self.previous_completed + self.current_completed + self.materials_stored self.retainage = total_completed * retainage_rate @dataclass class PaymentApplication: application_number: int project_name: str contractor: str period_from: datetime period_to: datetime # Contract info original_contract: float change_orders: float = 0.0 contract_sum: float = 0.0 # Line items line_items: List[SOVLineItem] = field(default_factory=list) # Totals total_scheduled: float = 0.0 previous_completed: float = 0.0 current_completed: float = 0.0 materials_stored: float = 0.0 total_completed: float = 0.0 percent_complete: float = 0.0 balance_to_finish: float = 0.0 retainage: float = 0.0 net_amount_due: float = 0.0 # Status status: PayAppStatus = PayAppStatus.DRAFT submitted_date: Optional[datetime] = None approved_date: Optional[datetime] = None approved_amount: float = 0.0 paid_date: Optional[datetime] = None paid_amount: float = 0.0 # Retainage rates work_retainage_rate: float = 0.10 stored_retainage_rate: float = 0.10 @dataclass class G702Data: """AIA G702 Application and Certificate for Payment data.""" application_number: int period_to: datetime project_name: str owner: str contractor: str architect: str original_contract_sum: float net_change_orders: float contract_sum_to_date: float total_completed_stored: float retainage: float total_earned_less_retainage: float less_previous_certificates: float current_payment_due: float balance_to_finish: float class PaymentApplicationProcessor: """Process construction payment applications.""" def __init__(self, project_name: str, contractor: str, original_contract: float): self.project_name = project_name self.contractor = contractor self.original_contract = original_contract self.change_orders_total = 0.0 self.schedule_of_values: List[SOVLineItem] = [] self.pay_apps: Dict[int, PaymentApplication] = {} self.next_app_number = 1 self.work_retainage_rate = 0.10 self.stored_retainage_rate = 0.10 def set_retainage_rates(self, work_rate: float, stored_rate: float = None): """Set retainage rates.""" self.work_retainage_rate = work_rate self.stored_retainage_rate = stored_rate if stored_rate else work_rate def add_sov_item(self, number: str, description: str, scheduled_value: float) -> SOVLineItem: """Add line item to schedule of values.""" item = SOVLineItem( number=number, description=description, scheduled_value=scheduled_value ) self.schedule_of_values.append(item) return item def import_sov(self, items: List[Dict]) -> int: """Import schedule of values from list.""" count = 0 for item in items: self.add_sov_item( item['number'], item['description'], item['value'] ) count += 1 return count def add_change_order(self, amount: float, description: str = ""): """Add approved change order to contract.""" self.change_orders_total += amount # Add as new SOV item co_number = f"CO-{len([i for i in self.schedule_of_values if 'CO-' in i.number])+1:03d}" self.add_sov_item(co_number, description or f"Change Order", amount) def create_pay_app(self, period_from: datetime, period_to: datetime) -> PaymentApplication: """Create new payment application.""" app_number = self.next_app_number # Copy SOV with previous values line_items = [] for sov_item in self.schedule_of_values: # Get previous completed from last pay app prev_completed = 0.0 if app_number > 1: prev_app = self.pay_apps.get(app_number - 1) if prev_app: prev_item = next( (i for i in prev_app.line_items if i.number == sov_item.number), None ) if prev_item: prev_completed = (prev_item.previous_completed + prev_item.current_completed + prev_item.materials_stored) line_items.append(SOVLineItem( number=sov_item.number, description=sov_item.description, scheduled_value=sov_item.scheduled_value, previous_completed=prev_completed )) pay_app = PaymentApplication( application_number=app_number, project_name=self.project_name, contractor=self.contractor, period_from=period_from, period_to=period_to, original_contract=self.original_contract, change_orders=self.change_orders_total, contract_sum=self.original_contract + self.change_orders_total, line_items=line_items, work_retainage_rate=self.work_retainage_rate, stored_retainage_rate=self.stored_retainage_rate ) self.pay_apps[app_number] = pay_app self.next_app_number += 1 return pay_app def update_line_item(self, app_number: int, line_number: str, current_completed: float = None, materials_stored: float = None) -> SOVLineItem: """Update line item progress.""" if app_number not in self.pay_apps: raise ValueError(f"Pay app {app_number} not found") pay_app = self.pay_apps[app_number] item = next((i for i in pay_app.line_items if i.number == line_number), None) if not item: raise ValueError(f"Line item {line_number} not found") if current_completed is not None: item.current_completed = current_completed if materials_stored is not None: item.materials_stored = materials_stored item.calculate(pay_app.work_retainage_rate) # Recalculate pay app totals self._calculate_totals(pay_app) return item def update_by_percentage(self, app_number: int, line_number: str, percent_complete: float) -> SOVLineItem: """Update line item by percentage complete.""" if app_number not in self.pay_apps: raise ValueError(f"Pay app {app_number} not found") pay_app = self.pay_apps[app_number] item = next((i for i in pay_app.line_items if i.number == line_number), None) if not item: raise ValueError(f"Line item {line_number} not found") # Calculate current completed from percentage total_earned = item.scheduled_value * (percent_complete / 100) item.current_completed = total_earned - item.previous_completed - item.materials_stored if item.current_completed < 0: item.current_completed = 0 item.calculate(pay_app.work_retainage_rate) self._calculate_totals(pay_app) return item def _calculate_totals(self, pay_app: PaymentApplication): """Calculate pay app totals.""" pay_app.total_scheduled = sum(i.scheduled_value for i in pay_app.line_items) pay_app.previous_completed = sum(i.previous_completed for i in pay_app.line_items) pay_app.current_completed = sum(i.current_completed for i in pay_app.line_items) pay_app.materials_stored = sum(i.materials_stored for i in pay_app.line_items) pay_app.total_completed = ( pay_app.previous_completed + pay_app.current_completed + pay_app.materials_stored ) pay_app.percent_complete = ( pay_app.total_completed / pay_app.total_scheduled * 100 if pay_app.total_scheduled > 0 else 0 ) pay_app.balance_to_finish = pay_app.contract_sum - pay_app.total_completed # Calculate retainage work_retainage = (pay_app.previous_completed + pay_app.current_completed) * pay_app.work_retainage_rate stored_retainage = pay_app.materials_stored * pay_app.stored_retainage_rate pay_app.retainage = work_retainage + stored_retainage # Previous certificates previous_certs = 0 if pay_app.application_number > 1: prev_app = self.pay_apps.get(pay_app.application_number - 1) if prev_app: previous_certs = prev_app.total_completed - prev_app.retainage # Net amount due pay_app.net_amount_due = ( pay_app.total_completed - pay_app.retainage - previous_certs ) def submit_pay_app(self, app_number: int) -> PaymentApplication: """Submit pay app for review.""" if app_number not in self.pay_apps: raise ValueError(f"Pay app {app_number} not found") pay_app = self.pay_apps[app_number] pay_app.status = PayAppStatus.SUBMITTED pay_app.submitted_date = datetime.now() return pay_app def approve_pay_app(self, app_number: int, approved_amount: float = None) -> PaymentApplication: """Approve pay app.""" if app_number not in self.pay_apps: raise ValueError(f"Pay app {app_number} not found") pay_app = self.pay_apps[app_number] pay_app.status = PayAppStatus.APPROVED pay_app.approved_date = datetime.now() pay_app.approved_amount = approved_amount if approved_amount else pay_app.net_amount_due return pay_app def record_payment(self, app_number: int, paid_amount: float, paid_date: datetime = None) -> PaymentApplication: """Record payment received.""" if app_number not in self.pay_apps: raise ValueError(f"Pay app {app_number} not found") pay_app = self.pay_apps[app_number] pay_app.paid_date = paid_date or datetime.now() pay_app.paid_amount = paid_amount if paid_amount >= pay_app.approved_amount: pay_app.status = PayAppStatus.PAID else: pay_app.status = PayAppStatus.PARTIAL_PAID return pay_app def generate_g702(self, app_number: int, owner: str, architect: str) -> G702Data: """Generate AIA G702 data.""" if app_number not in self.pay_apps: raise ValueError(f"Pay app {app_number} not found") pay_app = self.pay_apps[app_number] # Previous certificates previous_certs = 0 if app_number > 1: prev_app = self.pay_apps.get(app_number - 1) if prev_app: previous_certs = prev_app.total_completed - prev_app.retainage return G702Data( application_number=pay_app.application_number, period_to=pay_app.period_to, project_name=pay_app.project_name, owner=owner, contractor=pay_app.contractor, architect=architect, original_contract_sum=pay_app.original_contract, net_change_orders=pay_app.change_orders, contract_sum_to_date=pay_app.contract_sum, total_completed_stored=pay_app.total_completed, retainage=pay_app.retainage, total_earned_less_retainage=pay_app.total_completed - pay_app.retainage, less_previous_certificates=previous_certs, current_payment_due=pay_app.net_amount_due, balance_to_finish=pay_app.balance_to_finish ) def generate_g703(self, app_number: int) -> str: """Generate AIA G703 continuation sheet.""" if app_number not in self.pay_apps: return "Pay app not found" pay_app = self.pay_apps[app_number] lines = [ "# AIA G703 - Continuation Sheet", "", f"**Application Number:** {pay_app.application_number}", f"**Period To:** {pay_app.period_to.strftime('%Y-%m-%d')}", "", "| Item | Description | Scheduled | Previous | This Period | Stored | Total | % | Balance | Retainage |", "|------|-------------|-----------|----------|-------------|--------|-------|---|---------|-----------|" ] for item in pay_app.line_items: lines.append( f"| {item.number} | {item.description[:20]} | " f"${item.scheduled_value:,.0f} | ${item.previous_completed:,.0f} | " f"${item.current_completed:,.0f} | ${item.materials_stored:,.0f} | " f"${item.previous_completed + item.current_completed + item.materials_stored:,.0f} | " f"{item.percent_complete:.0f}% | ${item.balance_to_finish:,.0f} | " f"${item.retainage:,.0f} |" ) lines.extend([ "", f"| **TOTALS** | | ${pay_app.total_scheduled:,.0f} | " f"${pay_app.previous_completed:,.0f} | ${pay_app.current_completed:,.0f} | " f"${pay_app.materials_stored:,.0f} | ${pay_app.total_completed:,.0f} | " f"{pay_app.percent_complete:.0f}% | ${pay_app.balance_to_finish:,.0f} | " f"${pay_app.retainage:,.0f} |" ]) return "\n".join(lines) def get_billing_summary(self) -> Dict: """Get billing summary across all pay apps.""" total_billed = sum(pa.total_completed for pa in self.pay_apps.values()) total_paid = sum(pa.paid_amount for pa in self.pay_apps.values()) total_retainage = sum(pa.retainage for pa in self.pay_apps.values()) pending = [pa for pa in self.pay_apps.values() if pa.status in [PayAppStatus.SUBMITTED, PayAppStatus.UNDER_REVIEW, PayAppStatus.APPROVED]] return { "contract_sum": self.original_contract + self.change_orders_total, "total_billed": total_billed, "total_paid": total_paid, "total_retainage": total_retainage, "outstanding": total_billed - total_paid - total_retainage, "pending_apps": len(pending), "pending_amount": sum(pa.net_amount_due for pa in pending), "percent_billed": total_billed / (self.original_contract + self.change_orders_total) * 100 }
pythonfrom datetime import datetime # Initialize processor processor = PaymentApplicationProcessor( project_name="Office Tower", contractor="ABC Construction", original_contract=5000000 ) # Import schedule of values processor.import_sov([ {"number": "01", "description": "General Conditions", "value": 250000}, {"number": "02", "description": "Sitework", "value": 300000}, {"number": "03", "description": "Concrete", "value": 800000}, {"number": "04", "description": "Structural Steel", "value": 1200000}, {"number": "05", "description": "MEP", "value": 1500000}, {"number": "06", "description": "Finishes", "value": 950000}, ]) # Create pay application pay_app = processor.create_pay_app( period_from=datetime(2024, 1, 1), period_to=datetime(2024, 1, 31) ) # Update progress processor.update_by_percentage(pay_app.application_number, "01", 100) # GC 100% processor.update_by_percentage(pay_app.application_number, "02", 75) # Site 75% processor.update_by_percentage(pay_app.application_number, "03", 50) # Concrete 50% processor.update_line_item(pay_app.application_number, "04", current_completed=200000, materials_stored=300000) # Submit and approve processor.submit_pay_app(pay_app.application_number) processor.approve_pay_app(pay_app.application_number) # Generate G703 print(processor.generate_g703(pay_app.application_number)) # Get summary summary = processor.get_billing_summary() print(f"Percent Billed: {summary['percent_billed']:.1f}%")
bashpip install (no external dependencies)
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-05 | pass→pass | 8,658 | 6,749 | -22% | 1 | 1 | 0% | 1,771 | 6,694 | +278% | 0 | 0 | — |
case-01 | fail→fail | 12,795 | 19,101 | +49% | 1 | 1 | 0% | 2,343 | 7,927 | +238% | 0 | 0 | — |
case-02 | pass→pass | 19,628 | 29,852 | +52% | 1 | 1 | 0% | 3,658 | 11,436 | +213% | 0 | 0 | — |
case-03 | pass→pass | 14,896 | 26,754 | +80% | 1 | 1 | 0% | 2,594 | 11,086 | +327% | 0 | 0 | — |
case-04 | pass→pass | 11,071 | 7,693 | -31% | 1 | 1 | 0% | 2,556 | 7,015 | +174% | 0 | 0 | — |
case-06 | pass→pass | 8,223 | 5,240 | -36% | 1 | 1 | 0% | 1,603 | 6,258 | +290% | 0 | 0 | — |
case-07 | pass→pass | 7,574 | 6,823 | -10% | 1 | 1 | 0% | 1,633 | 6,856 | +320% | 0 | 0 | — |
case-08 | pass→pass | 10,898 | 6,954 | -36% | 1 | 1 | 0% | 2,443 | 6,804 | +179% | 0 | 0 | — |
case-09 | pass→pass | 7,636 | 4,749 | -38% | 1 | 1 | 0% | 1,725 | 6,326 | +267% | 0 | 0 | — |
case-10 | pass→pass | 15,101 | 18,164 | +20% | 1 | 1 | 0% | 3,018 | 8,931 | +196% | 0 | 0 | — |
case-11 | fail→pass | 7,992 | 12,212 | +53% | 1 | 1 | 0% | 1,733 | 6,162 | +256% | 0 | 0 | — |
case-12 | pass→pass | 7,045 | 4,157 | -41% | 1 | 1 | 0% | 1,407 | 6,074 | +332% | 0 | 0 | — |
case-13 | pass→pass | 6,598 | 6,635 | +1% | 1 | 1 | 0% | 1,469 | 6,784 | +362% | 0 | 0 | — |
case-14 | pass→pass | 18,568 | 12,956 | -30% | 1 | 1 | 0% | 4,157 | 8,004 | +93% | 0 | 0 | — |
case-15 | pass→pass | 9,144 | 3,708 | -59% | 1 | 1 | 0% | 1,844 | 5,992 | +225% | 0 | 0 | — |
case-16 | pass→pass | 7,066 | 6,481 | -8% | 1 | 1 | 0% | 1,527 | 6,712 | +340% | 0 | 0 | — |
case-17 | fail→pass | 7,913 | 4,863 | -39% | 1 | 1 | 0% | 1,678 | 6,211 | +270% | 0 | 0 | — |
case-18 | fail→pass | 10,491 | 6,705 | -36% | 1 | 1 | 0% | 2,113 | 6,594 | +212% | 0 | 0 | — |
case-19 | pass→pass | 3,639 | 3,565 | -2% | 1 | 1 | 0% | 746 | 6,037 | +709% | 0 | 0 | — |
case-20 | pass→pass | 8,028 | 6,721 | -16% | 1 | 1 | 0% | 1,681 | 6,696 | +298% | 0 | 0 | — |
case-21 | pass→pass | 11,425 | 6,777 | -41% | 1 | 1 | 0% | 2,171 | 6,602 | +204% | 0 | 0 | — |
case-22 | pass→pass | 9,118 | 7,365 | -19% | 1 | 1 | 0% | 1,980 | 7,003 | +254% | 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. 22 cases were attempted. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 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.