Skip to content

Scheduled jobs

A scheduled job is a class implementing ScheduledJob from platform:scheduled-job. The interface is one method:

export interface ScheduledJob {
run(): Promise<void>;
}

No arguments and no return value — a job decides for itself what to look at.

import type { ScheduledJob } from "platform:scheduled-job";
import { sql, tableRef } from "platform:stdlib";
import { Comment, Issue } from "platform:entities";
const i = tableRef(Issue);
export default class StaleIssueTriage implements ScheduledJob {
async run(): Promise<void> {
const overdue = await sql.systemMode.query<Issue>(`
SELECT ${i.issue_id}, ${i.record_id}
FROM ${i}
WHERE ${i.due_date} < CURRENT_DATE
AND ${i.status} <> 'closed'
`);
const comments = overdue.rows.map((issue) => {
const comment = new Comment();
comment.issue_id = issue.issue_id;
comment.body = "Overdue — please assign an owner.";
return comment;
});
await sql.systemMode.insertOrThrow(comments);
}
}

A job is not triggered by a person, so there is no user whose permissions apply. Plain sql therefore sees very little. Jobs almost always want sql.systemMode.

Writes are attributed to the built-in SYSTEM user, which exists in every workspace precisely so automated writes satisfy the foreign keys that reference users.

Build the full set, then issue one call. The example above collects every comment and inserts once. The shape to avoid:

// Don't: one round trip per row
for (const issue of overdue.rows) {
await sql.systemMode.insertOrThrow([makeComment(issue)]);
}

Jobs live in your configuration directory and deploy with everything else:

Terminal window
polymesa deploy ./config

Schedules are configured against the job in the app, so the same code can run at a different cadence per workspace.