# Scheduled Release Page Embargo Leak and Stale-Preview Gate (`kingii98/scheduled-release-page-embargo-leak-and-stale-preview-gate`) Actor

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

- **URL**: https://apify.com/kingii98/scheduled-release-page-embargo-leak-and-stale-preview-gate.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 run starteds

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

## Scheduled Release Page Embargo Leak and Stale-Preview Gate

Prove two facts for each scheduled release:

1. The outcome content was **not public before** the release time.
2. 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_cb` query
  parameter and the request headers `Cache-Control: no-cache` and `Pragma: 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_at` and `go_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:

```json
{
  "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 `http` and `https` URLs. 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_release` row. The other records
  run. An input that is not usable as a whole (for example
  `window_before_minutes: 999`) gives one `input_error` row 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

```bash
uv sync
uv run pytest
uv run ruff check .
```

# Actor input Schema

## `releases` (type: `array`):

1 to 50 release records. Each record has a "url", a "release\_at" time in ISO 8601 with an offset (for example "2026-10-15T08:30:00-04:00"), 1 to 5 "preview\_markers" and 1 to 5 "outcome\_markers" as literal strings, and an optional "expected\_reference\_period" string (for example "August 2026"). The Actor does not accept regular expressions: a marker matches as a literal text, without regard to letter case or white space. If you keep the two demo records shown below and set no "as\_of", the Actor checks the demo at 2026-09-01T12:40:00Z.

## `window_before_minutes` (type: `integer`):

The pre-release phase starts this many minutes before release\_at. In this phase, an outcome marker on the page is a leak.

## `window_after_minutes` (type: `integer`):

The post-release phase ends this many minutes after release\_at. Outside both phases, the Actor does not request the URL and does not charge for it.

## `late_tolerance_minutes` (type: `integer`):

After release\_at, the page has this many minutes to show the outcome markers and to remove the preview markers. Inside the tolerance, the verdict is pending, not a fault.

## `cache_bust` (type: `boolean`):

When true, the Actor also requests each URL with a unique query parameter and a no-cache request header. The dataset then shows the CDN copy and the cache-busted copy side by side.

## `alert_webhook_url` (type: `string`):

Optional. One public HTTPS URL. The Actor sends one JSON POST for the first fault of each class for each release URL. Private and reserved addresses are refused, and a redirect is not followed. Leave it empty to read the faults in the dataset only.

## `ledger_name` (type: `string`):

The record key of the phase ledger inside the named key-value store "release-embargo-ledger". Use one name for each release calendar. The ledger keeps the first time the Actor saw the outcome, the first time the preview was gone, the go-live time, and the fault classes already reported.

## `as_of` (type: `string`):

Optional. An ISO 8601 time with an offset. When set, the Actor picks each phase as if the time were this value, so you can test your markers before a release day. A simulated run does not read or write the ledger and does not charge fault events. Leave it empty for a scheduled run.

## Actor input object example

```json
{
  "releases": [
    {
      "url": "https://example.com/",
      "release_at": "2026-09-01T08:30:00-04:00",
      "preview_markers": [
        "Coming soon"
      ],
      "outcome_markers": [
        "Example Domain"
      ]
    },
    {
      "url": "https://example.org/",
      "release_at": "2026-09-01T09:00:00-04:00",
      "preview_markers": [
        "Example Domain"
      ],
      "outcome_markers": [
        "Consumer Price Index rose"
      ]
    }
  ],
  "window_before_minutes": 60,
  "window_after_minutes": 30,
  "late_tolerance_minutes": 5,
  "cache_bust": true,
  "alert_webhook_url": "",
  "ledger_name": "DEFAULT"
}
```

# Actor output Schema

## `dataset` (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 = {
    "releases": [
        {
            "url": "https://example.com/",
            "release_at": "2026-09-01T08:30:00-04:00",
            "preview_markers": [
                "Coming soon"
            ],
            "outcome_markers": [
                "Example Domain"
            ]
        },
        {
            "url": "https://example.org/",
            "release_at": "2026-09-01T09:00:00-04:00",
            "preview_markers": [
                "Example Domain"
            ],
            "outcome_markers": [
                "Consumer Price Index rose"
            ]
        }
    ],
    "window_before_minutes": 60,
    "window_after_minutes": 30,
    "late_tolerance_minutes": 5,
    "cache_bust": true,
    "alert_webhook_url": "",
    "ledger_name": "DEFAULT"
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/scheduled-release-page-embargo-leak-and-stale-preview-gate").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 = {
    "releases": [
        {
            "url": "https://example.com/",
            "release_at": "2026-09-01T08:30:00-04:00",
            "preview_markers": ["Coming soon"],
            "outcome_markers": ["Example Domain"],
        },
        {
            "url": "https://example.org/",
            "release_at": "2026-09-01T09:00:00-04:00",
            "preview_markers": ["Example Domain"],
            "outcome_markers": ["Consumer Price Index rose"],
        },
    ],
    "window_before_minutes": 60,
    "window_after_minutes": 30,
    "late_tolerance_minutes": 5,
    "cache_bust": True,
    "alert_webhook_url": "",
    "ledger_name": "DEFAULT",
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/scheduled-release-page-embargo-leak-and-stale-preview-gate").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 '{
  "releases": [
    {
      "url": "https://example.com/",
      "release_at": "2026-09-01T08:30:00-04:00",
      "preview_markers": [
        "Coming soon"
      ],
      "outcome_markers": [
        "Example Domain"
      ]
    },
    {
      "url": "https://example.org/",
      "release_at": "2026-09-01T09:00:00-04:00",
      "preview_markers": [
        "Example Domain"
      ],
      "outcome_markers": [
        "Consumer Price Index rose"
      ]
    }
  ],
  "window_before_minutes": 60,
  "window_after_minutes": 30,
  "late_tolerance_minutes": 5,
  "cache_bust": true,
  "alert_webhook_url": "",
  "ledger_name": "DEFAULT"
}' |
apify call kingii98/scheduled-release-page-embargo-leak-and-stale-preview-gate --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/scheduled-release-page-embargo-leak-and-stale-preview-gate"
        }
    }
}
```

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/B9AhcqdXh6p9rXltg/builds/hnZu4r7My7o5Ocsrq/openapi.json
