# Release Sentinel: SEO Regression Checker (Beta) (`intuitive_crevasse/release-sentinel`) Actor

Find new noindex, canonical, redirect and title regressions before a website release. Compare your supplied HTTP/HTML snapshots and get JSON evidence plus coverage gaps. Offline beta: no live crawling or browser rendering. $0.01 per complete comparison, platform usage included.

- **URL**: https://apify.com/intuitive_crevasse/release-sentinel.md
- **Developed by:** [Kestrelspan Tools](https://apify.com/intuitive_crevasse) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.01 / completed snapshot comparison

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Release Sentinel: snapshot comparison beta

Compare a baseline and candidate set of **owner-supplied response snapshots** against an explicit page contract. Get a structured JSON report of newly introduced, persisting, resolved and unknown conditions.

This is a narrow offline-analysis beta. It does not visit URLs, collect snapshots, run JavaScript, render pages, crawl websites, check robots.txt, verify search indexing/rankings, schedule checks or send notifications. No API key, cookies, credentials or account login is needed in INPUT. Supply synthetic or sanitized owner-authorized data only.

### Try a regression, then verify the correction

Start with [Catch accidental noindex before a website release](https://apify.com/intuitive_crevasse/release-sentinel/examples/catch-accidental-noindex-before-a-website-release), our synthetic example. No website capture or image upload is needed.

1. Open the example in Apify Console. Its `baseline`, `candidate` and `expectations` inputs are **JSON text fields**, already supplied. The candidate adds `X-Robots-Tag: noindex` to an otherwise unchanged response.
2. Review the active price and run the example. In the default key-value store's `OUTPUT` record, expect one new high-risk `RS_NOINDEX` finding and `report.exitCode: 1`. A successful Actor run means the comparison completed, **not** that the release passed.
3. For a second, corrected comparison, copy the complete `baseline` text into `candidate`, leaving `expectations` unchanged. Start a new run. This synthetic comparison returns `report.exitCode: 0`.

At the current price, two completed customer comparisons cost **US$0.02 in total, including platform usage**. The active Store price controls billing.

**Read the gate correctly:** 0 means no newly introduced high-risk finding in complete supported coverage; existing problems may persist. 1 means a newly introduced high-risk regression. 2 means incomplete or unknown coverage, never a clean release.

For future releases, keep an approved baseline and explicit expectations, then supply a freshly captured candidate from your authorized workflow. Review persistent findings too. Remove secrets and unnecessary personal data before submitting. Release Sentinel does not capture snapshots or schedule these checks for you.

### Input

Three required text fields:

- `baseline`: a complete raw JSON snapshot document.
- `candidate`: a complete raw JSON snapshot document.
- `expectations`: a complete raw JSON page-contract document.

The example input is a complete clean one-page synthetic comparison, provided in the schema prefills for all three required fields. Restore example input to use it. Missing input is not silently replaced.

Paste the JSON document as ordinary text in each field. Keeping the original text allows the parser to reject duplicate JSON keys. Do not pass a URL to download or a JavaScript function.

Example snapshot document for either baseline or candidate:

```json
{"version":1,"pages":[{"url":"https://demo.example/","status":200,"headers":[],"finalUrl":"https://demo.example/","html":"<html><head><title>Demo</title></head><body></body></html>"}]}
```

Matching expectations document:

```json
{"version":1,"pages":[{"url":"https://demo.example/","indexable":true,"requireTitle":true}]}
```

Snapshot page fields: exact `url`, recorded `status`, header `name`/`value` pairs, raw `html`, and recorded `finalUrl`; `location` may record a redirect destination. An empty header array means headers were observed and none recorded; omitted/null observations mean unknown, not a measured pass. Repeated headers remain separate pairs.

Each expectation requires exact `url` and boolean `indexable`. Optional `acceptableStatuses` defaults to `[200]`; `requireTitle` requests a nonempty title; `canonical` contains `required` and an optional absolute approved `target`; `redirect` contains approved redirect `statuses` and absolute `targets`. Canonical checks are opt-in. URL path/query/trailing-slash distinctions are preserved. Redirect chains are not reconstructed.

Limits: 200 pages per document; 256 KiB raw HTML and 200 headers per page; 8 KiB per header value; 4,096 URL characters; JSON depth 24. Combined input texts and reconstructed escaped INPUT are each limited to 10 MiB UTF-8. Runtime validation can be stricter than the form. A generated OUTPUT over 8 MiB fails before new persistence or any custom charge; reduce the page count or URL lengths rather than expecting a truncated report.

### Results

The default key-value store's `OUTPUT` record is the canonical JSON result with `status` and either `report` or `error`. Valid reports include summary counts, findings with bounded before/after evidence, coverage gaps, per-rule measurements and ignored URLs.

Each valid analysis invocation also appends one meaningful default-dataset summary row: actual status, gate code, versions, page/finding counts and coverage flags. This row does not contain URLs, HTML, headers or evidence. Invalid input does not create a dataset success row. Both storage writes are awaited; storage failures remain operational failures.

- `report.exitCode: 0`: complete in the supported scope with no new high-risk finding. Existing problems and warnings can still be present.
- `report.exitCode: 1`: a newly introduced high-risk regression.
- `report.exitCode: 2`: incomplete/unknown coverage; never treat this as a clean release.

Analysis success is not the same as a clean release: a successfully processed Actor run may contain code 1 or 2. Inspect OUTPUT rather than only operational run status. Invalid runtime input is stored as a bounded error and causes operational failure. Input rejected by the platform schema before execution may have no OUTPUT. The dataset uses `gateCode` with the same 0/1/2 meanings; a complete result with gate code 1 is a regression, not clean.

OUTPUT is written before the dataset append, and the writes are not a transaction. An interrupted/uncertain write may have persisted. There is no automatic wrapper retry. Platform restarts can append another summary, so exactly-once delivery across restarts is not guaranteed. Inspect storage after a failed run rather than assuming partial records establish success.

Checks cover declared status and redirect intent, supported noindex directives, opted-in HTML canonical conditions and title loss. Results apply only to supplied observations and the parser's supported markup. Incomplete coverage takes precedence over a regression code, while known findings remain visible.

### Comparison fees

The current price is **$0.01 USD per complete comparison, including platform usage**, whether clean (gate 0) or regressions found (gate 1). The supported custom event is `report-completed`, charged only after the complete report's OUTPUT and real summary are saved. Invalid input, incomplete coverage (gate 2), oversized output and failures before result persistence do not trigger that comparison fee. The active Store price controls the actual fee, within the adapter's $0.01 USD/event safety ceiling.

Owner tests and local/non-monetized use skip the custom event. Platform usage can still apply, including to owner tests. Supported paid configuration excludes startup/dataset event fees and separately charged platform usage. An insufficient comparison budget causes no custom charge attempt. Missing or unsafe pricing metadata fails closed; an operational failure can therefore occur even when OUTPUT is available.

A durable pending marker and fresh run billing counts prevent blind sequential retries. An uncertain charge is not retried automatically. The SDK client disables automatic request retries and uses a 30-second request timeout. Storage errors are deliberately less retry-tolerant. This is a one-submission policy with possible undercharging, not an exactly-once guarantee across concurrent processes or lost/modified storage. Summary rows may duplicate after a restart. Do not assume every failed run is uncharged: a later failure can follow a correctly charged, accessible complete report.

### Limitations and data handling

This is not a full HTML5 parser or general-site production monitor. Complex or ambiguous markup, including noscript, intentionally yields incomplete results. Supplied HTML is data, not executed code. There is no HTML output or embedded viewer.

A cloud run processes and stores INPUT/OUTPUT on Apify. Outputs may contain URLs, query strings and bounded metadata/header evidence. Remove personal data, authorization headers, cookies and secrets before submitting. No automatic redaction or special confidentiality/retention guarantee is provided. Access and retention depend on the platform/account configuration. Downstream systems must treat all report strings as untrusted data, not executable markup.

Version: adapter 0.1.3; frozen analysis engine 0.1.0. Local verification passed 137 adapter/config/billing tests, 177 frozen-core tests and 15 synthetic guide scenarios. Adapter 0.1.3 was cloud-tested using synthetic owner runs under Limited permissions: clean, regression and incomplete results matched their expected gates and summaries, and a clean owner run passed after monetization. Paid-customer charging and HTTP fault behavior have not been independently verified. Earlier adapter 0.1.2 also passed a practical 200-page synthetic cloud check with matching OUTPUT and one meaningful summary; that 653,311-byte INPUT used one 256 KiB HTML body per snapshot, not the full 10 MiB envelope or worst-case memory. Earlier adapter 0.1.1 passed prepublication and post-publication one-page checks. Synthetic and local checks do not establish general production readiness, confidentiality or customer demand.

### Guide: catch an accidental noindex before a website release

A page can look right, return HTTP 200 and still tell Google not to index it. A staging setting copied into production, a shared template change or a response-header rule can introduce that mistake without an obvious visual difference. Make indexing intent part of your release review, alongside the page's appearance and functionality.

#### 1. Define what should stay indexable

Choose important URLs from each changed template, such as a service page, product page, article and landing page. Record expected status and indexing intent. Keep deliberately excluded pages separate: `noindex` is not automatically a defect.

Review the baseline against that intent. An old response proves an earlier state, not that the state was correct. In Release Sentinel, `indexable: true` asks whether supported observations contain a conflicting restriction; it does not establish that Google has indexed the page. Setting it to false does not enforce the presence of `noindex`.

#### 2. Capture comparable evidence

Use websites you own or are authorized to test. Your HTTP test harness, an authorized crawler export or browser developer tools can provide response evidence. Convert it into the Input format above; screenshots and arbitrary exports are not accepted automatically. Release Sentinel does not fetch the URLs for you.

Keep the requested URL, recorded final URL, status, headers and raw HTML together. They must describe the same response. Record redirects explicitly rather than combining a redirect's headers with the destination HTML. Preserve repeated headers. An omitted observation means unknown; use an empty header array only when headers were observed and none were recorded.

Keep capture time, release and environment notes separately with your release evidence. Compare equivalent request conditions: an authenticated preview and anonymous production response may intentionally differ. Do not relabel unrelated URLs to make snapshots match. Remove credentials, cookies, tokens and unnecessary personal data before uploading.

#### 3. Inspect both HTML and headers

Google supports `noindex` in HTML meta directives and the `X-Robots-Tag` response header. Checking HTML alone can miss a hosting or delivery-layer restriction. Google must be able to access the response to discover the rule. See [Google's noindex guidance](https://developers.google.com/search/docs/crawling-indexing/block-indexing).

Inspect generic robots and applicable Googlebot-specific directives, including repeated tags or headers. In robots directives, `none` includes `noindex`. An `index` elsewhere does not cancel an applicable restrictive directive. Check syntax rather than treating every occurrence of “none” in a page as a finding. See [Google's robots directive specification](https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag).

Review robots.txt separately. It controls crawler access; it is not a dependable way to keep a web-page URL out of search results. Blocking crawling can prevent Google seeing `noindex`. Neither mechanism replaces access controls for private content. See [Google's robots.txt introduction](https://developers.google.com/search/docs/crawling-indexing/robots/intro).

#### 4. Try a deliberately broken comparison

Open [Catch accidental noindex before a website release](https://apify.com/intuitive_crevasse/release-sentinel/examples/catch-accidental-noindex-before-a-website-release), the synthetic example for this Actor. It represents an indexable service page with a clean baseline; the candidate adds `X-Robots-Tag: noindex` while still returning HTTP 200.

The expected result is a complete comparison with one new high-risk `RS_NOINDEX` finding and gate code 1. That is successful analysis of a regression, not a clean release. Inspect canonical `OUTPUT` for the evidence and the dataset summary for actual counts.

If both snapshots already contain the restriction, it persists rather than being newly introduced. Gate 0 can therefore coexist with an existing problem. Missing or unsupported required observations produce incomplete coverage, gate 2, and must not be treated as a pass.

#### 5. Correct the source, then capture again

Trace an unexpected directive to its CMS setting, template, environment configuration, server rule or delivery-layer setting. Correct the responsible setting instead of deleting every intentional exclusion. Capture the candidate again, then repeat against the public response after deployment: a corrected template does not prove a cached response changed.

Save the comparison with the release decision and review persistent findings too. Raw-response analysis does not execute JavaScript. Sampling cannot establish coverage of every URL, request variant or rendered state.

#### 6. Separate the release check from Google's observations

Use [Search Console's URL Inspection tool](https://support.google.com/webmasters/answer/9012289) separately when investigating Google's observations. Indexed-page information and live tests answer different questions; a successful live test does not establish indexing. Even meeting [Google's technical requirements](https://developers.google.com/search/docs/essentials/technical) does not guarantee inclusion or rankings.

Release Sentinel costs **$0.01 USD per complete comparison, including platform usage**, whether clean or regressions are found. Invalid or incomplete comparisons incur no comparison fee. It checks supported conditions in supplied snapshots, not live crawling, indexing or search performance.

# Actor input Schema

## `baseline` (type: `string`):

Complete raw text of a version-1 baseline snapshot document. Retaining the text allows duplicate-key detection.

## `candidate` (type: `string`):

Complete raw text of a version-1 candidate snapshot document. Supplied HTML is parsed as data and is never executed.

## `expectations` (type: `string`):

Complete raw text of a version-1 expectations manifest describing the approved page contracts.

## Actor input object example

```json
{
  "baseline": "{\n  \"version\": 1,\n  \"pages\": [\n    {\n      \"url\": \"https://demo.example/services\",\n      \"status\": 200,\n      \"headers\": [\n        {\n          \"name\": \"Content-Type\",\n          \"value\": \"text/html; charset=utf-8\"\n        }\n      ],\n      \"finalUrl\": \"https://demo.example/services\",\n      \"html\": \"<!doctype html><html><head><title>Our services</title><link rel=\\\"canonical\\\" href=\\\"https://demo.example/services\\\"></head><body><h1>Our services</h1></body></html>\"\n    }\n  ]\n}\n",
  "candidate": "{\n  \"version\": 1,\n  \"pages\": [\n    {\n      \"url\": \"https://demo.example/services\",\n      \"status\": 200,\n      \"headers\": [\n        {\n          \"name\": \"Content-Type\",\n          \"value\": \"text/html; charset=utf-8\"\n        }\n      ],\n      \"finalUrl\": \"https://demo.example/services\",\n      \"html\": \"<!doctype html><html><head><title>Our services</title><link rel=\\\"canonical\\\" href=\\\"https://demo.example/services\\\"></head><body><h1>Our services</h1></body></html>\"\n    }\n  ]\n}\n",
  "expectations": "{\n  \"version\": 1,\n  \"pages\": [\n    {\n      \"url\": \"https://demo.example/services\",\n      \"indexable\": true,\n      \"requireTitle\": true,\n      \"canonical\": {\n        \"required\": true,\n        \"target\": \"https://demo.example/services\"\n      }\n    }\n  ]\n}\n"
}
```

# Actor output Schema

## `summary` (type: `string`):

One count/status summary per valid analysis invocation. Gate code 1 is a regression; code 2 is incomplete, never clean. No page URLs, HTML or header evidence is copied into this dataset.

## `report` (type: `string`):

Contains a complete or incomplete engine report, or an invalid-input error. No live verification is performed.

# 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 = {
    "baseline": `{
  "version": 1,
  "pages": [
    {
      "url": "https://demo.example/services",
      "status": 200,
      "headers": [
        {
          "name": "Content-Type",
          "value": "text/html; charset=utf-8"
        }
      ],
      "finalUrl": "https://demo.example/services",
      "html": "<!doctype html><html><head><title>Our services</title><link rel=\"canonical\" href=\"https://demo.example/services\"></head><body><h1>Our services</h1></body></html>"
    }
  ]
}`,
    "candidate": `{
  "version": 1,
  "pages": [
    {
      "url": "https://demo.example/services",
      "status": 200,
      "headers": [
        {
          "name": "Content-Type",
          "value": "text/html; charset=utf-8"
        }
      ],
      "finalUrl": "https://demo.example/services",
      "html": "<!doctype html><html><head><title>Our services</title><link rel=\"canonical\" href=\"https://demo.example/services\"></head><body><h1>Our services</h1></body></html>"
    }
  ]
}`,
    "expectations": `{
  "version": 1,
  "pages": [
    {
      "url": "https://demo.example/services",
      "indexable": true,
      "requireTitle": true,
      "canonical": {
        "required": true,
        "target": "https://demo.example/services"
      }
    }
  ]
}`
};

// Run the Actor and wait for it to finish
const run = await client.actor("intuitive_crevasse/release-sentinel").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 = {
    "baseline": """{
  \"version\": 1,
  \"pages\": [
    {
      \"url\": \"https://demo.example/services\",
      \"status\": 200,
      \"headers\": [
        {
          \"name\": \"Content-Type\",
          \"value\": \"text/html; charset=utf-8\"
        }
      ],
      \"finalUrl\": \"https://demo.example/services\",
      \"html\": \"<!doctype html><html><head><title>Our services</title><link rel=\\\"canonical\\\" href=\\\"https://demo.example/services\\\"></head><body><h1>Our services</h1></body></html>\"
    }
  ]
}
""",
    "candidate": """{
  \"version\": 1,
  \"pages\": [
    {
      \"url\": \"https://demo.example/services\",
      \"status\": 200,
      \"headers\": [
        {
          \"name\": \"Content-Type\",
          \"value\": \"text/html; charset=utf-8\"
        }
      ],
      \"finalUrl\": \"https://demo.example/services\",
      \"html\": \"<!doctype html><html><head><title>Our services</title><link rel=\\\"canonical\\\" href=\\\"https://demo.example/services\\\"></head><body><h1>Our services</h1></body></html>\"
    }
  ]
}
""",
    "expectations": """{
  \"version\": 1,
  \"pages\": [
    {
      \"url\": \"https://demo.example/services\",
      \"indexable\": true,
      \"requireTitle\": true,
      \"canonical\": {
        \"required\": true,
        \"target\": \"https://demo.example/services\"
      }
    }
  ]
}
""",
}

# Run the Actor and wait for it to finish
run = client.actor("intuitive_crevasse/release-sentinel").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 '{
  "baseline": "{\\n  \\"version\\": 1,\\n  \\"pages\\": [\\n    {\\n      \\"url\\": \\"https://demo.example/services\\",\\n      \\"status\\": 200,\\n      \\"headers\\": [\\n        {\\n          \\"name\\": \\"Content-Type\\",\\n          \\"value\\": \\"text/html; charset=utf-8\\"\\n        }\\n      ],\\n      \\"finalUrl\\": \\"https://demo.example/services\\",\\n      \\"html\\": \\"<!doctype html><html><head><title>Our services</title><link rel=\\\\\\"canonical\\\\\\" href=\\\\\\"https://demo.example/services\\\\\\"></head><body><h1>Our services</h1></body></html>\\"\\n    }\\n  ]\\n}\\n",
  "candidate": "{\\n  \\"version\\": 1,\\n  \\"pages\\": [\\n    {\\n      \\"url\\": \\"https://demo.example/services\\",\\n      \\"status\\": 200,\\n      \\"headers\\": [\\n        {\\n          \\"name\\": \\"Content-Type\\",\\n          \\"value\\": \\"text/html; charset=utf-8\\"\\n        }\\n      ],\\n      \\"finalUrl\\": \\"https://demo.example/services\\",\\n      \\"html\\": \\"<!doctype html><html><head><title>Our services</title><link rel=\\\\\\"canonical\\\\\\" href=\\\\\\"https://demo.example/services\\\\\\"></head><body><h1>Our services</h1></body></html>\\"\\n    }\\n  ]\\n}\\n",
  "expectations": "{\\n  \\"version\\": 1,\\n  \\"pages\\": [\\n    {\\n      \\"url\\": \\"https://demo.example/services\\",\\n      \\"indexable\\": true,\\n      \\"requireTitle\\": true,\\n      \\"canonical\\": {\\n        \\"required\\": true,\\n        \\"target\\": \\"https://demo.example/services\\"\\n      }\\n    }\\n  ]\\n}\\n"
}' |
apify call intuitive_crevasse/release-sentinel --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,intuitive_crevasse/release-sentinel"
        }
    }
}
```

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/tTwvKiaW0Gc4twlyu/builds/aWFahsCnuFyfpxFuh/openapi.json
