Skip to content

Proposal: Scheduled runners (once/interval) within Primate context #255

Description

@liolocs

Proposal: Scheduled runners (once/interval) within Primate context

Description

  • Problem: Primate has no built-in way to run scheduled tasks (e.g., every 15 minutes) within the Primate server process. Users who want to run recurring jobs that interact with Primate stores (e.g., enriching raw data with AI) are forced to either couple logic awkwardly into request handlers, or spin up a completely separate process outside of Primate — losing access to ORM structures and the Primate context.
  • Proposal:
    • Add support for runners — tasks that execute either once on startup, or on a defined interval — running within a Primate context.
    • Allow runners to be defined in config/app.ts.
    • Runners should have full access to Primate stores and other framework primitives.

Current situation (no runners)

A user wanting to enrich data every 15 minutes has no idiomatic option:

// No built-in way to do this within Primate today.
// User must run a separate process outside of Primate,
// losing access to ORM/store context.
setInterval(async () => {
  // manually bootstrap DB connection, etc.
  await someRecurringDBFunction();
}, 15 * 60 * 1000);

This means either tight coupling inside request handlers or a fragmented architecture with a separate scheduler process.


Desired functionality

Define runners in config/app.ts:

import { interval, once } from "primate/runners";
import enrich from "../runners/enrich-data.ts";
import seed from "../runners/seed.ts";

export default config({
  // ...
  runners: [
    once(seed),                        // runs once on server start
    interval(enrich, { every: "15m" }) // runs every 15 minutes
  ]
});

Example runner (runners/enrich-data.ts):

import RawData from "../stores/RawData.ts";
import EnrichedData from "../stores/EnrichedData.ts";

export default async () => {
  const raw = await RawData.find({ enriched: false });

  for (const item of raw) {
    const result = await enrichWithAI(item);
    await EnrichedData.insert({ ...result, sourceId: item.id });
    await RawData.update(item.id, { enriched: true });
  }
};

Benefits

  • Native Primate context: Runners have full access to stores, types, and ORM structures without bootstrapping.
  • Separation of concerns: Enrichment/processing logic lives outside of request handlers, keeping systems decoupled.
  • Single process: No need for a separate scheduler process or external cron infrastructure.
  • Flexibility: Supports both one-time (once) and recurring (interval) execution patterns.
  • Colocation: Runners live alongside the rest of the app code, improving discoverability.

Acceptance criteria

  • once(fn) runner executes the function once when the Primate server starts.
  • interval(fn, { every }) runner executes the function repeatedly at the defined interval.
  • Runners are registered in config/app.ts under a runners key.
  • Runners execute within a full Primate context (stores, ORM, etc. are accessible).
  • Errors in runners are caught and logged without crashing the server.
  • Runner execution is non-blocking with respect to request handling.

Additional considerations

  • Named runners: Allow optional names for logging/debugging (e.g., interval(enrich, { every: "15m", name: "data-enrichment" })).
  • Run-once on deploy: Useful for warming caches or running post-deploy logic.
  • Graceful shutdown: Runners should be cleanly stopped when the server shuts down.
  • Overlap protection: Option to prevent a new interval tick from firing if the previous run hasn't completed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions