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.
Basic scheduled task
Section titled “Basic scheduled task”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.
Cron syntax
Section titled “Cron syntax”| Expression | Meaning |
|---|---|
* * * * * | Every minute |
0 * * * * | Every hour (on the hour) |
0 9 * * 1 | Every 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.
Timezones
Section titled “Timezones”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.
On-demand tasks (no schedule)
Section titled “On-demand tasks (no schedule)”Omit schedule for tasks that are only triggered explicitly via pop.schedule():
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!" });Delayed execution
Section titled “Delayed execution”Pass a delay in milliseconds as the third argument to pop.schedule():
// Run in 5 minutesawait pop.schedule("send-report", { channelId: "123" }, 5 * 60_000);
// Run in 24 hoursawait pop.schedule("reminder", { userId: pop.user.id }, 86_400_000);Payload
Section titled “Payload”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]; // ...},Retry logic
Section titled “Retry logic”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); },});| Option | Type | Default | Description |
|---|---|---|---|
retries | number | 0 | How many times to retry after a failure |
retryDelay | number | 1000 | Milliseconds to wait between retries |
Timeouts
Section titled “Timeouts”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(); },});Overlap
Section titled “Overlap”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"); },});The task context — TaskPop
Section titled “The task context — TaskPop”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!");},Triggering tasks from client directly
Section titled “Triggering tasks from client directly”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); }, };}Full options reference
Section titled “Full options reference”| Option | Type | Default | Description |
|---|---|---|---|
name | string | — | Unique task name, used in pop.schedule("name") |
schedule | string | — | Cron expression. Omit for on-demand-only tasks |
timezone | string | "UTC" | IANA timezone for the cron expression |
overlap | boolean | false | Allow concurrent runs |
retries | number | 0 | Retry count on failure |
retryDelay | number | 1000 | Delay between retries in ms |
timeoutMs | number | — | Max run duration in ms before the run is considered timed out |
run | (pop, payload?) => void | required | The function to execute |