---
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 ABI 3.x, a NATS project (the default of `abi new project` on 3.x) and a module listed under `modules:` in `config.yaml`. On ABI 2.x, or 3.x without NATS, schedule with [Dagster](/v2/capabilities/build/applications/dagster). [Create a module](/v2/capabilities/build/modules/creating-a-module) first.
next:
  - /v2/capabilities/build/orchestrations/run-a-job-by-hand
  - /v2/capabilities/build/pipelines/monitor-runs
---

> **You are reading the v2.x docs (engine modules). New projects use v3.** [Read this page in v3](/capabilities/build/orchestrations/schedule-a-job) · [Migrate from 2.x to 3.0](/capabilities/upgrade/migrate-to-3)

# Schedule a job

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

Jobs are ABI 3.x with NATS. On ABI 2.x, and on 3.x without NATS, a schedule is a Dagster schedule in a module orchestration (`abi new orchestration`): see [Dagster](/v2/capabilities/build/applications/dagster).

## 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](/v2/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

Import from `naas_abi_core.module.jobs` and put the trigger on a method of `ABIModule`:

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

from naas_abi_core.module.jobs import Cron, JobContext, job
from naas_abi_core.module.Module import BaseModule


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

A plain method runs in a worker thread; an `async` method runs on the module's loop. The engine needs `naas-abi-core[nats]` (part of `[all]`).

### 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_core.module.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
))
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 engine has loaded the module: restart the engine. 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](/v2/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](/v2/capabilities/build/modules/module-jobs#triggers).
