Tasks
Tasks are one-off runtime operations (migrations, cleanups, cache refresh). Experimental — enable the flag first. Tasks are scanned from serverDir, which must also be set.
import { defineConfig } from "nitro";
export default defineConfig({
serverDir: "./server",
experimental: { tasks: true },
});Defining tasks
Files in <serverDir>/tasks/[name].ts. Nested dirs join with : (e.g. tasks/db/migrate.ts → db:migrate). Import defineTask from nitro/task (no auto-imports in v3).
import { defineTask } from "nitro/task";
export default defineTask({
meta: {
name: "db:migrate",
description: "Run database migrations",
},
run({ payload, context }) {
console.log("Running DB migration...");
return { result: "Success" };
},
});run receives a TaskEvent with name, payload (Record<string, unknown>), and context (may include waitUntil). Return { result }.
Tasks can also be registered in config (config handler wins over a scanned file of the same name):
import { fileURLToPath } from "node:url";
export default defineConfig({
serverDir: "./server",
experimental: { tasks: true },
tasks: {
"db:migrate": { description: "Describe a scanned task" },
// A config handler is imported as-is (NOT resolved against rootDir) — use an absolute path:
"db:seed": {
handler: fileURLToPath(new URL("scripts/seed.ts", import.meta.url)),
description: "Seed the database",
},
},
});Scheduled tasks (cron)
Map cron expressions to task name(s). Multiple tasks under one expression run in parallel; scheduled runs get a payload.scheduledTime timestamp.
import { defineConfig } from "nitro";
export default defineConfig({
scheduledTasks: {
"* * * * *": ["cms:update"], // every minute
"0 0 * * *": "db:cleanup", // daily (string shorthand)
"*/5 * * * *": ["health:check", "metrics:collect"],
},
});Platform support:
dev,node_server,node_cluster,node_middleware,bun,deno_server→ croner engine.cloudflare_module/cloudflare_pages→ native Cron Triggers (wrangler config auto-generated).vercel→ native Cron Jobs (config auto-generated; secure withCRON_SECRET).
Running tasks programmatically
import { defineHandler } from "nitro";
import { runTask } from "nitro/task";
export default defineHandler(async (event) => {
// IMPORTANT: authenticate and validate before running!
const payload = Object.fromEntries(event.url.searchParams);
const { result } = await runTask("db:migrate", { payload });
return { result };
});runTask throws a 404 if the task doesn't exist, 501 if it has no handler; errors from run propagate to the caller.
Background work with waitUntil
import { defineTask } from "nitro/task";
export default defineTask({
run({ context }) {
const promise = fetch("https://api.example.com/sync");
context.waitUntil?.(promise);
return promise.then(() => ({ result: "ok" }));
},
});Dev server tools
While nitro dev runs:
GET /_nitro/tasks— list available tasks + scheduled tasks.GET|POST /_nitro/tasks/:name— execute (payload from query and/or JSON body under"payload").- CLI:
nitro task listandnitro task run db:migrate --payload "{}".
Key Points
- Requires
experimental.tasks: trueandserverDirset;defineTaskandrunTaskboth come fromnitro/task(no auto-imports in v3). - Each task has one running instance — parallel calls of the same name share a single run/result.
scheduledTaskscron config is translated to native triggers on Cloudflare and Vercel automatically.