---
title: Schedule handler
description: Read a scheduled invocation's headers, choose a response status, and distinguish a private-path refusal from a failed or timed-out run.
slug: /reference/schedule-handler
order: 10
visibility: public
redirects_from: []
---

# Schedule handler

A schedule handler receives one HTTP request for a declared schedule's due instant. This reference brings together its request headers, response outcomes, and the check that keeps the handler private.

## Request headers

The platform sends one `POST` with an empty body to the declared path. The [serving router](/cloud/concepts/glossary/#serving-router) alone sets these four headers:

| Header | Value |
| --- | --- |
| `x-turnzero-cloud-invocation` | `schedule` |
| `x-turnzero-cloud-schedule` | The schedule's name. |
| `x-turnzero-cloud-run` | The run identifier. |
| `x-turnzero-cloud-schedule-due` | The [due instant](/cloud/concepts/glossary/#scheduled-run-and-due-instant) in ISO 8601 form in UTC, to the millisecond, such as `2026-10-08T05:00:00.000Z`. |

The router never passes a client's copies of these headers to the application. The platform checks the request before the handler runs; there is no signature or token for the handler to verify. Never log or echo other `x-turnzero-cloud-` headers, or log a request's headers whole. The platform removes the router's mark before the handler runs.

## Private-path check

On a deployed copy, the [Node.js Runtime harness](/cloud/concepts/glossary/#runtime-harness) first checks the router's mark. An unmarked request is refused 403 `router_mark_required`; a loopback request is exempt from that mark check.

After that check passes, a request on a declared handler path without the router-set invocation header receives 404 `scheduled_handler_only`. The harness refuses it before application code runs, so the declared handler path is not a public endpoint.

Keep a handler's own check where a framework routes more widely than the exact declared path, such as a prefix or wildcard mount. A handler without the [Schedule](/cloud/backend-packages/schedule/) package's router checks for `x-turnzero-cloud-invocation: schedule` and returns 404 otherwise.

The [Schedule router](/cloud/backend-packages/schedule/#building-and-mounting-the-router) checks its declared paths. Its `readScheduledRun(headers)` returns no run unless all four headers are present exactly once. The [plain Node.js handler](/cloud/backend-packages/schedule/#a-handler-without-the-package) shows the check without the package.

On your machine, use the package's [test double](/cloud/backend-packages/schedule/#test-with-the-double) or [local request recipe](/cloud/backend-packages/schedule/#send-a-run-to-your-machine). A local run loads no runtime harness.

## Response status and outcome

The platform records the handler's HTTP status and the run's outcome separately:

| What happens | Recorded outcome | What to do |
| --- | --- | --- |
| The handler responds with a 2xx status inside the window. | `succeeded` | End the response after the work finishes. |
| The handler responds with any other status, including 3xx, 4xx, or 5xx. | `failed` | Put the reason in the response body; the platform keeps its first bytes. |
| The upstream fails after the router starts the request. | `failed` | Read the run's detail and application logs. |
| The request reaches its execution window before completion. | `window_ended` | Stop work when the request's abort signal fires. |

With the [Schedule](/cloud/backend-packages/schedule/) package, a handler that returns nothing gets a 200 response. A handler may return a status and body or end the response itself. If it throws, the package router responds 500 `handler_failed`.

The window is 230 seconds by default. Read its current length as `window_seconds` in `submit_manifest` or `read_schedules`. Waking a stopped environment spends part of that window. A response sent after the window ends cannot change the outcome. Start no timer, polling loop, or child process that outlives the response.

## Repeats and runs that never reach the handler

The platform retries no run. Make the handler safe to repeat for the same schedule name and due instant, using a record in storage or a declared database. The Schedule package supplies that pair as `run.key`; a run identifier alone does not identify the due instant.

A due time while the previous run remains in flight is `skipped`, never queued. Named application admission refusals also record `skipped`: the application is unknown or undeployed, its owner is suspended, or its monthly backend-action or data-transfer limit is exceeded. The run's detail gives the reason.

Failure to reach the router, an invoke-secret refusal, or a router failure before it can forward the request records `unreachable`. A due time too old to start is `missed`. A run left without a recorded end after its window is `abandoned`, whose outcome is unknown.

These outcomes are not HTTP responses from the handler. For declaration, deployment, halt, and quota refusals, use [Add a scheduled job: Refusals](/cloud/guides/add-a-scheduled-job/#refusals) and the [refusals reference](/cloud/reference/refusals/).

## Related

- [Add a scheduled job](/cloud/guides/add-a-scheduled-job/) declares, deploys, and checks a job, including a manual run.
- [Run scheduled work](/cloud/guides/author-the-manifest/#run-scheduled-work) describes the declaration and when it starts in each environment.
- [Schedule](/cloud/backend-packages/schedule/) describes the package's router, handlers, and test double.
- [Read logs and counters](/cloud/guides/read-logs-and-counters/) reads the platform's run events and the handler's console.
