Skip to content
alyn
datadrivenconstruction

Permit Tracking Automation skill

Automate construction permit tracking and management. Monitor application status, track renewal deadlines, manage document requirements, and integrate with municipal systems.

$npx skills add datadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction --skill permit-tracking-automation
View this skill on GitHubdatadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction/5_DDC_Innovative/permit-tracking-automation/SKILL.md
Pack stars
402
Pack installs
26k
Alyn Score
57/99
Updated
22 days ago
License
MIT
– views – installs via Alyn

Install Permit Tracking Automation

Pick your agent. Every command is ready to paste.

$npx skills add datadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction --skill permit-tracking-automation

The open skills CLI detects your agents (Claude Code, Cursor, Codex and 70+ more) and installs to the right folder. Add -g to install globally.

01

Install it

Run the command above, or upload the folder in Claude.

02

Ask naturally

Describe the task. Your agent loads permit-tracking-automation when the request matches its description.

03

Name it if needed

If it does not trigger, say “use the permit-tracking-automation skill”. In Codex, type $permit-tracking-automation.

The skill itself

SKILL.md

FolderGitHub

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 = []

    # 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

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

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

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

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

Next Steps

  • See document-classification-nlp for document processing
  • See n8n-workflow-automation for notification workflows
  • See safety-compliance-checker for inspection integration

Tips for Permit Tracking Automation

  • • Start a fresh session after installing so your agent picks up the new skill.
  • • It is part of a pack with 30 skills. Install only the ones you need with --skill to keep triggers precise.
  • • If two skills overlap, mention the one you want by name.

Questions about Permit Tracking Automation

What does the Permit Tracking Automation skill do?

Automate construction permit tracking and management. Monitor application status, track renewal deadlines, manage document requirements, and integrate with municipal systems.

How do I install Permit Tracking Automation?

Run "npx skills add datadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction --skill permit-tracking-automation" in your project. The skills CLI installs it for Claude Code, Cursor, Codex and 70+ other agents. In Claude on the web, upload the skill folder as a ZIP under Customize > Skills.

Does Permit Tracking Automation work in Cursor and Codex?

Yes. Permit Tracking Automation uses the open SKILL.md format, so it works in Claude Code, Claude, Cursor, OpenAI Codex, Gemini CLI, GitHub Copilot and other agents that support agent skills.

Is Permit Tracking Automation free?

Permit Tracking Automation is open source on GitHub under the MIT license. It is free to install; check the license before commercial use.

Who made Permit Tracking Automation?

Permit Tracking Automation is published by datadrivenconstruction in the datadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction repository, together with 29 other skills.

Similar skills. From other creators.