# Etimad Pre-Planning Radar — Upcoming Saudi Government Projects (`dottti/etimad-preplanning-radar`) Actor

See what a Saudi government agency plans to procure before it reaches tender. Pulls Etimad pre-planning records by agency, with Arabic text repaired so keyword filters actually match.

- **URL**: https://apify.com/dottti/etimad-preplanning-radar.md
- **Developed by:** [Mohanad Alshaka](https://apify.com/dottti) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 planned project delivereds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — 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

## Etimad Pre-Planning Radar — Upcoming Saudi Government Projects

See what a Saudi government agency plans to buy **before it reaches tender**.

Saudi government bodies publish their forward procurement plans on [Etimad](https://tenders.etimad.sa) under التخطيط المسبق: the project, its nature, the planned duration, and the quarter it is expected in. This Actor pulls those plans for the agencies you care about and returns them as clean JSON, CSV or Excel.

Etimad's tender feed tells you what is out for bid today. This tells you what is coming.

### Why an agency is required

Etimad filters pre-planning records by agency code and by nothing else. There is no server-side keyword, year or region filter on this endpoint, and the page size is capped at 24 rows. An unscoped run would have to walk roughly 96,000 records to find anything.

So you pick agencies, either by name or by code, and the Actor pulls their plans. Search by name and it resolves against Etimad's own directory of 1,819 government agencies, with Arabic normalised, so `امانة` finds `أمانة`.

### Newest plans first

Etimad returns pre-planning records **oldest first**. On أمانة منطقة المدينة المنورة, 2,169 plans span 91 pages and the 2026 entries sit on page 90.

When you set a year filter, this Actor walks the pages backwards from the end and stops as soon as it reaches a page older than the year you asked for. On that agency it reads 12 records instead of roughly 2,160, and finishes in about six seconds.

### Arabic text is repaired

Etimad publishes a large share of its text in Arabic Presentation Forms-B (U+FE70–U+FEFF). Those characters look like normal Arabic and are different code points, so a plain keyword search over the raw feed silently misses them.

Every text field is normalised before matching, so `أعمال` matches records published in presentation forms, and `انارة`/`إنارة`, `مستشفي`/`مستشفى`, `صيانه`/`صيانة` all match each other. The original wording is kept in a `raw` object on every record so you can still find it on the portal.

### Output

```json
{
  "agencyCode": "019003001000",
  "agencyName": "أمانة منطقة المدينة المنورة",
  "projectName": "اعمال صيانة مقابر الحسو والقري التابعة لها بلدية الحسو",
  "projectNature": "دراسات",
  "projectDescription": "...",
  "projectTypes": ["انشطة الاستشارات الادارية"],
  "projectTypeIds": [1603],
  "raw": { "projectName": "..." },
  "year": "2026",
  "yearQuarterId": 2,
  "yearQuarterName": "الربع الثانى",
  "durationInDays": 0,
  "durationInMonths": 0,
  "durationInYears": 2,
  "statusName": "معتمد",
  "insideKSAString": "داخل المملكة"
}
```

### Input

| Field | What it does |
| --- | --- |
| `agencySearch` | Agency name substring, matched against Etimad's directory after Arabic normalisation. |
| `agencyCodes` | Exact agency codes if you already have them, for example `018014000000`. |
| `keywords` | Matched against project name, nature, description and type. |
| `matchMode` | `any` (default) or `all`. |
| `years` | Plan years to keep, for example `["2026"]`. Also switches on newest-first scanning. |
| `quarterIds` | `1` الربع الأول, `2` الربع الثاني, `3` الربع الثالث, `4` الربع الرابع. |
| `maxAgencies` | Cap on how many agencies a broad name search expands to. |
| `maxItems` | Hard cap on delivered records, and therefore on cost. |

#### Example: what one municipality plans for 2026

```json
{
  "agencySearch": "أمانة منطقة المدينة المنورة",
  "years": ["2026"],
  "maxItems": 500
}
```

#### Example: IT projects planned across every أمانة

```json
{
  "agencySearch": "أمانة",
  "keywords": ["حاسب", "تقنية", "أنظمة", "برمجيات"],
  "years": ["2026"],
  "maxAgencies": 50,
  "maxItems": 1000
}
```

### Personal data

Etimad's raw pre-planning record includes a `createdBy` string holding the civil servant's full name and national ID number. That is personal data under the Saudi Personal Data Protection Law and it has no bearing on the procurement opportunity, so **this Actor drops it**. It is never written to the dataset.

### Reliability

Etimad throttles bursts with HTTP 429 and answers deep pagination with 5xx. Requests are spaced out and retried with exponential backoff, honouring `Retry-After` when present, so a throttled page is retried rather than reported as an empty result.

### Notes and limits

- Public visitor data. No login, no credentials.
- A plan is a stated intention, not a commitment. Projects get rescheduled, merged or dropped, and the `statusName` field carries Etimad's own status for the record.
- Page size is fixed at 24 by Etimad. Requesting more has no effect.

### Development

```bash
npm install
npm test
node src/main.js
```

# Actor input Schema

## `agencySearch` (type: `string`):

Matched against Etimad's own directory of 1,819 government agencies, after Arabic normalisation, so أمانة also matches امانة. Every matching agency is pulled, up to the agency cap below. Example: أمانة منطقة الرياض

## `agencyCodes` (type: `array`):

Exact Etimad agency codes, if you already know them. Example: 018014000000. Combined with anything found by the name search above.

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

Matched against project name, nature, description and project type, after Arabic normalisation. Leave empty to return the agency's whole plan.

## `matchMode` (type: `string`):

any: keep a project when at least one keyword matches. all: keep it only when every keyword matches.

## `years` (type: `array`):

Keep only projects planned for these years, for example 2026. Etimad's archive goes back to 2020, so set this unless you want the history too.

## `quarterIds` (type: `array`):

Quarter of the plan year: 1 الربع الأول, 2 الربع الثاني, 3 الربع الثالث, 4 الربع الرابع.

## `maxAgencies` (type: `integer`):

Cap on how many agencies a broad name search may expand to. A search like أمانة matches dozens.

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

Hard cap on delivered records, and therefore on cost.

## `requestDelayMs` (type: `integer`):

Etimad returns HTTP 429 on bursts.

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

Retries with exponential backoff when Etimad answers 429 or 5xx.

## Actor input object example

```json
{
  "agencySearch": "أمانة منطقة الرياض",
  "matchMode": "any",
  "maxAgencies": 25,
  "maxItems": 500,
  "requestDelayMs": 700,
  "maxRetries": 5
}
```

# Actor output Schema

## `plannedProjects` (type: `string`):

Projects a Saudi government agency plans to procure: project name, nature, description, project types, plan year and quarter, expected duration and approval status. Personal data present in the source is excluded.

# 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 = {
    "agencySearch": "أمانة منطقة الرياض"
};

// Run the Actor and wait for it to finish
const run = await client.actor("dottti/etimad-preplanning-radar").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 = { "agencySearch": "أمانة منطقة الرياض" }

# Run the Actor and wait for it to finish
run = client.actor("dottti/etimad-preplanning-radar").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 '{
  "agencySearch": "أمانة منطقة الرياض"
}' |
apify call dottti/etimad-preplanning-radar --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dottti/etimad-preplanning-radar"
        }
    }
}
```

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/2iJ43QJa4S2zxwObu/builds/uOJHAMog5ygbfNFdL/openapi.json
