opsforenergy
How I Built an AI Agent to Track AHJ Permits Across Multiple Jurisdictions
Build Log

How I Built an AI Agent to Track AHJ Permits Across Multiple Jurisdictions

OpsForEnergy··8 min read

The Fresno County building department sends permit updates from an address that includes "fresno.ca.gov" and formats the subject line as "Permit Status Update - #2024-12345." Kern County uses "planning@co.kern.ca.us" and puts the permit number in the body. Bakersfield city sends plain-text emails from a generic noreply address with no subject line convention at all.

This is the reality of AHJ communication: every jurisdiction is a snowflake. And it is exactly why permit tracking is so hard to automate. The problem is not that the information is missing. It is that the format is unpredictable.

I built the Permit Agent to solve this. It monitors a dedicated inbox, parses AHJ emails, extracts permit numbers and statuses, matches them to projects in Supabase, and routes actionable updates to the PM Telegram channel. Here is how it works.

Step 1: Ingestion. The agent wakes every 2 hours and checks permits@ops.opsforenergy.com via AgentMail. It fetches unread emails, marks them as processed, and passes them to the parsing layer.

Step 2: Parsing. The core challenge is that AHJ emails have no standard format. I solved this with a two-layer parser. The first layer uses regex heuristics to extract permit numbers from known jurisdictions. The second layer falls back to Claude Sonnet 4 for unstructured emails. The LLM is prompted with the full email body, a list of active permit numbers from Supabase, and instructions to return a structured JSON object with permit number, status, and any deadlines mentioned.

Step 3: Matching. Once the permit number is extracted, the agent queries Supabase to match it to an active project. If the match is unambiguous, it proceeds. If there are multiple candidates or no match, it flags the email for human review rather than guessing.

Step 4: Action. Depending on the status, the agent calls one of several MCP tools. A status of "Approved" triggers update_permit_status and notify_pm_channel. A revision request triggers flag_deadline_risk if a due date is detected. An inspection pass triggers advance_milestone_readiness.

Step 5: Delivery. Every action generates a concise Telegram message sent to the PM channel. The message includes the project reference, the AHJ name, the status, and the exact action the agent took. No dashboards. No logins. Just the information the PM needs, delivered where she already works.

The schema. The Supabase permits table has columns for project_id, jurisdiction, permit_number, status, submitted_date, expiry_date, and last_update_source. The agent writes to last_update_source with its own ID and a timestamp, creating an audit trail for every change.

The metric: In shadow mode over two weeks, the agent processed 47 AHJ emails across 4 jurisdictions. It correctly classified and matched 43 of them (91% accuracy). The 4 failures were all from a single jurisdiction that had recently changed its email template — a pattern we now catch with a fallback parser.

An honest limitation: This agent does not handle portal-only jurisdictions well. Some AHJs do not send email updates at all; they require logging into an online portal. For those, we are building a lightweight scraper sub-agent. Until then, portal-only jurisdictions still need manual checks.

Want to see this in action? Here's the demo →

Download

AHJ Permit Tracking Calendar — Google Sheets template with expiry date warnings.

Get the calendar →