diff --git a/.gitignore b/.gitignore
index b4a772505d0062..b46a4f61e2a759 100644
--- a/.gitignore
+++ b/.gitignore
@@ -91,6 +91,9 @@ public/mdx-images/*
public/og-images/*
!public/og-images/README.md
+# Ignore LLM plans
+/docs/plans/
+
# yalc
.yalc
yalc.lock
diff --git a/app/[[...path]]/page.tsx b/app/[[...path]]/page.tsx
index 7d3c950ed1d5a0..c5e6d24211a291 100644
--- a/app/[[...path]]/page.tsx
+++ b/app/[[...path]]/page.tsx
@@ -11,6 +11,9 @@ import {Home} from 'sentry-docs/components/home';
import {Include} from 'sentry-docs/components/include';
import {PageLoadMetrics} from 'sentry-docs/components/pageLoadMetrics';
import {PlatformContent} from 'sentry-docs/components/platformContent';
+import {SpecChangelog} from 'sentry-docs/components/specChangelog';
+import {isSpecStatus} from 'sentry-docs/components/specConstants';
+import {SpecMeta} from 'sentry-docs/components/specMeta';
import {
DocNode,
getCurrentPlatformOrGuide,
@@ -47,23 +50,58 @@ export async function generateStaticParams() {
export const dynamicParams = false;
export const dynamic = 'force-static';
-const mdxComponentsWithWrapper = mdxComponents(
- {Include, PlatformContent},
- ({children, frontMatter, nextPage, previousPage}) => (
-
- {children}
-
- )
-);
+function mdxComponentsForFrontMatter(frontMatter: Record) {
+ const specOverrides: Record = {};
+ const changelog = Array.isArray(frontMatter.spec_changelog)
+ ? (frontMatter.spec_changelog as Array>)
+ .filter(
+ entry => entry.version != null && entry.date != null && entry.summary != null
+ )
+ .map(entry => ({
+ version: String(entry.version),
+ date:
+ entry.date instanceof Date
+ ? entry.date.toISOString().split('T')[0]
+ : String(entry.date),
+ summary: String(entry.summary),
+ }))
+ : undefined;
+ if (changelog && changelog.length > 0) {
+ specOverrides.SpecChangelog = function () {
+ return ;
+ };
+ }
+ if (frontMatter.spec_version && isSpecStatus(frontMatter.spec_status)) {
+ const boundProps = {
+ version: String(frontMatter.spec_version),
+ status: frontMatter.spec_status,
+ };
+ specOverrides.SpecMeta = function () {
+ return ;
+ };
+ }
+ return mdxComponents(
+ {Include, PlatformContent, ...specOverrides},
+ ({children, frontMatter: fm, nextPage, previousPage}) => (
+
+ {children}
+
+ )
+ );
+}
function MDXLayoutRenderer({mdxSource, ...rest}) {
const MDXLayout = useMemo(() => getMDXComponent(mdxSource), [mdxSource]);
- return ;
+ const components = useMemo(
+ () => mdxComponentsForFrontMatter(rest.frontMatter || {}),
+ [rest.frontMatter]
+ );
+ return ;
}
export default async function Page(props: {params: Promise<{path?: string[]}>}) {
diff --git a/develop-docs/sdk/telemetry/check-ins.mdx b/develop-docs/sdk/telemetry/check-ins.mdx
index d9dfaa08cdee39..6d3a23aadbe322 100644
--- a/develop-docs/sdk/telemetry/check-ins.mdx
+++ b/develop-docs/sdk/telemetry/check-ins.mdx
@@ -1,86 +1,191 @@
---
-title: Check-Ins
+title: Cron Check-Ins
+spec_id: sdk/telemetry/check-ins
+spec_version: 1.6.0
+spec_status: stable
+spec_platforms:
+ - backend
+spec_depends_on:
+ - id: sdk/foundations/data-model/envelopes
+ version: ">=1.0.0"
+spec_changelog:
+ - version: 1.6.0
+ date: 2025-09-18
+ summary: Added hook requirements (beforeSend exclusion, optional beforeSendCheckIn)
+ - version: 1.5.0
+ date: 2024-04-24
+ summary: Added owner field to monitor_config
+ - version: 1.4.0
+ date: 2023-12-13
+ summary: Added failure_issue_threshold and recovery_threshold to monitor_config
+ - version: 1.3.0
+ date: 2023-06-26
+ summary: Added trace context support for linking check-ins to errors
+ - version: 1.2.0
+ date: 2023-06-05
+ summary: Added zero UUID support for updating most recent in_progress check-in
+ - version: 1.1.0
+ date: 2023-04-18
+ summary: Added monitor upsert support with schedule configuration
+ - version: 1.0.0
+ date: 2023-03-21
+ summary: Initial spec — basic check-in payload
sidebar_order: 1
---
-## Cron Monitor Check-In Payload
+
-A check-in is an item in an envelope called `check_in`. It consists
-of a JSON payload that looks roughly like this:
+
-```json
-{
- "check_in_id": "83a7c03ed0a04e1b97e2e3b18d38f244",
- "monitor_slug": "my-monitor",
- "status": "in_progress",
- "duration": 10.0,
- "release": "1.0.0",
- "environment": "production",
- "contexts": {
- "trace": {
- "trace_id": "8f431b7aa08441bbbd5a0100fd91f9fe"
- }
- }
-}
+## Overview
+
+Cron check-ins allow Sentry to monitor the health of recurring jobs (cron jobs, scheduled tasks, periodic workers). An SDK sends check-in envelopes to report when a job starts, succeeds, or fails. Sentry uses this data to detect missed, late, and failing executions.
+
+Related specs:
+- [Envelopes](/sdk/foundations/data-model/envelopes/) — transport format
+- [Envelope Items](/sdk/foundations/data-model/envelope-items/) — `check_in` item type constraints
+
+---
+
+## Concepts
+
+### Monitors and Check-Ins
+
+A **monitor** is a named entity in Sentry that tracks a recurring job. Each monitor is identified by a unique `monitor_slug`. Monitors can be created manually in Sentry or upserted by the SDK alongside check-in data.
+
+A **check-in** is a single status report for one execution of a monitored job, sent as a `check_in` envelope item.
+
+### Check-In Lifecycle
+
+Each job execution produces two check-ins that share the same `check_in_id`:
+
+```
+[job starts] → in_progress → [job ends] → ok / error
```
-The following fields exist:
-`check_in_id`
+- `in_progress` — the job has started
+- `ok` — the job completed successfully
+- `error` — the job failed
+
+Sentry uses the time between the `in_progress` and terminal (`ok`/`error`) check-ins to compute duration, and the absence of a terminal check-in within `max_runtime` to detect timeouts.
+
+### Monitor Upsert
+
+SDKs can send monitor configuration (schedule, thresholds, timezone) alongside a check-in. Sentry creates the monitor if it doesn't exist or updates it if it does. This eliminates the need for manual monitor setup.
+
+---
+
+## Behavior
-: **String, required**. Check-In ID (unique and client generated).
+### Check-In Lifecycle
- This may be provided as a empty UUID (128 bit zero value) to indicate to
- Sentry that the checkin should update the most recent "in_progress" check-in.
- If the most recent check-in is not in progress a new one will be created
- instead.
+
-`monitor_slug`
+SDKs **MUST** generate a unique `check_in_id` (UUID v4) when a job execution starts and send a check-in with status `in_progress`.
-: **String, required**. The distinct slug of the monitor.
+When the job completes, SDKs **MUST** send a second check-in with the same `check_in_id` and status `ok` (success) or `error` (failure).
-`status`
+SDKs **SHOULD** compute the `duration` field for terminal check-ins (`ok` or `error`) by measuring the elapsed time since the `in_progress` check-in was sent.
-: **String, required**. The status of the check-in. Can be one of the following:
- - `in_progress`: The check-in has started.
- - `ok`: The check-in has completed successfully.
- - `error`: The check-in has failed.
+
-`duration`
+### Trace Context Linking
-: _Number, optional_. The duration of the check-in in seconds. Will only take effect if the
- status is `ok` or `error`.
+
-`release`
+SDKs **SHOULD** include a `contexts.trace` object with a `trace_id` field to link the check-in to associated errors and traces.
-: _String, optional_. The release.
+If the job execution is part of an active trace, the SDK **SHOULD** use that trace's `trace_id`. Otherwise, the SDK **MAY** generate a new `trace_id` for correlation purposes.
-`environment`
+
-: _String, optional_. The environment.
+### Updating in_progress Check-Ins
-`monitor_config`
+
-: _Object, optional_. A monitor configuration (defined below) that is stored with the
- check-in in order to verify the state of the monitor at the time of the check-in.
+SDKs **MAY** send a check-in with `check_in_id` set to an empty UUID (128-bit zero value, `00000000000000000000000000000000`) to indicate that Sentry **SHOULD** update the most recent `in_progress` check-in for the given monitor.
-`contexts`
+If no `in_progress` check-in exists, Sentry creates a new one.
-: _Object, optional_. A dictionary of contextual information about the environment running
- the check-in. Right now we only support the [trace context](/sdk/foundations/data-model/event-payloads/contexts/#trace-context)
- and use the `trace_id` in order to link check-ins to associated errors.
+This is useful for fire-and-forget patterns where the SDK does not retain the original `check_in_id`.
-## Monitor upsert support
+
-In addition to sending check-in details, the SDK may also provide monitor
-configuration, allowing monitors to be created or updated when sending
-check-ins.
+### Monitor Upsert
+
+
+SDKs **MAY** include a `monitor_config` object in the check-in payload. When present, Sentry creates the monitor if it does not exist, or updates its configuration if it does.
+
+SDKs **SHOULD** only send `monitor_config` on the first check-in of a job execution (the `in_progress` check-in), not on every check-in.
+
+
+
+### Auto-Instrumentation
+
+
+
+SDKs **MAY** provide integrations that automatically instrument popular scheduling libraries. When available, these integrations **SHOULD**:
+
+1. Automatically discover scheduled jobs and derive `monitor_slug` values.
+2. Send `in_progress` and terminal check-ins for each job execution.
+3. Upsert monitor configuration based on the library's schedule definition.
+
+Known integrations include Celery Beat (Python), node-cron / cron / node-schedule (JavaScript), Sidekiq-Cron (Ruby), and Oban/Quantum (Elixir).
+
+
+
+---
+
+## Wire Format
+
+### Check-In Payload
+
+
+
+A check-in is sent as a `check_in` envelope item containing a JSON object.
```json
{
"check_in_id": "83a7c03ed0a04e1b97e2e3b18d38f244",
- "monitor_slug": "b7645b8e-b47d-4398-be9a-d16b0dac31cb",
+ "monitor_slug": "my-monitor",
"status": "in_progress",
+ "duration": 10.0,
+ "release": "1.0.0",
+ "environment": "production",
+ "contexts": {
+ "trace": {
+ "trace_id": "8f431b7aa08441bbbd5a0100fd91f9fe"
+ }
+ }
+}
+```
+
+| Field | Type | Required | Since | Description |
+|-------|------|----------|-------|-------------|
+| `check_in_id` | String | **REQUIRED** | 1.0.0 | Client-generated UUID identifying this job execution. **MAY** be an empty UUID (all zeros) to update the most recent `in_progress` check-in (since 1.2.0). |
+| `monitor_slug` | String | **REQUIRED** | 1.0.0 | The distinct slug identifying the monitor. |
+| `status` | String | **REQUIRED** | 1.0.0 | One of `in_progress`, `ok`, or `error`. |
+| `duration` | Number | **OPTIONAL** | 1.0.0 | Duration of the job execution in seconds. Only takes effect for `ok` or `error` status. |
+| `release` | String | **OPTIONAL** | 1.0.0 | The release version. |
+| `environment` | String | **OPTIONAL** | 1.0.0 | The environment name. |
+| `monitor_config` | Object | **OPTIONAL** | 1.1.0 | Monitor configuration for upsert. See [Monitor Configuration Payload](#monitor-configuration-payload). |
+| `contexts` | Object | **OPTIONAL** | 1.3.0 | Contextual information. Currently supports [trace context](/sdk/foundations/data-model/event-payloads/contexts/#trace-context) with `trace_id` for linking check-ins to errors. |
+
+**Envelope constraints:**
+- A `check_in` item **MUST** occur at most once per envelope.
+- Maximum item size: 100 KiB.
+
+
+
+### Monitor Configuration Payload
+
+
+
+When included, the `monitor_config` object supports the following fields:
+
+```json
+{
"monitor_config": {
"schedule": {
"type": "crontab",
@@ -95,71 +200,195 @@ check-ins.
}
}
```
-The following fields exist under the `monitor_config` key:
-`schedule`
+| Field | Type | Required | Since | Description |
+|-------|------|----------|-------|-------------|
+| `schedule` | Object | **REQUIRED** | 1.1.0 | Schedule configuration. See [Schedule Configuration](#schedule-configuration). |
+| `checkin_margin` | Number | **OPTIONAL** | 1.1.0 | Allowed margin in minutes after the expected check-in time before the monitor is considered missed. |
+| `max_runtime` | Number | **OPTIONAL** | 1.1.0 | Allowed duration in minutes that a monitor may be `in_progress` before being considered failed due to timeout. |
+| `failure_issue_threshold` | Number | **OPTIONAL** | 1.4.0 | Number of consecutive failed check-ins before an issue is created. |
+| `recovery_threshold` | Number | **OPTIONAL** | 1.4.0 | Number of consecutive OK check-ins before an issue is resolved. |
+| `timezone` | String | **OPTIONAL** | 1.1.0 | A [tz database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) string for the monitor's execution schedule timezone. |
+| `owner` | String | **OPTIONAL** | 1.5.0 | An actor identifier string, e.g. `user:john@example.com` or `team:a-sentry-team`. |
+
+
-: **Object, required**. [See schedule configuration](#schedule-configuration).
+### Schedule Configuration
-`checkin_margin`
+
+
+
+This configuration format differs slightly from what is accepted in the
+monitors frontend APIs.
+
-: _Number, optional_. The allowed margin of minutes after the expected
-check-in time that the monitor will not be considered missed for.
+The `schedule` object **MUST** contain a `type` field set to either `crontab` or `interval`.
-`max_runtime`
+**Crontab schedule:**
-: _Number, optional_. The allowed duration in minutes that the monitor
-may be `in_progress` for before being considered failed due to timeout.
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `type` | String | **REQUIRED** | `"crontab"` |
+| `value` | String | **REQUIRED** | A crontab expression, e.g. `"0 * * * *"`. |
-`failure_issue_threshold`
+**Interval schedule:**
-: _Number, optional_. The number of consecutive failed check-ins it takes
-before an issue is created.
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| `type` | String | **REQUIRED** | `"interval"` |
+| `value` | Number | **REQUIRED** | The interval value. |
+| `unit` | String | **REQUIRED** | One of `year`, `month`, `week`, `day`, `hour`, `minute`. |
-`recovery_threshold`
+
-: _Number, optional_. The number of consecutive OK check-ins it takes
-before an issue is resolved.
+---
-`timezone`
+## Public API
-: _String, optional_. A [`tz
-database`](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) string
-representing the timezone which the monitor's execution schedule is in.
+### Capture Check-In
-`owner`
+
-: _String, optional_. An actor identifier string. This looks like
-`user:john@example.com` `team:a-sentry-team`. IDs can also be used but will
-result in a poor DX.
+SDKs **MUST** expose a top-level function to send a check-in:
-### Schedule configuration
+```
+captureCheckIn(checkIn, monitorConfig?) -> checkInId
+```
-
-This configuration format differs slightly from what is accepted in the
-monitors frontend APIs.
-
+**Parameters:**
+- `checkIn` — Object containing at minimum `monitorSlug` and `status`. For terminal check-ins, also includes `checkInId` (from the initial `in_progress` call) and optionally `duration`.
+- `monitorConfig` — **OPTIONAL**. Monitor configuration for upsert (schedule, margins, thresholds).
-`type`
+**Returns:** The `checkInId` (string), so callers can correlate the `in_progress` and terminal check-ins.
-: **String, required**. One of `crontab` or `interval`.
+Naming **SHOULD** follow the SDK's language conventions:
+- `captureCheckIn` (JavaScript, Java)
+- `capture_check_in` (Python, Ruby, Elixir)
+- `CaptureCheckIn` (Go, .NET)
-#### Using `crontab`
+
-`value`
+### Monitor Wrapper
-: **String, required**. The crontab schedule string, e.g. `0 * * * *`.
+
-#### Using `interval`
+SDKs **SHOULD** expose a higher-level wrapper that automatically sends `in_progress` and `ok`/`error` check-ins around a callback:
-`value`
+```
+withMonitor(monitorSlug, callback, monitorConfig?) -> callbackResult
+```
+
+The wrapper **MUST**:
+1. Send an `in_progress` check-in before invoking the callback.
+2. Send an `ok` check-in if the callback completes without error.
+3. Send an `error` check-in if the callback throws or returns a failure.
+4. Compute and include `duration` in the terminal check-in.
+
+SDKs **MAY** additionally provide language-idiomatic alternatives:
+- **Decorators** (Python): `@monitor(monitor_slug='slug')`
+- **Context managers** (Python): `with monitor(monitor_slug='slug'):`
+- **Mixins** (Ruby): `include Sentry::Cron::MonitorCheckIns`
+- **Annotations** (Java/Kotlin): framework-specific annotations
+
+
+
+### Hooks
+
+
+
+Check-in events **MUST NOT** go through the `beforeSend` hook. The `beforeSend` hook is reserved for error events.
-: **Number, required**. The interval value.
+SDKs **MAY** implement a dedicated `beforeSendCheckIn` hook that applies only to check-in events.
+
+
+
+---
+
+## Examples
+
+### Basic manual check-in (two-step)
+
+```
+// Start
+checkInId = captureCheckIn({
+ monitorSlug: "data-sync",
+ status: "in_progress"
+})
+
+// ... execute job ...
+
+// Complete
+captureCheckIn({
+ monitorSlug: "data-sync",
+ checkInId: checkInId,
+ status: "ok",
+ duration: 12.5
+})
+```
-`unit`
+Envelope payload for the first check-in:
-: **String, required**. The interval unit. Should be one of `year`, `month`, `week`, `day`, `hour`, `minute`.
+```json
+{
+ "check_in_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
+ "monitor_slug": "data-sync",
+ "status": "in_progress"
+}
+```
+
+Envelope payload for the terminal check-in:
+
+```json
+{
+ "check_in_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
+ "monitor_slug": "data-sync",
+ "status": "ok",
+ "duration": 12.5
+}
+```
+
+### Wrapper with monitor upsert
+
+```
+result = withMonitor("daily-report", () => {
+ return generateReport()
+}, {
+ schedule: { type: "crontab", value: "0 2 * * *" },
+ checkinMargin: 10,
+ maxRuntime: 60,
+ timezone: "America/New_York"
+})
+```
+
+The `in_progress` envelope payload includes the monitor config:
+
+```json
+{
+ "check_in_id": "f7e8d9c0b1a2f7e8d9c0b1a2f7e8d9c0",
+ "monitor_slug": "daily-report",
+ "status": "in_progress",
+ "monitor_config": {
+ "schedule": { "type": "crontab", "value": "0 2 * * *" },
+ "checkin_margin": 10,
+ "max_runtime": 60,
+ "timezone": "America/New_York"
+ }
+}
+```
+
+The terminal envelope payload omits the config:
+
+```json
+{
+ "check_in_id": "f7e8d9c0b1a2f7e8d9c0b1a2f7e8d9c0",
+ "monitor_slug": "daily-report",
+ "status": "ok",
+ "duration": 45.3
+}
+```
+
+---
-## Interaction with `beforeSend`
+## Changelog
-Check-in events in the SDK should **not** go through `beforeSend`. SDKs can optionally implement a dedicated `beforeSendCheckIn` hook that only applies to check-in events.
+
diff --git a/src/components/specChangelog.tsx b/src/components/specChangelog.tsx
new file mode 100644
index 00000000000000..ebc4c7caa09636
--- /dev/null
+++ b/src/components/specChangelog.tsx
@@ -0,0 +1,45 @@
+type ChangelogEntry = {
+ date: string;
+ summary: string;
+ version: string;
+};
+
+type SpecChangelogProps = {
+ changelog: ChangelogEntry[];
+};
+
+export function SpecChangelog({changelog}: SpecChangelogProps) {
+ if (!changelog || changelog.length === 0) {
+ return null;
+ }
+
+ return (
+