# Regione Lombardia Open Grants & Tenders Registry Delta Monitor (`stefano_seggio/regione-lombardia-grants-registry-monitor`) Actor

Delta monitor for Regione Lombardia's Socrata open-data grants/tenders registry (bandi). Computes a real OPEN/UPCOMING/CLOSED status from source dates, classifies each bando into a policy-area taxonomy, and emits NEW\_LISTING/STATUS\_CHANGE/UPDATED events. Pay-per-event: billed only for what changed.

- **URL**: https://apify.com/stefano\_seggio/regione-lombardia-grants-registry-monitor.md
- **Developed by:** [Stefano Seggio](https://apify.com/stefano_seggio) (community)
- **Categories:** Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 new listing / status changes

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Regione Lombardia Open Grants & Tenders Registry Delta Monitor

#### Stop refreshing Lombardy's grants portal by hand — get notified only when a bando actually opens, closes, or changes

Regione Lombardia publishes its bandi (grants and tenders) through a Socrata open-data registry, but the raw feed won't tell you whether a bando is actually open, upcoming, or already closed — you'd have to work that out yourself from its apertura/chiusura dates, and re-pulling the whole registry every time just to catch a change wastes both time and money. Anyone tracking regional funding — businesses, consultants, grant writers — needs a live signal, not a static export they have to re-diff themselves. **This Actor solves that**: it monitors the registry continuously, computes a real OPEN/UPCOMING/CLOSED status from the source dates, classifies each bando into a policy-area taxonomy, and emits an event only when something is genuinely new or has changed.

***

### Why this outperforms a standard scraper

- **Delta tracking, not re-scraping.** Every record is fingerprinted on every run. Unchanged records are never re-delivered — and never billed.
- **Pay only for what's new.** A new bando or one whose computed status just changed (e.g. it flipped from UPCOMING to OPEN) costs $0.02. A smaller change to a non-status field costs $0.008. Everything unchanged costs nothing.
- **Real status computation, not raw dates.** Instead of handing you the source's apertura/chiusura dates and leaving you to work out what they mean, this Actor computes an actual OPEN/UPCOMING/CLOSED status itself and classifies every bando into its own policy-area taxonomy — turning raw open-data fields into something you can filter and act on directly.

### See it before you trust it

```json
{
  "bandoId": "FDR-2026-00981",
  "titolo": "Bando per l'innovazione digitale delle PMI",
  "status": "OPEN",
  "policyArea": "Sviluppo economico",
  "dataApertura": "2026-09-01",
  "dataChiusura": "2026-11-30",
  "eventType": "NEW_LISTING"
}
```

The `status` and `eventType` fields are what make this valuable: `status` is computed live from the source's own dates instead of left for you to parse, and `eventType` tells you exactly why this record was delivered.

### Zero-risk trial

Unchanged records cost **$0.00**. Run it once against real data before you commit to anything:

```bash
curl -X POST "https://api.apify.com/v2/acts/f0xRlvzERsbgbU1ru/run-sync-get-dataset-items?token=<YOUR_API_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"onlyNew":true,"maxItems":100}'
```

```python
import requests

response = requests.post(
    "https://api.apify.com/v2/acts/f0xRlvzERsbgbU1ru/run-sync-get-dataset-items",
    params={"token": "<YOUR_API_TOKEN>"},
    json={"onlyNew": True, "maxItems": 100},
)
records = response.json()
print(f"{len(records)} records returned")
```

```javascript
const response = await fetch(
  "https://api.apify.com/v2/acts/f0xRlvzERsbgbU1ru/run-sync-get-dataset-items?token=<YOUR_API_TOKEN>",
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ onlyNew: true, maxItems: 100 }),
  }
);
const records = await response.json();
console.log(records.length + " records returned");
```

### Pricing

| Event | What it means | Price |
|---|---|---|
| New listing / status change | A new bando, or one whose computed status changed. | $0.02 |
| Bando field updated | A non-status field update. | $0.008 |

Actor-start fee: $0.00005/GB-memory (one-time per run, not per record).

### What you get on every record

- A computed status (OPEN/UPCOMING/CLOSED) derived from the source's own apertura/chiusura dates, not left for you to calculate.
- A policy-area classification from this Actor's own taxonomy mapper, so you can filter bandi by sector without reading each one.
- An explicit event type — NEW\_LISTING, STATUS\_CHANGE, or UPDATED — telling you exactly why the record was delivered.
- Full-text keyword search passed straight through to Socrata's own query parameter, letting you narrow the registry at the source rather than after the fact.

### Input parameters

| Field | Type | Description | Default |
|---|---|---|---|
| onlyNew | boolean | Only NEW\_LISTING, STATUS\_CHANGE and UPDATED events are delivered. | true |
| statusFilter | array | Deliver only bandi whose computed status (from apertura/chiusura dates) matches. | \[] (all) |
| policyAreaFilter | array | Deliver only bandi classified into these policy areas by this Actor's own taxonomy mapper. | \[] (all) |
| keyword | string | Full-text search via Socrata's own query parameter. | none |
| maxItems | integer | Stops the run once this many records have been pushed. | none (unlimited) |

### Source & reliability

Data comes directly from Regione Lombardia's official Socrata open-data platform, specifically its grants/tenders (bandi) registry. Because this Actor is a delta monitor, every run re-fingerprints each record against its last-seen state, so unchanged bandi are silently skipped and never billed — the only cost you carry is $0.02 for a genuinely new or status-changed listing, or $0.008 for a minor field edit.

# Changelog

This Actor's version history is a separate document: https://apify.com/stefano\_seggio/regione-lombardia-grants-registry-monitor/changelog.md

# Actor input Schema

## `onlyNew` (type: `boolean`):

When true (default), only NEW\_LISTING, STATUS\_CHANGE and UPDATED events are delivered - never a BASELINE\_SNAPSHOT or SNAPSHOT\_NO\_DIFF row. When false, every bando currently in scope is delivered every run, including the free first-run baseline and unchanged rows - useful to preview the full dataset before committing spend.

## `eventTypes` (type: `array`):

Restricts which of the three charged event types are delivered. Leave empty to deliver all three. Has no effect on BASELINE\_SNAPSHOT/SNAPSHOT\_NO\_DIFF, which are controlled solely by onlyNew.

## `statusFilter` (type: `array`):

Deliver only bandi whose computed status (derived by this Actor from apertura\_adesione/chiusura\_adesione against the run's own clock - the source itself publishes no lifecycle-status column) is one of these. This is a delivery-time filter only - every bando is still tracked internally regardless, so one that transitions into or out of your filtered status is still correctly reported as STATUS\_CHANGE the moment it does. Leave empty to deliver all statuses.

## `policyAreaFilter` (type: `array`):

Deliver only bandi classified into one of these policy areas by this Actor's own taxonomy mapper (built from the real, observed direzione\_generale values in the live dataset - a free-text field with no controlled vocabulary in the source itself). Delivery-time only, same tracking guarantee as statusFilter. Leave empty for all areas. See README for the full taxonomy and its source mapping.

## `keyword` (type: `string`):

Full-text search passed server-side as Socrata's own $q parameter, matched against every text column (titolo\_bando, direzione\_generale, ente, tipo\_strumento). Example: "formazione" or "ICT". Leave empty to skip.

## `directorateFilter` (type: `array`):

Deliver only bandi whose direzione\_generale case-insensitively contains one of these values. Free-text matching against whatever Regione Lombardia's own back office typed in, not a stable enum - real values observed live include "ISTRUZIONE, FORMAZIONE, LAVORO", "CULTURA", "ARIA SPA". Delivery-time only. Leave empty for all directorates.

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

Stops the run once this many records have been pushed (and charged, where applicable). Records not yet reached when the limit is hit are simply re-evaluated fresh next run. Leave unset for no limit - the full register is only ~1,912 rows, so an unlimited run is inexpensive even on a full onlyNew:false baseline pass.

## `deltaStateName` (type: `string`):

Names the persistent Key-Value Store this run's delta memory lives in. Use a distinct name per independent schedule/filter configuration so they don't share baseline progress.

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

When true, ignores all previously-remembered bandi for this deltaStateName and re-baselines from scratch (every bando becomes a fresh, uncharged BASELINE\_SNAPSHOT again, subject to onlyNew).

## `socrataAppToken` (type: `string`):

An optional Socrata app token (free, self-service at dati.lombardia.it) sent as the X-App-Token header on every request. Unauthenticated requests share a per-IP throttle Socrata does not fully document publicly. Supplying your own token removes the shared-IP ceiling entirely. Never logged or persisted beyond this run.

## `requestTimeoutSecs` (type: `integer`):

Aborts a single Socrata API request that takes longer than this, so one slow response never stalls the whole run. Capped at 30 (down from an earlier 120) because this Actor's own configured run timeout is 300s and a full baseline walk needs 2 Socrata pages ($limit=1000 against the live ~1,912-row dataset): worst case per page is (maxRetries+1) attempts x this timeout, plus backoff. Even holding maxRetries at its own new maximum of 2 (3 attempts/page), the old 120s ceiling alone gave 2 pages x \[3x120s + ~3.5s backoff] = 727s - over 2x the run timeout before a single row was even parsed. 30 keeps that same worst case at 187s (62% of the 300s budget) with real margin left for per-row processing.

## `maxRetries` (type: `integer`):

Retry attempts (exponential backoff with jitter) before a single page fetch is given up on as failed. Applies only to retryable outcomes (429 and 5xx responses, or a request timeout) - any other non-2xx response is treated as final immediately. Capped at 2 (down from an earlier 8, default down from 4) so this call site's own worst case can never exceed this Actor's configured 300s run timeout: a full baseline walk is 2 Socrata pages, and at the new maxRetries=2 ceiling with requestTimeoutSecs at its own new 30s ceiling, worst case is 2 x \[(2+1)x30s + ~3.5s backoff] = 187s (62% of budget) - still 3 total attempts per page, just no longer enough retries-x-timeout to blow the run's own time budget on their own.

## Actor input object example

```json
{
  "onlyNew": true,
  "eventTypes": [],
  "statusFilter": [],
  "policyAreaFilter": [],
  "keyword": "",
  "directorateFilter": [],
  "maxItems": 20,
  "deltaStateName": "default",
  "resetState": false,
  "requestTimeoutSecs": 30,
  "maxRetries": 2
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("stefano_seggio/regione-lombardia-grants-registry-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 = {}

# Run the Actor and wait for it to finish
run = client.actor("stefano_seggio/regione-lombardia-grants-registry-monitor").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{}' |
apify call stefano_seggio/regione-lombardia-grants-registry-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,stefano_seggio/regione-lombardia-grants-registry-monitor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/f0xRlvzERsbgbU1ru/builds/VfN69YljL8DRbJifF/openapi.json
