# How to Test Cloudflare Cron Triggers with Local Explorer

> Local Explorer now fires a Worker's scheduled() handler from your browser. Version needs, ad hoc crons, scheduled times, the API for scripts, and the gotchas.

By Boris Tane, Founder of Polylane · Published October 18, 2026 · 13 min read
Canonical: https://polylane.com/learn/continuous-integration/how-to-test-cron-triggers-with-cloudflare-workers-local-explorer/

Start your dev server with `cf dev`, `wrangler dev` or the Cloudflare Vite plugin, open `/cdn-cgi/local/explorer` on the server URL, select your Worker, then go to Cron Triggers and click Trigger next to a configured or ad hoc expression. You need Wrangler 4.139.0 or later, or Cloudflare Vite plugin 1.60.0 or later. Nothing runs on a schedule locally, so each click is one invocation. For scripts and coding agents, POST to `/cdn-cgi/local/explorer/api/local/scheduled?worker=<name>` with a JSON body holding the cron expression and, if you want one, a scheduled time.

Cloudflare announced on 17 October 2026 that Local Explorer can now send one-off Cron Trigger invocations to a Worker's `scheduled()` handler during local development. You start your usual local server, open `/cdn-cgi/local/explorer` in the browser, pick **Cron Triggers**, choose a scheduled time and click **Trigger**. You get back the handler outcome, whether it called `noRetry()`, the expression that was sent and the scheduled time.

The same operation is available over the Local Explorer API, so a test script or a coding agent can fire a cron without anyone opening a browser. This page covers what you need installed, how to fire configured and ad hoc crons, how to test date logic with a chosen scheduled time, how to script it, and the mistakes that catch people out.

## What the new Cron Triggers panel in Local Explorer does

Local Explorer is the browser interface that comes with your local development server. It already let you browse and edit local KV, R2, D1, Durable Objects and Workflows data, and view traces and logs for each invocation. The 17 October 2026 release added a **Cron Triggers** group. It shows the exact Cron Triggers configured for the active environment and lets you invoke the `scheduled()` handler with any of them, or with an expression you type in on the spot.

Before this, testing a cron locally meant building a URL by hand. Your dev server exposes `/cdn-cgi/local/scheduled`, and you passed the cron pattern as a query parameter with its spaces encoded as `+`, plus a `time` parameter in epoch milliseconds if you wanted to fake the scheduled time. That route still works. Cloudflare's announcement says the panel removes the need to build those request URLs when you test handler behaviour.

It suits three kinds of work:

- **Writing a new scheduled job.** You click Trigger after each change and read the outcome, without waiting for the clock or remembering the URL format.
- **Testing a Worker with several crons.** One `scheduled()` handler serves every Cron Trigger on a Worker, and the code branches on `controller.cron`. The panel lists each configured expression so you can fire every branch in turn.
- **Testing time-dependent logic.** Month-end reports, weekday-only jobs and anything that reads `controller.scheduledTime` can be run as if it were a different date.

Two boundaries matter from the start. Local development does not run Cron Triggers on their schedule, ever: every run is one you start. And ad hoc triggers and scheduled time presets live in your browser. They do not change `cloudflare.config.ts`, your Wrangler configuration or the Cron Triggers on your deployed Worker.

## What you need before the panel appears

### Versions

Local Explorer itself needs Wrangler 4.118.0 or later, or Cloudflare Vite plugin 1.50.0 or later. The Cron Triggers panel needs newer builds: **Wrangler 4.139.0 or later, or Cloudflare Vite plugin 1.60.0 or later**. If you use `cf dev`, the same requirements apply to the development server it starts, because `cf dev` runs the server from your framework or installed Cloudflare build tool.

Check what you have:

```bash
npx wrangler --version
npm ls @cloudflare/vite-plugin
```

If Local Explorer opens but there is no **Cron Triggers** group, an older version is the first thing to check.

### A scheduled() handler

Your Worker must export a `scheduled()` handler. Here is a small one that branches on two crons and reads the scheduled time:

```ts
interface Env {
  MY_KV: KVNamespace;
}

export default {
  async scheduled(
    controller: ScheduledController,
    env: Env,
    ctx: ExecutionContext,
  ) {
    const runAt = new Date(controller.scheduledTime);
    console.log({ cron: controller.cron, runAt: runAt.toISOString() });

    switch (controller.cron) {
      case "*/5 * * * *":
        await fetch("https://example.com/api/sync");
        break;
      case "0 0 * * *":
        await env.MY_KV.put("last-cleanup", runAt.toISOString());
        break;
      default:
        console.warn({ msg: "no branch for cron", cron: controller.cron });
    }
  },
} satisfies ExportedHandler<Env>;
```

The `default` branch is worth keeping. It makes a cron that matches no case show up in your logs, which matters when you start firing ad hoc expressions.

### At least one configured cron, if you want the Configured Crons list

To test a configured expression, define at least one Cron Trigger for the active environment. Ad hoc expressions need no configuration at all.

With Wrangler:

```jsonc
// wrangler.jsonc
{
  "name": "my-worker",
  "main": "src/index.ts",
  "triggers": {
    "crons": ["*/5 * * * *", "0 0 * * *"]
  }
}
```

With the Cloudflare CLI and `cloudflare.config.ts`:

```ts
import { defineConfig, triggers } from "cf/config";
import * as entrypoint from "./src/index.ts" with { type: "cf-worker" };

export default defineConfig({
  worker: {
    name: "my-worker",
    entrypoint,
    compatibilityDate: "<COMPATIBILITY_DATE>",
    triggers: [triggers.scheduled({ schedule: "0 * * * *" })],
  },
});
```

If you set crons per environment under `env.<name>.triggers` in Wrangler, the panel shows the crons for whichever environment the dev session is running.

## Firing a configured cron from the browser

1. Start the dev server in the project directory with one of these:

```bash
cf dev
# or
npx wrangler dev
# or, with the Cloudflare Vite plugin
npx vite
```

2. Append `/cdn-cgi/local/explorer` to the URL the server prints. For default ports that is `http://localhost:8787/cdn-cgi/local/explorer` for Wrangler or `http://localhost:5173/cdn-cgi/local/explorer` for Vite.
3. Select the Worker you want to test.
4. Select **Cron Triggers** > **Configured Crons**.
5. Under **Scheduled time**, select *Now*, or add a preset for another time (next section).
6. Select **Trigger** next to the expression you want to run.
7. Read the result: the handler outcome, the `noRetry()` status, the expression that was dispatched and the scheduled time.

Repeat step 6 for each configured expression to exercise every branch of the handler.

## Testing a schedule you have not configured yet

Ad hoc triggers let you try an expression before you commit it to config. Select **Cron Triggers** > **Ad-Hoc Triggers**, then **Add trigger**, enter or build an expression, and select **Trigger**.

There are two ways to write one:

- **Expression** takes the exact string you type. Use it for any syntax the builder lacks.
- **Build expression** creates a common five-field UTC expression from form fields. It handles minute, hour, day-of-month and month intervals, plus daily, selected-weekday and monthly schedules. It also offers the Quartz-like options Cloudflare supports: `L` for the last day or last named weekday, `W` for the nearest weekday, `LW` for the last weekday of the month, and `#` for a given occurrence of a named weekday.

The builder follows a few rules that are easy to misread. Step values start at the field's minimum and reset at its boundary, so day steps start on day `1` and month steps start in January. Dates `29` through `31` do not run in months that lack them. Switching to **Build expression** keeps the current expression until you change a valid builder field, which then regenerates it. Whatever the row shows is exactly what gets sent.

Ad hoc triggers are scoped to the local project and Worker and stored in your browser. When you are happy with an expression, copy it into your Wrangler file or `cloudflare.config.ts` yourself, then deploy it.

## Picking the scheduled time to test date logic

The scheduled time becomes `controller.scheduledTime` in your handler, in milliseconds since the Unix epoch, UTC. Under **Scheduled time**, select **Add preset** and enter a UTC date and time, or epoch milliseconds. Presets are reusable and, like ad hoc triggers, they stay in your browser.

This is where the panel saves the most time. A job on `59 23 LW * *` runs at 23:59 UTC on the last weekday of the month. To test it locally you want to run it with a scheduled time on a month's last weekday, then with one on an ordinary day, and confirm your handler does the right thing both times. Add one preset for each, fire the same expression twice, and compare.

Keep two Cloudflare cron rules in mind while you pick times:

- **Cron Triggers run on UTC.** Enter preset times in UTC, and write handler date logic against UTC.
- **Weekdays run from 1 = Sunday to 7 = Saturday.** Some other cron systems use 0 = Sunday and 6 = Saturday. Cloudflare's docs suggest three-letter names such as `SUN` or `MON` to avoid the confusion.

## Reading the outcome, noRetry and the logs behind it

Each trigger returns a result with the handler `outcome` and a `noRetry` value. `noRetry` is `true` when the handler calls `controller.noRetry()`. If your handler calls it on certain failures, this is how you check that the call fires on the paths you expect.

The result tells you whether the run passed. The logs and traces tell you why. Full invocation logs live in the development session that owns the Worker, and that session must have **Observability** turned on. Local Explorer captures traces and logs for every local invocation with no code change: the **Logs** view collects all `console.*` output with filters by level and text search, and the **Traces** view shows every binding operation with timing, status and error details. So if your nightly job does two D1 writes and the second fails, the trace shows which one broke.

Having built and led the Workers observability team at Cloudflare, my rule for scheduled code is to turn Observability on before the first local trigger. A cron that fails in production has no user to report it, so the local run is the cheapest place to see what it actually does.

## Scripting the same test with HTTP requests

The browser panel is for poking at a handler. For a repeatable test, use one of the two HTTP endpoints your dev server exposes. Use the port your server prints: 8787 is Wrangler's default and 5173 is Vite's.

```mermaid alt="Choosing between the Local Explorer panel, the local scheduled route and the Local Explorer API for a cron test"
graph TB
A[You need to run a scheduled handler] --> B{Testing by hand in a browser?}
B -->|yes| C[Open Local Explorer, Cron Triggers]
B -->|no| D{Need to target an exact Worker?}
D -->|yes| E[POST to the Local Explorer API]
D -->|no| F[curl the local scheduled route]
C --> G[Read outcome and noRetry]
F --> G
E --> H[Check result.outcome in the body]
H --> G
style H fill:#d1fae5,stroke:#6ee7b7,color:#065f46
```

### The scheduled route

`/cdn-cgi/local/scheduled` invokes the scheduled handler. It returns the outcome as text by default, or JSON with `?format=json`:

```bash
curl "http://localhost:8787/cdn-cgi/local/scheduled?format=json"
```

```json
{
  "outcome": "ok",
  "noRetry": false
}
```

Pass `cron` to set the pattern, with spaces encoded as `+`, and `time` to override `controller.scheduledTime`:

```bash
curl "http://localhost:8787/cdn-cgi/local/scheduled?cron=*+*+*+*+*&time=1745856238000&format=json"
```

### The Local Explorer API

The API endpoint is the one the announcement highlights for tools and AI agents. It targets an exact Worker by name and takes a JSON body. First list the Worker names the session knows about:

```bash
curl "http://localhost:8787/cdn-cgi/local/explorer/api/local/workers"
```

Then fire a cron. The `cron` field is required and must not be empty. `scheduled_time` is optional, in epoch milliseconds, and defaults to the current time:

```bash
curl -X POST \
  "http://localhost:8787/cdn-cgi/local/explorer/api/local/scheduled?worker=my-worker" \
  -H "Content-Type: application/json" \
  --data '{"cron":"0 * * * *","scheduled_time":1745856238000}'
```

The response uses the Local Explorer API envelope, and its `result` holds `outcome` and `noRetry`. A successful API request does not mean the handler succeeded, so a script must read `result.outcome`:

```bash
OUTCOME=$(curl -s -X POST \
  "http://localhost:8787/cdn-cgi/local/explorer/api/local/scheduled?worker=my-worker" \
  -H "Content-Type: application/json" \
  --data '{"cron":"0 0 * * *"}' | jq -r '.result.outcome')

if [ "$OUTCOME" != "ok" ]; then
  echo "scheduled handler returned: $OUTCOME"
  exit 1
fi
```

The full API is described by an OpenAPI spec served at `/cdn-cgi/local/explorer/api`, so you can check the exact response shape against your installed version.

## Letting a coding agent fire the cron and read the trace

When the dev server detects that it is running inside an AI agent session, it prints a hint with the Local Explorer API address. The agent can fetch the OpenAPI spec there, invoke a `scheduled()` handler with a chosen cron expression and time, and query traces and logs with SQL through `POST /cdn-cgi/local/explorer/api/local/observability/query`. That closes the loop: the agent runs the job, reads what failed, changes the code and runs it again.

```text
> Run the 0 0 * * * cron for my-worker locally, find why it fails, fix it and re-run it
```

*Start wrangler dev or cf dev inside the agent session so it gets the Local Explorer API hint.*

## Mistakes that break local cron tests, and the fix for each

### Waiting for the schedule to fire on its own

Local development does not run Cron Triggers automatically, so a dev server left running overnight runs nothing. **Fix:** start every run yourself, with the Trigger button, a curl to `/cdn-cgi/local/scheduled` or a POST to the Local Explorer API.

### Expecting an ad hoc trigger to reach production

Ad hoc expressions and time presets are stored in the browser for that project and Worker. Your Wrangler file, `cloudflare.config.ts` and deployed crons stay as they were. **Fix:** copy the tested expression into `triggers.crons` or `triggers.scheduled()` and deploy it.

### Treating a successful API response as a passing test

The API request can succeed while the handler throws. **Fix:** read `result.outcome` in the response body and fail the script when it is anything other than `ok`, as in the script above.

### Getting the Worker name slightly wrong

The `worker` query parameter must match the exact Worker name. **Fix:** list the names with `GET /cdn-cgi/local/explorer/api/local/workers` and copy one from the response.

### An ad hoc expression that hits no branch

`controller.cron` is the exact string that was dispatched, and a `switch` on it matches character for character, including spacing. An ad hoc `0 0 * * *` typed with an extra space falls through every case, and the run still reports success. **Fix:** log in a `default` branch, and check the dispatched expression shown in the result against your `case` strings.

### Weekdays off by one

In Cloudflare crons, 1 is Sunday and 7 is Saturday. An expression copied from a system where 0 is Sunday runs on the wrong day. **Fix:** write weekdays as `MON` to `SUN`, which Cloudflare accepts in any case.

### Opening the wrong port

Vite defaults to 5173 and Wrangler to 8787. **Fix:** use the URL your server prints, including when you start it with `cf dev`.

### No logs for the run

Without Observability on in the session that owns the Worker, you get the outcome and little else. **Fix:** turn Observability on for that session, then use the Logs and Traces views to see each `console.*` line and binding call.

### Missing panel after an upgrade elsewhere

Local Explorer can open without a Cron Triggers group on Wrangler older than 4.139.0 or Vite plugin older than 1.60.0. **Fix:** upgrade the tool that actually runs the dev server, which under `cf dev` is your framework or installed Cloudflare build tool.

### Assuming a local pass means production is ready

Adding, changing or removing a Cron Trigger can take up to 15 minutes to reach Cloudflare's network after deploy. The dashboard's Cron Events view keeps the 100 most recent invocations, and on a new or renamed Worker events can take up to 30 minutes to appear. **Fix:** after deploy, confirm the first real run in Cron Events or in Workers Logs, which keeps cron invocation logs for longer with a query interface.

### Wiping deployed crons by accident

When Wrangler deploys, an empty `crons` array removes every Cron Trigger, while an undefined `triggers` or `crons` leaves the deployed ones in place. No local test catches this. **Fix:** check the `triggers` block in code review whenever the Wrangler file changes.

## Watching the cron after it ships, with Polylane

Local Explorer proves the handler works on your machine. Once the Worker is deployed, Polylane connects to Cloudflare, so when the cron starts failing it works out why and opens a pull request with the fix, ready for your team's normal review.

Running on Cloudflare? See [how Polylane monitors Cloudflare in production](https://polylane.com/for/cloudflare/).

## Common questions

**When did Cloudflare add Cron Trigger testing to Local Explorer?**

Cloudflare announced it in the Workers and Cloudflare CLI changelog on 17 October 2026. It adds a Cron Triggers group to Local Explorer that sends one-off invocations to a Worker's scheduled() handler during local development.

**Which Wrangler version do I need to test crons in Local Explorer?**

Wrangler 4.139.0 or later, or Cloudflare Vite plugin 1.60.0 or later. The same requirement applies to the development server that cf dev starts. Local Explorer itself appears from Wrangler 4.118.0 or Vite plugin 1.50.0, but without the Cron Triggers panel before those newer versions.

**Will my cron run on its schedule while wrangler dev is running?**

No. Local development does not run Cron Triggers automatically. Each invocation is a one-off test you start from Local Explorer, the /cdn-cgi/local/scheduled route or the Local Explorer API.

**Does an ad hoc trigger in Local Explorer change my deployed Cron Triggers?**

No. Ad hoc triggers and scheduled time presets are stored in your browser, scoped to the local project and Worker. They do not change cloudflare.config.ts, your Wrangler configuration or deployed Cron Triggers, so add the expression to config and deploy when you want it live.

**How do I test a cron as if it ran on a different date?**

In Local Explorer, select Add preset under Scheduled time and enter a UTC date and time or epoch milliseconds, then trigger the cron. Over HTTP, pass time on the /cdn-cgi/local/scheduled route or scheduled_time in the Local Explorer API body. Either sets controller.scheduledTime in your handler.

**The Local Explorer API returned success but my job failed. Why?**

A successful API request does not mean the handler succeeded. The response wraps the result in the Local Explorer API envelope, and you need to read result.outcome for the invocation result, along with result.noRetry.

**Why can't I see logs for my local cron run?**

Full invocation logs are available in the development session that owns the Worker, and that session must have Observability turned on. With it on, Local Explorer's Logs and Traces views show console output and every binding operation with timing and errors.

**Can a coding agent trigger my scheduled() handler locally?**

Yes. When the dev server detects an agent session it prints the Local Explorer API address. The agent can POST to /cdn-cgi/local/explorer/api/local/scheduled?worker=NAME with a cron expression and optional scheduled time, then query traces and logs with SQL through the observability query endpoint.

## Sources

- [Test Cron Triggers with Local Explorer · Cloudflare changelog](https://developers.cloudflare.com/changelog/post/2026-10-17-local-explorer-cron-triggers/)
- [Cron Triggers · Cloudflare Workers docs](https://developers.cloudflare.com/workers/configuration/cron-triggers/)
- [Local Explorer · Cloudflare Workers docs](https://developers.cloudflare.com/workers/local-development/local-explorer/)
- [Scheduled Handler · Cloudflare Workers docs](https://developers.cloudflare.com/workers/runtime-apis/handlers/scheduled/)
- [Polylane documentation](https://docs.polylane.com/llms-full.txt)
- [Polylane full content](https://polylane.com/llms-full.txt)

## About the author

Boris Tane is the founder of Polylane. He previously founded Baselime, observability for the future of the cloud, which Cloudflare acquired. At Cloudflare he built and led the Workers observability team.

## Related

- [How to Debug Cloudflare Workers Errors: Logs, Traces and Error Codes](https://polylane.com/learn/troubleshooting/how-to-debug-cloudflare-workers-errors/): Debug Cloudflare Workers errors step by step: read 1101 and 1102 codes, enable Workers Logs and source maps, use wrangler tail, DevTools and local traces.
- [Cloudflare Workers error 1101: causes and how to fix it](https://polylane.com/learn/troubleshooting/cloudflare-workers-error-1101/): Error 1101 means your Cloudflare Worker threw an uncaught JavaScript exception. Find the exception in logs, match it to its cause, fix it and roll back fast.
- [Fix Cloudflare “Error 1102: Worker exceeded resource limits”](https://polylane.com/learn/troubleshooting/cloudflare-error-1102-worker-exceeded-resource-limits/): Why a Cloudflare Worker returns Error 1102, how to tell a CPU time overrun from a 128 MB memory overrun, and the code and Wrangler changes that fix each.
- [How to fix a broken build on main](https://polylane.com/learn/continuous-integration/how-to-fix-a-broken-build-on-main/): How to fix a broken build on main: read the failing job, decide between rerun, revert and fix forward, find the breaking commit and stop the next one.
- [Workers Issues: route Cloudflare errors to coding agents](https://polylane.com/learn/incident-response/cloudflare-workers-issues-error-monitoring-agents/): Cloudflare's new Workers Issues groups exceptions and 5xx errors and sends them to Claude Code, Cursor, Devin or a webhook. Setup, automations and limits.

Get started with one command: `curl -fsSL https://polylane.com/setup | bash` installs the CLI, connects your coding agents, and creates the account.
