# Building Permit Monitor - Construction Leads (`lowlanddata/building-permit-monitor`) Actor

Get alerted the moment a matching building permit is issued in Chicago, Austin, San Francisco, Seattle or NYC. Filter by keywords, permit type and valuation; alerts only on never-seen permits via official city open-data APIs. Project and contractor business data, no homeowner identities.

- **URL**: https://apify.com/lowlanddata/building-permit-monitor.md
- **Developed by:** [Lowland Data](https://apify.com/lowlanddata) (community)
- **Categories:** Lead generation, Automation, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 1,000 baseline permits

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js/docs.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python/docs.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## Building Permit Monitor — Construction Leads

Watches US city building-permit portals — Chicago, Austin, San Francisco,
Seattle and New York City — for newly issued permits matching your profile: keywords,
permit types, minimum project valuation. It alerts the moment a matching permit
is issued, and it remembers what it has already told you — so an alert always
means a genuinely new project, never a re-scrape of last week's list.

It reads each city's official open-data API (the access route the cities publish
for exactly this), and the output is clean by construction: project, address,
valuation and contractor **business** — never homeowner or applicant identities.
Safe to store, share with your sales team, or import into a CRM.

### Quick start (30 seconds, no account keys needed)

1. Run the actor with the prefilled input (Seattle, last 14 days) — it finishes
   in a few seconds and shows a baseline of recent permits.
2. Replace the demo profile with your own: your cities, your trade's keywords or
   permit types, your minimum project size.
3. Add a **daily schedule** (Actor page → Schedule). From then on, runs are
   quiet until a permit you care about is actually issued.

### Who uses this

- **A roofing contractor in Chicago** monitors `keywords: ["roof", "reroof"]`
  daily. When a permit is issued two neighborhoods over, the alert lands with
  the `address`, `valuationUsd` and `issuedDate` — a lead the same week the
  project goes live, not after the crews are already on site.
- **A solar installer in Austin** watches `keywords: ["solar", "photovoltaic"]`
  — every new solar permit shows which `contractorName` won the job, mapping
  both leads and competitors in one feed.
- **A building-materials supplier** runs `permitTypes: ["new construction"]`
  with `minValuationUsd: 500000` across all five cities — only projects big
  enough to supply, straight into the CRM via the API.
- **A market analyst** tracks permit volumes and valuations per city as a
  construction-economy indicator — the baseline plus daily deltas is a clean
  time series.
- **An HVAC company** pairs `permitTypes: ["mechanical"]` with
  `alertOn: "new-and-updated"` to also see status changes on projects it quoted.

### What you get

One JSON item per **event**: a baseline snapshot per permit on the first run,
then only transitions — newly issued matching permits, and (opt-in) status
changes on permits you have already seen. A new-permit alert looks like:

```json
{
  "portal": "austin",
  "permitNumber": "2026-091811 EP",
  "permitType": "Electrical Permit",
  "permitClass": "Residential",
  "workClass": "New",
  "description": "Install rooftop solar array, 24 panels, 9.6 kW",
  "status": "Active",
  "address": "8316 ROBERT ZION WAY",
  "city": "Austin",
  "state": "TX",
  "zip": "78744",
  "valuationUsd": 48500,
  "appliedDate": "2026-07-16",
  "issuedDate": "2026-07-17",
  "expiresDate": "2027-01-13",
  "contractorName": "Powerhouse Electrical Services, Inc.",
  "contractorTrade": "Electrical Contractor",
  "url": "https://abc.austintexas.gov/web/permit/public-search-other",
  "isBaseline": false,
  "isUpdate": false
}
````

Field honesty notes:

- Every portal has its own vocabulary — `permitType`, `permitClass` and
  `workClass` carry the city's own labels, unnormalized, and every item's labels
  are reusable as your `permitTypes` filter.
- `valuationUsd` appears only when the city publishes a project cost — many
  permits carry none, and absence does not mean small.
- `contractorName` is the business on the permit where the city publishes one
  (Chicago and Austin do; San Francisco and Seattle mostly don't).
- `address` is the project site the city publishes for public accountability.
  Who lives there is deliberately not in the data — see the compliance section.

### How much does permit monitoring cost?

Pay per outcome — checking costs nothing; you pay when the monitor tells you
something new:

- **New matching permit (the alert): $0.02** — also for status-change alerts.
- Baseline item (first run only): $0.002.
- Actor start: $0.005. Everything included - there are no separate
  platform-usage charges, and higher Apify plans get automatic unit-price
  discounts.

Cost feel: a focused trade profile in two cities sees a handful of new permits
per working day → roughly **$2–8/month** for complete coverage, with the
one-time baseline (say 100 permits) costing about $0.20. A run with zero new
matches costs $0.005 all-in. Lead-gen services sell the same permit leads for
$50–300/month.

#### Why an alert costs more than a scraped row

The store's headline ("from $2.00 / 1,000 baseline results") is what a trial
costs: a 100-permit baseline ≈ $0.20. Apify displays every event per 1,000, so
in the pricing table the alert event reads "$20.00 / 1,000" next to raw permit
scrapers at $2–30 per 1,000 rows — but that comparison is between different
things:

- A **raw permit scraper** sells rows: to monitor with rows you re-download
  each city's recent permits every day and diff them yourself.
- **This monitor** sells the transition: it remembers state in your account and
  charges $0.02 exactly when a permit you have not seen matches your profile.
  Nobody buys 1,000 alerts a month.

### Not technical? Let your AI assistant set it up

Paste this prompt together with a line about your trade and cities:

```text
Set up the Apify actor lowlanddata/building-permit-monitor to watch US building
permits for me. Build the input JSON from my business description:
- portals: which of ["chicago","austin","san-francisco","seattle","nyc"] I can serve
- keywords: 3-8 words my kind of project's description would contain
  (e.g. ["roof","reroof"] for roofing, ["solar"] for solar installers)
- permitTypes: my trade's permit labels if I named any (e.g. ["electrical"])
- minValuationUsd: only if I gave a minimum project size
Leave alertOn, emitBaseline and maxItems at their defaults. Run it once and
show me the baseline as a table (description, address, city, valuation,
contractor, issued date). Then create a daily schedule for it and set up a
notification so I get an email when a run produces items.
```

***

### Coverage — measured, not promised

- **Sources:** each city's official open-data permit dataset (Socrata APIs) —
  Chicago, Austin, San Francisco, Seattle and New York City (DOB NOW). Keyless,
  sanctioned access; together several thousand permits/week unfiltered (measured
  July 2026, NYC alone ~3,000/week).
- **More cities are on the roadmap**; each city ships only after its dataset
  passes the same field-level privacy verification.
- Cities update their datasets on their own cadence — typically daily with a
  small lag after issuance. The monitor alerts when the *dataset* publishes the
  permit; that is as fast as official data allows.
- **Permit-type vocabularies differ per city** — `permitTypes` is an honest
  substring match over each city's own labels, not a fake universal taxonomy.
- A portal outage or dataset migration never fails the run: the affected city
  is reported in the run status (uncharged) and its permits alert once it
  recovers.

### How the monitor thinks

- **First run = baseline.** It fetches your profile's matches from the last
  `lookbackDays` (default 14), pushes them flagged `isBaseline: true` at the
  low baseline price, and memorizes them. `emitBaseline: false` seeds silently.
- **After that, only transitions.** Each run queries from the previous run (one
  day of overlap; the memory absorbs duplicates) and pushes only permits it has
  never alerted. Status changes alert as `isUpdate: true` when
  `alertOn: "new-and-updated"`; with the default they stay silent but are
  remembered.
- **Editing the profile starts a fresh baseline** — matching-relevant inputs
  are the profile's identity, so a changed profile can never flood you with
  alerts computed against another profile's memory. `maxItems`, `emitBaseline`
  and schedule changes do not restart anything.
- **Memory** lives in a named key-value store (`stateStoreName`, default
  `permit-monitor-state`) in your account. Run several profiles on separate
  schedules against the same store — they never interfere. `resetState: true`
  wipes the current profile's memory.
- A capped baseline samples every selected city round-robin instead of
  exhausting the first one.
- Re-running after a crash or overlap re-alerts at most the last few unsaved
  items — state is checkpointed during the run.

### Input reference

| Field             | Type    | Default                  | Notes                                                   |
| ----------------- | ------- | ------------------------ | ------------------------------------------------------- |
| `portals`         | array   | all five                 | `chicago`, `austin`, `san-francisco`, `seattle`, `nyc`. |
| `keywords`        | array   | `[]`                     | OR, case-insensitive, over description + type labels.   |
| `permitTypes`     | array   | `[]`                     | Substring match over the city's type/class labels only. |
| `minValuationUsd` | integer | —                        | Permits without a published valuation always pass.      |
| `lookbackDays`    | integer | `14`                     | Baseline reach on the first run (max 60).               |
| `alertOn`         | string  | `"new"`                  | `"new-and-updated"` adds status-change alerts.          |
| `emitBaseline`    | boolean | `true`                   | `false` = silent seeding.                               |
| `resetState`      | boolean | `false`                  | One-shot: wipe this profile's memory.                   |
| `maxItems`        | integer | `500`                    | Overflow alerts on the next run, never lost.            |
| `stateStoreName`  | string  | `"permit-monitor-state"` | Only change to fully isolate monitor instances.         |

### Scheduling and API use

Run it daily and pair the schedule with a Monitoring alert on "dataset items

> 0" to get an email exactly when a permit matched.

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/lowlanddata~building-permit-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"portals": ["austin"], "keywords": ["solar"]}'
```

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('lowlanddata/building-permit-monitor').call({
  portals: ['chicago', 'austin'],
  keywords: ['roof', 'reroof'],
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items); // only new permits since the last run
```

```python
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("lowlanddata/building-permit-monitor").call(run_input={
    "portals": ["seattle"],
    "permitTypes": ["mechanical"],
})
items = client.dataset(run["defaultDatasetId"]).list_items().items
```

Daily runs finish in well under a minute, comfortably inside the 300s sync-call
wall; a 60-day four-city baseline is the one case that can take a couple of
minutes — run it async once.

### Use it with AI agents (MCP)

Claude, Cursor and other MCP-capable agents can run this monitor as a tool
through Apify's hosted MCP server: the agent builds the profile from a
plain-language brief, runs it, and reads the leads back — no glue code.

Claude Code:

```bash
claude mcp add apify --transport http "https://mcp.apify.com?actors=lowlanddata/building-permit-monitor"
```

Cursor or Claude Desktop (add a custom connector / MCP server with this URL):

```text
https://mcp.apify.com?actors=lowlanddata/building-permit-monitor
```

Sign in with your Apify account when prompted — runs are billed to it. Setup
details per client: [Apify MCP docs](https://docs.apify.com/platform/integrations/mcp),
or start from this actor's own MCP page:
[apify.com/lowlanddata/building-permit-monitor/api/mcp](https://apify.com/lowlanddata/building-permit-monitor/api/mcp).

Prompts that work once connected:

- "Watch Chicago and Austin for new roofing permits and show me today's leads."
- "What solar permits over $30k were issued in Austin this week, and which
  contractors got them?"
- "Track new-construction permits over $1M across all five cities and alert me
  daily."

### Is it legal to monitor building permits?

Yes — building permits are public records, and these five cities publish them
through official open-data APIs exactly so they can be reused. No login walls,
no anti-bot bypass — if a portal is unreachable, the run reports it rather than
fighting it. And the output is safe by construction: homeowner and applicant
identities are never extracted (see below).

How you contact a lead is on you — this tool gives you the public project
record, not marketing-law advice for your state.

### Compliance: safe to store, safe to CRM-import

Permit records as published by the cities can contain personal data — owner and
applicant names, phone numbers, even the applicant's own address. This actor's
guarantee is structural: each city's mapper whitelists fields IN (project,
address, valuation, dates, contractor business name/trade), so person-fields
never map at all, and an automated test proves it against each city's real
column names. The project `address` stays — it is the publicly published
project site and the lead's core value; who lives there is deliberately
unreachable. One honest limit: `description` is text the applicant wrote — on
rare occasions someone writes a name into free text. We cannot rewrite public
records; everything structured is guaranteed clean.

### FAQ

**Why did my second run return nothing?** Because nothing new matched — that is
the product working. The first run is the baseline; afterwards you only pay for
permits you have not seen. The run status says how many permits were checked.

**Does the output contain homeowner names or phone numbers?** No — by design.
The city mappers never touch owner/applicant fields. You get the project, the
address, the valuation and the contractor business.

**Which cities are covered?** Chicago, Austin, San Francisco, Seattle and New
York City — cities with official open-data permit APIs. More follow (New York
next); tell us which city you need via the Issues tab.

**Can I monitor several trades or cities separately?** Yes — one schedule per
profile, same actor, same state store. Each profile keeps its own memory.

**How fresh are the alerts?** As fresh as the city's dataset — typically the
permit appears within a day or two of issuance. The monitor can't be faster
than the official data, and doesn't pretend to be.

**What does a status-change alert mean?** With `alertOn: "new-and-updated"`,
a seen permit whose status moves (e.g. Active → Finaled) alerts once as
`isUpdate: true` — useful for tracking projects you quoted.

### Troubleshooting

- **"Every portal failed"** — the open-data platforms are down or unreachable;
  re-run later. Single-portal failures never fail the run.
- **"<city> truncated by the request budget"** — the lookback window matched
  more than ~10,000 permits; lower `lookbackDays` or narrow the profile.
- **"Monitor state record … is not readable"** — the state was written by a
  newer version; run once with `resetState: true` to start a fresh baseline.

Something else off? Open an issue on the actor's Issues tab — reports are read
and answered.

# Actor input Schema

## `portals` (type: `array`):

Which city permit portals to monitor. All five are official open-data APIs - no keys needed.

## `keywords` (type: `array`):

Alert on permits whose description or type contains ANY of these words (case-insensitive). Example: \["solar", "roof", "HVAC"]. Leave empty to match everything and filter by permit type or valuation instead.

## `permitTypes` (type: `array`):

Only alert on permits whose type or class label contains any of these (case-insensitive substring - portal vocabularies differ). Example: \["electrical", "demolition", "new construction"]. Every dataset item carries its portal's own labels, reusable here.

## `minValuationUsd` (type: `integer`):

Only alert on permits with a reported project cost of at least this many dollars. Permits without a published valuation always pass - portals make the field optional, and dropping them would hide real leads.

## `lookbackDays` (type: `integer`):

How many days of already-issued permits the FIRST run of a profile loads as its baseline. Later runs automatically query from the previous run with one day of overlap - re-alerting a seen permit is impossible.

## `alertOn` (type: `string`):

"new" alerts once per permit. "new-and-updated" also alerts when a seen permit's status changes (marked isUpdate: true), e.g. issued to finaled.

## `emitBaseline` (type: `boolean`):

On the very first run of a profile, push the currently matching permits (flagged isBaseline: true, charged at the lower baseline price) so you start with the recent picture. Turn off to seed silently and only ever receive new alerts.

## `resetState` (type: `boolean`):

Discard this profile's memory of already-seen permits and start over with a fresh baseline run. One-shot switch: turn it back off afterwards.

## `maxItems` (type: `integer`):

Upper bound on items pushed per run (baseline or alerts). Matches beyond the cap are not lost: they alert on the next run.

## `stateStoreName` (type: `string`):

Named key-value store holding the monitor's seen-permit memory. Only change this to run fully isolated monitor instances.

## Actor input object example

```json
{
  "portals": [
    "seattle"
  ],
  "lookbackDays": 14,
  "alertOn": "new",
  "emitBaseline": true,
  "resetState": false,
  "maxItems": 10,
  "stateStoreName": "permit-monitor-state"
}
```

# Actor output Schema

## `permits` (type: `string`):

Building permits matching your profile, one JSON object per permit. On the first run: the recent baseline; afterwards: only new (or status-changed) matches.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "portals": [
        "seattle"
    ],
    "lookbackDays": 14,
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("lowlanddata/building-permit-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "portals": ["seattle"],
    "lookbackDays": 14,
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("lowlanddata/building-permit-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "portals": [
    "seattle"
  ],
  "lookbackDays": 14,
  "maxItems": 10
}' |
apify call lowlanddata/building-permit-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=lowlanddata/building-permit-monitor",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Building Permit Monitor - Construction Leads",
        "description": "Get alerted the moment a matching building permit is issued in Chicago, Austin, San Francisco, Seattle or NYC. Filter by keywords, permit type and valuation; alerts only on never-seen permits via official city open-data APIs. Project and contractor business data, no homeowner identities.",
        "version": "0.1",
        "x-build-id": "VCvlcJdxRSvkkuxcD"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/lowlanddata~building-permit-monitor/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-lowlanddata-building-permit-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/lowlanddata~building-permit-monitor/runs": {
            "post": {
                "operationId": "runs-sync-lowlanddata-building-permit-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/lowlanddata~building-permit-monitor/run-sync": {
            "post": {
                "operationId": "run-sync-lowlanddata-building-permit-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "properties": {
                    "portals": {
                        "title": "Cities",
                        "type": "array",
                        "description": "Which city permit portals to monitor. All five are official open-data APIs - no keys needed.",
                        "items": {
                            "type": "string",
                            "enum": [
                                "chicago",
                                "austin",
                                "san-francisco",
                                "seattle",
                                "nyc"
                            ],
                            "enumTitles": [
                                "Chicago, IL",
                                "Austin, TX",
                                "San Francisco, CA",
                                "Seattle, WA",
                                "New York City"
                            ]
                        },
                        "default": [
                            "chicago",
                            "austin",
                            "san-francisco",
                            "seattle",
                            "nyc"
                        ]
                    },
                    "keywords": {
                        "title": "Keywords",
                        "type": "array",
                        "description": "Alert on permits whose description or type contains ANY of these words (case-insensitive). Example: [\"solar\", \"roof\", \"HVAC\"]. Leave empty to match everything and filter by permit type or valuation instead.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "permitTypes": {
                        "title": "Permit types",
                        "type": "array",
                        "description": "Only alert on permits whose type or class label contains any of these (case-insensitive substring - portal vocabularies differ). Example: [\"electrical\", \"demolition\", \"new construction\"]. Every dataset item carries its portal's own labels, reusable here.",
                        "items": {
                            "type": "string"
                        }
                    },
                    "minValuationUsd": {
                        "title": "Minimum valuation (USD)",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Only alert on permits with a reported project cost of at least this many dollars. Permits without a published valuation always pass - portals make the field optional, and dropping them would hide real leads."
                    },
                    "lookbackDays": {
                        "title": "Baseline lookback (days)",
                        "minimum": 1,
                        "maximum": 60,
                        "type": "integer",
                        "description": "How many days of already-issued permits the FIRST run of a profile loads as its baseline. Later runs automatically query from the previous run with one day of overlap - re-alerting a seen permit is impossible.",
                        "default": 14
                    },
                    "alertOn": {
                        "title": "Alert on",
                        "enum": [
                            "new",
                            "new-and-updated"
                        ],
                        "type": "string",
                        "description": "\"new\" alerts once per permit. \"new-and-updated\" also alerts when a seen permit's status changes (marked isUpdate: true), e.g. issued to finaled.",
                        "default": "new"
                    },
                    "emitBaseline": {
                        "title": "Emit baseline on first run",
                        "type": "boolean",
                        "description": "On the very first run of a profile, push the currently matching permits (flagged isBaseline: true, charged at the lower baseline price) so you start with the recent picture. Turn off to seed silently and only ever receive new alerts.",
                        "default": true
                    },
                    "resetState": {
                        "title": "Reset monitor state",
                        "type": "boolean",
                        "description": "Discard this profile's memory of already-seen permits and start over with a fresh baseline run. One-shot switch: turn it back off afterwards.",
                        "default": false
                    },
                    "maxItems": {
                        "title": "Max items per run",
                        "minimum": 1,
                        "maximum": 10000,
                        "type": "integer",
                        "description": "Upper bound on items pushed per run (baseline or alerts). Matches beyond the cap are not lost: they alert on the next run.",
                        "default": 500
                    },
                    "stateStoreName": {
                        "title": "State store name",
                        "type": "string",
                        "description": "Named key-value store holding the monitor's seen-permit memory. Only change this to run fully isolated monitor instances.",
                        "default": "permit-monitor-state"
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
