Scheduled Release Page Embargo Leak and Stale-Preview Gate
Pricing
from $5.00 / 1,000 run starteds
Scheduled Release Page Embargo Leak and Stale-Preview Gate
Check each scheduled release page from outside, inside its release window. The Actor fails a gate when the outcome content is public before the release time, or when the preview content is still public after it. It compares the CDN copy with a cache-buste
Pricing
from $5.00 / 1,000 run starteds
Rating
0.0
(0)
Developer
kingii98
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
Share
Prove two facts for each scheduled release:
- The outcome content was not public before the release time.
- The preview content was gone after the release time.
Statistics publishers, investor-relations teams and research publishers release content at a fixed time. A deploy that is early, a preview page that stays in production, or a CDN that serves an old copy can break the embargo. Your own pre-publication check runs in your repository. This Actor checks the live site from outside, as a reader sees it.
Put the Actor on an Apify schedule every 5 minutes across the release window. In each run, the Actor requests each release URL that is inside its window. It searches the page for your preview markers and your outcome markers, and it gives a verdict. It also requests a cache-busted copy, so you can see a difference between the CDN copy and the fresh copy.
The Actor uses HTTP only. It uses no browser and no proxy. It does not run JavaScript, so it sees the HTML that the server sends.
What you get
- One row for each release URL inside its window: the phase, the verdict, the
faults, and separate fields for the CDN copy and the cache-busted copy (HTTP
status, final URL after redirects,
Age,Cache-Control, CDN cache-status headers, markers found, reference period match). - One fault row for the first fault of each class for each URL.
- One summary row: URLs in window, faults by class, and the boolean
gate_pass. - Optional: one JSON POST to your webhook for each first fault.
Phases and windows
| Phase | Time | What must be true |
|---|---|---|
pre-release | from release_at minus window_before_minutes to release_at | No outcome marker shows. |
post-release | from release_at to release_at plus window_after_minutes | After late_tolerance_minutes, the outcome markers show and the preview markers are gone. |
| outside the window | all other times | The Actor does not request the URL and does not charge for it. |
Verdicts
The Actor gives each copy a verdict. The row verdict is the most urgent verdict of the two copies.
| Verdict | Fault | Meaning |
|---|---|---|
leak | yes | Before the release, an outcome marker shows. |
stale-preview | yes | After the release and the tolerance, a preview marker still shows. |
late | yes | After the release and the tolerance, no outcome marker shows. |
period-mismatch | yes | After the release, an outcome marker shows, but the expected_reference_period text does not. |
pending | no | After the release, but inside the tolerance, the page has not switched yet. |
ok | no | The page is in the correct state for its phase. |
not-checked | no | The Actor could not search any copy (timeout, DNS failure, refused target, HTTP 5xx or 429, run deadline, or charge limit). |
One copy can have two faults. For example, a page that did not switch at all after
the release has stale-preview and late. An HTTP 4xx page is searched like any
other page, so a 404 page after the release is late.
Markers
- A marker is a literal string. The Actor does not accept regular expressions.
- The match ignores letter case and white space, and it decodes HTML entities.
- The Actor searches the raw HTML (so it finds a marker in an attribute or an
inline JSON script) and the text without tags (so it finds a marker that the
markup splits, as in
<b>August</b> 2026). - One marker must not be both a preview marker and an outcome marker.
- The Actor reads at most 2 MB of each copy. A marker after 2 MB is not found, and
the row shows
cdn_body_truncated: true.
The cache-busted copy
When cache_bust is true, the Actor requests each URL two times:
- the CDN copy: the URL as a reader requests it;
- the cache-busted copy: the URL with a unique
_release_gate_cbquery parameter and the request headersCache-Control: no-cacheandPragma: no-cache.
When the two copies have a different verdict or different markers,
copies_differ is true. A typical case: the fresh copy is correct, but the CDN
still serves the preview page after the release.
The phase ledger
The Actor keeps one entry for each release in the named key-value store
release-embargo-ledger, under the record key that ledger_name gives. The key
of one release is its UTC release time and its URL, so one URL on two release days
is two releases. Each entry holds:
first_outcome_seen_at: the first check that found an outcome marker;first_preview_absent_at: the first post-release check with no preview marker;go_live_atandgo_live_delay_minutes: the first post-release check where each copy showed the outcome and no preview (the real go-live time);phases_seen: the phases that the ledger covers;faults_reported: each fault class already reported, with its first time.
The instant verdicts do not need the ledger. The ledger stops a repeat charge and a repeat alert for one fault. The Actor writes the record one time, at the end of the run. The record holds at most 1,000 releases; the oldest releases go first.
Input
| Field | Default | Description |
|---|---|---|
releases | two demo records | 1 to 50 records. Each record has url, release_at (ISO 8601 with an offset), 1 to 5 preview_markers, 1 to 5 outcome_markers, and an optional expected_reference_period. |
window_before_minutes | 60 | Length of the pre-release phase, 1 to 240. |
window_after_minutes | 30 | Length of the post-release phase, 1 to 240. |
late_tolerance_minutes | 5 | Time after release_at before late and stale-preview count, 0 to 240. |
cache_bust | true | Also request a cache-busted copy. |
alert_webhook_url | empty | One public HTTPS URL for fault alerts. |
ledger_name | DEFAULT | Record key of the ledger. Use one name for each release calendar. |
as_of | empty | Optional simulation time. See below. |
Example:
{"releases": [{"url": "https://www.example-stats.org/cpi/latest","release_at": "2026-10-15T08:30:00-04:00","preview_markers": ["Next release: 15 October 2026"],"outcome_markers": ["The Consumer Price Index rose"],"expected_reference_period": "September 2026"}],"window_before_minutes": 60,"window_after_minutes": 30,"late_tolerance_minutes": 5}
Simulation and the demo
Set as_of to test your markers before a release day. The Actor then picks each
phase as if the time were as_of. A simulated run does not read or write the
ledger, does not charge fault events and does not send alerts.
The demo input checks https://example.com/ 10 minutes after its release time and
https://example.org/ 20 minutes before its release time, at
2026-09-01T12:40:00Z. If you keep the two demo records and leave as_of empty,
the Actor uses that demo time. Both verdicts are ok.
Safety limits
- Only public
httpandhttpsURLs. The Actor refuses localhost, private and reserved addresses, and URLs with credentials. It checks each redirect hop again. - At most 5 redirects, 15 seconds for each request, 30 seconds for each copy, 2 MB for each body, 5 URLs at the same time, and 240 seconds for all requests of one run.
- An unusable release record becomes a
rejected_releaserow. The other records run. An input that is not usable as a whole (for examplewindow_before_minutes: 999) gives oneinput_errorrow and no charge. - A failed gate is a result, not an error: the run always ends with SUCCEEDED and a status message.
Output
The record_type field separates the rows.
Release row (release): url, release_at, release_at_utc, phase,
minutes_from_release, checked_at, simulated, verdict, faults,
new_faults, copies_differ, preview_markers_found, outcome_markers_found,
expected_reference_period, reference_period_match, live, charged, the
ledger fields, and for each of the prefixes cdn_ and busted_:
requested_url, reason_code, http_status, final_url, age,
cache_control, cache_status, preview_markers_found,
outcome_markers_found, reference_period_match, verdict, faults,
body_bytes, body_truncated, redirects, response_time_ms and error.
Fault row (fault): fault_class, url, release_at, phase,
minutes_from_release, verdict, cdn_verdict, busted_verdict,
copies_differ, the markers found, reference_period_match, checked_at and
alert_status.
Summary row (summary): gate_pass, checked_at, simulated,
release_count, rejected_count, urls_in_window, urls_checked,
urls_not_checked, urls_outside_window, pre_release_count,
post_release_count, verdict_counts, faults_by_class, faults_total,
new_faults_by_class, new_faults_total, alerts_sent, alerts_failed,
releases_outside_window (with window_opens_at and window_closes_at), the
window settings, ledger_used, ledger_store and ledger_name.
gate_pass is true when no URL in window has a fault, each URL in window was
checked, and no release record was rejected.
Webhook payload: the fault row without record_type and alert_status, plus
"event": "release-fault-flagged".
Reason codes
OK, SERVER_ERROR, TIMEOUT, DNS_FAIL, CONNECT_FAIL, BLOCKED_TARGET,
REDIRECT_LOOP, RUN_DEADLINE, CHARGE_LIMIT and INVALID_RELEASE. Only OK
means that the Actor searched the copy.
Pricing: pay per event
| Event | Unit | Counted as |
|---|---|---|
run-started | one Actor run | Charged one time, before the Actor requests a page. An input that is not usable is not charged. |
release-url-checked | one release URL checked inside its window in one run, normal and cache-busted copy | Charged one time for each release row with charged: true, that is, at least one copy was searched. A URL outside its window, a not-checked URL and a rejected record are not charged. |
release-fault-flagged | the first leak, stale-preview, late or period-mismatch fault of one class for one URL | Charged one time for each fault row. The ledger stops a repeat charge for the same class and URL in later runs. A simulated run does not charge this event. |
Before the Actor requests a page, it limits the URLs in window to the number of
release-url-checked events that the maximum total charge of the run allows. The
URLs outside that limit get the reason code CHARGE_LIMIT and are not requested.
Repeat use
Each entry in your release calendar is one window. Create an Apify Task with the
releases of one calendar and one ledger_name, and schedule it every 5 minutes
across the release window (for example with the cron */5 7-9 15 * * for a
release at 08:30 on the 15th). Runs outside each window cost only the
run-started event. After the release, the ledger shows the real go-live time and
each fault class that the Actor found.
Local development
uv syncuv run pytestuv run ruff check .