Automate construction permit tracking and management. Monitor application status, track renewal deadlines, manage document requirements, and integrate with municipal systems.
npx skills add https://github.com/datadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction --skill permit-tracking-automation
This skill implements automated permit tracking for construction projects. Monitor permit status, manage document requirements, track deadlines, and integrate with local authority systems.
Capabilities:
from dataclasses import dataclass, field
from datetime import date, datetime, timedelta
from typing import List, Dict, Optional
from enum import Enum
class PermitType(Enum):
BUILDING = "building"
ELECTRICAL = "electrical"
PLUMBING = "plumbing"
MECHANICAL = "mechanical"
FIRE = "fire"
DEMOLITION = "demolition"
EXCAVATION = "excavation"
OCCUPANCY = "occupancy"
ENVIRONMENTAL = "environmental"
SPECIAL_USE = "special_use"
class PermitStatus(Enum):
DRAFT = "draft"
SUBMITTED = "submitted"
UNDER_REVIEW = "under_review"
REVISION_REQUIRED = "revision_required"
APPROVED = "approved"
ISSUED = "issued"
ACTIVE = "active"
EXPIRED = "expired"
CLOSED = "closed"
@dataclass
class Permit:
permit_id: str
permit_type: PermitType
jurisdiction: str
status: PermitStatus
application_date: date
issued_date: Optional[date] = None
expiry_date: Optional[date] = None
description: str = ""
required_documents: List[str] = field(default_factory=list)
submitted_documents: List[str] = field(default_factory=list)
def check_permit_status(permit: Permit) -> Dict:
"""Check permit status and upcoming deadlines"""
today = date.today()
alerts = []
# Check expiry
if permit.expiry_date:
days_to_expiry = (permit.expiry_date - today).days
if days_to_expiry < 0:
alerts.append({'type': 'expired', 'message': 'Permit has expired'})
elif days_to_expiry <= 30:
alerts.append({'type': 'expiring_soon', 'days': days_to_expiry})
# Check missing documents
missing_docs = set(permit.required_documents) - set(permit.submitted_documents)
if missing_docs:
alerts.append({'type': 'missing_documents', 'documents': list(missing_docs)})
return {
'permit_id': permit.permit_id,
'status': permit.status.value,
'alerts': alerts,
'is_valid': permit.status in [PermitStatus.ACTIVE, PermitStatus.ISSUED] and
(permit.expiry_date is None or permit.expiry_date >= today)
}
# Example
permit = Permit(
permit_id="BP-2024-001",
permit_type=PermitType.BUILDING,
jurisdiction="City of Moscow",
status=PermitStatus.ACTIVE,
application_date=date(2024, 1, 15),
issued_date=date(2024, 2, 1),
expiry_date=date.today() + timedelta(days=25),
required_documents=["drawings", "specs", "survey"],
submitted_documents=["drawings", "specs"]
)
status = check_permit_status(permit)
print(f"Valid: {status['is_valid']}, Alerts: {status['alerts']}")
from dataclasses import dataclass, field
from datetime import date, datetime, timedelta
from typing import List, Dict, Optional, Tuple
from enum import Enum
import uuid
@dataclass
class Jurisdiction:
jurisdiction_id: str
name: str
region: str
country: str
permit_portal_url: Optional[str] = None
contact_email: Optional[str] = None
contact_phone: Optional[str] = None
typical_review_days: Dict[str, int] = field(default_factory=dict)
@dataclass
class RequiredDocument:
document_id: str
document_type: str
description: str
is_mandatory: bool = True
format_requirements: str = ""
template_url: Optional[str] = None
@dataclass
class SubmittedDocument:
document_id: str
document_type: str
filename: str
file_path: str
submitted_date: date
version: int = 1
status: str = "submitted" # submitted, accepted, rejected
reviewer_comments: str = ""
@dataclass
class Inspection:
inspection_id: str
inspection_type: str
scheduled_date: Optional[date] = None
completed_date: Optional[date] = None
inspector: str = ""
result: str = "" # passed, failed, conditional
notes: str = ""
required_corrections: List[str] = field(default_factory=list)
@dataclass
class Fee:
fee_id: str
fee_type: str
amount: float
due_date: date
paid_date: Optional[date] = None
receipt_number: str = ""
@dataclass
class PermitApplication:
# Identification
application_id: str
permit_number: Optional[str] = None
permit_type: PermitType = PermitType.BUILDING
jurisdiction: Jurisdiction = None
# Project reference
project_id: str = ""
project_name: str = ""
project_address: str = ""
parcel_number: str = ""
# Applicant
applicant_name: str = ""
applicant_company: str = ""
applicant_license: str = ""
owner_name: str = ""
# Status
status: PermitStatus = PermitStatus.DRAFT
current_phase: str = ""
submission_date: Optional[date] = None
approval_date: Optional[date] = None
issued_date: Optional[date] = None
expiry_date: Optional[date] = None
# Work scope
work_description: str = ""
project_value: float = 0
building_area_sqm: float = 0
occupancy_type: str = ""
# Documents
required_documents: List[RequiredDocument] = field(default_factory=list)
submitted_documents: List[SubmittedDocument] = field(default_factory=list)
# Inspections
inspections: List[Inspection] = field(default_factory=list)
# Fees
fees: List[Fee] = field(default_factory=list)
# Timeline
review_comments: List[Dict] = field(default_factory=list)
status_history: List[Dict] = field(default_factory=list)
def get_document_status(self) -> Dict:
"""Get document submission status"""
required_types = {d.document_type for d in self.required_documents if d.is_mandatory}
submitted_types = {d.document_type for d in self.submitted_documents}
return {
'required': len(required_types),
'submitted': len(submitted_types),
'missing': list(required_types - submitted_types),
'complete': required_types.issubset(submitted_types)
}
def get_fee_status(self) -> Dict:
"""Get fee payment status"""
total = sum(f.amount for f in self.fees)
paid = sum(f.amount for f in self.fees if f.paid_date)
overdue = [f for f in self.fees if not f.paid_date and f.due_date < date.today()]
return {
'total_amount': total,
'paid_amount': paid,
'outstanding': total - paid,
'overdue_fees': len(overdue)
}
from datetime import date, datetime, timedelta
from typing import List, Dict, Optional
import json
class PermitTracker:
"""Track and manage construction permits"""
def __init__(self, project_id: str):
self.project_id = project_id
self.applications: Dict[str, PermitApplication] = {}
self.jurisdictions: Dict[str, Jurisdiction] = {}
def add_jurisdiction(self, jurisdiction: Jurisdiction):
"""Register jurisdiction"""
self.jurisdictions[jurisdiction.jurisdiction_id] = jurisdiction
def create_application(self, permit_type: PermitType,
jurisdiction_id: str,
project_name: str,
project_address: str) -> PermitApplication:
"""Create new permit application"""
jurisdiction = self.jurisdictions.get(jurisdiction_id)
app = PermitApplication(
application_id=f"APP-{uuid.uuid4().hex[:8].upper()}",
permit_type=permit_type,
jurisdiction=jurisdiction,
project_id=self.project_id,
project_name=project_name,
project_address=project_address,
status=PermitStatus.DRAFT
)
# Load required documents for permit type
app.required_documents = self._get_required_documents(permit_type, jurisdiction_id)
self.applications[app.application_id] = app
return app
def _get_required_documents(self, permit_type: PermitType,
jurisdiction_id: str) -> List[RequiredDocument]:
"""Get required documents for permit type"""
# Standard requirements (would be loaded from database)
base_requirements = {
PermitType.BUILDING: [
RequiredDocument("DOC-001", "site_plan", "Site plan showing property boundaries"),
RequiredDocument("DOC-002", "floor_plans", "Architectural floor plans"),
RequiredDocument("DOC-003", "elevations", "Building elevations"),
RequiredDocument("DOC-004", "structural", "Structural drawings and calculations"),
RequiredDocument("DOC-005", "title_survey", "Title survey"),
RequiredDocument("DOC-006", "owner_auth", "Owner authorization letter"),
],
PermitType.ELECTRICAL: [
RequiredDocument("DOC-101", "electrical_plans", "Electrical plans"),
RequiredDocument("DOC-102", "load_calculations", "Electrical load calculations"),
RequiredDocument("DOC-103", "panel_schedule", "Panel schedule"),
],
PermitType.PLUMBING: [
RequiredDocument("DOC-201", "plumbing_plans", "Plumbing plans"),
RequiredDocument("DOC-202", "fixture_schedule", "Fixture schedule"),
RequiredDocument("DOC-203", "riser_diagrams", "Riser diagrams"),
]
}
return base_requirements.get(permit_type, [])
def submit_application(self, application_id: str) -> Dict:
"""Submit permit application"""
app = self.applications.get(application_id)
if not app:
return {'success': False, 'error': 'Application not found'}
# Check documents
doc_status = app.get_document_status()
if not doc_status['complete']:
return {
'success': False,
'error': 'Missing required documents',
'missing': doc_status['missing']
}
# Update status
app.status = PermitStatus.SUBMITTED
app.submission_date = date.today()
app.current_phase = "Initial Review"
# Record history
app.status_history.append({
'date': date.today().isoformat(),
'status': 'submitted',
'notes': 'Application submitted for review'
})
# Calculate expected timeline
jurisdiction = app.jurisdiction
if jurisdiction and jurisdiction.typical_review_days:
review_days = jurisdiction.typical_review_days.get(
app.permit_type.value, 30
)
expected_decision = date.today() + timedelta(days=review_days)
else:
expected_decision = date.today() + timedelta(days=30)
return {
'success': True,
'submission_date': app.submission_date.isoformat(),
'expected_decision': expected_decision.isoformat()
}
def update_status(self, application_id: str, new_status: PermitStatus,
notes: str = "", reviewer: str = ""):
"""Update application status"""
app = self.applications.get(application_id)
if not app:
return
old_status = app.status
app.status = new_status
if new_status == PermitStatus.APPROVED:
app.approval_date = date.today()
elif new_status == PermitStatus.ISSUED:
app.issued_date = date.today()
app.permit_number = f"P-{date.today().year}-{len(self.applications):05d}"
# Set expiry (typically 1-2 years)
app.expiry_date = date.today() + timedelta(days=365)
app.status_history.append({
'date': date.today().isoformat(),
'from_status': old_status.value,
'to_status': new_status.value,
'notes': notes,
'reviewer': reviewer
})
def add_document(self, application_id: str, document_type: str,
filename: str, file_path: str) -> SubmittedDocument:
"""Add document to application"""
app = self.applications.get(application_id)
if not app:
return None
# Check if updating existing document
existing = [d for d in app.submitted_documents if d.document_type == document_type]
version = max(d.version for d in existing) + 1 if existing else 1
doc = SubmittedDocument(
document_id=f"SUB-{uuid.uuid4().hex[:8].upper()}",
document_type=document_type,
filename=filename,
file_path=file_path,
submitted_date=date.today(),
version=version
)
app.submitted_documents.append(doc)
return doc
def schedule_inspection(self, application_id: str,
inspection_type: str,
requested_date: date) -> Inspection:
"""Schedule inspection"""
app = self.applications.get(application_id)
if not app:
return None
inspection = Inspection(
inspection_id=f"INS-{uuid.uuid4().hex[:8].upper()}",
inspection_type=inspection_type,
scheduled_date=requested_date
)
app.inspections.append(inspection)
return inspection
def record_inspection_result(self, application_id: str,
inspection_id: str,
result: str,
notes: str = "",
corrections: List[str] = None):
"""Record inspection result"""
app = self.applications.get(application_id)
if not app:
return
for inspection in app.inspections:
if inspection.inspection_id == inspection_id:
inspection.completed_date = date.today()
inspection.result = result
inspection.notes = notes
if corrections:
inspection.required_corrections = corrections
break
from datetime import date, timedelta
from typing import List, Dict
class DeadlineMonitor:
"""Monitor permit deadlines and send alerts"""
def __init__(self, tracker: PermitTracker):
self.tracker = tracker
self.alert_thresholds = {
'expiry': [90, 60, 30, 14, 7], # Days before expiry
'fee_due': [30, 14, 7, 1], # Days before fee due
'inspection': [7, 3, 1] # Days before inspection
}
def check_all_deadlines(self) -> List[Dict]:
"""Check all permit deadlines"""
alerts = []
today = date.today()
for app_id, app in self.tracker.applications.items():
# Check expiry
if app.expiry_date:
days_to_expiry = (app.expiry_date - today).days
for threshold in self.alert_thresholds['expiry']:
if days_to_expiry == threshold:
alerts.append({
'type': 'expiry_warning',
'application_id': app_id,
'permit_number': app.permit_number,
'permit_type': app.permit_type.value,
'expiry_date': app.expiry_date.isoformat(),
'days_remaining': days_to_expiry,
'priority': 'high' if days_to_expiry <= 14 else 'medium'
})
break
if days_to_expiry < 0:
alerts.append({
'type': 'expired',
'application_id': app_id,
'permit_number': app.permit_number,
'permit_type': app.permit_type.value,
'expiry_date': app.expiry_date.isoformat(),
'days_overdue': abs(days_to_expiry),
'priority': 'critical'
})
# Check fees
for fee in app.fees:
if not fee.paid_date:
days_to_due = (fee.due_date - today).days
for threshold in self.alert_thresholds['fee_due']:
if days_to_due == threshold:
alerts.append({
'type': 'fee_due',
'application_id': app_id,
'fee_type': fee.fee_type,
'amount': fee.amount,
'due_date': fee.due_date.isoformat(),
'days_remaining': days_to_due,
'priority': 'high' if days_to_due <= 7 else 'medium'
})
break
if days_to_due < 0:
alerts.append({
'type': 'fee_overdue',
'application_id': app_id,
'fee_type': fee.fee_type,
'amount': fee.amount,
'due_date': fee.due_date.isoformat(),
'days_overdue': abs(days_to_due),
'priority': 'critical'
})
# Check inspections
for inspection in app.inspections:
if inspection.scheduled_date and not inspection.completed_date:
days_to_inspection = (inspection.scheduled_date - today).days
for threshold in self.alert_thresholds['inspection']:
if days_to_inspection == threshold:
alerts.append({
'type': 'upcoming_inspection',
'application_id': app_id,
'inspection_type': inspection.inspection_type,
'scheduled_date': inspection.scheduled_date.isoformat(),
'days_remaining': days_to_inspection,
'priority': 'medium'
})
break
return sorted(alerts, key=lambda x: (
0 if x['priority'] == 'critical' else 1 if x['priority'] == 'high' else 2
))
def get_permit_calendar(self, months_ahead: int = 3) -> Dict[str, List[Dict]]:
"""Get calendar of permit events"""
today = date.today()
end_date = today + timedelta(days=months_ahead * 30)
calendar = {}
for app_id, app in self.tracker.applications.items():
# Expiry dates
if app.expiry_date and today <= app.expiry_date <= end_date:
date_str = app.expiry_date.isoformat()
if date_str not in calendar:
calendar[date_str] = []
calendar[date_str].append({
'type': 'expiry',
'application_id': app_id,
'description': f"{app.permit_type.value} permit expires"
})
# Inspections
for inspection in app.inspections:
if (inspection.scheduled_date and
not inspection.completed_date and
today <= inspection.scheduled_date <= end_date):
date_str = inspection.scheduled_date.isoformat()
if date_str not in calendar:
calendar[date_str] = []
calendar[date_str].append({
'type': 'inspection',
'application_id': app_id,
'description': f"{inspection.inspection_type} inspection"
})
# Fee due dates
for fee in app.fees:
if not fee.paid_date and today <= fee.due_date <= end_date:
date_str = fee.due_date.isoformat()
if date_str not in calendar:
calendar[date_str] = []
calendar[date_str].append({
'type': 'fee_due',
'application_id': app_id,
'description': f"{fee.fee_type} fee ${fee.amount}"
})
return dict(sorted(calendar.items()))
import pandas as pd
def generate_permit_report(tracker: PermitTracker, output_path: str) -> str:
"""Generate permit status report"""
with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
# Summary
summary_data = []
for app in tracker.applications.values():
doc_status = app.get_document_status()
fee_status = app.get_fee_status()
summary_data.append({
'Application ID': app.application_id,
'Permit Number': app.permit_number or 'Pending',
'Type': app.permit_type.value,
'Status': app.status.value,
'Submitted': app.submission_date,
'Issued': app.issued_date,
'Expires': app.expiry_date,
'Documents': f"{doc_status['submitted']}/{doc_status['required']}",
'Fees Outstanding': fee_status['outstanding']
})
pd.DataFrame(summary_data).to_excel(writer, sheet_name='Summary', index=False)
# Status by type
by_type = {}
for app in tracker.applications.values():
t = app.permit_type.value
if t not in by_type:
by_type[t] = {'total': 0, 'active': 0, 'pending': 0}
by_type[t]['total'] += 1
if app.status == PermitStatus.ACTIVE:
by_type[t]['active'] += 1
elif app.status in [PermitStatus.SUBMITTED, PermitStatus.UNDER_REVIEW]:
by_type[t]['pending'] += 1
pd.DataFrame(by_type).T.to_excel(writer, sheet_name='By_Type')
return output_path
| Permit Type | Typical Documents | Review Time |
|-------------|-------------------|-------------|
| Building | Site plan, drawings, calculations | 2-8 weeks |
| Electrical | E-plans, load calc, panel schedule | 1-4 weeks |
| Plumbing | P-plans, fixture schedule, risers | 1-4 weeks |
| Mechanical | M-plans, equipment schedule | 1-4 weeks |
| Fire | Fire alarm, sprinkler plans | 2-6 weeks |
| Demolition | Demo plan, survey, abatement | 1-3 weeks |
document-classification-nlp for document processingn8n-workflow-automation for notification workflowssafety-compliance-checker for inspection integrationComprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. When Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks
Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale.
Presentation creation, editing, and analysis. When Claude needs to work with presentations (.pptx files) for: (1) Creating new presentations, (2) Modifying or editing content, (3) Working with layouts, (4) Adding comments or speaker notes, or any other presentation tasks
Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.
Use this skill whenever the user wants to do anything with PDF files. This includes reading or extracting text/tables from PDFs, combining or merging multiple PDFs into one, splitting PDFs apart, rotating pages, adding watermarks, creating new PDFs, filling PDF forms, encrypting/decrypting PDFs, extracting images, and OCR on scanned PDFs to make them searchable. If the user mentions a .pdf file or asks to produce one, use this skill.
Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files). Triggers include: any mention of 'Word doc', 'word document', '.docx', or requests to produce professional documents with formatting like tables of contents, headings, page numbers, or letterheads. Also use when extracting or reorganizing content from .docx files, inserting or replacing images in documents, performing find-and-replace in Word files, working with tracked changes or comments, or converting content into a polished Word document. If the user asks for a 'report', 'memo', 'letter', 'template', or similar deliverable as a Word or .docx file, use this skill. Do NOT use for PDFs, spreadsheets, Google Docs, or general coding tasks unrelated to document generation.
Use this skill any time a .pptx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx file (even if the extracted content will be used elsewhere, like in an email or summary); editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates, layouts, speaker notes, or comments. Trigger whenever the user mentions \"deck,\" \"slides,\" \"presentation,\" or references a .pptx filename, regardless of what they plan to do with the content afterward. If a .pptx file needs to be opened, created, or touched, use this skill.
Create and edit Obsidian Flavored Markdown with wikilinks, embeds, callouts, properties, and other Obsidian-specific syntax. Use when working with .md files in Obsidian, or when the user mentions wikilinks, callouts, frontmatter, tags, embeds, or Obsidian notes.
Take datadrivenconstruction/permit-tracking-automation 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.