# UK Find a Tender: Contract End Dates & Amendments (`felixgoldwin/uk-contract-renewal-monitor`) Actor

Track published contract end dates, renewal terms and amendments in UK Find a Tender records. Monitor an OCID watchlist or update window, compare saved snapshots, and export buyer, supplier and value evidence with notice links. End dates indicate possible renewal windows.

- **URL**: https://apify.com/felixgoldwin/uk-contract-renewal-monitor.md
- **Developed by:** [Felix GoldWin](https://apify.com/felixgoldwin) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 contract evidence rows

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

## UK Public Contract Renewal Window Monitor

Find published contract end dates and changes in official Find a Tender OCDS procurement records. Use an explicit OCID watchlist for repeat monitoring, or discover processes updated in a bounded date window. Outputs include buyer and supplier organization references, contract or award value, CPV category evidence, published end dates, renewal/option descriptions, amendment evidence and official notice links.

A published end date is a **potential renewal window**, never a guaranteed retender. Missing dates, currencies and values stay missing. A missing record is not a cancellation. This Actor does not estimate dates from durations, merge partial releases itself, claim all UK spending is covered, or harvest contact fields.

### Scope and evidence

Only the public `www.find-tender.service.gov.uk/api/1.0/ocdsReleasePackages` and `/ocdsRecordPackages/{ocid}` endpoints are fetched. No credentials, browser/proxy or arbitrary URL input. Read-only requests only, with redirects rejected. Documentation: [data outputs](https://www.find-tender.service.gov.uk/Developer/Documentation), [releases](https://www.find-tender.service.gov.uk/apidocumentation/1.0/GET-ocdsReleasePackages), [records](https://www.find-tender.service.gov.uk/apidocumentation/1.0/GET-ocdsRecordPackages).

Discovery follows validated `links.next` cursors while retaining the exact update range and page size. It then loads the current `compiledRelease` for each discovered OCID. These records are current at fetch time, not historical snapshots as of `updatedTo`. Inputs are normalized to UTC and sent in the API's documented timezone-free `YYYY-MM-DDTHH:mm:ss` format; the source applies the update filter, which is distinct from a release publication timestamp. The API documentation does not state a parameter timezone, so do not use this window as proof of an exact publication-time interval. A complete scan means complete for your configured OCIDs or discovered update window, not a complete UK contract register. Use the **same exact range** when comparing discovery snapshots. For continuing monitoring, copy the discovered OCIDs into an explicit stable watchlist; moving windows intentionally have different scope keys.

Each published contract becomes a row. If no contracts exist, awards become separately labeled rows and only `award.contractPeriod.endDate` can supply their end date. If neither exists, one process row makes the absence of contract evidence visible. Tender submission deadlines are never used as contract end dates. Contract suppliers are joined only through their matching award ID; renewal terms are attached only to explicitly related lots. No inference links an unrelated award or lot. Unmapped extensions remain out of scope; read the linked procurement for additional terms.

### Run locally

Requires Node.js 22 or newer. From this unpacked folder:

```sh
npm ci --ignore-scripts
npm test
npm run validate:schema
npm run smoke
```

The smoke command performs one bounded real public request with `examples/input.json` and writes `artifacts/live-smoke.json`. Fixtures and tests require no network. `npm start` uses the normal Apify SDK input and storage interface; put your input at `storage/key_value_stores/default/INPUT.json` for a local run. Use a fresh `CRAWLEE_STORAGE_DIR` per independent run.

Input examples: [OCID watchlist](examples/input.json), [bounded discovery](examples/discovery-input.json). The sample procurement is a real source example, not an endorsement. No account publication or paid run was performed during development.

### Prefilled public demonstration

The Console and Store-test input prefills watch the verified public example `ocds-h6vhtk-0691e5`, with limits of one process, five rows, three HTTP attempts, 30 seconds, 5 MB per response and a $0.02 event budget. This procurement demonstrates date and notice evidence; it is not presented as an imminent renewal opportunity. `asOfDate` is not prefilled, so the Actor uses the current UTC run date. See [the exact extracted prefill input](examples/prefill-input.json).

Apify's [Store testing documentation](https://docs.apify.com/actors/publishing/test) says its automated check uses schema `prefill` values and expects a successful run with a nonempty default dataset within five minutes. [Prefill semantics](https://docs.apify.com/actors/development/actor-definition/input-schema/specification/v1#prefill-vs-default-vs-required) apply to the input interface; API calls do not inherit them. The existing general defaults and runtime behavior are unchanged. API callers still provide their own explicit OCIDs or date range. No snapshot or fixed historical evaluation date is prefilled.

The schema-derived input was validated and live-tested locally against the public source, including `OUTPUT.diagnostics.completeScan: true`; no hosted Store test or account run was performed at that stage. Hosted verification is recorded below. Public-source changes can still require updating this example.

### Reusing snapshots

Copy the complete `OUTPUT.previousSnapshot` object into the next input's `previousSnapshot`. The Actor accepts only the same canonical source scope, version 1 and `complete: true`. It checks keys, types, date evidence, array sizes and a 1.5 MB input ceiling. Snapshot evidence is strictly projected, prototype-like keys are rejected, and snapshots are capped at 1 MB. There is no global cross-user state, shared named dataset or external history service. You control where you save the output.

When every request succeeds and no bound is hit, `OUTPUT.comparison` contains exact before/after **selected fields** with `newly_observed`, `changed` or `not_observed_in_scope` labels. Source hash and fetch time are kept separately in diagnostics, so a new fetch alone does not manufacture a change. Changes in published release ID/date remain source changes. A disappearance is only an observation within this scope. Baseline runs and unchanged runs emit evidence rows with their corresponding status.

Incomplete/failed scans return `completeScan: false`, an explicit stop reason, no change events, and `previousSnapshot: null`. Partial observed evidence rows may still be useful. No missing row is interpreted as a deletion or cancellation. Oversized text or snapshots are stopped rather than silently truncated and described as exact. A delivery limit also invalidates the reusable snapshot; treat OUTPUT as authoritative before applying dataset changes.

### Output

The default dataset holds `key`, `evidence`, `renewal`, `change`, `completeScan`, `scopeKey`, `observedAt`, `sourceUrl`, and an interpretation note. `evidence.endDate` includes the source string, validated value or null, and `valid`/`missing`/`invalid` status. `renewal` is a UTC evaluation of days to the published end date; it is separate from source change comparison.

The `OUTPUT` key-value record contains diagnostics, scope, every successful source response's URL/time/byte count/SHA256, comparison events (including old-only records), and a reusable snapshot only after a complete scan. [Example output](examples/output.json) is fixture-derived; [live smoke evidence](artifacts/live-smoke.json) is clearly labeled live. Personal contact points, addresses, emails and phone fields are not selected. Buyer/supplier labels are those the source publishes as organizations; the Actor does not certify legal entity status.

### Limits

| Control | Default | Hard maximum |
| --- | ---: | ---: |
| Release page size | 20 | 100 |
| Discovery pages | 5 | 20 |
| Processes | 20 | 50 |
| Result rows | 100 | 250 |
| HTTP attempts including retries | 30 | 100 |
| Retries per request | 2 | 3 |
| Scan wall time | 90 s | 300 s |
| Bytes per response | 5 MB | 10 MB |
| Total response bytes | 20 MB | 20 MB |
| Snapshot evidence | 1 MB | 1 MB |
| Date discovery window | required | 31 days |

Sequential requests bound concurrency to one. Response bytes are hashed before strict UTF-8 decoding; malformed UTF-8 stops the scan instead of replacing source characters. A request has a 20-second timeout further reduced to remaining scan time. The source `Retry-After` for 429/503 is respected; if it cannot fit the remaining run time, the Actor stops. Access denials are never retried or bypassed. Large framework records can exceed response limits and will produce an incomplete diagnostic; narrow the watchlist or raise the explicit byte limit up to the hard maximum. Some official records exceed even the hard ceiling and are intentionally unsupported.

256 MB is the declared default. Local small-sample feasibility is recorded in QA.md, and bounded hosted checks are recorded below. These small-sample checks do not establish maximum-size workload behavior.

### Approved listing rates

User-approved `renewal-record`: **$0.002 per emitted evidence row** ($2/1,000), plus the **approved automatic start price of $0.005 per started 1 GB memory unit**. This approved listing rate covers baseline request setup and evidence normalization on very small runs. It is not the platform default: the documented automatic-start default at verification was $0.00005; no account price was changed during that local verification. The approved record rate reflects compiled-record retrieval, date/evidence projection and snapshot comparison; it is not a validated demand or profitability claim. Unchanged/baseline/unknown rows cost the same if this event is configured. No event is charged for OUTPUT alone. An empty scan can still incur the platform's automatic start fee.

These listing rates are configured for this Actor. The code never manually charges `apify-actor-start`. It reads actual configured PPE event prices, refuses a positive synthetic dataset-item price (which would double-bill), reserves any automatic start fee still missing from SDK counters against the platform cap, recomputes budget limits before scanning and each push, and checks charge results. Already charged events on resurrection reduce the application budget. Free configured result events are supported; zero input budget with the normal positive reserve makes no source request.

`maxBudgetUsd` is a strict conservative event-work cap using a start reserve plus result cost; locally it uses the approved listing rates without sending charges. Set the platform maximum total charge too. Compute, storage and other platform fees are outside this input cap. Invalid input can still incur the platform's automatic start charge because that charge precedes code execution. See PUBLISHING.md for verification gates.

### Hosted verification — 2026-10-12

The final runtime was built successfully on Apify as build 0.0.2 (Linux, Node 22.23.3, Apify 3.7.2), then checked in five fresh owner-development runs at 256 MB with a 60-second platform timeout. The Console input form and output/storage views were checked.

- Schema-prefill baseline: one dated contract row, one official request, 145,649 response bytes, complete scan and a reusable snapshot. Run `8sFRaymCMtsDBurSb`.
- Repeat with that exact snapshot: one row, `unchanged`, zero comparison events and another complete snapshot. Run `etdr7UnEWzdfypjgx`. The public source response hash matched the baseline; evaluation used the current UTC date. The evidence end date was `2027-06-30T23:59:59+01:00`, with three official notice links. This example was outside the default renewal window.
- Exact $0.005 platform cap: zero source requests and rows, `budget_limit`, suppressed comparison and no reusable snapshot. Run `HHa6fRZg3f5SGmJsq`.
- Zero application event budget with a $0.02 platform cap: zero source requests and rows, suppressed comparison and no reusable snapshot. Run `FbYHNZ06ONAf12VGb`.
- Exact $0.007 platform cap: capacity one, one delivered row, a complete scan and an unchanged comparison. Run `OFgd1Kxzl64InXyjt`.

The active listing configuration was verified as $0.005 automatic start plus $0.002 per `renewal-record`, with no positive synthetic dataset-item charge and platform usage included for customers. Successful result runs used the platform pay-per-event code path and passed its custom-event count/delivery checks. These owner-development runs incur platform usage; they do not prove a real customer's invoice, payout or profitability. Free-result pricing, controlled source changes, null-date cases and concurrency/race handling remain local fixture/SDK tests rather than additional hosted claims. Small hosted samples do not establish worst-case memory or response-size behavior.

The source was downloaded after import and compared against the tested 41-file package: all filenames and contents matched after outer-whitespace normalization by the Web IDE. Only this documentation was subsequently updated; runtime, dependency and schema contents were not changed. Earlier QA.md and PUBLISHING.md entries describe the historical local-only verification stage.

# Actor input Schema

## `ocids` (type: `array`):

Official FTS OCIDs. Use this list OR a date range. No URLs or credentials. Maximum 50. The prefilled OCID is a public demonstration contract, not a claim that renewal is imminent.

## `updatedFrom` (type: `string`):

ISO date-time with timezone. Required with updatedTo when ocids is empty. Maximum window 31 days.

## `updatedTo` (type: `string`):

ISO date-time with timezone. Discovery identifies updated processes, then reads their current compiled records.

## `asOfDate` (type: `string`):

Optional UTC YYYY-MM-DD. Defaults to run date. Does not affect source change comparison.

## `pageSize` (type: `integer`):

Hard runtime bound. Incomplete scans never generate changes or a reusable snapshot.

## `maxPages` (type: `integer`):

Hard runtime bound. Incomplete scans never generate changes or a reusable snapshot.

## `maxProcesses` (type: `integer`):

Hard runtime bound. Incomplete scans never generate changes or a reusable snapshot.

## `maxResults` (type: `integer`):

Hard runtime bound. Incomplete scans never generate changes or a reusable snapshot.

## `maxRequests` (type: `integer`):

Hard runtime bound. Incomplete scans never generate changes or a reusable snapshot.

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

Hard runtime bound. Incomplete scans never generate changes or a reusable snapshot.

## `maxSeconds` (type: `integer`):

Hard runtime bound. Incomplete scans never generate changes or a reusable snapshot.

## `maxResponseBytes` (type: `integer`):

Hard runtime bound. Incomplete scans never generate changes or a reusable snapshot.

## `renewalWindowDays` (type: `integer`):

Published end dates only; no guaranteed retender claim.

## `maxBudgetUsd` (type: `number`):

Caps estimated/local or configured event charges, including automatic start reserve per 1GB unit. Does not cap platform compute/storage. No manual start event is charged.

## `previousSnapshot` (type: `object`):

Paste only OUTPUT.previousSnapshot from a complete scan for the same OCIDs or exact update window. Do not include secrets. Maximum total input 1.5 MB.

## Actor input object example

```json
{
  "ocids": [
    "ocds-h6vhtk-0691e5"
  ],
  "pageSize": 20,
  "maxPages": 5,
  "maxProcesses": 1,
  "maxResults": 5,
  "maxRequests": 3,
  "maxRetries": 2,
  "maxSeconds": 30,
  "maxResponseBytes": 5000000,
  "renewalWindowDays": 180,
  "maxBudgetUsd": 0.02
}
```

# Actor output Schema

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

No description

## `diagnostics` (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 = {
    "ocids": [
        "ocds-h6vhtk-0691e5"
    ],
    "maxProcesses": 1,
    "maxResults": 5,
    "maxRequests": 3,
    "maxSeconds": 30,
    "maxResponseBytes": 5000000,
    "maxBudgetUsd": 0.02
};

// Run the Actor and wait for it to finish
const run = await client.actor("felixgoldwin/uk-contract-renewal-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 = {
    "ocids": ["ocds-h6vhtk-0691e5"],
    "maxProcesses": 1,
    "maxResults": 5,
    "maxRequests": 3,
    "maxSeconds": 30,
    "maxResponseBytes": 5000000,
    "maxBudgetUsd": 0.02,
}

# Run the Actor and wait for it to finish
run = client.actor("felixgoldwin/uk-contract-renewal-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 '{
  "ocids": [
    "ocds-h6vhtk-0691e5"
  ],
  "maxProcesses": 1,
  "maxResults": 5,
  "maxRequests": 3,
  "maxSeconds": 30,
  "maxResponseBytes": 5000000,
  "maxBudgetUsd": 0.02
}' |
apify call felixgoldwin/uk-contract-renewal-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,felixgoldwin/uk-contract-renewal-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/2tpnNQtV1uMY3qeEU/builds/YGUwitJNPi6D9jnmz/openapi.json
