---
question: How do I run a job every day, every week, or every few minutes?
kind: task
via_web: na
via_cli: na
via_python: available
via_api: na
via_config: na
ref_python: Cron / Every
prereq: You need a NATS project (the default of `abi new project`) and a module. [Create a module](/capabilities/build/modules/creating-a-module) first.
next:
  - /capabilities/build/orchestrations/run-a-job-by-hand
  - /capabilities/build/pipelines/monitor-runs
---

# Schedule a job

A schedule is a trigger on the job: `Cron` for a calendar time, `Every` for a fixed interval. When the module starts, it publishes one JetStream message schedule per trigger. The broker fires each tick, and one replica of the module runs it.

## Web UI

Not applicable. Nexus shows each job's schedule and next tick under System › Jobs, and starts a run with **Run now**, but no screen sets a schedule: triggers are declared in code. Use Python, then [run the job by hand](/capabilities/build/orchestrations/run-a-job-by-hand?via=web) to test it.

## CLI

Not applicable. No `abi` command creates or schedules a job. Use Python.

## Python

### Declare the job

In a remote module (what `abi new module` creates), import from `naas_abi_sdk.jobs` and put the trigger on a method of `ABIModule`:

```python title="src/reports/__init__.py"
from datetime import timedelta

from naas_abi_sdk import BaseModule
from naas_abi_sdk.jobs import Cron, JobContext, job


class ABIModule(BaseModule):
    @job(triggers=(Cron("@daily"),), timeout=timedelta(minutes=10))
    async def export_report(self, ctx: JobContext) -> dict:
        rows = await self.build_report()  # your code
        ctx.log(f"{len(rows)} rows")
        return {"rows": len(rows)}  # kept on the run record
```

A module the engine loads (`abi new module --in-engine`) imports the same names from `naas_abi_core.module.jobs`; its handler may also be a plain function, which runs in a worker thread.

### Write the cron expression

`Cron` takes **six fields, seconds first**, as NATS message schedules do:

```text
┌───────────── second        0-59
│ ┌─────────── minute        0-59
│ │  ┌──────── hour          0-23
│ │  │ ┌────── day of month  1-31
│ │  │ │ ┌──── month         1-12 or jan-dec
│ │  │ │ │ ┌── day of week   0-6 (0 is Sunday) or sun-sat
│ │  │ │ │ │
0 30 9 * * mon-fri
```

Each field takes `*`, a value, a list (`1,15`), a range (`mon-fri`) or a step (`*/15`). A five-field expression from another scheduler raises `ValueError`: add a leading `0` for the second. When day of month and day of week are both set, a day that matches either one fires.

### Or use an alias

`Cron` also takes one of these aliases. Any other word after `@` raises `ValueError`.

| Alias | Same as | Fires |
|---|---|---|
| `@yearly` | `0 0 0 1 1 *` | January 1 at 00:00 |
| `@annually` | `0 0 0 1 1 *` | January 1 at 00:00 |
| `@monthly` | `0 0 0 1 * *` | The 1st of each month at 00:00 |
| `@weekly` | `0 0 0 * * 0` | Sunday at 00:00 |
| `@daily` | `0 0 0 * * *` | Every day at 00:00 |
| `@midnight` | `0 0 0 * * *` | Every day at 00:00 |
| `@hourly` | `0 0 * * * *` | Every hour, on the hour |

These are the expressions the NATS server substitutes for each alias. Times are in UTC unless the trigger sets a time zone.

### Set the time zone

A `Cron` without `time_zone` runs in **UTC**. Pass an IANA zone name to run on local time; it applies to aliases too:

```python
Cron("@daily", time_zone="Europe/Paris")             # 00:00 in Paris
Cron("@weekly", time_zone="America/New_York")         # Sunday 00:00 in New York
Cron("0 30 8 * * mon-fri", time_zone="Asia/Tokyo")    # weekdays at 08:30 in Tokyo
```

Time zones, like cron and interval schedules themselves, need **NATS 2.14 or later**. The Docker stacks of `abi new project` run 2.14.7. A 2.12 server knows only one-off delayed messages: it does not fire a `Cron` or an `Every`.

Nexus describes each trigger in words, with its zone: `Every day at 00:00 (Europe/Paris)`, `At 08:30 on weekdays (Asia/Tokyo)`.

### Or run on an interval

`Every` takes a Go duration (`"30s"`, `"15m"`, `"1h30m"`) or a `timedelta` of whole seconds, at least one second. It has no time zone:

```python
from datetime import timedelta
from naas_abi_sdk.jobs import Every

Every("15m")
Every(timedelta(hours=6))
```

The interval counts from the last tick, not from the clock: `Every("1h")` does not fire on the hour. Use `Cron("@hourly")` for that.

### Combine triggers

A job takes up to 16 triggers, of any kind. Each fires on its own:

```python
@job(triggers=(
    Cron("0 0 6 * * mon-fri", time_zone="Europe/Paris"),   # weekdays at 06:00
    Cron("0 0 9 * * sat", time_zone="Europe/Paris"),       # Saturday at 09:00
))
async def export_report(self, ctx: JobContext) -> dict:
    ...
```

With the default `max_concurrency=1`, a tick that fires while a run is still going waits for it: runs never overlap.

### Start the module

The schedule exists once the module has started: run it, or restart the engine for an in-engine module. The job then shows under System › Jobs with its next tick. Changing a trigger replaces its schedule at the next start; removing one deletes it.

## REST API

Not applicable. No route sets a schedule. `POST /api/admin/system/jobs/<module>/<job>/trigger` starts one run of a declared job, so an outside scheduler can call it: see [Run a job by hand](/capabilities/build/orchestrations/run-a-job-by-hand?via=api).

## Config

Not applicable. `config.yaml` has no per-job key: triggers live in code. `nats.jobs` holds only process-wide settings, such as `enabled: false` to keep a process from hosting any job.

## Missed ticks and clock changes

- **Missed ticks fire once.** If the broker was down, a schedule that came due fires once when it is back, not once per missed tick.
- **Ticks expire.** A tick waits in the stream while no replica of the module is running: a `Cron` tick for an hour, an `Every` tick for one interval (at least a minute, at most an hour). A module back after a longer stop runs at its next tick, not once per tick it missed.
- **Daylight saving time.** A cron in a zone with daylight saving time follows the wall clock: a time that does not exist on the spring-forward day (02:30 in most of Europe) can be skipped or shifted, and a time that happens twice on the fall-back day can fire twice. Schedule outside 01:00 to 03:00 local time, or use UTC.
- **At least once.** A run can start again after a crash. Make the job safe to repeat: write the report under a dated key, not appended to the last one.

The options (`timeout`, `max_attempts`, `max_concurrency`) and `JobContext` are on [Module jobs](/capabilities/build/modules/module-jobs#triggers).
