permit-tracking-automation
skillAutomate construction permit tracking and management. Monitor application status, track renewal deadlines, manage document requirements, and integrate with municipal systems.
apm::install
apm install @datadrivenconstruction/permit-tracking-automationapm::skill.md
---
name: "permit-tracking-automation"
description: "Automate construction permit tracking and management. Monitor application status, track renewal deadlines, manage document requirements, and integrate with municipal systems."
homepage: "https://datadrivenconstruction.io"
metadata: {"openclaw": {"emoji": "🚀", "os": ["darwin", "linux", "win32"], "homepage": "https://datadrivenconstruction.io", "requires": {"bins": ["python3"]}}}
---
# Permit Tracking Automation
## Overview
This skill implements automated permit tracking for construction projects. Monitor permit status, manage document requirements, track deadlines, and integrate with local authority systems.
**Capabilities:**
- Permit application tracking
- Document management
- Deadline monitoring
- Status notifications
- Compliance checking
- Renewal automation
## Quick Start
```python
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']}")
```
## Comprehensive Permit Management System
### Permit Data Model
```python
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)
}
```
### Permit Tracking Engine
```python
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
```
### Deadline Monitoring
```python
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()))
```
### Reporting
```python
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
```
## Quick Reference
| 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 |
## Resources
- **International Building Code (IBC)**: Building standards
- **Local AHJ Websites**: Authority Having Jurisdiction portals
- **DDC Website**: https://datadrivenconstruction.io
## Next Steps
- See `document-classification-nlp` for document processing
- See `n8n-workflow-automation` for notification workflows
- See `safety-compliance-checker` for inspection integration