| 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":"[Truncated]"}}} |
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
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 = []
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})
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)
}
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
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:
file_path:
submitted_date: date
version: =
status: =
reviewer_comments: =
:
inspection_id:
inspection_type:
scheduled_date: [date] =
completed_date: [date] =
inspector: =
result: =
notes: =
required_corrections: [] = field(default_factory=)
:
fee_id:
fee_type:
amount:
due_date: date
paid_date: [date] =
receipt_number: =
:
application_id:
permit_number: [] =
permit_type: PermitType = PermitType.BUILDING
jurisdiction: Jurisdiction =
project_id: =
project_name: =
project_address: =
parcel_number: =
applicant_name: =
applicant_company: =
applicant_license: =
owner_name: =
status: PermitStatus = PermitStatus.DRAFT
current_phase: =
submission_date: [date] =
approval_date: [date] =
issued_date: [date] =
expiry_date: [date] =
work_description: =
project_value: =
building_area_sqm: =
occupancy_type: =
required_documents: [RequiredDocument] = field(default_factory=)
submitted_documents: [SubmittedDocument] = field(default_factory=)
inspections: [Inspection] = field(default_factory=)
fees: [Fee] = field(default_factory=)
review_comments: [] = field(default_factory=)
status_history: [] = field(default_factory=)
() -> :
required_types = {d.document_type d .required_documents d.is_mandatory}
submitted_types = {d.document_type d .submitted_documents}
{
: (required_types),
: (submitted_types),
: (required_types - submitted_types),
: required_types.issubset(submitted_types)
}
() -> :
total = (f.amount f .fees)
paid = (f.amount f .fees f.paid_date)
overdue = [f f .fees f.paid_date f.due_date < date.today()]
{
: total,
: paid,
: total - paid,
: (overdue)
}
Permit Tracking Engine
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
)
app.required_documents = ._get_required_documents(permit_type, jurisdiction_id)
.applications[app.application_id] = app
app
() -> [RequiredDocument]:
base_requirements = {
PermitType.BUILDING: [
RequiredDocument(, , ),
RequiredDocument(, , ),
RequiredDocument(, , ),
RequiredDocument(, , ),
RequiredDocument(, , ),
RequiredDocument(, , ),
],
PermitType.ELECTRICAL: [
RequiredDocument(, , ),
RequiredDocument(, , ),
RequiredDocument(, , ),
],
PermitType.PLUMBING: [
RequiredDocument(, , ),
RequiredDocument(, , ),
RequiredDocument(, , ),
]
}
base_requirements.get(permit_type, [])
() -> :
app = .applications.get(application_id)
app:
{: , : }
doc_status = app.get_document_status()
doc_status[]:
{
: ,
: ,
: doc_status[]
}
app.status = PermitStatus.SUBMITTED
app.submission_date = date.today()
app.current_phase =
app.status_history.append({
: date.today().isoformat(),
: ,
:
})
jurisdiction = app.jurisdiction
jurisdiction jurisdiction.typical_review_days:
review_days = jurisdiction.typical_review_days.get(
app.permit_type.value,
)
expected_decision = date.today() + timedelta(days=review_days)
:
expected_decision = date.today() + timedelta(days=)
{
: ,
: app.submission_date.isoformat(),
: expected_decision.isoformat()
}
():
app = .applications.get(application_id)
app:
old_status = app.status
app.status = new_status
new_status == PermitStatus.APPROVED:
app.approval_date = date.today()
new_status == PermitStatus.ISSUED:
app.issued_date = date.today()
app.permit_number =
app.expiry_date = date.today() + timedelta(days=)
app.status_history.append({
: date.today().isoformat(),
: old_status.value,
: new_status.value,
: notes,
: reviewer
})
() -> SubmittedDocument:
app = .applications.get(application_id)
app:
existing = [d d app.submitted_documents d.document_type == document_type]
version = (d.version d existing) + existing
doc = SubmittedDocument(
document_id=,
document_type=document_type,
filename=filename,
file_path=file_path,
submitted_date=date.today(),
version=version
)
app.submitted_documents.append(doc)
doc
() -> Inspection:
app = .applications.get(application_id)
app:
inspection = Inspection(
inspection_id=,
inspection_type=inspection_type,
scheduled_date=requested_date
)
app.inspections.append(inspection)
inspection
():
app = .applications.get(application_id)
app:
inspection app.inspections:
inspection.inspection_id == inspection_id:
inspection.completed_date = date.today()
inspection.result = result
inspection.notes = notes
corrections:
inspection.required_corrections = corrections
Deadline Monitoring
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],
'fee_due': [30, 14, 7, 1],
'inspection': [7, 3, 1]
}
def check_all_deadlines(self) -> List[Dict]:
"""Check all permit deadlines"""
alerts = []
today = date.today()
for app_id, app in self.tracker.applications.items():
if app.expiry_date:
days_to_expiry = (app.expiry_date - today).days
for threshold in self.alert_thresholds['expiry']:
days_to_expiry == threshold:
alerts.append({
: ,
: app_id,
: app.permit_number,
: app.permit_type.value,
: app.expiry_date.isoformat(),
: days_to_expiry,
: days_to_expiry <=
})
days_to_expiry < :
alerts.append({
: ,
: app_id,
: app.permit_number,
: app.permit_type.value,
: app.expiry_date.isoformat(),
: (days_to_expiry),
:
})
fee app.fees:
fee.paid_date:
days_to_due = (fee.due_date - today).days
threshold .alert_thresholds[]:
days_to_due == threshold:
alerts.append({
: ,
: app_id,
: fee.fee_type,
: fee.amount,
: fee.due_date.isoformat(),
: days_to_due,
: days_to_due <=
})
days_to_due < :
alerts.append({
: ,
: app_id,
: fee.fee_type,
: fee.amount,
: fee.due_date.isoformat(),
: (days_to_due),
:
})
inspection app.inspections:
inspection.scheduled_date inspection.completed_date:
days_to_inspection = (inspection.scheduled_date - today).days
threshold .alert_thresholds[]:
days_to_inspection == threshold:
alerts.append({
: ,
: app_id,
: inspection.inspection_type,
: inspection.scheduled_date.isoformat(),
: days_to_inspection,
:
})
(alerts, key= x: (
x[] == x[] ==
))
() -> [, []]:
today = date.today()
end_date = today + timedelta(days=months_ahead * )
calendar = {}
app_id, app .tracker.applications.items():
app.expiry_date today <= app.expiry_date <= end_date:
date_str = app.expiry_date.isoformat()
date_str calendar:
calendar[date_str] = []
calendar[date_str].append({
: ,
: app_id,
:
})
inspection app.inspections:
(inspection.scheduled_date
inspection.completed_date
today <= inspection.scheduled_date <= end_date):
date_str = inspection.scheduled_date.isoformat()
date_str calendar:
calendar[date_str] = []
calendar[date_str].append({
: ,
: app_id,
:
})
fee app.fees:
fee.paid_date today <= fee.due_date <= end_date:
date_str = fee.due_date.isoformat()
date_str calendar:
calendar[date_str] = []
calendar[date_str].append({
: ,
: app_id,
:
})
((calendar.items()))
Reporting
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_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)
by_type = {}
for app in tracker.applications.values():
t = app.permit_type.value
if t not in by_type:
by_type[t] = {'total': 0, : , : }
by_type[t][] +=
app.status == PermitStatus.ACTIVE:
by_type[t][] +=
app.status [PermitStatus.SUBMITTED, PermitStatus.UNDER_REVIEW]:
by_type[t][] +=
pd.DataFrame(by_type).T.to_excel(writer, sheet_name=)
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
Next Steps
- See
document-classification-nlp for document processing
- See
n8n-workflow-automation for notification workflows
- See
safety-compliance-checker for inspection integration