npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@luisrodrigues/nestjs-scheduler-dashboard

v0.2.0

Published

Dashboard for @nestjs/schedule — embedded UI, job history, metrics, stop/trigger controls

Downloads

2,091

Readme

@luisrodrigues/nestjs-scheduler-dashboard

A plug-and-play dashboard for @nestjs/schedule. Visualize cron job executions, track history and metrics, trigger jobs manually, and stop running executions — all from an embedded UI served on your existing application port.


Features

  • Mounted directly on your app — no separate port or process
  • Execution history per job with status, duration, and error details
  • Persistent metrics: total runs, failed runs, average duration — independent of history retention
  • Manual job triggering and execution stop from the UI
  • Concurrency control: limit how many jobs run simultaneously, with automatic queuing
  • No-overlap mode: skip a job if it is already running
  • Optional HTTP Basic Auth to protect the dashboard
  • Light / dark mode

Installation

npm install @luisrodrigues/nestjs-scheduler-dashboard
# or
pnpm add @luisrodrigues/nestjs-scheduler-dashboard

Peer dependencies (install if not already present):

npm install @nestjs/common @nestjs/core @nestjs/schedule express

Quick start

1. Import SchedulerDashModule in your AppModule

import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { SchedulerDashModule } from '@luisrodrigues/nestjs-scheduler-dashboard';

@Module({
  imports: [
    ScheduleModule.forRoot(),
    SchedulerDashModule.forRoot({ route: '_scheduler' }),
  ],
})
export class AppModule {}

2. Replace @Cron with @TrackJob on every job

@TrackJob is a wrapper around NestJS's @Cron decorator. It registers the cron schedule exactly as @Cron does, and additionally records every execution (start time, duration, status, errors) so the dashboard can display them.

Jobs decorated with @Cron instead of @TrackJob will still run on schedule but will not appear in the dashboard history.

import { Injectable } from '@nestjs/common';
import { CronExpression } from '@nestjs/schedule';
import { TrackJob } from '@luisrodrigues/nestjs-scheduler-dashboard';

@Injectable()
export class ReportJob {
  // Before: @Cron(CronExpression.EVERY_HOUR)
  @TrackJob(CronExpression.EVERY_HOUR, { name: 'generate-report' })
  async run() {
    // your job logic — no other changes needed
  }
}

That's it. Start your app and open http://localhost:3000/_scheduler.


Configuration

SchedulerDashModule.forRoot(options?) accepts:

| Option | Type | Default | Description | |---|---|---|---| | route | string | '_scheduler' | URL path where the dashboard is mounted | | storage | Storage | new MemoryStorage() | Storage backend for execution history and metrics | | maxConcurrent | number | — | Maximum number of jobs that can run simultaneously. Excess jobs are queued | | noOverlap | boolean | false | Globally prevent a job from starting if it is already running | | auth | { username, password } | — | Protect the dashboard with HTTP Basic Auth |

route

The URL path where the dashboard is served, relative to your app's root.

SchedulerDashModule.forRoot({ route: 'admin/scheduler' })
// dashboard → http://localhost:3000/admin/scheduler

storage

The default MemoryStorage keeps everything in-process. You can limit how many history entries are kept per job:

import { MemoryStorage } from '@luisrodrigues/nestjs-scheduler-dashboard';

SchedulerDashModule.forRoot({
  storage: new MemoryStorage({
    historyRetention: 50, // keep the last 50 executions per job (default: unlimited)
  }),
})

Metrics are independent of historyRetention. Even after old history entries are trimmed, the counters for total runs, failed runs, and average duration keep accumulating.

To use a custom storage backend, extend the abstract Storage class:

import { Storage, IStorageOptions, JobExecution, JobMetrics } from '@luisrodrigues/nestjs-scheduler-dashboard';

export class RedisStorage extends Storage {
  constructor(options: IStorageOptions = {}) {
    super(options);
  }

  save(execution: JobExecution): void { /* ... */ }
  update(id: string, data: Partial<Pick<JobExecution, 'finishedAt' | 'status' | 'error'>>): void { /* ... */ }
  findByJob(jobName: string): JobExecution[] { /* ... */ }
  findAll(): Record<string, JobExecution[]> { /* ... */ }
  getMetrics(jobName: string): JobMetrics { /* ... */ }
  getAllMetrics(): Record<string, JobMetrics> { /* ... */ }
}

maxConcurrent

Limits how many @TrackJob jobs can run simultaneously across the entire application. Jobs that exceed the limit are saved to storage with status "queued" and run in FIFO order as slots free up.

SchedulerDashModule.forRoot({ maxConcurrent: 5 })

noOverlap

Prevents a job from firing again if it is still running. Applies globally to all @TrackJob methods.

SchedulerDashModule.forRoot({ noOverlap: true })

Can also be overridden per job via the decorator:

@TrackJob(CronExpression.EVERY_MINUTE, { name: 'sync', noOverlap: true })
async sync() { /* ... */ }

auth

SchedulerDashModule.forRoot({
  auth: {
    username: process.env.DASH_USER ?? 'admin',
    password: process.env.DASH_PASS ?? 'secret',
  },
})

@TrackJob decorator

@TrackJob is a wrapper around @Cron from @nestjs/schedule. It accepts every argument that @Cron accepts and passes them through unchanged, so migrating an existing job is a one-line change. The only addition is the noOverlap option and the automatic execution tracking.

Every scheduled job must use @TrackJob instead of @Cron. Jobs that remain on @Cron will run normally but their executions will not be recorded or visible in the dashboard.

@TrackJob(cronTime, options?)

| Argument | Type | Description | |---|---|---| | cronTime | string \| CronExpression | Cron expression or CronExpression enum value | | options.name | string | Job name shown in the dashboard. Defaults to ClassName.methodName | | options.noOverlap | boolean | Skip this job if it is already running. Overrides the global setting | | options.* | — | All other CronOptions are passed through |

@TrackJob('0 */6 * * *', {
  name: 'cleanup-old-records',
  noOverlap: true,
  timeZone: 'Europe/Lisbon',
})
async cleanup() {
  // runs every 6 hours, skipped if previous run is still in progress
}

API

The dashboard exposes a small REST API under the configured route.

| Method | Path | Description | |---|---|---| | GET | /<route>/api | Returns all jobs with history and metrics | | GET | /<route>/api/:name | Returns a single job with history and metrics | | POST | /<route>/api/:name/trigger | Manually trigger a cron job by name | | POST | /<route>/api/executions/:id/stop | Stop a running or queued execution by ID |


Full example

// app.module.ts
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { SchedulerDashModule, MemoryStorage } from '@luisrodrigues/nestjs-scheduler-dashboard';
import { ReportJob } from './jobs/report.job';

@Module({
  imports: [
    ScheduleModule.forRoot(),
    SchedulerDashModule.forRoot({
      route: '_scheduler',
      storage: new MemoryStorage({ historyRetention: 100 }),
      maxConcurrent: 3,
      noOverlap: true,
      auth: {
        username: process.env.DASH_USER ?? 'admin',
        password: process.env.DASH_PASS ?? 'secret',
      },
    }),
  ],
  providers: [ReportJob],
})
export class AppModule {}
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
  // Dashboard at http://localhost:3000/_scheduler
}

bootstrap();

Inspiration

This project was inspired by Hangfire — a .NET background job framework with a built-in dashboard for monitoring and managing recurring jobs. The goal was to bring a similar developer experience to the NestJS ecosystem.


License

MIT