# Scheduled jobs

Source: https://modulify.ai/docs/automations/scheduled-jobs

Call one of your own URLs on a schedule and read what it returned.

A cron calls one URL on a schedule. Modulify runs it from its own servers, records the response, and keeps a history you can read afterwards.

Crons belong to a project. Any member of the workspace that owns the project can create, edit and run them.

## Before you begin

The request URL has to be public and use `https`. A cron cannot reach `localhost` or an address on a private network, so an endpoint that only exists on your machine will not work.

Crons keep firing whether or not the site is published. If you have never published this project, the Crons tab shows a banner saying so: "Crons run on our servers, so they keep firing whether or not this site is published. A cron that calls this site will fail until you publish it."

If you want the request to carry an API key, add it in the [Secrets](/docs/data/secrets) tab first. A cron can point a header at a site secret, but it cannot create one.

## Open the Crons tab

Click **More** in the tab strip, then **Crons**. The address ends in `/crons`.

Three tabs sit under the heading:

- **Crons**, the list of jobs on this site
- **Runs**, the history of what each one did
- **Settings**, export and the danger zone

An empty site shows **No crons** with the line "Schedule a URL call to run automatically." and a **Create cron** button. Once you have at least one, an **Add cron** button appears at the top right of the list instead.

A site can hold **10 crons**. At the limit the **Add cron** button is disabled and a warning explains that you have to remove one to add another.

## Create a cron

Click **Create cron** or **Add cron**. The form asks for five things.

**Name** is what the list and the run history call this job. It is limited to 64 characters. The placeholder is `Nightly cleanup`.

**Runs** is the schedule. See the next section.

**Method** is one of `POST`, `GET`, `PUT`, `PATCH` or `DELETE`. It defaults to `POST`.

**Request URL** is the endpoint to call, up to 2048 characters. The placeholder is `https://mysite.com/api/cleanup`.

**Headers** and the body are optional, and are covered below.

The submit button reads **Create cron**. If it is disabled, hover it: the tooltip names the one thing that is blocking you, such as "Give this cron a name." or "Enter a valid https request URL."

## Choose a schedule

The **Runs** dropdown offers five kinds:

| Runs | What you pick | Example |
|---|---|---|
| Every hour | A minute | Every hour at :15 |
| Every day | An hour and a minute | Every day at 09:00 |
| Every week | One or more weekdays, plus a time | Mon, Thu at 07:30 |
| Every month | A day of the month, plus a time | Day 1 at 00:00 |
| Custom (cron) | A five field cron expression | `0 9 * * 1-5` |

Hours are offered on the hour, from `00:00` to `23:00`. Minutes are offered in five minute steps. The monthly picker offers **Day 1** to **Day 28**, so a schedule never falls on a date that some months do not have.

Weekdays are chips you toggle: **Mon**, **Tue**, **Wed**, **Thu**, **Fri**, **Sat**, **Sun**. Picking none is rejected when you save.

### Custom cron expressions

The **Custom (cron)** kind takes a standard five field expression: minute, hour, day of month, month, day of week. Ranges (`1-5`), lists (`0,30`), steps (`*/15`) and month or weekday names (`JAN`, `MON`) all work. Day of week accepts `0` to `7`, where both `0` and `7` mean Sunday.

The shortcuts `@hourly`, `@daily`, `@midnight`, `@weekly`, `@monthly`, `@yearly` and `@annually` are accepted and expand to the matching expression. Anything else starting with `@` is rejected.

### The five minute floor

A cron runs **at most once every five minutes**. A schedule that would fire more often than that is refused when you save, with the message "A cron can run at most once every 5 minutes. Space the runs further apart." So `*/5 * * * *` is fine and `*/2 * * * *` is not.

### Next runs and the timezone

Under the pickers, the **Next runs** box lists the next five times this schedule will fire, refreshed as you edit. If the schedule cannot be read, the reason appears there in red instead.

Below the list, "Times shown in" names the timezone, and the timezone itself is a searchable dropdown. The schedule is stored with that timezone, so a daily 09:00 job keeps firing at 09:00 local time across a daylight saving change. New crons start on your browser's timezone.

## Send headers and a body

The **Headers** section starts empty with the line "No headers. Add one to send an API key or a content type." Use the plus button to add a row. A step can carry **20 headers**.

Each row has a name, a source and a value. The source is either **Value**, which you type, or **Site secret**, which points at a key from the [Secrets](/docs/data/secrets) tab. If the site has no secrets yet, the picker reads **No secrets yet** and the form tells you to add one in the Secrets tab first.

Header values are encrypted. Once saved, a typed value is never shown again: the row displays four dots, followed by the last four characters if what you entered was at least eight characters long. Leave the field blank when editing and the stored value is kept.

Header names are checked as you save. The message reads "Use only letters, numbers and - . _ with no spaces!" Transport headers such as `Host`, `Content-Length`, `Connection` and anything starting with `proxy-` are rejected outright.

The body editor is enabled for `POST`, `PUT`, `PATCH` and `DELETE`, and is limited to 32,000 characters. For `GET` the editor is disabled and the note reads "GET requests do not send a body. This body is removed when you save."

A body that starts with `{` or `[` is checked as JSON, and the form blocks saving with "This does not look like valid JSON yet." until it parses. Anything else is sent as you typed it. Nothing sets a content type for you, so add a `Content-Type` header if the endpoint expects one.

Inside the body you can reference a [secret](/docs/data/secrets) as `{{secrets.KEY}}` and it is substituted at request time. The JSON check ignores those tokens. Secrets are not substituted in the URL: put them in a header instead.

```json
{
    "task": "cleanup",
    "token": "{{secrets.CLEANUP_TOKEN}}"
}
```

## What happens when a cron fires

One HTTP request goes out with your method, headers and body. It has **30 seconds** to answer.

A status in the 200 to 299 range is a success. Anything else is a failure, and the response body is recorded with it, up to 8,000 characters. A successful run records the status and the duration but not the body. A request that never completes records "The request could not be completed. The host may be unreachable or blocked."

Up to five redirects are followed. A redirect to a different origin strips headers whose name mentions auth, cookie, token, secret or a key, and drops the body. If the body was dropped on the way, the run fails and tells you to point the cron at the final URL. A response larger than 32,000 bytes is not recorded.

## Run, pause and edit

Each row in the list shows the method, the URL, the schedule in plain words, when the next run is due, and a green or red dot for how the last run went.

The switch on the right pauses and resumes the cron. Pausing clears its next run time, and resuming recomputes it from now and resets the failure count.

The three dots menu holds four actions:

- **Run now** fires the cron immediately
- **Run history** jumps to the Runs tab filtered to this cron
- **Edit** opens the form inline in the row
- **Delete** removes the cron and its run history, after a confirmation

**Run now** is recorded like any other run, but it does not move the schedule and it does not count towards the consecutive failure total. When it finishes, a toast reports what the endpoint answered and points you at Runs for the full response.

## Read the run history

The **Runs** tab lists recent runs, newest first, with a **Success** or **Failed** badge, the cron name, how long ago it ran and how many milliseconds it took. A run started by **Run now** carries a **Manual** tag.

Click a row to expand it. You get the step name, the response status, the duration, the method and URL that were actually called, the error if there was one, and the recorded response body.

The URL shown in an expanded run has its query string removed, so a token passed as a query parameter is never written into the history.

Three dropdowns filter the list: by cron (**All crons**), by outcome (**All runs**, **Successful**, **Failed**, **Manual runs**) and by order (**Newest first**, **Oldest first**). **Refresh** re-reads the list, **Load more** pages through it, and the X clears the filters once any are set.

Hovering a row reveals a small trash icon that removes that single run from the history.

Runs are kept for **30 days**.

## When a cron switches itself off

A cron disables itself in three cases, and the reason appears in amber under its name in the list:

- **20 consecutive failures.** The reason reads "Disabled after 20 consecutive failures." A successful run resets the count to zero, so only an unbroken run of failures trips it.
- **The project was deleted.** The reason reads "Disabled because this project was deleted."
- **The schedule can no longer be resolved**, which carries the parser's own explanation.

Flipping the switch back on clears the reason and the failure count.

If a scheduled slot is missed, the cron does not fire repeatedly to catch up. It skips forward to the next slot in the future.

## Export and clear

The **Settings** tab holds three things.

**Export crons** downloads every cron on the site as JSON, including its schedule, URL, method, header names and body. Header values and secret values are never included. The file is named after the project slug.

**Clear run history** empties the Runs tab and leaves the crons running.

**Delete all crons** permanently deletes every cron on the site along with its history. Both of the danger zone buttons are disabled unless your workspace role can delete projects, and both ask you to confirm first.


## From an AI client over MCP

Everything on this page is reachable from a connected AI client: `list_site_crons`, `create_site_cron`, `update_site_cron`, `delete_site_cron`, `delete_all_site_crons`, `toggle_site_cron`, `run_site_cron` and `export_site_crons` for the jobs themselves, and `list_site_cron_runs`, `delete_site_cron_run` and `clear_site_cron_runs` for the history. `preview_cron_schedule` checks a schedule before it is saved.

Creating and changing jobs needs the `config:write` scope, which is unticked by default. See [MCP tools](/docs/mcp/tools).
## Next

- [Webhooks](/docs/automations/webhooks) go the other way: Modulify calls your endpoint when the site publishes.
- [Publish a site](/docs/publish/publish-a-site) covers getting the site live so a cron pointed at it can reach it.
- [Tabs and visibility](/docs/editor/tabs-and-visibility) explains the More menu the Crons tab lives in.