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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "lien-waiver-tracker" with this command: npx skills add datadrivenconstruction/ddc_skills_for_ai_agents_in_construction/datadrivenconstruction-ddc-skills-for-ai-agents-in-construction-lien-waiver-tracker

Lien Waiver Tracker

Overview

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

┌─────────────────────────────────────────────────────────────────┐ │ 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 │ │ │ └─────────────────────────────────────────────────────────────────┘

Technical Implementation

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)

Quick Start

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="john@abcmech.com" )

tracker.add_subcontractor( "SUB-002", "XYZ Electric", "Electrical", contract_amount=350000, contact_name="Jane Doe", contact_email="jane@xyzelectric.com" )

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())

Requirements

pip install (no external dependencies)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

cad-to-data

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

drawing-analyzer

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

dwg-to-excel

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

cost-estimation-resource

No summary provided by upstream source.

Repository SourceNeeds Review