Track and manage construction lien waivers. Monitor conditional and unconditional waivers, ensure compliance before payments, and prevent lien exposure.
npx skills add https://github.com/datadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction --skill lien-waiver-tracker
Track and manage lien waivers throughout the construction payment process. Ensure proper waivers are received before releasing payments, monitor waiver status by subcontractor, and minimize lien exposure.
┌─────────────────────────────────────────────────────────────────┐
│ LIEN WAIVER TYPES │
├─────────────────────────────────────────────────────────────────┤
│ │
│ CONDITIONAL UNCONDITIONAL │
│ ─────────── ───────────── │
│ 📋 Progress - Conditional ✅ Progress - Unconditional │
│ Effective when paid Immediately effective │
│ Use with payment Use after check clears │
│ │
│ 📋 Final - Conditional ✅ Final - Unconditional │
│ For final payment For final payment │
│ Upon receipt of funds After funds received │
│ │
└─────────────────────────────────────────────────────────────────┘
from dataclasses import dataclass, field
from typing import List, Dict, Optional
from datetime import datetime, timedelta
from enum import Enum
class WaiverType(Enum):
CONDITIONAL_PROGRESS = "conditional_progress"
UNCONDITIONAL_PROGRESS = "unconditional_progress"
CONDITIONAL_FINAL = "conditional_final"
UNCONDITIONAL_FINAL = "unconditional_final"
class WaiverStatus(Enum):
REQUESTED = "requested"
RECEIVED = "received"
VERIFIED = "verified"
REJECTED = "rejected"
MISSING = "missing"
class PaymentStatus(Enum):
PENDING = "pending"
APPROVED = "approved"
HELD = "held"
RELEASED = "released"
@dataclass
class Subcontractor:
id: str
name: str
trade: str
contract_amount: float
contact_name: str
contact_email: str
tier: int = 1 # 1 = direct, 2 = sub-sub
@dataclass
class LienWaiver:
id: str
subcontractor_id: str
waiver_type: WaiverType
payment_application: int # Pay app number
through_date: datetime
amount: float
status: WaiverStatus = WaiverStatus.REQUESTED
requested_date: datetime = field(default_factory=datetime.now)
received_date: Optional[datetime] = None
verified_by: str = ""
file_path: str = ""
notes: str = ""
@dataclass
class PaymentApplication:
number: int
period_end: datetime
subcontractor_id: str
amount_requested: float
amount_approved: float
retainage: float
status: PaymentStatus = PaymentStatus.PENDING
waivers_complete: bool = False
payment_date: Optional[datetime] = None
@dataclass
class LienExposure:
subcontractor_id: str
subcontractor_name: str
total_paid: float
unconditional_waivers: float
conditional_pending: float
exposure: float
class LienWaiverTracker:
"""Track and manage construction lien waivers."""
def __init__(self, project_id: str, project_name: str):
self.project_id = project_id
self.project_name = project_name
self.subcontractors: Dict[str, Subcontractor] = {}
self.waivers: Dict[str, LienWaiver] = {}
self.pay_apps: Dict[str, PaymentApplication] = {}
def add_subcontractor(self, id: str, name: str, trade: str,
contract_amount: float, contact_name: str,
contact_email: str, tier: int = 1) -> Subcontractor:
"""Add subcontractor to tracking."""
sub = Subcontractor(
id=id,
name=name,
trade=trade,
contract_amount=contract_amount,
contact_name=contact_name,
contact_email=contact_email,
tier=tier
)
self.subcontractors[id] = sub
return sub
def create_payment_application(self, number: int, period_end: datetime,
subcontractor_id: str, amount_requested: float,
retainage_rate: float = 0.10) -> PaymentApplication:
"""Create payment application record."""
if subcontractor_id not in self.subcontractors:
raise ValueError(f"Subcontractor {subcontractor_id} not found")
retainage = amount_requested * retainage_rate
amount_approved = amount_requested - retainage
pay_app = PaymentApplication(
number=number,
period_end=period_end,
subcontractor_id=subcontractor_id,
amount_requested=amount_requested,
amount_approved=amount_approved,
retainage=retainage
)
key = f"{subcontractor_id}-{number}"
self.pay_apps[key] = pay_app
# Create waiver request
self.request_waiver(subcontractor_id, number, period_end, amount_approved)
return pay_app
def request_waiver(self, subcontractor_id: str, pay_app_number: int,
through_date: datetime, amount: float,
waiver_type: WaiverType = WaiverType.CONDITIONAL_PROGRESS) -> LienWaiver:
"""Request lien waiver from subcontractor."""
waiver_id = f"LW-{subcontractor_id}-{pay_app_number}"
waiver = LienWaiver(
id=waiver_id,
subcontractor_id=subcontractor_id,
waiver_type=waiver_type,
payment_application=pay_app_number,
through_date=through_date,
amount=amount
)
self.waivers[waiver_id] = waiver
return waiver
def receive_waiver(self, waiver_id: str, file_path: str,
verified_by: str = "") -> LienWaiver:
"""Record receipt of lien waiver."""
if waiver_id not in self.waivers:
raise ValueError(f"Waiver {waiver_id} not found")
waiver = self.waivers[waiver_id]
waiver.status = WaiverStatus.RECEIVED
waiver.received_date = datetime.now()
waiver.file_path = file_path
waiver.verified_by = verified_by
# Check if all waivers for pay app complete
self._check_pay_app_waivers(waiver.subcontractor_id, waiver.payment_application)
return waiver
def verify_waiver(self, waiver_id: str, verified_by: str) -> LienWaiver:
"""Verify waiver details are correct."""
if waiver_id not in self.waivers:
raise ValueError(f"Waiver {waiver_id} not found")
waiver = self.waivers[waiver_id]
waiver.status = WaiverStatus.VERIFIED
waiver.verified_by = verified_by
return waiver
def reject_waiver(self, waiver_id: str, reason: str) -> LienWaiver:
"""Reject waiver (incorrect amount, wrong form, etc.)."""
if waiver_id not in self.waivers:
raise ValueError(f"Waiver {waiver_id} not found")
waiver = self.waivers[waiver_id]
waiver.status = WaiverStatus.REJECTED
waiver.notes = f"Rejected: {reason}"
return waiver
def convert_to_unconditional(self, waiver_id: str, payment_date: datetime) -> LienWaiver:
"""Convert conditional waiver to unconditional after payment clears."""
if waiver_id not in self.waivers:
raise ValueError(f"Waiver {waiver_id} not found")
waiver = self.waivers[waiver_id]
# Create new unconditional waiver
new_type = (WaiverType.UNCONDITIONAL_PROGRESS
if waiver.waiver_type == WaiverType.CONDITIONAL_PROGRESS
else WaiverType.UNCONDITIONAL_FINAL)
return self.request_waiver(
waiver.subcontractor_id,
waiver.payment_application,
waiver.through_date,
waiver.amount,
new_type
)
def _check_pay_app_waivers(self, subcontractor_id: str, pay_app_number: int):
"""Check if all waivers for pay app are received."""
key = f"{subcontractor_id}-{pay_app_number}"
if key not in self.pay_apps:
return
pay_app = self.pay_apps[key]
# Check all related waivers
related_waivers = [
w for w in self.waivers.values()
if w.subcontractor_id == subcontractor_id
and w.payment_application == pay_app_number
]
pay_app.waivers_complete = all(
w.status in [WaiverStatus.RECEIVED, WaiverStatus.VERIFIED]
for w in related_waivers
)
def calculate_exposure(self, subcontractor_id: str) -> LienExposure:
"""Calculate lien exposure for subcontractor."""
if subcontractor_id not in self.subcontractors:
raise ValueError(f"Subcontractor {subcontractor_id} not found")
sub = self.subcontractors[subcontractor_id]
# Sum payments
total_paid = sum(
pa.amount_approved for pa in self.pay_apps.values()
if pa.subcontractor_id == subcontractor_id
and pa.status == PaymentStatus.RELEASED
)
# Sum unconditional waivers
unconditional = sum(
w.amount for w in self.waivers.values()
if w.subcontractor_id == subcontractor_id
and w.waiver_type in [WaiverType.UNCONDITIONAL_PROGRESS, WaiverType.UNCONDITIONAL_FINAL]
and w.status in [WaiverStatus.RECEIVED, WaiverStatus.VERIFIED]
)
# Sum conditional pending
conditional = sum(
w.amount for w in self.waivers.values()
if w.subcontractor_id == subcontractor_id
and w.waiver_type in [WaiverType.CONDITIONAL_PROGRESS, WaiverType.CONDITIONAL_FINAL]
and w.status in [WaiverStatus.RECEIVED, WaiverStatus.VERIFIED]
)
# Exposure = Paid - Unconditional waivers
exposure = total_paid - unconditional
return LienExposure(
subcontractor_id=subcontractor_id,
subcontractor_name=sub.name,
total_paid=total_paid,
unconditional_waivers=unconditional,
conditional_pending=conditional,
exposure=exposure
)
def get_missing_waivers(self) -> List[Dict]:
"""Get list of missing or pending waivers."""
missing = []
for waiver in self.waivers.values():
if waiver.status in [WaiverStatus.REQUESTED, WaiverStatus.MISSING]:
sub = self.subcontractors.get(waiver.subcontractor_id)
missing.append({
"waiver_id": waiver.id,
"subcontractor": sub.name if sub else waiver.subcontractor_id,
"pay_app": waiver.payment_application,
"amount": waiver.amount,
"type": waiver.waiver_type.value,
"requested_date": waiver.requested_date,
"days_outstanding": (datetime.now() - waiver.requested_date).days
})
return sorted(missing, key=lambda x: -x["days_outstanding"])
def get_waiver_status_by_sub(self, subcontractor_id: str) -> Dict:
"""Get waiver status summary for subcontractor."""
if subcontractor_id not in self.subcontractors:
raise ValueError(f"Subcontractor {subcontractor_id} not found")
sub = self.subcontractors[subcontractor_id]
waivers = [w for w in self.waivers.values() if w.subcontractor_id == subcontractor_id]
by_status = {}
for w in waivers:
s = w.status.value
by_status[s] = by_status.get(s, 0) + 1
return {
"subcontractor": sub.name,
"total_waivers": len(waivers),
"by_status": by_status,
"total_amount": sum(w.amount for w in waivers),
"verified_amount": sum(w.amount for w in waivers if w.status == WaiverStatus.VERIFIED)
}
def can_release_payment(self, subcontractor_id: str, pay_app_number: int) -> Dict:
"""Check if payment can be released."""
key = f"{subcontractor_id}-{pay_app_number}"
if key not in self.pay_apps:
return {"can_release": False, "reason": "Payment application not found"}
pay_app = self.pay_apps[key]
# Check for conditional waiver
waivers = [
w for w in self.waivers.values()
if w.subcontractor_id == subcontractor_id
and w.payment_application == pay_app_number
and w.waiver_type == WaiverType.CONDITIONAL_PROGRESS
]
if not waivers:
return {"can_release": False, "reason": "No conditional waiver received"}
for w in waivers:
if w.status not in [WaiverStatus.RECEIVED, WaiverStatus.VERIFIED]:
return {"can_release": False, "reason": f"Waiver {w.id} not verified"}
return {"can_release": True, "reason": "All waivers verified", "amount": pay_app.amount_approved}
def generate_report(self) -> str:
"""Generate lien waiver status report."""
lines = [
"# Lien Waiver Status Report",
"",
f"**Project:** {self.project_name}",
f"**Date:** {datetime.now().strftime('%Y-%m-%d')}",
"",
"## Summary",
"",
f"- Total Subcontractors: {len(self.subcontractors)}",
f"- Total Waivers Tracked: {len(self.waivers)}",
f"- Missing Waivers: {len(self.get_missing_waivers())}",
"",
"## Exposure by Subcontractor",
"",
"| Subcontractor | Paid | Unconditional | Exposure |",
"|---------------|------|---------------|----------|"
]
total_exposure = 0
for sub_id in self.subcontractors:
exposure = self.calculate_exposure(sub_id)
total_exposure += exposure.exposure
lines.append(
f"| {exposure.subcontractor_name} | ${exposure.total_paid:,.0f} | "
f"${exposure.unconditional_waivers:,.0f} | ${exposure.exposure:,.0f} |"
)
lines.extend([
f"| **TOTAL** | | | **${total_exposure:,.0f}** |",
"",
"## Missing Waivers",
""
])
missing = self.get_missing_waivers()
if missing:
lines.append("| Subcontractor | Pay App | Amount | Days Outstanding |")
lines.append("|---------------|---------|--------|------------------|")
for m in missing[:10]:
lines.append(
f"| {m['subcontractor']} | #{m['pay_app']} | "
f"${m['amount']:,.0f} | {m['days_outstanding']} |"
)
else:
lines.append("*No missing waivers*")
return "\n".join(lines)
# Initialize tracker
tracker = LienWaiverTracker("PRJ-001", "Office Tower")
# Add subcontractors
tracker.add_subcontractor(
"SUB-001", "ABC Mechanical", "HVAC",
contract_amount=500000,
contact_name="John Smith",
contact_email="[email protected]"
)
tracker.add_subcontractor(
"SUB-002", "XYZ Electric", "Electrical",
contract_amount=350000,
contact_name="Jane Doe",
contact_email="[email protected]"
)
# Create payment applications
pa1 = tracker.create_payment_application(
number=1,
period_end=datetime.now(),
subcontractor_id="SUB-001",
amount_requested=50000
)
# Receive waiver
waiver_id = f"LW-SUB-001-1"
tracker.receive_waiver(waiver_id, "/waivers/sub001_pa1.pdf", "PM")
# Check if can release payment
result = tracker.can_release_payment("SUB-001", 1)
print(f"Can release: {result['can_release']} - {result['reason']}")
# Calculate exposure
exposure = tracker.calculate_exposure("SUB-001")
print(f"Lien exposure: ${exposure.exposure:,.2f}")
# Generate report
print(tracker.generate_report())
pip install (no external dependencies)
Automatically organizes invoices and receipts for tax preparation by reading messy files, extracting key information, renaming them consistently, and sorting them into logical folders. Turns hours of manual bookkeeping into minutes of automated organization.
Expert guidance for systematic backtesting of trading strategies. Use when developing, testing, stress-testing, or validating quantitative trading strategies. Covers "beating ideas to death" methodology, parameter robustness testing, slippage modeling, bias prevention, and interpreting backtest results. Applicable when user asks about backtesting, strategy validation, robustness testing, avoiding overfitting, or systematic trading development.
This skill calculates key financial ratios and metrics from financial statement data for investment analysis
This skill provides an advanced financial modeling suite with DCF analysis, sensitivity testing, Monte Carlo simulations, and scenario planning for investment decisions
This skill retrieves upcoming earnings announcements for US stocks using the Financial Modeling Prep (FMP) API. Use this when the user requests earnings calendar data, wants to know which companies are reporting earnings in the upcoming week, or needs a weekly earnings review. The skill focuses on mid-cap and above companies (over $2B market cap) that have significant market impact, organizing the data by date and timing in a clean markdown table format. Supports multiple environments (CLI, Desktop, Web) with flexible API key management.
Crypto wallet operations via the awal CLI — sign in, check balances, send USDC/ETH/POL/SOL, trade tokens, fund the wallet, and use the x402 payment protocol to discover paid services, pay for API calls, monetize an API, or query onchain data. Use whenever the user mentions signing in, login, authentication, wallet status, balance, address, sending money, paying someone, transferring tokens, ENS names, swapping/trading/converting tokens, funding/topping up/onramp, USDC, ETH, POL, SOL, the x402 bazaar, paid APIs, monetizing an endpoint, or querying onchain data on Base.
Access real-time and historical stock market data, forex rates, cryptocurrency prices, commodities, economic indicators, and 50+ technical indicators via the Alpha Vantage API. Use when fetching stock prices (OHLCV), company fundamentals (income statement, balance sheet, cash flow), earnings, options data, market news/sentiment, insider transactions, GDP, CPI, treasury yields, gold/silver/oil prices, Bitcoin/crypto prices, forex exchange rates, or calculating technical indicators (SMA, EMA, MACD, RSI, Bollinger Bands). Requires a free API key from alphavantage.co.
Braintree Automation: manage payment processing via Stripe-compatible tools for customers, subscriptions, payment methods, and transactions
Take datadrivenconstruction/lien-waiver-tracker from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference pip.
Without those the skill loads but fails at the first command.