# AusTender Contract Expiry & Recompete Monitor (`automa-flow/austender-contract-expiry-monitor`) Actor

Track AusTender contract expiry, extensions and supplier changes. Monitor Australian Government contracts by CN ID, agency or supplier, with persistent baselines and 365/180/90/60/30-day recompete alerts. Export changes as JSON, CSV or an API feed.

- **URL**: https://apify.com/automa-flow/austender-contract-expiry-monitor.md
- **Developed by:** [Vadim Bezrukov](https://apify.com/automa-flow) (community)
- **Categories:** Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $15.00 / 1,000 recompete signal or contract 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?

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

## AusTender Contract Expiry & Recompete Monitor

Monitor Australian Government contract expiry on AusTender and get verified
alerts for renewal windows, extensions, value changes and supplier changes.
Use exact Contract Notice IDs or discover a portfolio by supplier, agency,
UNSPSC category and value. Export baseline records and subsequent changes as
JSON, CSV or an API feed for a CRM, bid pipeline or procurement dashboard.

A persistent baseline makes daily monitoring useful: `changesOnly` returns the
changes since your previous run. A quiet check costs $0.10 and writes no rows.
Market discovery additionally requires a $1.49 setup per monitor key and filter;
exact-ID watches have no setup fee. See [pricing](#pricing) for result charges.

### What a recompete window is

A **recompete window** is a countdown, not a prediction.

```text
days_to_expiry = contract_end_date - today   (Australian calendar)
```

You choose the thresholds - 365, 180, 90, 60 and 30 days by default. The first
time a watched contract crosses from **outside** a threshold to **inside** it,
you get one signal:

```text
previous run:  days_to_expiry = 186
this run:      days_to_expiry = 179   ->  ENTERED_180_DAY_RECOMPETE_WINDOW
```

The end date itself is the last threshold: when a watched contract passes it you
get one `CONTRACT_EXPIRED` signal, on the same once-only rule.

And then never again for that threshold, for as long as the end date stands.
The standing position is always in `days_to_expiry` and `recompete_windows`, so
nothing is hidden - the *event* is reserved for real movement. If an extension
pushes the contract back outside a window, that window can fire again when the
calendar catches up, and the extension is linked in the same change record.

A recompete window says a contract is approaching its published end date. It is
**not** a statement that a replacement tender will be run, or when.

### What it detects

| Signal | Meaning |
| --- | --- |
| `ENTERED_365_DAY_RECOMPETE_WINDOW` … `ENTERED_30_DAY_RECOMPETE_WINDOW` | The contract crossed into one of your windows. One event name per configured threshold. |
| `NEW_MATCH` | A contract entered the watched portfolio - a new award, or an amendment that brought it into your filter or your horizon. |
| `CONTRACT_END_CHANGED` + `CONTRACT_EXTENDED` / `CONTRACT_SHORTENED` | The published end date moved, and which way. |
| `VALUE_CHANGED` | The published contract value or currency moved. |
| `SUPPLIER_CHANGED` | The awarded supplier, their ABN or their country changed. |
| `AGENCY_CHANGED` | The procuring entity or its ABN changed. |
| `STATUS_CHANGED` | active, cancelled, pending or terminated. |
| `UNSPSC_CHANGED` | The category codes changed. |
| `PROCUREMENT_METHOD_CHANGED` | Open or limited tender, or the limited-tender exemption flag. |
| `TITLE_CHANGED`, `DESCRIPTION_CHANGED`, `CONTRACT_START_CHANGED` | The remaining published fields. |
| `CONTRACT_EXPIRED` | The contract reached its published end date. Fires once, and re-arms if an extension puts it back in the future. |
| `NOT_FOUND` / `FOUND` | A watched Contract Notice left, or returned to, AusTender. |

Every signal is confirmed against the live source before it is emitted. A
threshold crossing computed from a stale stored end date is exactly the alert a
capture team must not receive.

### Who uses it

- **Suppliers to the Australian Government** watching their own contracts, so a
  renewal conversation starts before the incumbent advantage evaporates.
- **Capture and bid teams** watching a competitor's or an agency's expiring
  portfolio, for pre-tender engagement.
- **Procurement-intelligence and market-research teams** building an Australian
  government contract monitor without maintaining state themselves.
- **Developers** who want an Australian Government procurement API feed that
  emits deltas rather than snapshots.

### 30-second quickstart

The Console prefill checks two exact Contract Notice IDs in `snapshotAndChanges`
mode under the `demo` monitor key. It reads no market windows and returns verified
records on both the first and repeat runs. For a scheduled changes-only watch,
use your own monitor key and `changesOnly` mode.

Watch two contracts you care about:

```json
{ "contracts": ["CN4258099", "CN4265960"] }
```

Or watch a market:

```json
{
  "agencyNames": ["Department of Defence"],
  "unspscPrefixes": ["43"],
  "minValueAud": 1000000,
  "lookaheadDays": 365,
  "monitorKey": "defence-it"
}
```

Run it once to establish the baseline, then schedule it daily. From the second
run on, the dataset contains only signals and changes.

### Choose your monitoring workflow

| Workflow | Starting input | First run and later runs |
| --- | --- | --- |
| [Contract watchlist](https://apify.com/automa-flow/austender-contract-expiry-monitor/examples/austender-contract-watchlist) | Replace `CN4258099` and `CN4265960` with your CN IDs; use `snapshotAndChanges`. | First baseline, then current records and verified changes on every run. No market setup fee. |
| [Defence ICT expiry](https://apify.com/automa-flow/austender-contract-expiry-monitor/examples/austender-defence-ict-expiry) | `agencyNames: ["Department of Defence"]`, `unspscPrefixes: ["43"]`, `minValueAud: 1000000`, `lookaheadDays: 31`, `recompeteWindowsDays: [30]`. | Near-term baseline, then changes and 30-day crossings in `changesOnly` mode. |
| [Supplier expiry by ABN](https://apify.com/automa-flow/austender-contract-expiry-monitor/examples/austender-supplier-contract-expiry) | Replace Dell Australia's `supplierAbns: ["46003855561"]` with your supplier's ABN; use `lookaheadDays: 90`, `recompeteWindowsDays: [90, 60, 30]`. | Baseline across agencies, then changes and window crossings in `changesOnly` mode. |

Start each portfolio with its own `monitorKey`, retain it on later runs, and
leave unrelated filter arrays empty. Keep `baselineMode: "emitSnapshot"` for a
visible first result. Market workflows incur setup; see [pricing](#pricing).
On quiet market checks the dataset may be empty. Check `RUN_SUMMARY` for verified
coverage and `status` / `error` for failed or skipped work before interpreting
an empty result. Discovery samples roll forward with the run date; a supplier
with no upcoming contracts can legitimately return no matches.

### Sample recompete signal

```json
{
  "record_type": "CHANGE",
  "status": "SUCCESS",
  "monitor_key": "defence-it",
  "cn_id": "CN4258099",
  "title": "3000657415",
  "description": "Ammunitions",
  "supplier": "RWM ITALIA SPA",
  "agency": "Department of Defence",
  "contract_end": "2027-03-30T13:00:00Z",
  "contract_end_date": "2027-03-31",
  "days_to_expiry": 179,
  "recompete_windows": [365, 180],
  "value_aud": 4337124.0,
  "currency": "AUD",
  "change_types": ["ENTERED_180_DAY_RECOMPETE_WINDOW"],
  "changes": { "recompete_windows_entered": [180] },
  "source_url": "https://api.tenders.gov.au/ocds/findById/CN4258099",
  "observed_at": "2026-10-03T02:00:00Z"
}
```

An extension looks like this:

```json
{
  "record_type": "CHANGE",
  "cn_id": "CN4263346",
  "amendment_id": "CN4263346-A2",
  "change_types": ["CONTRACT_END_CHANGED", "CONTRACT_EXTENDED", "VALUE_CHANGED"],
  "changes": {
    "contract_end": { "previous": "2026-10-01T14:00:00Z", "current": "2026-10-07T13:00:00Z" },
    "value_aud": { "previous": "32004", "current": "61722" }
  }
}
```

### Watch exact contracts

The safest monitoring primitive. Every listed Contract Notice is checked on every
run, whatever the market filters say.

```json
{
  "contracts": ["CN4258099", "cn4265960", "CN4220252-A1"],
  "monitorKey": "our-contracts",
  "mode": "changesOnly"
}
```

Case, spaces, a leading `#` and a trailing amendment suffix are normalized away,
so `CN4220252`, `cn4220252` and `CN4220252-A1` are all the same contract - an
amendment ID resolves to its parent's whole release chain. A repeated ID is
fetched, charged and compared once. Up to 1,000 per monitor key.

### Watch a supplier, agency or category

```json
{
  "supplierNames": ["Accenture"],
  "agencyNames": ["Department of Defence", "Services Australia"],
  "unspscPrefixes": ["43", "8111"],
  "keywords": ["cloud", "software"],
  "minValueAud": 250000,
  "lookaheadDays": 365,
  "recompeteWindowsDays": [365, 180, 90, 60, 30],
  "monitorKey": "defence-cloud"
}
```

The filter groups combine like this, and this is the only rule to learn:

```text
(supplierNames OR supplierAbns)
  AND (agencyNames OR agencyAbns)
  AND (any unspscPrefixes)
  AND (any keywords)
  AND value >= minValueAud
```

An empty group is not a condition. Names match case-insensitively as substrings;
ABNs match exactly.

#### How the first run differs from every run after it

A market watch has to discover its portfolio before it can monitor it, and the
AusTender API only filters by date - so discovery means reading the expiry
horizon. That is not cheap. Measured against the live API:

| Expiry horizon read | Requests | Contracts seen | Downloaded | Wall time |
| --- | --- | --- | --- | --- |
| next 90 days | 148 | 8,603 | 27 MB | 8 min |
| next 180 days | 310 | 16,874 | 56 MB | 18 min |
| next 365 days | 687 | 37,229 | 124 MB | 40 min |

So the horizon is walked **soonest expiry first**, and one run covers at most
180 days of it. A first run aims to deliver the contracts inside every
default recompete window (30, 60, 90 and 180 days) in about twenty minutes in the source-only benchmark, and
the rest of a 365-day horizon arrives over the next two runs. The summary says so
(`horizon_capped`), and coverage is recorded only for the range that was actually
read - so a capped run is a shorter range, never a skipped one.

Every run after discovery reads only the newly uncovered tail of the horizon plus
the contracts AusTender says changed since the stored cursor. Runtime depends on
source volume and latency. Measured against the live API, a 31-day Defence market watch cost 71
requests and 11 MB to establish, and 13 requests and 2 MB to run again. New
awards and newly matching contracts arrive through that second, cheap path: a
contract that enters your filter between horizon scans is picked up from the
changed-contracts feed, not by re-reading the year. That is what makes a daily
schedule cheap, and it is the reason this Actor keeps state.

The same bound protects a monitor that has been paused: reconciliation catches up
two weeks at a time, with the regular $0.10 fee on each catch-up run, and the
cursor still advances only over what was actually read.

Changing the filter under the same monitor key re-discovers the horizon (the
previously covered range was read with the old filter). The contracts already in
the portfolio stay in it; use a new `monitorKey` for a clean portfolio.

### Schedule it, and get a webhook when something moves

Schedule the Actor daily or weekly in the Apify console (Actor → Schedules), then
add a webhook on **Run succeeded** pointed at your own endpoint:

```json
{
  "eventTypes": ["ACTOR.RUN.SUCCEEDED"],
  "requestUrl": "https://hooks.example.com/austender-recompete",
  "payloadTemplate": "{\"runId\":{{resource.id}},\"datasetId\":{{resource.defaultDatasetId}}}"
}
```

Then read only the signals:

```text
GET https://api.apify.com/v2/datasets/{datasetId}/items?view=recompete&clean=true
```

In `changesOnly` mode that dataset is empty on a quiet day, so an empty response
is itself the "nothing to do" answer.

### Pricing

All prices are USD, pay per event, with the same rates on every Apify plan.
Actor compute and source transfer are included in these event prices.

| Event | Price | Charged when |
| --- | --- | --- |
| `monitor-run` | $0.100 | Once after at least one conclusive contract lookup or date window, including a verified quiet check. |
| `market-setup` | $1.490 | Once per monitor key and market filter after its first healthy discovery stage. Includes continuation through up to 730 days and later rolling discovery. Exact-ID-only watches have no setup fee. |
| `contract-observed` | $0.003 | Each complete baseline or snapshot written to the dataset. |
| `contract-signal` | $0.015 | Each verified change record written, even if it contains several change types. |

```text
Demo with two exact contracts:      $0.10 + 2 x $0.003 = $0.106
100-contract exact baseline:        $0.10 + 100 x $0.003 = $0.40
Quiet daily run:                    $0.10 (30 days = $3.00)
A day with 10 change records:       $0.10 + 10 x $0.015 = $0.25
Market setup with 100 baselines:    $1.49 + $0.10 + 100 x $0.003 = $1.89
365-day discovery over three runs,
50 first-run baselines and
50 later NEW_MATCH records:         $1.49 + 3 x $0.10 + 50 x $0.003 + 50 x $0.015 = $2.69
```

Setup is charged even when a healthy date range contains no matches for your
filter. It is not a guarantee that qualifying contracts exist. Changing market
filters or choosing a new `monitorKey` requires a new setup; increasing only
`lookaheadDays` continues the purchased discovery. Additional runs have the
regular run fee. A failed or suspicious discovery stage does not incur setup.
Only the first successful run establishes the monitor's initial baseline.
Contracts first verified on later runs are `NEW_MATCH` changes, including those
found in a continuation stage or recovered after an unverified first lookup.
They use the $0.015 signal price. `baselineMode: storeOnly` suppresses only the
initial baseline; it does not suppress later `NEW_MATCH` records.

Set the run charge limit to at least **$0.115 for an exact watch** or **$1.605
for a new market watch**, plus enough for the records you expect. The Console
default is $5.00. Budget is checked before source work. A limit that cannot cover
the required fees and one signal produces `budget_preflight_blocked: true` in
`RUN_SUMMARY`, no source requests and no charges. Individual unfunded records
become `SKIPPED`; their baselines and discovery positions are preserved.
At the 1,000-contract ceiling, the maximum record bill is $15.00 when every
record is a change: $15.10 including the check, or $16.59 if setup is also due.
Apify may lower the requested limit to the account's remaining credits.

**Never charged as records:** retries, failed or malformed responses, duplicate
CN IDs, suppressed unchanged comparisons, rejected input, discarded anomalies,
or unfunded rows. A standing `NOT_FOUND` has no record fee; disappearance of a
previously found contract is a verified change and has the signal fee. Source
failure for the entire run incurs none of the four events. Internal pages,
requests and state writes have no separate customer charge.

Source-only measurements above are development observations. Private hosted
verification at 512 MB returned 100 exact records in 35 seconds and built a
31-day filtered portfolio in about three minutes. These are individual runs,
not service guarantees or paid-customer revenue measurements.

### Failure semantics

These four states are different, and this Actor never collapses them:

```text
NO_MATCH  !=  NO_CHANGE  !=  NOT_FOUND  !=  SOURCE_FAILED
```

| Situation | `status` | What happens to your baseline |
| --- | --- | --- |
| A bounded date search with no matching contracts | window reported empty | untouched |
| The contract exists and nothing meaningful changed | `SUCCESS`, no `change_types` | updated |
| AusTender confirms there is no such Contract Notice | `NOT_FOUND` | updated (and reported as a change if it used to exist) |
| 429 or 5xx after the retry budget, a timeout, malformed JSON, an HTML body behind HTTP 200, or a response missing OCDS identity | `SOURCE_FAILED` | **untouched** |
| Most of the portfolio appears to vanish or lose its dates at once | `PARTIAL` | **untouched** - treated as a source incident, not a mass removal |
| An amendment's position in the chain cannot be read | `SOURCE_FAILED` | **untouched** - publishing the superseded parent as current would understate an amended contract |

AusTender itself reports "no records found" as **HTTP 400** with `errorCode: 100`
rather than as an empty list. This Actor reads that error code: code 100 is an
answer, every other 400 is a fault. Getting that backwards is how a monitor
reports an empty market as an outage, or a broken request as an empty market.

One failed contract never denies the others their answer, and a failed date slice
never advances the incremental cursor past the data it could not read - the next
run re-reads it. If a window succeeds but a contract lookup fails, its CN ID is
saved for a direct recheck on the next market-watch run, even after the release
leaves the last-modified overlap. Pending CN IDs reserve portfolio capacity and
are counted as `pending_rechecks` in `RUN_SUMMARY`. Invalid contract dates are
source failures, and failed checks retain their baseline even after its normal
retention period. A run with only invalid CN IDs leaves monitor state untouched.
The same cursor rule covers the run charge limit: a run that discovers
contracts it cannot fund rows for keeps its discovery position where it was, so
those contracts are found again rather than silently skipped.

### Data and source limitations

- **Contract Notices only.** Not open tenders, not Approaches to Market, not
  planned procurements, not standing offers, not state or territory portals.
- **Only what AusTender publishes.** No company enrichment, no contacts, no
  AI scoring, no summaries, no predictions about future tenders.
- **Dates are Australian.** AusTender publishes contract periods as the UTC
  instant of Australian local midnight, so `contract_end` keeps the source
  instant verbatim and `contract_end_date` is the Australian calendar date it
  means. Reading the UTC date of the instant would be one day early.
- **A contract's current state is its highest-numbered amendment.** Some
  amendments are superseded by later ones, including amendments marked
  `cancelled`, so this Actor never reads a chain by date order.
- **No modification timestamp exists.** The API stamps a release with its own
  publication date, so incremental reconciliation re-reads a 24-hour overlap and
  deduplicates by release id.
- **One monitor key watches at most 1,000 contracts.** A market watch that
  matches more says so in an explicit overflow row rather than silently
  monitoring a different sample each run. Once the portfolio is full the Actor
  stops scanning the horizon for new contracts it could not admit anyway;
  everything already watched keeps being monitored.
- **No personal contact data is collected.** AusTender publishes agency officer
  names, emails and phone numbers in `parties[].contactPoint`; this Actor never
  reads that object.

### Use it from the API or MCP

For a short exact watch, run and read records in one request:

```bash
curl -X POST "https://api.apify.com/v2/acts/automa-flow~austender-contract-expiry-monitor/run-sync-get-dataset-items?maxTotalChargeUsd=0.20&timeout=120&clean=true" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"contracts":["CN4258099","CN4265960"],"monitorKey":"api-demo","mode":"snapshotAndChanges"}'
```

Use asynchronous `POST /v2/acts/automa-flow~austender-contract-expiry-monitor/runs`
for market discovery, which can exceed the synchronous API timeout. Keep the
same monitor key on scheduled runs and read `RUN_SUMMARY` before acting on data.

The run summary - source requests, windows read, failed windows, cursor position,
signal tallies and what was charged - is in the key-value store:

```text
GET https://api.apify.com/v2/key-value-stores/{storeId}/records/RUN_SUMMARY
```

Connect an MCP client using Streamable HTTP and your Apify account (OAuth or
a Bearer token). The direct tool endpoint is:

```
https://mcp.apify.com?tools=automa-flow/austender-contract-expiry-monitor
```

Example agent request:

> Run automa-flow/austender-contract-expiry-monitor for CN4258099 and CN4265960,
> monitorKey capture-team, mode changesOnly. Establish a baseline if none exists;
> otherwise show verified changes and cite source\_url. Check RUN\_SUMMARY and
> report source failures separately. Do not infer a future tender from an expiry.

The first `changesOnly` run writes baselines; a quiet repeat may have an empty
dataset. AI clients should inspect `record_type`, `status` and `change_types`,
then fetch the summary using the returned run/storage identifiers.

Output surfaces: the `recompete` dataset view for expiry alerts, `changes` for
what moved and by how much, `observations` for the current record of every
watched contract, `diagnostics` for anything unverified, and `RUN_SUMMARY` in the
key-value store for run health.

### Frequently asked questions

#### How does this differ from a general AusTender scraper?

It stores a comparison baseline for AusTender contracts. After the first run,
your dataset can contain only verified changes and threshold crossings, so an
Australian government contract monitor does not need to reconstruct history
from repeated exports.

#### Can it find contract renewal opportunities?

It highlights published expiry dates and government contract recompete windows.
These are research signals for a bid pipeline, not proof that an agency will
issue a replacement tender.

#### Can I use it as an AusTender supplier monitor?

Yes. Select supplier names or ABNs, optionally combined with agency, category
and value filters. Exact CN IDs are always checked independently of filters.
The official Australian Government procurement API provides the source records;
the Actor adds persistent comparison state and verified change records.

### Attribution and status

Data comes from the official **AusTender OCDS API**
(`https://api.tenders.gov.au`), published by the Australian Government Department
of Finance under a **Creative Commons Attribution 3.0 Australia (CC BY 3.0 AU)**
licence. Every dataset row carries `source`, `source_url`, `source_publisher` and
`source_licence` so attribution travels with the data.

**This Actor is not affiliated with, endorsed by, or connected to AusTender, the
Department of Finance, or the Australian Government.** It reads a public API.
Nothing it returns is legal, procurement-compliance or financial advice, and a
recompete window is not a statement that any tender will be run.

### Technical source details

- Endpoints used: `GET /ocds/findById/{cn}` for the authoritative amendment
  chain, `GET /ocds/findByDates/contractEnd/{from}/{to}` for expiry discovery,
  and `GET /ocds/findByDates/contractLastModified/{from}/{to}` for incremental
  reconciliation. All anonymous, all plain HTTPS.
- No API key, no login, no cookies, no browser, no proxy, no CAPTCHA solving.
- Pagination is cursor-based at 100 releases per page; the absence of the `links`
  key is the only end-of-results signal, and off-host `next` links are refused.
- Retries: at most 3 attempts for transport errors, 429 and retryable 5xx, with
  exponential backoff, jitter, and `Retry-After` honoured up to 60 seconds.
  Deterministic 4xx is never retried. Three requests in flight at most.
- State: one compact record per monitor key in the Apify key-value store,
  holding each watched contract's last **successful** observation, its
  fingerprint, the recompete windows already emitted, how far the expiry horizon
  has been discovered, the last-modified cursor and pending CN rechecks. Two runs
  on one monitor key cannot overlap. Hard crashes keep a fail-closed lock. Stop
  all runs for that key and reconcile the interrupted dataset and charges before
  removing its specific lock marker. Automatic restart is disabled; do not
  blindly resurrect a run that already delivered records.
- Every row carries a SHA-256 `fingerprint` over the semantic fields only, plus
  `observed_at` and `schema_version`, so rows are append-only observations that
  join to their own history.

# Actor input Schema

## `contracts` (type: `array`):

Exact Contract Notice IDs, the safest thing to monitor. Case, spaces, a leading # and a trailing amendment suffix are normalized away, so CN4258099, cn4258099 and CN4258099-A1 are the same contract - an amendment ID resolves to its parent's whole release chain. A repeated ID is fetched, charged and compared once. Every listed contract is checked on every run.

## `supplierNames` (type: `array`):

Case-insensitive partial match on the awarded supplier's name. Combined with supplier ABNs as OR.

## `supplierAbns` (type: `array`):

Exact match on the supplier's 11-digit ABN as AusTender publishes it. Spaces and dashes are normalized away. The ABN check digit is deliberately not validated: a wrong ABN simply matches nothing, and a false rejection would silently stop a monitor.

## `agencyNames` (type: `array`):

Case-insensitive partial match on the procuring entity. Combined with agency ABNs as OR.

## `agencyAbns` (type: `array`):

Exact match on the procuring entity's 11-digit ABN.

## `unspscPrefixes` (type: `array`):

Digits only. A prefix matches any UNSPSC code on the contract, so 43 covers all IT and telecommunications categories and 4323 narrows to software.

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

Case-insensitive partial match on the contract title or description. Bounded text matching only - no semantic search.

## `minValueAud` (type: `integer`):

Only watch contracts published at or above this value. A contract AusTender publishes without a value never clears the floor.

## `lookaheadDays` (type: `integer`):

How far ahead the market watch looks for expiring contracts. Discovery covers at most 180 days per run, soonest expiry first; later runs continue uncovered ranges and reconcile modifications. Ignored for exact Contract Notice IDs, which are always checked.

## `recompeteWindowsDays` (type: `array`):

Emit a signal when a contract crosses from outside one of these windows to inside it. Each threshold fires once and is not repeated while the end date stands; if an extension pushes the contract back outside a window, that window can fire again when the calendar catches up. Values must be within the expiry horizon; the defaults are clipped to it automatically. Leave empty to use 365, 180, 90, 60 and 30 days clipped to lookaheadDays.

## `monitorKey` (type: `string`):

Names the stored baseline this run compares against. Use one key per portfolio (for example defence-it). Changing it starts a fresh baseline. Two runs sharing a key never run at the same time; the second waits or fails rather than corrupting the baseline.

## `mode` (type: `string`):

changesOnly returns the first-run baseline and, after that, only recompete signals and contract changes - and it only re-reads contracts that could have moved, which is what makes a scheduled run cheap. snapshotAndChanges re-verifies every watched contract on every run and also returns the unchanged ones.

## `baselineMode` (type: `string`):

emitSnapshot returns the current record for every contract discovered on the first run. storeOnly saves the baseline silently, so the first scheduled alert is a real signal rather than a full export.

## Actor input object example

```json
{
  "contracts": [
    "CN4258099",
    "cn4265960"
  ],
  "supplierNames": [
    "Accenture"
  ],
  "supplierAbns": [
    "62 950 639 680"
  ],
  "agencyNames": [
    "Department of Defence"
  ],
  "unspscPrefixes": [
    "43",
    "81111"
  ],
  "keywords": [
    "cloud",
    "software"
  ],
  "lookaheadDays": 365,
  "monitorKey": "demo",
  "mode": "snapshotAndChanges",
  "baselineMode": "emitSnapshot"
}
```

# Actor output Schema

## `recompete` (type: `string`):

One row per change type; unwinding omits rows with no change types, including baselines and diagnostics. Filter change\_types for ENTERED\_ to isolate expiry-window signals.

## `changes` (type: `string`):

Field-level movement behind every change record: extensions, end-date, value, supplier, agency, status, category and procurement-method changes.

## `observations` (type: `string`):

In snapshotAndChanges, the current record for every watched contract. changesOnly suppresses unchanged observations; storeOnly can suppress the first-run baseline.

## `diagnostics` (type: `string`):

Failure-focused columns for all rows; filter the status field client-side.

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

Everything this run wrote, unprojected.

## `runSummary` (type: `string`):

Source requests, windows read, failed windows, cursor position, signal and change tallies, and what was charged.

# 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 = {
    "contracts": [
        "CN4258099",
        "CN4265960"
    ],
    "monitorKey": "demo",
    "mode": "snapshotAndChanges"
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/austender-contract-expiry-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 = {
    "contracts": [
        "CN4258099",
        "CN4265960",
    ],
    "monitorKey": "demo",
    "mode": "snapshotAndChanges",
}

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/austender-contract-expiry-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 '{
  "contracts": [
    "CN4258099",
    "CN4265960"
  ],
  "monitorKey": "demo",
  "mode": "snapshotAndChanges"
}' |
apify call automa-flow/austender-contract-expiry-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automa-flow/austender-contract-expiry-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/H2oxmrXRMCWcX2PoY/builds/04hDOsNz2g9tUnfRf/openapi.json
