<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Public Data Automation]]></title><description><![CDATA[Public Data Automation]]></description><link>https://public-data-automation.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Public Data Automation</title><link>https://public-data-automation.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 11 Sep 2026 22:22:54 GMT</lastBuildDate><atom:link href="https://public-data-automation.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a UK and EU Tender Data Pipeline with Node.js and Apify]]></title><description><![CDATA[Public procurement data is spread across different portals and formats. A useful internal tool needs to normalize those records before a bid or sales team can filter them.
This tutorial builds a small]]></description><link>https://public-data-automation.hashnode.dev/building-a-uk-and-eu-tender-data-pipeline-with-node-js-and-apify</link><guid isPermaLink="true">https://public-data-automation.hashnode.dev/building-a-uk-and-eu-tender-data-pipeline-with-node-js-and-apify</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[web scraping]]></category><category><![CDATA[api]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[MD JAKARIA MIRZA]]></dc:creator><pubDate>Sat, 04 Jul 2026 09:20:16 GMT</pubDate><content:encoded><![CDATA[<p>Public procurement data is spread across different portals and formats. A useful internal tool needs to normalize those records before a bid or sales team can filter them.</p>
<p>This tutorial builds a small pipeline that:</p>
<ol>
<li><p>Collects UK and EU tender records.</p>
</li>
<li><p>Returns structured dataset items.</p>
</li>
<li><p>Filters opportunities by deadline and keyword.</p>
</li>
<li><p>Removes records already processed.</p>
</li>
<li><p>Produces a compact review list.</p>
</li>
</ol>
<h2>Data Sources</h2>
<p>The example uses:</p>
<ul>
<li><p>UK Contracts Finder</p>
</li>
<li><p>EU TED</p>
</li>
<li><p>SAM.gov when an API key is supplied</p>
</li>
</ul>
<p>We will use UK Contracts Finder and EU TED because they can be tested without an additional source API key.</p>
<h2>Run a Small Test</h2>
<p>Use a narrow input first:</p>
<pre><code class="language-json">{
  "sources": ["uk_contracts_finder", "ted"],
  "keywords": ["software"],
  "noticeStatus": "active",
  "maxResults": 10,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
</code></pre>
<p>The important settings are:</p>
<ul>
<li><p><code>sources</code>: procurement systems to query</p>
</li>
<li><p><code>keywords</code>: terms used to narrow the results</p>
</li>
<li><p><code>noticeStatus</code>: limits the search to active opportunities</p>
</li>
<li><p><code>maxResults</code>: controls the maximum dataset size</p>
</li>
<li><p><code>proxyConfiguration</code>: official APIs normally do not require a proxy</p>
</li>
</ul>
<h2>Expected Record Structure</h2>
<p>Each normalized result contains fields such as:</p>
<pre><code class="language-json">{
  "source": "uk_contracts_finder",
  "contractId": "ocds-example-id",
  "title": "Software Support Services",
  "buyerName": "Example Public Authority",
  "buyerCountry": "England",
  "buyerRegion": "London",
  "noticeType": "tender",
  "procurementMethod": "Open procedure",
  "contractValue": null,
  "currency": null,
  "publishedDate": "2026-06-27T05:42:14.000Z",
  "deadlineDate": "2026-07-20T11:00:00.000Z",
  "status": "active",
  "classificationCodes": [],
  "description": "Summary of the procurement requirement",
  "contractUrl": "https://official-procurement-source.example/notice",
  "scrapedAt": "2026-06-28T17:12:43.000Z"
}
</code></pre>
<p>A stable <code>contractId</code> is especially useful because it can be used for deduplication.</p>
<h2>Run It from Node.js</h2>
<p>Install the official API client:</p>
<pre><code class="language-bash">npm install apify-client
</code></pre>
<p>Store your Apify token in an environment variable instead of placing it directly in the source code.</p>
<pre><code class="language-javascript">import { ApifyClient } from 'apify-client';

const client = new ApifyClient({
  token: process.env.APIFY_TOKEN,
});

const input = {
  sources: ['uk_contracts_finder', 'ted'],
  keywords: ['software'],
  noticeStatus: 'active',
  maxResults: 10,
  proxyConfiguration: {
    useApifyProxy: false,
  },
};

const run = await client
  .actor('fascinating_lentil/global-government-contracts-aggregator')
  .call(input);

const { items } = await client
  .dataset(run.defaultDatasetId)
  .listItems();

console.log(`Received ${items.length} records`);
console.dir(items, { depth: null });
</code></pre>
<p>The Actor call waits for the run to finish. The resulting dataset ID is then used to retrieve the structured records.</p>
<h2>Filter Useful Opportunities</h2>
<p>A team may only want active software opportunities with future deadlines.</p>
<pre><code class="language-javascript">const now = new Date();

const relevant = items.filter((item) =&gt; {
  const text = `${item.title ?? ''} ${item.description ?? ''}`.toLowerCase();
  const deadline = item.deadlineDate
    ? new Date(item.deadlineDate)
    : null;

  return (
    item.status === 'active' &amp;&amp;
    text.includes('software') &amp;&amp;
    deadline &amp;&amp;
    deadline &gt; now
  );
});
</code></pre>
<p>This is deliberately simple. Production filters could also use:</p>
<ul>
<li><p>Buyer name</p>
</li>
<li><p>Country or region</p>
</li>
<li><p>Contract value</p>
</li>
<li><p>Notice type</p>
</li>
<li><p>Procurement stage</p>
</li>
<li><p>Classification or CPV codes</p>
</li>
<li><p>Deadline window</p>
</li>
</ul>
<h2>Prevent Duplicate Alerts</h2>
<p>Save processed contract IDs and exclude them from later runs.</p>
<pre><code class="language-javascript">const previouslySeen = new Set([
  'ocds-previously-processed-id',
]);

const newOpportunities = relevant.filter(
  (item) =&gt; !previouslySeen.has(item.contractId),
);
</code></pre>
<p>For a small workflow, the IDs can be stored in a JSON file, spreadsheet, or database table. A larger system can use a persistent key-value store.</p>
<h2>Create a Compact Digest</h2>
<p>Bid teams rarely need every field in the first notification. A smaller object is easier to review.</p>
<pre><code class="language-javascript">const digest = newOpportunities.map((item) =&gt; ({
  title: item.title,
  buyer: item.buyerName,
  region: item.buyerRegion ?? item.buyerCountry,
  value: item.contractValue,
  currency: item.currency,
  deadline: item.deadlineDate,
  source: item.source,
  officialUrl: item.contractUrl,
}));

console.table(digest);
</code></pre>
<p>The resulting digest can be sent through email, Slack, a webhook, or another workflow tool.</p>
<h2>Scheduling the Pipeline</h2>
<p>A basic automated workflow can run once per day:</p>
<ol>
<li><p>Run the collection job.</p>
</li>
<li><p>Read the resulting dataset.</p>
</li>
<li><p>Apply keyword and deadline filters.</p>
</li>
<li><p>Remove previously seen contract IDs.</p>
</li>
<li><p>Send only new opportunities.</p>
</li>
<li><p>Save the newly processed IDs.</p>
</li>
</ol>
<p>Start with a narrow keyword and a low result limit. Expand only after checking the relevance of the output.</p>
<h2>Important Limitations</h2>
<p>Public procurement records are not always complete.</p>
<ul>
<li><p>Some notices omit contract values.</p>
</li>
<li><p>Classification codes may vary between sources.</p>
</li>
<li><p>Deadlines can be updated after publication.</p>
</li>
<li><p>Duplicate or related notices may appear under different IDs.</p>
</li>
<li><p>The official notice should remain the final source of truth.</p>
</li>
</ul>
<p>A pipeline should therefore help prioritize opportunities, not replace procurement review.</p>
<h2>Conclusion</h2>
<p>The useful engineering problem is not simply collecting tenders. It is converting several source formats into stable records that can be filtered, deduplicated, and delivered consistently.</p>
<p>The Actor used for this technical example is available here:</p>
<p><a href="https://apify.com/fascinating_lentil/global-government-contracts-aggregator">Global Government Contracts &amp; Tenders Scraper</a></p>
]]></content:encoded></item></channel></rss>