---
title: "Tasks"
description: "Scheduled and manual tasks with Nitro's defineTask, run by wervt."
canonical_url: "https://wervt.app/guides/tasks"
---
# Tasks

> Scheduled and manual tasks with Nitro's defineTask, run by wervt.

Periodic work, like sweeping stale rows, renewing webhooks or syncing a calendar, goes into
tasks. Write them the Nitro way; wervt runs them on schedule.

```ts [server/tasks/presence/sweep.ts]
import { defineTask, useDb } from '@wervt/nuxt/server'
import { lt } from 'drizzle-orm'
import { presence } from '../../db/schema'

export default defineTask({
  meta: { description: 'Remove presence older than 5 minutes' },
  async run({ payload }) {
    const cutoff = new Date(Date.now() - 5 * 60_000)
    await useDb().delete(presence).where(lt(presence.seenAt, cutoff))
    return { result: 'ok' }
  },
})
```

The file path is the task's name: `server/tasks/presence/sweep.ts` is `presence:sweep`. Schedule it
in `nuxt.config.ts` with standard cron expressions:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  modules: ['@wervt/nuxt'],
  nitro: {
    scheduledTasks: {
      '*/5 * * * *': ['presence:sweep'],
      '0 4 * * *': ['eventsub:renew'],
    },
  },
  // Optional: the schedules' time zone. UTC by default.
  wervt: { timezone: 'Europe/Berlin' },
})
```

Schedules deploy and roll back with the code. `nuxt dev` runs them locally with Nitro's own
scheduler.

## How wervt runs them

Isolates stop when they're idle, so an app can't keep a clock itself. The control plane does: on
every deploy, activation or rollback it loads the app's schedules, and when one is due it calls the
task in the app's isolate through the runtime.

- **Limits:** a run has the limits of a request: 5 minutes and limited CPU time. Keep tasks short;
split big jobs into batches across runs.
- **No overlap:** if a task is still running when it's due again, that run is skipped.
- **No catch-up:** runs that were due while wervt was down are skipped, not made up later. Write tasks so
the next run does what a missed one would have.
- **At most once a minute.** Cron expressions have minute precision.

## Running and watching tasks

The app's **Tasks** page in the dashboard lists its tasks with their schedules and next runs, the
last result, and a **Run now** button. Tasks without a schedule can be run there too, for
one-off jobs like a backfill.

```bash
wervt tasks my-app                                   # tasks, schedules, next runs
wervt tasks my-app --run presence:sweep              # run now and wait for the result
wervt tasks my-app --run import:users --payload '{"since":"2026-01-01"}'
wervt logs my-app 'type:=Task' --since 1d            # every run: status, duration, errors
```

A manual run gets its `--payload` as `payload`; scheduled runs get `{ scheduledTime }`.


## Sitemap

See the full [sitemap](https://wervt.app/sitemap.md) for all pages.
