Punch List Manager for Construction Closeout
SkillDocs & knowledgeDigital punch list management for construction project closeout. Track deficiencies, assign corrections, photo documentation, and completion verification.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Punch List Manager for Construction Closeout skill
What this skill tells your AI
The instructions your AI receives, as published by datadrivenconstruction/ddc_skills_for_ai_agents_in_construction in 1_DDC_Toolkit/Field-Operations/punch-list-manager/SKILL.md and read by ahel’s review.
Complete system for managing construction punch lists from creation through final acceptance.
Business Case
Problem: Punch list management is inefficient:
- Paper lists get lost or outdated
- Difficult to track completion status
- Photos disconnected from items
- Back-charges delayed due to poor documentation
- Multiple walks create duplicate items
Solution: Digital punch list system that:
- Creates items with photos and location markup
- Assigns to responsible parties with deadlines
- Tracks completion with before/after photos
- Generates back-charge documentation
- Provides real-time completion dashboards
ROI: 50% faster closeout, 80% reduction in disputed back-charges
Punch List Workflow
┌──────────────────────────────────────────────────────────────────────┐
│ PUNCH LIST WORKFLOW │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ CREATION ASSIGNMENT COMPLETION │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Walk │────────►│ Assign │────────►│ Correct │ │
│ │ Site │ │ Items │ │ Items │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Log │ │ Notify │ │ Submit │ │
│ │ Items │ │ Parties │ │ Photo │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Photo │ │ Set │ │ Mark │ │
│ │ + Tag │ │ Deadline│ │ Complete│ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │
│ ▼ │
│ VERIFICATION CLOSEOUT ┌─────────┐ │
│ ┌─────────┐ ┌─────────┐ │ Verify │ │
│ │ Re-walk │◄────────│ Accept │◄───────│ Work │ │
│ │ Site │ │ Items │ └─────────┘ │
│ └─────────┘ └─────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────┐ ┌─────────┐ │
│ │ New │ │ Final │ │
│ │ Items? │────NO──►│ Accept │ │
│ └────┬────┘ └─────────┘ │
│ │YES │
│ └──────────────► Back to CREATION │
│ │
└──────────────────────────────────────────────────────────────────────┘
Data Structure
from dataclasses import dataclass, field
from datetime import datetime, date
from enum import Enum
from typing import List, Optional
import uuid
class PunchItemStatus(Enum):
OPEN = "Open"
ASSIGNED = "Assigned"
IN_PROGRESS = "In Progress"
READY_FOR_VERIFICATION = "Ready for Verification"
VERIFIED = "Verified"
REJECTED = "Rejected"
ACCEPTED = "Accepted"
class PunchItemPriority(Enum):
CRITICAL = "Critical" # Life safety / code compliance
HIGH = "High" # Affects occupancy
MEDIUM = "Medium" # Standard punch
LOW = "Low" # Minor / cosmetic
OBSERVATION = "Observation"
class TradeCategory(Enum):
GENERAL = "General Contractor"
ELECTRICAL = "Electrical"
PLUMBING = "Plumbing"
HVAC = "HVAC"
FIRE_PROTECTION = "Fire Protection"
DRYWALL = "Drywall/Painting"
FLOORING = "Flooring"
MILLWORK = "Millwork/Casework"
GLAZING = "Glazing"
ROOFING = "Roofing"
SITEWORK = "Sitework"
LANDSCAPING = "Landscaping"
CONTROLS = "Controls/BMS"
OTHER = "Other"
@dataclass
class PunchItem:
item_id: str
punch_list_id: str
description: str
location: str
trade: TradeCategory
priority: PunchItemPriority
# Location details
building: str = ""
floor: str = ""
room: str = ""
# Assignment
assigned_to: str = ""
assigned_date: date = None
due_date: date = None
# Documentation
photo_before: str = ""
photo_after: str = ""
drawing_markup: str = ""
spec_reference: str = ""
# Status tracking
status: PunchItemStatus = PunchItemStatus.OPEN
created_by: str = ""
created_date: date = field(default_factory=date.today)
# Completion
completed_by: str = ""
completed_date: date = None
completion_notes: str = ""
# Verification
verified_by: str = ""
verified_date: date = None
verification_notes: str = ""
# Back-charge
back_charge: bool = False
back_charge_amount: float = 0.0
back_charge_ref: str = ""
# History
history: List[dict] = field(default_factory=list)
@dataclass
class PunchList:
list_id: str
project_id: str
name: str
walk_date: date
walk_attendees: List[str]
items: List[PunchItem] = field(default_factory=list)
status: str = "Active" # Active, Complete
created_by: str = ""
created_date: date = field(default_factory=date.today)
area: str = "" # Building/floor/zone covered
list_type: str = "Punch" # Punch, Pre-Punch, Final
Python Implementation
import pandas as pd
from datetime import datetime, date, timedelta
from typing import List, Dict, Optional
from collections import defaultdict
class PunchListManager:
"""Construction punch list management system"""
def __init__(self, project_id: str, storage_path: str = None):
self.project_id = project_id
self.storage_path = storage_path or f"punch_{project_id}"
self.punch_lists: Dict[str, PunchList] = {}
self.items: Dict[str, PunchItem] = {}
def create_punch_list(
self,
name: str,
walk_date: date,
attendees: List[str],
area: str = "",
list_type: str = "Punch",
created_by: str = ""
) -> PunchList:
"""Create new punch list from walk"""
list_id = f"PL-{datetime.now().strftime('%Y%m%d%H%M%S')}"
punch_list = PunchList(
list_id=list_id,
project_id=self.project_id,
name=name,
walk_date=walk_date,
walk_attendees=attendees,
area=area,
list_type=list_type,
created_by=created_by
)
self.punch_lists[list_id] = punch_list
return punch_list
def add_item(
self,
punch_list_id: str,
description: str,
location: str,
trade: TradeCategory,
priority: PunchItemPriority = PunchItemPriority.MEDIUM,
building: str = "",
floor: str = "",
room: str = "",
photo_before: str = "",
drawing_markup: str = "",
spec_reference: str = "",
created_by: str = ""
) -> PunchItem:
"""Add item to punch list"""
if punch_list_id not in self.punch_lists:
raise ValueError(f"Punch list {punch_list_id} not found")
# Generate item ID
punch_list = self.punch_lists[punch_list_id]
item_num = len(punch_list.items) + 1
item_id = f"{punch_list_id}-{item_num:04d}"
item = PunchItem(
item_id=item_id,
punch_list_id=punch_list_id,
description=description,
location=location,
trade=trade,
priority=priority,
building=building,
floor=floor,
room=room,
photo_before=photo_before,
drawing_markup=drawing_markup,
spec_reference=spec_reference,
created_by=created_by
)
# Add history entry
item.history.append({
'date': datetime.now(),
'action': 'Created',
'by': created_by,
'notes': ''
})
self.items[item_id] = item
punch_list.items.append(item)
return item
def assign_item(
self,
item_id: str,
assigned_to: str,
due_date: date = None,
assigned_by: str = ""
) -> PunchItem:
"""Assign item to responsible party"""
item = self.items.get(item_id)
if not item:
raise ValueError(f"Item {item_id} not found")
if due_date is None:
# Default due dates by priority
days = {
PunchItemPriority.CRITICAL: 1,
PunchItemPriority.HIGH: 3,
PunchItemPriority.MEDIUM: 7,
PunchItemPriority.LOW: 14,
PunchItemPriority.OBSERVATION: 30
}
due_date = date.today() + timedelta(days=days.get(item.priority, 7))
item.assigned_to = assigned_to
item.assigned_date = date.today()
item.due_date = due_date
item.status = PunchItemStatus.ASSIGNED
item.history.append({
'date': datetime.now(),
'action': 'Assigned',
'by': assigned_by,
'notes': f'Assigned to {assigned_to}, due {due_date}'
})
# Trigger notification
self._notify_assignment(item)
return item
def mark_complete(
self,
item_id: str,
completed_by: str,
photo_after: str = "",
completion_notes: str = ""
) -> PunchItem:
"""Mark item as completed by trade"""
item = self.items.get(item_id)
if not item:
raise ValueError(f"Item {item_id} not found")
item.completed_by = completed_by
item.completed_date = date.today()
item.photo_after = photo_after
item.completion_notes = completion_notes
item.status = PunchItemStatus.READY_FOR_VERIFICATION
item.history.append({
'date': datetime.now(),
'action': 'Completed',
'by': completed_by,
'notes': completion_notes
})
return item
def verify_item(
self,
item_id: str,
verified_by: str,
accepted: bool,
notes: str = ""
) -> PunchItem:
"""Verify completed item"""
item = self.items.get(item_id)
if not item:
raise ValueError(f"Item {item_id} not found")
item.verified_by = verified_by
item.verified_date = date.today()
item.verification_notes = notes
if accepted:
item.status = PunchItemStatus.ACCEPTED
action = 'Accepted'
else:
item.status = PunchItemStatus.REJECTED
action = 'Rejected'
# Re-assign for rework
item.assigned_date = date.today()
item.due_date = date.today() + timedelta(days=3)
item.history.append({
'date': datetime.now(),
'action': action,
'by': verified_by,
'notes': notes
})
return item
def add_back_charge(
self,
item_id: str,
amount: float,
reference: str = ""
) -> PunchItem:
"""Add back-charge to item"""
item = self.items.get(item_id)
if not item:
raise ValueError(f"Item {item_id} not found")
item.back_charge = True
item.back_charge_amount = amount
item.back_charge_ref = reference
item.history.append({
'date': datetime.now(),
'action': 'Back Charge',
'by': '',
'notes': f'Amount: ${amount:.2f}, Ref: {reference}'
})
return item
def get_items_by_trade(self, trade: TradeCategory) -> List[PunchItem]:
"""Get all items for a specific trade"""
return [i for i in self.items.values() if i.trade == trade]
def get_items_by_status(self, status: PunchItemStatus) -> List[PunchItem]:
"""Get items by status"""
return [i for i in self.items.values() if i.status == status]
def get_overdue_items(self) -> List[PunchItem]:
"""Get overdue items"""
today = date.today()
return [
i for i in self.items.values()
if i.status in [PunchItemStatus.OPEN, PunchItemStatus.ASSIGNED, PunchItemStatus.IN_PROGRESS]
and i.due_date and i.due_date < today
]
def get_statistics(self) -> dict:
"""Get punch list statistics"""
all_items = list(self.items.values())
if not all_items:
return {'total': 0}
by_status = defaultdict(int)
by_trade = defaultdict(lambda: {'total': 0, 'open': 0})
by_priority = defaultdict(int)
for item in all_items:
by_status[item.status.value] += 1
by_trade[item.trade.value]['total'] += 1
if item.status not in [PunchItemStatus.ACCEPTED, PunchItemStatus.VERIFIED]:
by_trade[item.trade.value]['open'] += 1
by_priority[item.priority.value] += 1
# Calculate completion rate
accepted = len([i for i in all_items if i.status == PunchItemStatus.ACCEPTED])
completion_rate = accepted / len(all_items) * 100 if all_items else 0
# Back charges
back_charge_items = [i for i in all_items if i.back_charge]
total_back_charges = sum(i.back_charge_amount for i in back_charge_items)
return {
'total': len(all_items),
'by_status': dict(by_status),
'by_trade': dict(by_trade),
'by_priority': dict(by_priority),
'completion_rate': round(completion_rate, 1),
'overdue': len(self.get_overdue_items()),
'back_charge_count': len(back_charge_items),
'back_charge_total': total_back_charges
}
def generate_trade_report(self, trade: TradeCategory) -> str:
"""Generate report for specific trade"""
items = self.get_items_by_trade(trade)
report = f"""
╔══════════════════════════════════════════════════════════════╗
║ PUNCH LIST - {trade.value.upper():<30} ║
║ Project: {self.project_id:<40} ║
║ Date: {date.today().strftime('%d.%m.%Y'):<43} ║
╠══════════════════════════════════════════════════════════════╣
Total Items: {len(items)}
Open: {len([i for i in items if i.status not in [PunchItemStatus.ACCEPTED]])}
Due Today: {len([i for i in items if i.due_date == date.today()])}
Overdue: {len([i for i in items if i.due_date and i.due_date < date.today() and i.status not in [PunchItemStatus.ACCEPTED]])}
ITEMS REQUIRING ACTION
───────────────────────────────────────────────────────────────
"""
for item in items:
if item.status not in [PunchItemStatus.ACCEPTED]:
overdue_flag = "🔴" if item.due_date and item.due_date < date.today() else ""
report += f"""
{overdue_flag} [{item.item_id}] {item.priority.value}
Location: {item.location}
Description: {item.description}
Status: {item.status.value}
Due: {item.due_date}
"""
report += """
╚══════════════════════════════════════════════════════════════╝
"""
return report
def generate_summary_dashboard(self) -> str:
"""Generate overall punch list dashboard"""
stats = self.get_statistics()
report = f"""
╔══════════════════════════════════════════════════════════════════╗
║ PUNCH LIST DASHBOARD ║
║ Project: {self.project_id:<40} ║
║ Date: {date.today().strftime('%d.%m.%Y'):<43} ║
╠══════════════════════════════════════════════════════════════════╣
📊 OVERALL STATUS
───────────────────────────────────────────────────────────────────
Total Items: {stats['total']}
Completion Rate: {stats['completion_rate']}%
Overdue Items: {stats['overdue']}
📈 BY STATUS
───────────────────────────────────────────────────────────────────
"""
for status, count in stats['by_status'].items():
bar = "█" * int(count / max(stats['by_status'].values()) * 20) if stats['by_status'] else ""
report += f" {status:<25} {count:>5} {bar}\n"
report += """
🔧 BY TRADE (Open Items)
───────────────────────────────────────────────────────────────────
"""
for trade, data in sorted(stats['by_trade'].items(), key=lambda x: x[1]['open'], reverse=True):
if data['open'] > 0:
report += f" {trade:<25} {data['open']:>5} open / {data['total']} total\n"
report += f"""
💰 BACK CHARGES
───────────────────────────────────────────────────────────────────
Items with Back Charges: {stats['back_charge_count']}
Total Back Charges: ${stats['back_charge_total']:,.2f}
╚══════════════════════════════════════════════════════════════════╝
"""
return report
def _notify_assignment(self, item: PunchItem):
"""Send notification for assigned item"""
print(f"📋 Punch item assigned: {item.item_id}")
print(f" To: {item.assigned_to}")
print(f" Due: {item.due_date}")
print(f" Location: {item.location}")
def export_to_excel(self, output_path: str) -> str:
"""Export punch list to Excel"""
records = []
for item in self.items.values():
records.append({
'Item ID': item.item_id,
'Description': item.description,
'Location': item.location,
'Building': item.building,
'Floor': item.floor,
'Room': item.room,
'Trade': item.trade.value,
'Priority': item.priority.value,
'Status': item.status.value,
'Assigned To': item.assigned_to,
'Due Date': item.due_date,
'Completed By': item.completed_by,
'Completed Date': item.completed_date,
'Back Charge': 'Yes' if item.back_charge else 'No',
'Back Charge Amount': item.back_charge_amount if item.back_charge else '',
'Photo Before': item.photo_before,
'Photo After': item.photo_after
})
df = pd.DataFrame(records)
df.to_excel(output_path, index=False)
return output_path
# Usage Example
if __name__ == "__main__":
# Initialize manager
manager = PunchListManager(project_id="PROJECT-2026-001")
# Create punch list from walk
punch_list = manager.create_punch_list(
name="Floor 5 Pre-Final Walk",
walk_date=date.today(),
attendees=["PM", "Architect", "GC Super"],
area="Building A, Floor 5",
list_type="Pre-Final",
created_by="PM"
)
# Add items
item1 = manager.add_item(
punch_list_id=punch_list.list_id,
description="Touch up paint at door frame Room 501",
location="Room 501, door frame",
trade=TradeCategory.DRYWALL,
priority=PunchItemPriority.LOW,
building="A",
floor="5",
room="501",
created_by="PM"
)
item2 = manager.add_item(
punch_list_id=punch_list.list_id,
description="Missing cover plate on electrical outlet",
location="Room 502, east wall",
trade=TradeCategory.ELECTRICAL,
priority=PunchItemPriority.MEDIUM,
building="A",
floor="5",
room="502",
created_by="PM"
)
# Assign items
manager.assign_item(
item_id=item1.item_id,
assigned_to="ABC Painting",
assigned_by="GC Super"
)
manager.assign_item(
item_id=item2.item_id,
assigned_to="XYZ Electric",
due_date=date.today() + timedelta(days=2),
assigned_by="GC Super"
)
# Mark complete
manager.mark_complete(
item_id=item1.item_id,
completed_by="ABC Painting",
completion_notes="Paint touched up"
)
# Verify
manager.verify_item(
item_id=item1.item_id,
verified_by="PM",
accepted=True,
notes="Looks good"
)
# Generate reports
print(manager.generate_summary_dashboard())
print(manager.generate_trade_report(TradeCategory.ELECTRICAL))
Telegram Bot Integration
name: Punch List Bot
commands:
/newitem:
steps:
- Ask: Photo of deficiency
- Ask: Location (Building/Floor/Room)
- Ask: Description
- Ask: Trade (show buttons)
- Ask: Priority (show buttons)
- Confirm and create item
/myitems:
- Show open items assigned to user
- Buttons: [Mark Complete] [View Details]
/complete:
- Select item from list
- Ask for completion photo
- Ask for notes
- Submit for verification
/dashboard:
- Show summary statistics
- Open items by trade
- Overdue items
"The last 10% of punch takes 50% of the time. Start early, stay organized."
Signals
- GitHub stars
- 308
- Forks
- 79
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
punch-list-manager- Source
- github.com/datadrivenconstruction/ddc_skills_for_ai_agents_in_construction