API Reference
API Reference
GitHub ↗
Admin Panel

Scheduled Jobs

View and manage scheduled background jobs

Mailyard runs scheduled background jobs for maintenance tasks. All times are UTC.

List Jobs

GET /api/v1/admin/jobs

Response:

{
    "jobs": [
        {
            "name": "retention-cleanup",
            "schedule": "0 3 * * *",
            "running": false,
            "last_run_at": "2026-08-06T03:00:00Z",
            "last_error": "",
            "next_run_at": "2026-08-07T03:00:00Z",
            "last_duration_ms": 412
        }
    ]
}

Run a Job Now

POST /api/v1/admin/jobs/{name}/run

Runs the job immediately, out of band, and answers with the refreshed job list plus what the run did:

{
    "jobs": [ ... ],
    "ran": "retention-cleanup",
    "failed": false
}

A job that ran and failed is still an answered request: 200 with failed: true, and the failure is on that job’s row as last_error. A name this node does not run answers 404 - which jobs a node runs depends on its role - and a job already in flight answers 409 rather than being started twice. Both routes require the platform admin role.

Each entry carries name, which is what the run route takes, and schedule, which is display only — see the scheduling model below.

The rest is state, and all of it belongs to the node that answered the request: running says whether it is in flight there, last_run_at and last_duration_ms describe that node’s previous run, and next_run_at is when it will go again. Both timestamps are null until the job has run once since that process started, so a freshly restarted node reports nulls rather than history.

last_error is empty when the last run succeeded, and holds the failure message otherwise. It is the only place a failed sweep surfaces — a job that errors is logged and retried on its next tick, and nothing raises an alert about it.

Built-in Jobs

JobScheduleRoleDescription
retention-cleanupDaily at 03:00 UTCworkerPurges expired email logs, received mail, attachment blobs, webhook deliveries, tracking events, audit entries and read notifications according to the retention settings . Also drops spent password reset tokens, expired sessions and the volume counters.
partition-maintenanceEvery hourworkerCreates the daily partitions of the email log ahead of time. Also run once at boot.
partition-ceilingDaily at 03:30 UTCworkerMails the administrators when the email log nears its partition ceiling, which only an installation keeping everything forever reaches.
email-statsEvery 10 minutesworkerRecomputes the daily delivery trend the dashboard chart reads.
bounce-alertEvery 15 minutesworkerJudges the recent bounce rate and raises a notification when it crosses the configured threshold. A rate is a property of a window, so it cannot be evaluated from the delivery path.
certificate-expiryEvery 6 hoursworkerMails the administrators about TLS certificates nearing expiry, at most once a day.
signing-key-expiryEvery 6 hoursworkerWarns a project about sender signing certificates nearing expiry.
relay-assignmentsEvery minuteworkerTakes back messages handed to a pull-mode relay node that stopped claiming, so the next candidate delivers them.
settings-refreshEvery 5 minuteseveryReloads platform settings from the database so a node that did not serve the write converges.

Scheduling Model

Schedules come in two shapes: daily at a fixed UTC time, and every N. There is no cron-expression parser - the schedule field renders cron syntax where it maps purely so the value is familiar to read.

Jobs do not backfill. A node that is down at 03:00 does not run the sweep late, it runs it at 03:00 the next day.

Every node runs every job it registers

There is no leader election. Each node runs its full job set on its own schedule. That is safe for the built-in jobs because each one converges rather than conflicts when two nodes run it at once: the sweeps delete by age, the rollup recomputes in one transaction, the partition job creates what is missing, and the alerts are deduped - but any job added later must be idempotent and tolerate concurrent execution.

Which jobs a node registers depends on its role, as the table says. Every job marked worker is registered by serve and worker nodes only. settings-refresh is registered by every role including api, because it is not maintenance: it is how a node’s settings cache learns about a change written somewhere else. So GET /api/v1/admin/jobs on an api node lists one job, and that is correct.

A panicking job is contained: the failure is recorded as that job’s last_error and the scheduler keeps running.