Skip to content

Tasks

A task is a background function that runs on a cron schedule, on demand from a command, or both. Files in src/tasks/ are auto-loaded at startup — no registration call required.

src/tasks/daily-cleanup.ts
import { task } from "popii-framework";
export default task({
name: "daily-cleanup",
schedule: "0 3 * * *", // every day at 03:00 UTC
async run(pop) {
const db = (pop.client as any).db;
db.exec("DELETE FROM expired_sessions WHERE expires_at < ?", Date.now());
pop.log.info("Cleaned up expired sessions");
},
});

schedule is a standard five-field cron expression. The task runs at the specified time without blocking the bot’s event loop.

ExpressionMeaning
* * * * *Every minute
0 * * * *Every hour (on the hour)
0 9 * * 1Every Monday at 09:00
0 0 1 * *First day of every month at midnight
*/15 * * * *Every 15 minutes

The fields are: minute hour day-of-month month day-of-week.

Add a timezone to make the cron expression run in local time instead of UTC:

export default task({
name: "morning-report",
schedule: "0 9 * * 1-5", // 9am weekdays
timezone: "America/New_York", // in ET, not UTC
async run(pop) {
pop.log.info("Good morning from ET!");
},
});

Pass any IANA timezone name.

Omit schedule for tasks that are only triggered explicitly via pop.schedule():

src/tasks/send-report.ts
import { task } from "popii-framework";
export default task({
name: "send-report",
async run(pop, payload) {
const { channelId, text } = payload as { channelId: string; text: string };
const channel = pop.client.discord.channels.cache.get(channelId);
if (channel?.isTextBased()) await channel.send(text);
},
});

Trigger it from any command, snap, or event handler:

await pop.schedule("send-report", { channelId: "123456789", text: "Your report is ready!" });

Pass a delay in milliseconds as the third argument to pop.schedule():

// Run in 5 minutes
await pop.schedule("send-report", { channelId: "123" }, 5 * 60_000);
// Run in 24 hours
await pop.schedule("reminder", { userId: pop.user.id }, 86_400_000);

payload is anything you pass as the second argument to pop.schedule(). It’s undefined when the task runs on its scheduled time. Check for it when a task can be both scheduled and triggered manually:

async run(pop, payload) {
const targetId = payload?.userId ?? pop.client.config.owners?.[0];
// ...
},
export default task({
name: "fetch-stats",
schedule: "* * * * *",
retries: 3, // retry up to 3 times on failure
retryDelay: 2_000, // wait 2s between attempts
async run(pop) {
const stats = await fetchExternalStats(); // if this throws, Popii retries
storeStats(stats);
},
});
OptionTypeDefaultDescription
retriesnumber0How many times to retry after a failure
retryDelaynumber1000Milliseconds to wait between retries

Cap how long a task is allowed to run:

export default task({
name: "long-export",
schedule: "0 2 * * *",
timeoutMs: 30_000, // cancel if still running after 30 seconds
async run(pop) {
await generateAndUploadReport();
},
});

By default, if a scheduled task is still running when the next tick fires, the second run is skipped. Set overlap: true to allow concurrent runs:

export default task({
name: "ping-check",
schedule: "* * * * *",
overlap: true, // allow parallel runs even if the previous hasn't finished
async run(pop) {
await pop.fetchJSON("https://api.example.com/ping");
},
});

Task run functions receive a TaskPop object — the same as EventPop plus a discord shorthand:

async run(pop) {
pop.client // PopiiClient — full client access
pop.discord // shorthand for pop.client.discord
pop.log // structured logger
pop.state // shared bot state
pop.cooler // persistent KV store
pop.cache(...) // in-process cache with TTL
pop.t(key) // localization
pop.schedule(...) // trigger another task
pop.fetchJSON(url) // typed HTTP helpers
},

pop.guild and pop.channel are null inside tasks unless you resolve them yourself:

async run(pop) {
const guild = pop.discord.guilds.cache.get("1234567890");
const channel = guild?.channels.cache.get("9876543210");
if (channel?.isTextBased()) await channel.send("Scheduled ping!");
},

Outside of a handler (e.g. in a plugin’s ready() hook), use client.schedule():

export function myPlugin(): PopiiPlugin {
return {
name: "my-plugin",
ready(client) {
client.schedule("send-report", { channelId: "123" }, 10_000);
},
};
}
OptionTypeDefaultDescription
namestringUnique task name, used in pop.schedule("name")
schedulestringCron expression. Omit for on-demand-only tasks
timezonestring"UTC"IANA timezone for the cron expression
overlapbooleanfalseAllow concurrent runs
retriesnumber0Retry count on failure
retryDelaynumber1000Delay between retries in ms
timeoutMsnumberMax run duration in ms before the run is considered timed out
run(pop, payload?) => voidrequiredThe function to execute