All guides

Build a code violation research spreadsheet

Collect public enforcement cases, keep the source links, and build a sheet you can use for property research and repeat checks.

What you’ll make

A spreadsheet of enforcement cases with case numbers, record types, dates, statuses, and available property addresses. You’ll need an Apify account and a spreadsheet app. This walkthrough starts with Atlanta’s Enforcement module, which publishes complaints, registrations, and other case types.

A case is a record to investigate. A complaint or property registration alone does not establish a confirmed violation, an unresolved problem, or an owner’s intention to sell. Use the public case page to check what the record means.

1. Start with a small run

Open Code Violation Leads on Apify and paste this into the JSON input.

Recent enforcement cases
{
  "agency": "ATLANTA_GA",
  "module": "Enforcement",
  "lastDays": 14,
  "includeDetails": false,
  "maxResults": 20
}

lastDays: 14 searches the last 14 calendar days, including today in UTC. It overrides fixed dateFrom and dateTo values. For a repeatable historical search, set lastDays: 0 and supply both dates as YYYY-MM-DD; both endpoints are included.

maxResults caps this sample at 20 delivered rows. Leave includeDetails off initially. Turn it on later if you need available parcel information or details from the public case page. Review the actor’s current pricing and set a maximum charge before starting.

2. Choose the case types you need

Inspect the first output before narrowing the search. To keep complaints or violation notices, add "permitTypeContains": ["complaint", "violation"] to the input. A row matches if its type contains either phrase, without regard to case. This filters the record type, not the description or status.

To research another jurisdiction, choose its agency key and confirm that its portal exposes an Enforcement module. For a custom portal, follow the actor’s supported-host instructions for portalUrl. Changing the agency does not guarantee that enforcement records are available.

3. Export and check the records

After the run succeeds, open its output dataset. Check the agency, dates, and case types, then export CSV or Excel. In Google Sheets, choose File → Import → Upload and select the export.

Illustrative output — these are invented rows, not collected cases.
permitNumberpermitTypestatus
DEMO-CASE-001Zoning ComplaintOpen
DEMO-CASE-002Property RegistrationIn review
DEMO-CASE-003Violation NoticeClosed

The actor uses permitNumber for the case number and permitType for the case type. Keep those fields alongside agencyKey, status, appliedDate, addressFull, description, and detailUrl. Use addressCity for the property’s city; city describes the agency.

For Atlanta, appliedDate can be the most recent activity date. An older case can therefore appear in a recent search. Missing source values are null; a missing status does not mean “open.” Cases still being filed may have a temporaryNumber and no detail link.

4. Refresh without multiplying rows

Save the input as an Apify task and schedule it when you want regular exports. A rolling window can overlap earlier runs. When combining exports, match agencyKey and permitNumber, updating the existing row while keeping a dated copy if you need history. Recheck temporary-number records when a permanent number appears.

For date-based incremental delivery, create a dedicated Apify key-value store, set its ID as stateStoreId, and add sinceLastRun: true. A complete run saves the newest applied date it delivered; later runs emit rows dated after that marker. This is a date filter, not a guarantee that each case is delivered only once. An updated case can reappear, while a late-published case with an older or equal date can be missed.

Use full-window refreshes if you need to reconcile late or corrected records. If a run reaches a cap or stops early, check its log before using the export; an incomplete run does not advance the incremental marker.

5. Watch for status changes

For status monitoring, create a separate key-value store and set its ID as statusStateStoreId. The first complete run delivers the initial records and saves their statuses. Later runs deliver changes to known records with previousStatus and statusChanged. Newly encountered records establish a baseline rather than appearing as status transitions.

Keep the agency, filters, and a date window covering the cases you want to revisit. Records outside that window are not checked. Use one store per schedule and let runs finish one at a time. Do not combine this mode with sinceLastRun, statusBaseline, or statusChangesOnly. Inspect the log for incomplete scans before treating an empty dataset as “no changes.”

If no cases come back

Confirm the agency has an Enforcement module, widen the date range, or remove the type filter. Review the log for unsupported modules, access blocks, and result or spending limits. A blocked portal is a failed run, not evidence that no cases exist. If you need help, open an issue on the actor’s Apify page with the agency and date range.

Start with one jurisdiction.

Check a small set of cases before turning the export into a recurring research workflow.

Open the actor on Apify