
# Tasks

> Nitro tasks allow one-off operations at runtime: named server-side actions such as database migrations, cache cleanup, or content syncing that you can run on demand, on a schedule, or from the CLI.

## Opt-in to the experimental feature

::important
Tasks support is currently experimental.
See [nitrojs/nitro#1974](https://github.com/nitrojs/nitro/issues/1974) for the relevant discussion.
::

In order to use the tasks API you need to enable the experimental feature flag. Tasks are scanned from your [`serverDir`](/docs/configuration), which has to be set as well:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  serverDir: "./server",
  experimental: {
    tasks: true
  }
})
```

All examples on this page assume this config.

## Define tasks

Tasks can be defined in `tasks/[name].ts` files inside your [`serverDir`](/docs/configuration).

::note
Like routes and plugins, tasks are only scanned when [`serverDir`](/docs/configuration) (or `scanDirs`) is set; no directories are scanned by default.
::

Nested directories are supported. The task name will be joined with `:`.

```txt
server/
  tasks/
    cleanup.ts       <-- cleanup
    db/
      migrate.ts     <-- db:migrate
nitro.config.ts
```

**Example:**

```ts [server/tasks/db/migrate.ts]
import { defineTask } from "nitro/task";

export default defineTask({
  meta: {
    name: "db:migrate",
    description: "Run database migrations",
  },
  run({ payload, context }) {
    console.log("Running DB migration task...");
    return { result: "Success" };
  },
});
```

### Task interface

The `defineTask` helper accepts an object with the following properties:

- **`meta`** (optional): An object with optional `name` and `description` string fields used for display in the dev server and CLI.
- **`run`** (required): A function that receives a [`TaskEvent`](#taskevent) and returns (or resolves to) an object with an optional `result` property.

```ts
interface Task<RT = unknown> {
  meta?: { name?: string; description?: string };
  run(event: TaskEvent): { result?: RT } | Promise<{ result?: RT }>;
}
```

### `TaskEvent`

The `run` function receives a `TaskEvent` object with the following properties:

- **`name`**: The name of the task being executed.
- **`payload`**: An object (`Record<string, unknown>`) containing any data passed to the task.
- **`context`**: A `TaskContext` object (may include `waitUntil` depending on the runtime).

```ts
interface TaskEvent {
  name: string;
  payload: TaskPayload;
  context: TaskContext;
}
```

### Registering tasks via config

In addition to file-based scanning, tasks can be registered directly in the Nitro config. This is useful for adding a description to a scanned task, or for tasks provided by modules and pointing to custom handler paths.

```ts [nitro.config.ts]
import { fileURLToPath } from "node:url";
import { defineConfig } from "nitro";

export default defineConfig({
  serverDir: "./server",
  experimental: {
    tasks: true
  },
  tasks: {
    // Describe a task scanned from `server/tasks/db/migrate.ts`
    "db:migrate": {
      description: "Run database migrations"
    },
    // Register a task from a custom location
    "db:seed": {
      handler: fileURLToPath(new URL("scripts/seed.ts", import.meta.url)),
      description: "Seed the database"
    }
  }
})
```

::warning
Unlike route handlers, a task `handler` is imported as-is and is **not** resolved against the project root, so a path such as `"./scripts/seed.ts"` will fail to resolve at build time. Use an absolute path (for example via `fileURLToPath(new URL(..., import.meta.url))`) or a package name.
::

If a task is both scanned from the `tasks/` directory and defined in the config, the config-defined `handler` takes precedence.

## Scheduled tasks

You can define scheduled tasks using Nitro configuration to automatically run at the specified cron-formatted times.

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  scheduledTasks: {
    // Run `cms:update` task every minute
    '* * * * *': ['cms:update'],
    // Run a single task (string shorthand)
    '0 * * * *': 'db:cleanup'
  }
})
```

The `scheduledTasks` config maps cron expressions to either a single task name (string) or an array of task names. When multiple tasks are assigned to the same cron expression, they run in parallel.

::tip
You can use [crontab.guru](https://crontab.guru/) to easily generate and understand cron tab patterns.
::

When a scheduled task runs, it automatically receives a `payload` with `scheduledTime` set to the current timestamp (`Date.now()`).

::note
Prefer `scheduledTasks` when your deployment target supports it (see [platform support](#platform-support)). Nitro wires up the platform's native scheduler for you. If your platform is not supported, you can still trigger tasks from an external scheduler by calling [`runTask`](#programmatically-run-tasks) from an authenticated HTTP endpoint.
::

### Platform support

- **`dev`**, **`node_server`**, **`node_cluster`**, **`node_middleware`**, **`bun`** and **`deno_server`** presets are supported by the [croner](https://croner.56k.guru/) engine.
- **`cloudflare_module`** and **`cloudflare_pages`** presets have native integration with [Cron Triggers](https://developers.cloudflare.com/workers/configuration/cron-triggers/). Nitro automatically generates the cron triggers in the wrangler config at build time - no manual wrangler setup required.
- **`vercel`** preset has native integration with [Vercel Cron Jobs](https://vercel.com/docs/cron-jobs). Nitro automatically generates the cron job configuration at build time - no manual `vercel.json` setup required. You can secure cron endpoints by setting the `CRON_SECRET` environment variable.
- More presets (with native primitives support) are planned to be supported!

## `waitUntil`

When running background tasks, you might want to make sure the server or worker waits until the task is done.

An optional `context.waitUntil` function _might_ be available depending on the runtime.

```ts
import { defineTask } from "nitro/task";

export default defineTask({
  run({ context }) {
    const promise = fetch(...)
    context.waitUntil?.(promise);
    await promise;
    return { result: "Success" };
  },
});
```

## Programmatically run tasks

To manually run tasks, you can use the `runTask(name, { payload?, context? })` function from `nitro/task`.

**Example:**

```ts [server/api/migrate.ts]
import { defineHandler } from "nitro";
import { runTask } from "nitro/task";

export default defineHandler(async (event) => {
  // IMPORTANT: Authenticate user and validate payload!
  const payload = Object.fromEntries(event.url.searchParams);
  const { result } = await runTask("db:migrate", { payload });

  return { result };
});
```

### Error handling

`runTask` throws an HTTP error if:

- The task does not exist (status `404`).
- The task has no handler implementation (status `501`).

Any errors thrown inside the task's `run` function will propagate to the caller.

## Run tasks with dev server

Nitro's built-in dev server exposes tasks to be easily executed without programmatic usage.

### Using API routes

#### `/_nitro/tasks`

This endpoint returns a list of available task names and their meta.

```json
// [GET] /_nitro/tasks
{
  "tasks": {
    "db:migrate": {
      "description": "Run database migrations"
    },
     "cms:update": {
      "description": "Update CMS content"
    }
  },
  "scheduledTasks": [
    {
      "cron": "* * * * *",
      "tasks": [
        "cms:update"
      ]
    }
  ]
}
```

#### `/_nitro/tasks/:name`

This endpoint executes a task. You can provide a payload using both query parameters and body JSON payload. The payload sent in the JSON body payload must be under the `"payload"` property.

::code-group
```ts [server/tasks/echo/payload.ts]
import { defineTask } from "nitro/task";

export default defineTask({
  meta: {
    name: "echo:payload",
    description: "Returns the provided payload",
  },
  run({ payload, context }) {
    console.log("Running echo task...");
    return { result: payload };
  },
});
```
```json [GET]
// [GET] /_nitro/tasks/echo:payload?field=value&array=1&array=2
{
  "field": "value",
  "array": ["1", "2"]
}
```
```json [POST]
/**
 * [POST] /_nitro/tasks/echo:payload?field=value
 * body: {
 *   "payload": {
 *     "answer": 42,
 *     "nested": {
 *       "value": true
 *     }
 *   }
 * }
 */
{
  "field": "value",
  "answer": 42,
  "nested": {
    "value": true
  }
}
```
::

::note
Keys specified in the JSON body overwrite any values present in the query parameters.
::

### Using the CLI

::important
It is only possible to run these commands while the **dev server is running**. You should run them in a second terminal.
::

#### List tasks

```sh
nitro task list
```

#### Run a task

```sh
nitro task run db:migrate --payload "{}"
```

The `--payload` flag accepts a JSON string that will be parsed and passed to the task. If the value is not a valid JSON object, the task runs without a payload.

::read-more{to="/docs/cli#nitro-task"}
See the CLI reference for all `nitro task` subcommands and flags.
::

## Notes

### Concurrency

Task runs are deduplicated by **task name**: each task can have at most one running instance per server instance. Calling `runTask` for a task that is already running does not start a new run: all concurrent callers, even those passing different payloads, share the result of the first in-flight run.
