Tender changes · Slack delivery
Send tender deadline changes to Slack with replay checks and delivery evidence.
Replay a real tender deadline extension with pinned source releases, a Slack message preview and a Python delivery ledger. Test duplicate suppression, rate-limit retry and uncertain outcomes.
Government contract Slack alerts need a reproducible change event and a delivery record, not just a webhook POST. This Python workflow compares two real tender releases, builds a message with the old and new deadlines, and prevents an acknowledged event from being sent again when the same input is replayed.
The worked example is the Financial Reporting Council's Audit Qualification Literature Review tender. Its published deadline moved from January 29, 2026 at 12:00 UTC to February 6, 2026 at 12:00 UTC. This is a historical demonstration, not a currently open opportunity.
The download includes the original Find a Tender responses, their hashes, a ready-to-inspect message payload, a SQLite delivery ledger implementation and automated tests. The tests use a local HTTP receiver. No live Slack-channel delivery was performed for this article.
Start with two published deadline values
The two selected releases share the procurement identity ocds-h6vhtk-0601ff. Release 001527-2026, published on January 8, contains the January deadline. Release 007271-2026, published on January 27, contains the February deadline. Both expose the compared field as tender.tenderPeriod.endDate.
The later notice explicitly describes an extension. You can inspect the original selected release, the changed release and the published change notice. The local copies were retrieved on September 28, 2026; retrieval time is not the publication time or the alert time.
The script verifies the downloaded bytes against the saved SHA-256 receipts. It requires matching OCIDs and increasing release publication times before comparing deadlines. It compares timezone-aware timestamps, so a different textual representation of the same instant does not become a deadline-change alert.
A missing deadline is not converted into an extension, a cancellation or an empty-string update. This bounded recipe stops on missing required fields. For a wider feed, send those records to a separate review queue and define their interpretation explicitly.
This implementation compares two selected releases from one process. It is not a Find a Tender collector, a full OCDS release-merging engine or a SAM.gov integration. The government contract tracker guide covers the broader collection and routing design; this page supplies the executable delivery example.
Generate the Slack message without sending it
Unzip the bundle and run these commands from its directory. Python 3.10 or later is sufficient; the script uses only the standard library.
python -B alert.py
python -B test_alert.py
The first command prints a JSON message without contacting Slack or creating a delivery ledger. The second runs the replay and failure tests, including a temporary receiver bound to your own machine.
The message's main content is:
DEADLINE CHANGE — historical replay example
Audit Qualification Literature Review
Before: 2026-01-29T12:00:00Z
After: 2026-02-06T12:00:00Z
Published: 2026-01-27T16:22:27Z
Source: https://www.find-tender.service.gov.uk/Notice/007271-2026
Event: [the generated SHA-256 event key]
The downloadable preview includes the complete event key. The payload uses a plain-text Block Kit section so a source title cannot introduce Slack mention markup. Its top-level fallback text also labels the message as historical. The preview is generated from the same function used by the sender, rather than maintained as a separate mock message.
Keep the old value, new value, source and event key visible in your own notification design. A message that says only “tender updated” forces the recipient to rediscover the change and gives the operator little evidence when investigating a duplicate.
Deduplicate the event and preserve corrections
The event key hashes the source, OCID, before-and-after release identities, field path, deadline values, publication timestamp, schema version and optional superseded-event reference. It excludes retrieval time and presentation text. Replaying the same pinned evidence therefore produces the same key.
Queue the event twice to inspect the replay behavior:
python -B alert.py --enqueue --db delivery.sqlite3
python -B alert.py --enqueue --db delivery.sqlite3
The database's primary key permits one delivery row for that event. Keep this ledger between runs. Deleting it removes the memory of acknowledged messages. Use a separate database for each destination in this small implementation; it does not implement multi-channel routing.
An explicit correction gets its own identity. The --supersedes option accepts the full key of a prior event and adds that reference to the new message. This is an operator-supplied relationship, not an automatically inferred conclusion about why a buyer changed a deadline. The test suite exercises it synthetically; the published preview remains the real deadline extension.
Use a follow-up correction message when an earlier alert needs qualification. Do not treat an incoming webhook as an editing interface: Slack documents that webhook posting does not return the message timestamp, and the webhook cannot delete the posted message. More advanced message lifecycle operations need a different integration. Slack incoming webhook documentation.
Retry known rejections and review uncertain deliveries
Before sending, the worker atomically claims a due row and commits its status as in_flight. Only a response with HTTP 200 and body ok moves it to sent. A subsequent replay leaves that acknowledged event alone.
The implementation distinguishes these outcomes:
- HTTP 429: save
retryand a next-attempt time derived fromRetry-After. If the header is unusable, wait 60 seconds. An early invocation does nothing. - Other HTTP 4xx: save
blocked. Inspect the webhook configuration or payload before another attempt. - Timeout, connection exception, HTTP 5xx or unexpected response: save
uncertain. The example does not automatically retry an outcome that might have reached the destination. - Process crash after claiming a row: leave
in_flightfor operator review. Another run does not blindly reclaim it.
Slack documents HTTP 429 responses and the Retry-After header. Its general message-posting guidance is approximately one message per second per channel, with some burst allowance. This example sends at most one due event per invocation; schedule a single worker at a suitably conservative interval and respect the stored retry time. Slack rate-limit documentation.
This is not an exactly-once guarantee. A process can receive a successful Slack response and crash before recording sent. Conversely, a network timeout can occur after Slack accepted the message. The event key helps an operator reconcile the channel with the ledger, but Slack does not use that custom key as a webhook idempotency token.
For an uncertain row, inspect the channel for the event key. If the message is present, reconcile its local status as sent; if absence is established and a resend is appropriate, reset it to pending under a documented recovery procedure. Keep the ledger and an audit note. Do not resolve uncertainty by deleting the database and replaying everything.
Connect a test channel and send one event
Create or select a Slack app, enable incoming webhooks, and authorize a webhook for a dedicated test channel. The destination comes from the webhook configuration rather than a channel field in this script. Treat the URL as a secret and supply it through your environment or secret manager. Slack's setup instructions.
Set SLACK_WEBHOOK_URL in your environment without adding it to a checked-in file. After reviewing the preview and queuing the event, the following command performs the real outbound action:
python -B alert.py --send --db delivery.sqlite3
It sends one due event to the configured channel. It does not enqueue new events. Running it again after a recorded success prints nothing_due. The command validates the Slack webhook host, refuses redirects and avoids printing network exceptions that could expose the secret URL.
The bundled message is deliberately labelled a historical replay. Leave that label in place for your first channel test. Check that the source URL, both timestamps and event key are readable, and verify the local status only after observing the result. A local test cannot establish that your actual Slack app is authorized or your chosen channel is configured correctly.
Inspect what the replay tests actually prove
The local HTTP test returns 429 with a two-second retry interval on its first request and 200 ok on its second. A simulated clock checks that no request occurs before the interval. Both recorded requests carry the same payload; a later invocation makes no third request after acknowledgment.
Other tests cover repeated enqueue, acknowledged replay, missing and unchanged deadlines, mismatched procurement identity, reverse release order, explicit correction references, permanent rejection, ambiguous timeouts, server errors and interrupted delivery state. These are controlled failure scenarios; they are not claims that the public tender source or Slack produced those failures.
The evidence supports a narrow conclusion: the supplied Python workflow handles the tested states with the pinned event. It does not prove continuous collection coverage, production delivery latency, Slack rendering in your workspace or correctness of arbitrary source amendments.
Connect the example to a maintained event feed
For a recurring service, replace the fixed input adapter with a collector that retains source versions, detects meaningful changes and feeds events into a durable queue. Start with the government tender data Python guide for working with the public dataset, then agree exactly which sources and deadline fields your team needs.
Keep publication time, retrieval time, event-detection time and delivery time separate so latency can be measured honestly. Define an initial-load policy: historical records should populate state without flooding a live team channel. Decide how you handle withdrawn notices, missing fields, corrections and older versions arriving late.
This example owns the Slack delivery step. It does not claim SAM.gov coverage or discover opportunities from keywords. A SAM.gov Slack-alert workflow needs its own source adapter and update strategy before it can reuse the delivery approach.
The public example and source evidence remain free. A managed service should be evaluated on the agreed source scope, collection checks, event semantics, delivery window and recovery responsibilities, rather than access to this one public change.
Frequently asked questions
Does this script send anything when I first run it?
No. Its default mode prints a preview. Enqueuing writes only to the local ledger. Sending requires the explicit --send command and a configured webhook URL.
Was this tested in a real Slack channel?
No. The published test evidence uses a local HTTP receiver plus controlled failure responses. The article includes a separate live test-channel procedure for your own webhook configuration.
Does replay deduplication guarantee exactly-once Slack delivery?
No. It suppresses already acknowledged events in the retained ledger. Network ambiguity and a crash between remote acceptance and the local commit still require reconciliation.
Can I use this for SAM.gov Slack alerts?
The delivery approach can be reused, but the bundled adapter reads two Find a Tender releases. A SAM.gov collector, version history and source-specific change rules are separate work.