Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

Back to skills

Scheduled Jobs

ASecurity

Recurring background work on OwlMeans queues — declareSchedule in @owlmeans/queue, reconciled into BullMQ job schedulers by @owlmeans/redis-queue when a listening worker starts. Covers interval vs cron schedules, the processor a sweep runs, overlap and restart semantics, renaming and removing a schedule, multi-replica and rolling-deploy behaviour, and testing. Use when adding a nightly or periodic job, replacing a platform CronJob or a setInterval with a schedule, or diagnosing a schedule tha...

3 stars
0 votes
0 copies
0 views
Added 9/22/2026
devopstypescriptgotesting

Security Analysis

A100/100

Scanned 9/22/2026

Install to Claude Code

$npx -y skills add owlmeans/common --skill scheduled-jobs --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Scheduled Jobs?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Scheduled Jobs
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/owlmeans-scheduled-jobs-common/badge)](https://www.skillsdirectory.com/skills/owlmeans-scheduled-jobs-common)

More formats (shields.io, HTML) on the badges page.

Download Zip
Files
SKILL.md
---
name: scheduled-jobs
description: Recurring background work on OwlMeans queues — declareSchedule in @owlmeans/queue, reconciled into BullMQ job schedulers by @owlmeans/redis-queue when a listening worker starts. Covers interval vs cron schedules, the processor a sweep runs, overlap and restart semantics, renaming and removing a schedule, multi-replica and rolling-deploy behaviour, and testing. Use when adding a nightly or periodic job, replacing a platform CronJob or a setInterval with a schedule, or diagnosing a schedule that does not run.
user-invocable: false
---
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->

# Scheduled jobs

**Layer:** Infra · **Packages:** `@owlmeans/queue` (lead — the declaration), `@owlmeans/redis-queue`
(the reconciliation)

A schedule is a declaration that makes the broker produce an ordinary job on an interval or a cron
pattern. A worker that consumes the queue runs it with a processor. There is no second scheduler
process, no cron table and no timer in application code.

## When a schedule is the answer

| Situation | Answer |
|---|---|
| Periodic sweep, reconciliation, digest, retention delete, rollup | A schedule on a queue a worker consumes |
| "Remind this user in 20 minutes" | `JobOptions.delay` on one job — a schedule is not per record |
| A far-future or cancellable per-record deadline | A row with a due date, swept by one schedule |
| A deployment that runs no worker | A platform `CronJob` calling a guarded entrypoint |
| A `setInterval` inside a request handler or a long-lived service | A schedule — the interval dies with the process and runs once per replica |

Whether the work belongs on a queue at all is `resource-choice`.

## Declaring

In the SHARED package every process loads, after the queue:

```typescript
declareQueue(cfg, APP_MAINTENANCE, ['app:maintenance:reconcile', 'app:maintenance:purge'], {
  worker: { concurrency: 1, lockDuration: 60_000 }
})
declareSchedule(cfg, {
  id: 'app-nightly-reconcile', queue: APP_MAINTENANCE, name: 'app:maintenance:reconcile',
  pattern: '17 3 * * *', tz: 'UTC', data: {},
})
declareSchedule(cfg, {
  id: 'app-purge-expired', queue: APP_MAINTENANCE, name: 'app:maintenance:purge', every: 15 * 60_000,
})
```

And only in the process that runs them: `listenQueues(cfg, APP_MAINTENANCE)` plus a processor per
job name, registered while the context is wired.

- **`pattern` for the calendar, `every` for a frequency.** A nightly job is a pattern with an
  explicit `tz`; "every quarter hour, whenever" is `every`. Pick an odd minute (`17 3 * * *`) so
  many applications' sweeps do not stampede one Redis at the top of the hour.
- **`every` runs at once** when the scheduler is created (unless `startDate` says otherwise); a
  pattern waits for its next match unless `immediately: true`, which fires only when the scheduler
  is created or its declaration changes — never on an unchanged restart.
- **The id is permanent.** It is the scheduler's identity in the broker: any other field changes in
  place, a renamed id is a removed schedule plus a new one. Prefix it with the application
  (`app-…`) — ids share one namespace per queue.
- **Declaration errors surface at boot.** `declareSchedule` throws `UnknownQueue`,
  `UnknownJobName` or `ScheduleMisdeclared` (`<id>:<reason>`) rather than letting the broker fail
  on every tick.
- **`opts` sets retries and retention for every run; `id` and `delay` are refused** — the broker
  names each run and places it on the schedule.

## Writing the processor

```typescript
worker.process<Record<string, never>, SweepReport>(APP_MAINTENANCE, 'app:maintenance:reconcile',
  async job => {
    const report = { scanned: 0, repaired: 0, failed: 0 }
    for await (const page of organizations({ size: 200 })) {
      await job.touch()
      for (const organization of page) {
        try {
          report.repaired += await reconcile(organization.entityId) ? 1 : 0
        } catch (error) {
          report.failed++
          console.error(`reconcile ${organization.entityId}`, error)
        }
        report.scanned++
      }
    }
    console.info(`[maintenance] ${job.scheduled}`, report)
    return report
  })
```

- **A scheduled run has no caller.** No credentials, no entity, no request: it is a processor job,
  never a guarded entrypoint. A step that must go through another service's guard calls that
  service with service credentials.
- **Idempotent per item, not per run.** A worker rolled out mid-sweep, a stalled lock or a manual
  re-run repeats the whole sweep; each item's repair must be a no-op the second time.
- **One item's failure is logged and counted, never thrown.** A thrown sweep is retried from the
  start, or dropped after its attempts — either way the other items pay for one bad record.
- **`touch()` per page.** A sweep over every organization outlives `lockDuration` easily; silence
  hands the job to another worker while this one is still going.
- **Return a small report.** It lands on the completed job, where an operator reads it.

## Overlap, replicas and deploys

- **One job per tick, whatever the replica count.** The broker produces one job; one worker takes
  it. Several replicas listening to the queue do not multiply runs.
- **Runs can overlap on a concurrent queue.** The next run is produced when the previous one
  starts, so a run that outlasts the interval meets its successor. Put sweeps on their own queue
  with `concurrency: 1`, apart from interactive lanes.
- **A schedule needs a consumer.** Only a listening worker creates the scheduler, and the next run
  is produced only when the previous one starts processing. A queue nothing consumes stops.
- **Every listener reconciles at start, from its own configuration.** It creates or updates what it
  declares and removes the schedules it owns (`owlmeans:` scheduler ids) that it does not. Every
  process listening to one queue must load the same schedule list; during a rolling deploy the
  newest process to start wins, and the old pods leave the broker alone after their own start.
- **Removing a schedule is deleting its declaration.** The next worker start removes the scheduler.
  So is a declaration whose job name the queue no longer accepts, or whose `endDate` has passed.
- **Scheduling never costs consumption.** A refused declaration or a broker failure while
  reconciling is logged and skipped; the worker keeps consuming. Look for
  `redis-queue-schedules:<queue>` in the worker's log when a schedule does not appear.

## Diagnosing "it never ran"

1. Is some process listening to the queue (`listenQueues`) and is its worker up?
2. Did that process's start log a `redis-queue-schedules:<queue>` refusal?
3. Does `new Queue(queue, { connection, prefix }).getJobSchedulers()` list `owlmeans:<id>` with the
   expected `pattern`/`every`, `tz` and `next`?
4. Is a processor registered for the job name? A run without one fails once as `UnknownJobName`
   and shows in the failed state.
5. Is a previous run still active on a `concurrency: 1` queue?

## Testing

- **The declaration** is category A in the application's own shared package: build the config,
  `assertSchedules(cfg)`, and pin ids and job names against the queue declaration.
- **The processor** is tested as a function over a fake `JobContext` or a real context — it is
  ordinary code with a `touch`.
- **Against a broker** (Redis-gated): the worker's Ready-stage start is not awaited by
  `context.init()`, so poll the broker (`getJobSchedulers`) or wait for the first run instead of
  asserting straight after boot. A queue's schedulers go with `obliterate()`.

## External docs

- https://docs.bullmq.io/guide/job-schedulers — `upsertJobScheduler(id, { every | pattern, tz,
  startDate, endDate, limit, immediately }, { name, data, opts })` creates or updates one scheduler
  per id and replaces its pending run; `getJobSchedulers()` / `removeJobScheduler(id)`. The next job
  is added when the previous one starts processing, so a worker must be consuming. Produced jobs
  carry `repeatJobKey` = the scheduler id.

## Related

- `queue` — `declareSchedule`, `JobContext.scheduled`, processor rules
- `redis-queue` — `syncSchedules`, the `owlmeans:` ownership prefix, the reconciliation rules
- `resource-choice` — whether recurring work belongs on a queue at all

Attribution

owlmeansowlmeans
View sourceMore from owlmeans →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Terraform Module Library

Build reusable Terraform modules for AWS, Azure, and GCP infrastructure following infrastructure-as-code best practices. Use when creating infrastructure modules, standardizing cloud provisioning, or implementing reusable IaC components.

397921 votes

sematext-otel

Wire a service's OpenTelemetry output to Sematext Cloud. Walks through region, App-type, instrumentation flow (managed OTLP endpoint vs Sematext Agent), and signal selection (traces/metrics/logs), then produces the exact env-var block and points at a runnable reference example in this repo. Invoke when instrumenting a new app for Sematext.

01 votes

Deployment Patterns

Deployment workflows, CI/CD pipeline patterns, Docker containerization, health checks, rollback strategies, and production readiness checklists for web applications. Use when setting up deployment infrastructure or planning releases.

2459130 votes

Babysit

Watch a pull request or review cycle until it is ready to merge. Use when asked to babysit, monitor, or keep checking PR comments, reviews, and CI until all actionable issues are resolved.

942310 votes

V7 Roster

Interact with the Paperclip control plane API for task coordination and governance. Use when checking assignments, updating issue status, posting comments, delegating work, managing routines, or calling Paperclip API endpoints.

805540 votes
View all in devops →