Job Extension
Add background tasks with the jobs() method, implement the Job trait's execute(), set a 6-field cron schedule, and run them via systemprompt infra jobs.
On this page
Extensions add background tasks via the jobs() method.
Jobs Method
fn jobs(&self) -> Vec<Arc<dyn Job>> {
vec![
Arc::new(CleanupJob),
Arc::new(SyncJob),
]
}
Job Trait
use systemprompt::database::DbPool;
use systemprompt::traits::{Job, JobContext, JobResult, ProviderError};
#[derive(Debug, Clone, Copy, Default)]
pub struct CleanupJob;
#[async_trait::async_trait]
impl Job for CleanupJob {
fn name(&self) -> &'static str {
"cleanup"
}
fn description(&self) -> &'static str {
"Clean up expired records"
}
fn schedule(&self) -> &'static str {
"0 0 * * * *" // Every hour
}
async fn execute(&self, ctx: &JobContext) -> Result<JobResult, ProviderError> {
let db = ctx.db_pool::<DbPool>()
.ok_or_else(|| ProviderError::Internal("Database not available".into()))?;
let pool = db.pool()
.ok_or_else(|| ProviderError::Internal("PgPool not available".into()))?;
let deleted = sqlx::query!(
"DELETE FROM temp_records WHERE expires_at < NOW()"
)
.execute(pool.as_ref())
.await
.map_err(|e| ProviderError::Internal(e.to_string()))?
.rows_affected();
Ok(JobResult::success().with_message(format!("Deleted {} records", deleted)))
}
}
Startup Jobs
Jobs can run once, serially, before the cron loop begins. This is an operations decision, not a trait method: list the job under bootstrap_jobs in services/scheduler/config.yaml:
scheduler:
bootstrap_jobs:
- governance_bootstrap
- publish_pipeline
A job listed only in bootstrap_jobs runs at startup and is never cron-scheduled. A job that also has a jobs: entry with enabled: true runs at startup and then follows its cron schedule.
Cron Schedule Format
6-field cron expression: second minute hour day-of-month month day-of-week
┌───────────── second (0-59)
│ ┌───────────── minute (0-59)
│ │ ┌───────────── hour (0-23)
│ │ │ ┌───────────── day of month (1-31)
│ │ │ │ ┌───────────── month (1-12)
│ │ │ │ │ ┌───────────── day of week (0-6, Sun=0)
│ │ │ │ │ │
* * * * * *
Examples:
0 0 * * * *- Every hour at minute 00 */15 * * * *- Every 15 minutes0 0 0 * * *- Daily at midnight0 30 2 * * *- Daily at 2:30 AM0 0 0 * * 1- Every Monday at midnight
JobContext
async fn execute(&self, ctx: &JobContext) -> Result<JobResult, ProviderError> {
// Get the type-erased database pool
let db = ctx.db_pool::<DbPool>();
// The actor authorizing this run (from `owner:` in scheduler config)
let actor = ctx.actor();
// Per-run string parameters
let params = ctx.parameters();
// Type-erased app context and paths
// ctx.app_context::<T>() / ctx.app_paths::<T>()
}
JobResult
// Success
Ok(JobResult::success())
Ok(JobResult::success().with_message("Processed 100 items"))
Ok(JobResult::success().with_stats(100, 0).with_duration(duration_ms))
// Failure
Ok(JobResult::failure("Database connection failed"))
Configuration Override
Override job schedules in services/scheduler/config.yaml. Every job declares an explicit owner:, a real user name that becomes JobContext.actor for each run:
scheduler:
enabled: true
jobs:
- name: cleanup
extension: my-extension
owner: admin
schedule: "0 */30 * * * *" # Override to every 30 minutes
enabled: true
CLI Commands
# Run job manually
systemprompt infra jobs run cleanup
# List all jobs
systemprompt infra jobs list
# Show detailed information about a job
systemprompt infra jobs show cleanup
# View job execution history
systemprompt infra jobs history
Typed Extension
use systemprompt::extension::prelude::JobExtensionTyped;
impl JobExtensionTyped for MyExtension {
fn jobs(&self) -> Vec<Arc<dyn Job>> {
vec![Arc::new(CleanupJob)]
}
}