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.
Proposal: Scheduled runners (once/interval) within Primate context
Description
config/app.ts.Current situation (no runners)
A user wanting to enrich data every 15 minutes has no idiomatic option:
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:Example runner (
runners/enrich-data.ts):Benefits
once) and recurring (interval) execution patterns.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.config/app.tsunder arunnerskey.Additional considerations
interval(enrich, { every: "15m", name: "data-enrichment" })).