Job Extension

Add background jobs and scheduled tasks to your extension.

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_provider_contracts::{Job, JobContext, JobResult};

#[derive(Debug, Clone, Copy, Default)]
pub struct CleanupJob;

#[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
    }

    fn run_on_startup(&self) -> bool {
        false  // Set to true to run once at scheduler startup
    }

    async fn execute(&self, ctx: &JobContext) -> anyhow::Result<JobResult> {
        let pool = ctx.db_pool::<Arc<PgPool>>()
            .ok_or_else(|| anyhow::anyhow!("Database not available"))?;

        let deleted = sqlx::query!(
            "DELETE FROM temp_records WHERE expires_at < NOW()"
        )
        .execute(&*pool)
        .await?
        .rows_affected();

        Ok(JobResult::success().with_message(format!("Deleted {} records", deleted)))
    }
}

Startup Jobs

Jobs can run immediately when the scheduler starts using run_on_startup():

fn run_on_startup(&self) -> bool {
    true  // Runs once at startup, then follows cron schedule
}

Important: Jobs only run on startup if BOTH conditions are met:

  1. run_on_startup() returns true in the job implementation
  2. Job is listed in services/scheduler/config.yaml with enabled: true

This two-layer design separates developer defaults from operations control.

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 0
  • 0 */15 * * * * - Every 15 minutes
  • 0 0 0 * * * - Daily at midnight
  • 0 30 2 * * * - Daily at 2:30 AM
  • 0 0 0 * * 1 - Every Monday at midnight

JobContext

async fn execute(&self, ctx: &JobContext) -> anyhow::Result<JobResult> {
    // Get database pool
    let pool = ctx.db_pool::<Arc<PgPool>>()?;

    // Get configuration
    let config = ctx.config();

    // Access other services
    // ...
}

JobResult

// Success
Ok(JobResult::success())
Ok(JobResult::success().with_message("Processed 100 items"))

// Failure (will be retried based on configuration)
Err(anyhow::anyhow!("Database connection failed"))

Configuration Override

Override job schedules in profile.yaml:

scheduler:
  jobs:
    - extension: my-extension
      job: cleanup
      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 job status
systemprompt infra jobs status cleanup

Typed Extension

use systemprompt::extension::prelude::JobExtensionTyped;

impl JobExtensionTyped for MyExtension {
    fn jobs(&self) -> Vec<Arc<dyn Job>> {
        vec![Arc::new(CleanupJob)]
    }
}