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

@chainsaws/scheduler

v0.1.2

Published

Minimal scheduler surface for two separate jobs:

Downloads

230

Readme

@chainsaws/scheduler

Minimal scheduler surface for two separate jobs:

  • SchedulerAPI Manage AWS EventBridge Scheduler resources.
  • runScheduledWorker / runScheduledWorkers Run local worker-thread jobs only when a cron expression matches the current time.

Recommended Runtime API

The intended in-process API is:

  • runScheduledWorker(...)
  • runScheduledWorkers([...])

Worker modules must use a default export.

// jobs/delete-pending-users.js
export default async function run(payload) {
  console.log("delete pending users", payload);
}

Lambda Handler Shape

This is the recommended Lambda style.

import { awsLambdaHandler } from "@chainsaws/lambda";
import { runScheduledWorker } from "@chainsaws/scheduler";

export const handler = awsLambdaHandler(
  async (_event, context) => {
    await runScheduledWorker({
      cron: "0 0 * * *",
      tz: "Asia/Seoul",
      worker: new URL("./jobs/delete-pending-users.js", import.meta.url),
      payload: {
        requestId: context.awsRequestId ?? null,
      },
    });
  },
  {
    hooks: {
      onError: async ({ phase, error }) => {
        await botError.sendText(`scheduler lambda failed (${phase}): ${String(error)}`);
      },
    },
  },
);

Lambda Handler Recipes

These are the most common shapes when pairing @chainsaws/lambda with the runtime worker API.

1. One daily job from a minutely EventBridge trigger

import { awsLambdaHandler } from "@chainsaws/lambda";
import { runScheduledWorker } from "@chainsaws/scheduler";

export const handler = awsLambdaHandler(async (_event, context) => {
  await runScheduledWorker({
    cron: "0 0 * * *",
    tz: "Asia/Seoul",
    worker: new URL("./jobs/delete-pending-users.js", import.meta.url),
    payload: {
      requestId: context.awsRequestId ?? null,
    },
  });
});

2. Several cadences in one handler

import { awsLambdaHandler } from "@chainsaws/lambda";
import { runScheduledWorkers } from "@chainsaws/scheduler";

export const handler = awsLambdaHandler(async (_event, context) => {
  await runScheduledWorkers([
    {
      cron: "*/5 * * * *",
      tz: "Asia/Seoul",
      worker: new URL("./jobs/sync-metrics.js", import.meta.url),
      payload: {
        requestId: context.awsRequestId ?? null,
      },
    },
    {
      cron: "0 * * * *",
      tz: "Asia/Seoul",
      worker: new URL("./jobs/hourly-cleanup.js", import.meta.url),
      payload: {
        requestId: context.awsRequestId ?? null,
      },
    },
    {
      cron: "0 0 * * *",
      tz: "Asia/Seoul",
      worker: new URL("./jobs/daily-rollup.js", import.meta.url),
      payload: {
        requestId: context.awsRequestId ?? null,
      },
    },
  ]);
});

3. Pass invocation metadata into the worker

await runScheduledWorker({
  cron: "*/10 * * * *",
  tz: "UTC",
  worker: new URL("./jobs/publish-heartbeat.js", import.meta.url),
  payload: {
    awsRequestId: context.awsRequestId ?? null,
    region: process.env.AWS_REGION ?? null,
    stage: process.env.STAGE ?? null,
  },
});

4. Keep error notification in the handler wrapper

export const handler = awsLambdaHandler(
  async (_event, context) => {
    await runScheduledWorker({
      cron: "0 0 * * *",
      tz: "Asia/Seoul",
      worker: new URL("./jobs/daily-maintenance.js", import.meta.url),
      payload: {
        requestId: context.awsRequestId ?? null,
      },
    });
  },
  {
    hooks: {
      onError: async ({ phase, error }) => {
        await botError.sendText(`scheduler lambda failed (${phase}): ${String(error)}`);
      },
    },
  },
);

5. Offload CPU-heavy work from the handler thread

await runScheduledWorker({
  cron: "0 * * * *",
  tz: "UTC",
  worker: new URL("./jobs/rebuild-search-index.js", import.meta.url),
  payload: {
    batchSize: 1000,
  },
});

Single Scheduled Worker

import { runScheduledWorker } from "@chainsaws/scheduler";

await runScheduledWorker({
  cron: "*/5 * * * *",
  tz: "UTC",
  worker: new URL("./jobs/sync-metrics.js", import.meta.url),
  payload: {
    source: "scheduler",
  },
});

Return value:

  • true: cron matched and the worker ran
  • false: cron did not match, so nothing ran

Multiple Scheduled Workers

import { runScheduledWorkers } from "@chainsaws/scheduler";

await runScheduledWorkers([
  {
    cron: "*/5 * * * *",
    tz: "Asia/Seoul",
    worker: new URL("./jobs/sync-metrics.js", import.meta.url),
    payload: {},
  },
  {
    cron: "0 * * * *",
    tz: "Asia/Seoul",
    worker: new URL("./jobs/hourly-cleanup.js", import.meta.url),
    payload: {},
  },
  {
    cron: "0 0 * * *",
    tz: "Asia/Seoul",
    worker: new URL("./jobs/delete-pending-users.js", import.meta.url),
    payload: {},
  },
]);

Return value:

  • number of worker jobs that actually ran

Why This API Is Minimal

This package intentionally does not expose runtime orchestration details like:

  • thread pool manager
  • manual join
  • export name selection
  • auto-join flags

Instead, the contract is fixed:

  • one worker file
  • one default export
  • one serializable payload

Worker Module Rules

1. Use a default export

export default async function run(payload) {
  // work here
}

2. Payload must be structured-clone serializable

Safe examples:

  • plain objects
  • arrays
  • strings
  • numbers
  • booleans
  • null

Avoid:

  • DB clients
  • AWS SDK client instances
  • class instances with methods
  • functions

3. Create clients inside the worker

import { DynamoDBAPI } from "@chainsaws/dynamodb";

export default async function run(payload) {
  const db = new DynamoDBAPI("my-table", {
    region: process.env.AWS_REGION ?? "ap-northeast-2",
  });

  // use db here
}

Common Gotchas

runScheduledWorker(...) is not AWS Scheduler

runScheduledWorker(...) and runScheduledWorkers([...]) do not create AWS schedule resources. They only decide whether a local worker module should run in the current invocation.

Use SchedulerAPI when you want AWS to own the schedule itself.

Runtime cron syntax is not AWS scheduler expression syntax

Runtime worker APIs use 5-field cron:

"*/5 * * * *"

SchedulerAPI uses AWS expressions such as:

"rate(5 minutes)"
"at(2026-04-16T00:00:00)"
"cron(0 9 ? * MON-FRI *)"

You should usually still await

Even though the real work runs in a worker thread, the invocation should usually stay open until that work completes. In Lambda, returning too early can end the invocation before the worker finishes.

Worker modules must default export a function

This will work:

export default async function run(payload) {}

This will not:

export async function run(payload) {}

unless you also re-export it as default.

Important Note About File Extensions

When you pass a worker module path from TypeScript source, prefer the runtime file extension that will actually exist after build.

That usually means using .js in new URL(...).

worker: new URL("./jobs/delete-pending-users.js", import.meta.url)

not

worker: new URL("./jobs/delete-pending-users.ts", import.meta.url)

Cron Semantics

runScheduledWorker and runScheduledWorkers use 5-field cron expressions:

  • minute
  • hour
  • day of month
  • month
  • day of week

Examples:

  • */5 * * * *
  • 0 * * * *
  • 0 0 * * *

AWS Scheduler API

If you want AWS to invoke a Lambda on a real schedule, use SchedulerAPI.

import {
  SchedulerAPI,
  ScheduleExpressionBuilder,
} from "@chainsaws/scheduler";

const scheduler = new SchedulerAPI("prod-jobs", {
  region: "ap-northeast-2",
  role_arn: "arn:aws:iam::123456789012:role/eventbridge-scheduler-role",
});

await scheduler.init_scheduler(
  "daily-report-lambda",
  ScheduleExpressionBuilder.daily_at(9, 0),
  "Daily report job",
  { job: "daily-report" },
);

Exports You Probably Want

Most users only need:

  • runScheduledWorker
  • runScheduledWorkers
  • SchedulerAPI
  • ScheduleExpression
  • ScheduleExpressionBuilder