OqronKitOqronKit

Job Registration

How OqronKit discovers and registers your job definitions — auto-discovery, explicit paths, and manual imports

Job Registration

OqronKit uses a self-registering factory pattern. Every time you call queue(), cron(), worker(), webhook(), batch(), or any other definition factory, it immediately pushes the config into a global registry. When OqronKit.init() boots, each module engine drains its registry and starts processing.

The key question is: how do those factory calls get executed? OqronKit supports three strategies.


Strategy 1: Auto-Discovery (Default)

When the triggers config option is omitted, OqronKit automatically scans these well-known directories (relative to cwd):

src/triggers/    triggers/    src/jobs/    jobs/

The first directory that exists is recursively scanned. All .ts, .js, .mjs, and .cjs files are dynamically imported. Since each factory call self-registers on import, no manual wiring is needed.

src/triggers/email-queue.ts
import { queue } from 'oqronkit'

// This self-registers into the global queue registry on import
export const emailQueue = queue<{ to: string; body: string }>({
  name: 'send-email',
  handler: async (ctx) => {
    await sendEmail(ctx.data.to, ctx.data.body)
  },
})
src/triggers/daily-cleanup.ts
import { cron } from 'oqronkit'

// This self-registers into the global cron registry on import
export const cleanup = cron({
  name: 'daily-cleanup',
  expression: '0 0 * * *',
  handler: async (ctx) => {
    ctx.log('info', 'Running cleanup...')
  },
})
index.ts
import { OqronKit, cronModule, queueModule } from 'oqronkit'

await OqronKit.init({
  config: {
    modules: [cronModule(), queueModule()],
    // triggers is omitted → auto-detects src/triggers/
  },
})

Auto-discovery stops at the first matching directory. If both src/triggers/ and jobs/ exist, only src/triggers/ is scanned.

my-app/
├── src/
│   ├── triggers/           ← auto-discovered
│   │   ├── queues/
│   │   │   ├── email.ts
│   │   │   └── pdf.ts
│   │   ├── crons/
│   │   │   └── cleanup.ts
│   │   ├── workers/
│   │   │   └── video-encode.ts
│   │   └── webhooks/
│   │       └── payment.ts
│   └── index.ts
└── package.json

You can nest subdirectories freely — the scanner is recursive.


Strategy 2: Explicit Path

Point triggers to a custom directory:

index.ts
await OqronKit.init({
  config: {
    modules: [cronModule(), queueModule(), workerModule()],
    triggers: './my-custom-jobs',  // relative to cwd
  },
})

OqronKit resolves this relative to cwd (or opts.cwd if provided) and recursively imports all files. If the directory doesn't exist, a warning is logged.

This is useful when your project structure doesn't follow the conventional directory names:

my-app/
├── lib/
│   └── background/         ← custom path
│       ├── billing-cron.ts
│       └── image-queue.ts
├── src/
│   └── index.ts
└── package.json
await OqronKit.init({
  config: {
    modules: [cronModule(), queueModule()],
    triggers: './lib/background',
  },
})

Strategy 3: Manual Imports

Disable auto-discovery entirely and import definition files yourself before calling OqronKit.init():

index.ts
// Import job definitions manually — each self-registers on import
import './jobs/email-queue.js'
import './jobs/billing-cron.js'
import './jobs/video-worker.js'

import { OqronKit, cronModule, queueModule, workerModule } from 'oqronkit'

await OqronKit.init({
  config: {
    modules: [cronModule(), queueModule(), workerModule()],
    triggers: false,  // disable scanning, silence warnings
  },
})

When using manual imports, you must import job files before OqronKit.init(). The init sequence drains the registries during boot — any definitions registered after init will be invisible to the engines.

When to Use Manual Imports

ScenarioWhy
Serverless (Lambda, Vercel)Filesystem scanning adds cold-start latency
Conditional loadingLoad different jobs per environment or role
Monorepo with shared definitionsImport specific definitions from another package
TestingRegister only the job under test

Conditional Registration Example

index.ts
// Only load heavy workers on dedicated worker nodes
import './jobs/common-queues.js'

if (process.env.NODE_ROLE === 'worker') {
  await import('./jobs/heavy-workers.js')
}

if (process.env.NODE_ROLE === 'scheduler') {
  await import('./jobs/crons.js')
}

await OqronKit.init({
  config: {
    modules: [cronModule(), queueModule(), workerModule()],
    triggers: false,
  },
})

How It Works Under the Hood

Each factory function pushes its config into a Symbol.for()-guarded global registry:

FactoryRegistry KeyDrain Function
queue()oqronkit:pending_queuesgetRegisteredQueues()
cron()oqronkit:pending_crons_drainPending()
schedule()oqronkit:pending_schedules_drainPendingSchedules()
worker()oqronkit:pending_workersgetRegisteredWorkers()
webhook()oqronkit:pending_webhooksgetRegisteredWebhooks()
batch()oqronkit:pending_batchesgetRegisteredBatches()

Using Symbol.for() on globalThis guarantees a single canonical array even in monorepo setups where multiple bundler copies of OqronKit might be evaluated at runtime.

Init Sequence

OqronKit.init()

  ├─ 1. Load & validate config
  ├─ 2. Boot engine (storage, broker, lock adapters)
  ├─ 3. Discover triggers ← imports files, populates registries
  ├─ 4. Boot each enabled module engine
  │      └─ Each engine drains its registry
  ├─ 5. Call init() on all modules
  └─ 6. Call start() on all modules → engines begin polling/ticking

If a definition is registered for a module that isn't enabled in config.modules, it will sit in the registry but never be drained or executed. No error is thrown — this is by design for publisher-only setups.


HMR & Deduplication

Each registry deduplicates by name. If a definition with the same name is registered twice (common during HMR/hot reload in development), the last registration wins:

// First registration
const q1 = queue({ name: 'emails', handler: handlerV1 })

// Second registration (e.g., HMR reload) — overwrites the first
const q2 = queue({ name: 'emails', handler: handlerV2 })

// Only handlerV2 will be used

Quick Reference

ConfigBehavior
triggers omittedAuto-detect: scan src/triggers/, triggers/, src/jobs/, jobs/
triggers: './path'Scan the specified directory recursively
triggers: falseNo scanning. Import files manually before init()

On this page