# Scheduled actions

Source: https://dabadata.com/docs/schedules/ (HTML) - https://dabadata.com/docs/schedules/index.md (this page as markdown)

A scheduled action is one of your [actions](https://dabadata.com/docs/api/index.md#actions) run by dabadata on a timetable: reset a daily quota at midnight, write a daily row that a webhook turns into a digest, ask a model for a summary every morning. No cron job, no server of your own. Only your declared action runs - its fixed steps, under its own `allow` rule - and every run is in the trace.

## Declaring a schedule

```ts
roles: ["admin", "member", "maintenance"],
actions: {
  reset_daily_quota: {
    allow: { role: "maintenance" },          // who may run it: the schedule's role
    params: { limit: { type: "int" } },
    steps: [{ op: "update", table: "quotas", id: "global", values: { used: 0, daily_limit: "$params.limit" } }],
  },
},
schedules: {
  dailyReset: {
    action: "reset_daily_quota",
    cron: "0 0 * * *",       // every day at 00:00 UTC
    as: "maintenance",       // the role the run acts as
    params: { limit: 100 },
  },
},
```

An action's steps work on rows by id (`check`, `insert`, `update`, `delete`), and it may end with a [model call](https://dabadata.com/docs/ai/index.md). A step may also change many rows at once with `where`, which is what cleanups need ([Many rows at once](https://dabadata.com/docs/rules/index.md#bulk-steps)):

```ts
expire_old_likes: {
  allow: { role: "maintenance" },
  steps: [{ op: "delete", table: "likes", where: "created_at < $now - 30 days" }],
},
```

| Key | What it does |
| --- | --- |
| `action` | A declared action. |
| `cron` | When, in UTC: five fields, `minute hour day-of-month month day-of-week`. See [below](https://dabadata.com/docs/schedules/index.md#cron). |
| `as` | A role your definition declares. The run's caller is `{ "id": "schedule:<name>", "role": "<as>" }`, and the action's `allow` rule is checked against it like for anyone else. In the steps, `$user.id` is `schedule:<name>`. |
| `params` | Literal values for the action's params, checked when the definition is applied. No placeholders. |
| `enabled` | `false` keeps it declared without running. |

Give the schedule a role of its own (here `maintenance`) and let the action allow only that role: then nobody else can call the action, and the trace says which schedule did what.

## Cron expressions

Five fields, always in UTC. Each field is `*`, a number, a range `a-b`, a list `a,b,c` or a step `*/n` (or `a-b/n`). Day of week is 0-7, where 0 and 7 are Sunday. When both day fields are set, a day matching either runs (as in standard cron).

| Cron | Runs |
| --- | --- |
| `0 3 * * *` | every day at 03:00 UTC |
| `0 * * * *` | every hour, on the hour |
| `*/15 * * * *` | every 15 minutes (paid plans) |
| `30 8 * * 1-5` | weekdays at 08:30 UTC |
| `0 0 1 * *` | the first of every month at midnight UTC |

Names (`MON`, `JAN`), `@daily` and seconds are not accepted.

## How runs happen

- The server checks every 15 seconds, so a run starts within about 15 seconds of its time.
- A run happens once, even with several servers.
- If the server was down at a run's time, it runs once when it is back - not once per missed time - and the next run is the next time after that.
- A run is the same as calling `POST /v1/actions/<action>`: the action's trace rows (`op: "action"` and one per step) carry `source: "schedule"`, its writes reach live subscribers, and its [webhooks](https://dabadata.com/docs/webhooks/index.md) fire.
- A refused or failed run is recorded with its reason (for example the `allow` rule's explanation) and the schedule goes on at its next time.
- While a Free project is paused for being idle, its runs are skipped; they do not wake it.

## Limits

|   | Free | Starter | Pro | Scale |
| --- | --- | --- | --- | --- |
| Schedules per environment | 2 | 10 | 50 | 100 |
| Shortest interval | 1 hour | 1 minute | 1 minute | 1 minute |

A definition over these is refused when you apply it, for example `schedules.cleanup: cron runs every 15 minutes; the free plan allows at most one run per 60 minutes`.

## On the console and the API

The console's **Schedules** page lists each schedule with its next run and its last run (status, error, a link to the trace), and **Run now** for developers and up. With the secret key:

```bash
curl https://dabadata.com/v1/project/schedules -H "Authorization: Bearer $DABADATA_SECRET_KEY"
# -> { "schedules": [{ "name", "action", "cron", "as", "params", "enabled", "nextAt", "last": { "at", "status", "error", "traceId", "durationMs" } }] }

curl -X POST https://dabadata.com/v1/project/schedules/nightlyCleanup/run -H "Authorization: Bearer $DABADATA_SECRET_KEY"
# -> { "ok": true, "status": "ok", "error": null, "traceId": 1234, "durationMs": 18 }
```

**Run now** runs it once, enabled or not, and leaves the next scheduled time as it was. Need logic an action cannot express? Pair a schedule with a [webhook](https://dabadata.com/docs/webhooks/index.md) on the rows it writes, or [call dabadata from your backend](https://dabadata.com/docs/backend/index.md) on your own cron.
