@chainsaws/scheduler
v0.1.2
Published
Minimal scheduler surface for two separate jobs:
Downloads
230
Maintainers
Readme
@chainsaws/scheduler
Minimal scheduler surface for two separate jobs:
SchedulerAPIManage AWS EventBridge Scheduler resources.runScheduledWorker/runScheduledWorkersRun 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 ranfalse: 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:
runScheduledWorkerrunScheduledWorkersSchedulerAPIScheduleExpressionScheduleExpressionBuilder
