Browse documentation

Motherbase has a Cron Jobs page in Project Studio for two kinds of recurring work:

  • pg_cron jobs - PostgreSQL-native jobs stored in the database and executed by the pg_cron extension
  • App Jobs - code-defined node-cron jobs registered by the backend process and exposed as read-only status

pg_cron jobs

Open Project Studio -> Cron Jobs -> pg_cron.

The tab lists jobs from cron.job with their name, schedule, SQL command, active state, database, and username. You can create, enable, disable, edit, and delete jobs from the UI.

Create a job with a standard five-field cron schedule:

SELECT cron.schedule(
  'nightly-cleanup',
  '0 3 * * *',
  'SELECT cleanup_expired_sessions();'
);

The Studio form stores the same values through the admin API:

{
  "name": "nightly-cleanup",
  "schedule": "0 3 * * *",
  "command": "SELECT cleanup_expired_sessions();"
}

Enable pg_cron

If pg_cron is not installed, the API returns extensionMissing: true and Studio shows an install banner.

Run this SQL from a privileged connection:

CREATE EXTENSION pg_cron;

Some PostgreSQL deployments also require pg_cron in shared_preload_libraries, which means the database server must be configured and restarted before CREATE EXTENSION works.

App jobs

Open Project Studio -> Cron Jobs -> App Jobs.

Three kinds of job share the list: jobs registered in backend code with helpers/cron.helper.ts, scheduled edge functions, and the platform's own schedulers (backups, retention, storage cleanup, realtime outbox, bucket deletion).

export const initCron = (_opts: IContextContainer) => () => {
  register('cleanup-expired-tokens', '0 3 * * *', async () => {
    // run backend maintenance work
  })
}

Every scheduler declares itself — what it does in one sentence, the file that owns it, and the console route that owns its configuration — next to the code that owns the behaviour. So a job that is configured but has never fired still appears, and a scheduled edge function appears once rather than twice.

Each row carries a state, chosen so that "we do not know" never renders as "fine":

StateMeaning
disabledConfigured off, no timer armed
scheduledArmed, no run on record yet
runningA run is in flight
ok / failedOutcome of the newest finished run
unknownArmed, but its history could not be read

Clicking a row opens the job: what it does, its source file, its cadence, a link to the settings that own it, and its recorded runs.

Run now

A job that declares itself runnable has a Run now button. It calls the same runOnce() the timer calls, so a test run cannot drift from what production does. The run is recorded with trigger: 'manual' and labelled as such in the history — a forced run must never be read as evidence that the schedule is alive.

Changing a cadence

A job whose schedule is data can be re-scheduled or disarmed from the drawer. A job whose schedule is a constant in code cannot: the drawer shows the cadence read-only and links to the settings page that owns it, instead of offering an input whose Save could only answer 400.

Run history

Runs are persisted in platform.job_runs, with the run's output and its trigger. For a scheduled edge function the ledger also keeps stdout and the returned value, including for a failed run, which is the most useful thing there is about a failure.

Note

The in-memory part is only the timer itself. Whether a job is armed right now resets when the project container restarts; the run history does not.

Admin endpoints

Project Studio uses admin-only endpoints:

GET    /api/admin/cron/pg/jobs
POST   /api/admin/cron/pg/jobs
PATCH  /api/admin/cron/pg/jobs/:id
DELETE /api/admin/cron/pg/jobs/:id
 
GET    /api/admin/cron/app/jobs
GET    /api/admin/jobs/runs
GET    /api/admin/jobs/alerts
PATCH  /api/admin/jobs/:id
POST   /api/admin/jobs/:id/run

PATCH /api/admin/jobs/:id accepts schedule (a cron string, or null to clear it), enabled, or both. A job whose cadence lives in code answers 400. Sending neither field also answers 400.

PATCH /api/admin/cron/pg/jobs/:id accepts:

{
  "active": false
}

These routes are for Studio and platform administration. Client applications should not call them from browser code.