NGN Logo

NGN

File-based task runner for Node.js with unmatched flexibility and DX.

What is ngn?

ngn is task scheduler for JavaScript and TypeScript. It allows you to write scheduled tasks as simple files, run one command, and get a built-in dashboard with execution history, logs, persistence layer, backup system, alerting and more. Single npm package is all you need to get started.


Workflow examples

Examples

bash
# Run all tasks
ngn run

# Run tasks in a specific folder
ngn run --match "tasks/reports/**"

# Run single task every 10 seconds
ngn run:single tasks/cleanup.ts --timing "*/10 * * * * *"

# Test a task without scheduling
ngn run:once tasks/send-email.ts

Command Description
ngn init Initialize ngn in current directory
ngn add <file> Create a new task file in tasks/
ngn run Start scheduler and dashboard
ngn run --match <pattern> Run only tasks matching glob pattern
ngn run:single <file> Run single task with optional custom timing
ngn run:once <file> Execute task once (no scheduling)
ngn http Run only the dashboard/API server (no scheduler)

Writing Tasks

A task file is as simple as this:

typescript
import { TaskContext } from "@apisurf/ngn";

// Cron pattern (6 fields: second minute hour day month weekday)
export const timing = "0 */5 * * * *"; // Every 5 minutes

// Task function
export const task = async (ctx: TaskContext) => {
  // Your code here
};

Task Context

Every task receives a context object with built-in utilities:

typescript
export const task = async (ctx: TaskContext) => {
  // Task metadata
  const { fileTaskId, fileTaskVersionId, file, tasksRootDir } = ctx.meta;

  // Environment variables from .env
  const apiKey = ctx.env!.API_KEY;

  // Key-value storage
  await ctx.kv.set("lastRun", new Date().toISOString());
  const lastRun = await ctx.kv.get("lastRun");
  await ctx.kv.delete("lastRun");

  // Structured logging (visible in dashboard)
  await ctx.log.info("Processing started");
  await ctx.log.warning("Rate limit approaching");
  await ctx.log.error("Failed to connect");

  // Performance timing
  const recordTime = ctx.timing.start("api-call");
  await fetch("https://api.example.com");
  await recordTime(); // Records duration in dashboard

  // Access configured plugins
  const sqlite = ctx.plugins.sqlite;
};

Lifecycle Hooks

Control task execution with optional exports:

typescript
// Skip execution conditionally (or pass true to skip unconditionally)
export const shouldSkip = async (ctx: TaskContext) => {
  const lastRun = await ctx.kv.get("lastRun");
  return lastRun === new Date().toDateString();
};

// Decide whether to retry after a failure
export const shouldRetry = async (ctx: TaskContext) => {
  return true;
};

// Runs after a successful execution
export const onSuccess = async (ctx: TaskContext) => {
  await ctx.log.info("Done");
};

// Runs after a failed execution
export const onError = async (err: Error, ctx: TaskContext) => {
  await ctx.log.error(err.message);
};

// Runs after the task settles, regardless of outcome
export const onComplete = async (ctx: TaskContext) => {};

Configuration

Create a ngn.config.ts file with defineConfig:

In-Memory Storage

Fast, volatile storage - data is lost on restart:

typescript
import { defineConfig } from "@apisurf/ngn";

export default defineConfig({
  dbPath: ":memory:",
  port: 4545,
  match: ["tasks/**/*.ts"],
  envFile: ".env"
});

File-Based Storage

Persistent SQLite database - survives restarts:

typescript
import { defineConfig } from "@apisurf/ngn";

export default defineConfig({
  dbPath: "file:./db.sqlite",
  port: 4546,
  match: ["tasks/**/*.ts"],
  envFile: ".env"
});
Option Example Description
dbPath :memory: or file:./db.sqlite SQLite database path (in-memory or file-based)
port 4545 Dashboard/API server port
match ["tasks/**/*.ts"] Glob patterns for task file discovery
envFile .env Environment variables file to load
plugins [sqlitePlugin(options)] Array of plugin configurations

Plugins

Extend ngn with plugins for databases, email, storage, and more. Plugins are registered in ngn.config.ts and accessed via ctx.plugins.

Registering plugins

Import each plugin from its subpath and pass it to defineConfig:

ngn.config.ts
import { defineConfig } from "@apisurf/ngn";
import { sqlitePlugin } from "@apisurf/ngn-plugin/sqlite";
import { resendPlugin } from "@apisurf/ngn-plugin/resend";
import { s3Plugin } from "@apisurf/ngn-plugin/s3";

export default defineConfig({
  dbPath: "file:./db.sqlite",
  plugins: [
    sqlitePlugin({ url: "file:./app.sqlite" }),
    resendPlugin({ apiKey: process.env.RESEND_API_KEY! }),
    s3Plugin({
      accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
      region: "us-east-1",
    }),
  ],
});

SQLite Plugin

Once registered, access it from any task via ctx.plugins.sqlite:

typescript
// tasks/sync-users.ts
export const task = async (ctx: TaskContext) => {
  const sqlite = ctx.plugins.sqlite;

  // Execute queries
  const result = await sqlite.execute("SELECT * FROM users");
  await ctx.log.info("Found " + result.rows.length + " users");
};

Resend Plugin (Email)

typescript
// tasks/send-report.ts
export const task = async (ctx: TaskContext) => {
  const result = await ctx.plugins.resend.send({
    from: "[email protected]",
    to: "[email protected]",
    subject: "Daily Report",
    text: "Your report is ready."
  });

  if (result.error) {
    await ctx.log.error("Send failed: " + result.error);
  } else {
    await ctx.log.info("Sent email " + result.id);
  }
};

S3 Plugin (Storage)

typescript
// tasks/backup.ts
export const task = async (ctx: TaskContext) => {
  const s3 = ctx.plugins.s3;

  const result = await s3.upload({
    bucket: "my-bucket",
    key: "backups/data.json",
    body: Buffer.from(JSON.stringify(data)),
    contentType: "application/json"
  });

  if (!result.success) {
    await ctx.log.error("Upload failed: " + result.error);
  }
};

Available Plugins

Plugin Description
@apisurf/ngn-plugin/sqlite SQLite database with migrations support
@apisurf/ngn-plugin/resend Email sending via Resend API
@apisurf/ngn-plugin/s3 AWS S3 file storage operations
@apisurf/ngn-plugin/supabase Supabase client integration
@apisurf/ngn-plugin/alerting Alert notifications

Cron Patterns

ngn uses 6-field cron expressions (includes seconds):

┌────────────── second (0-59)
│ ┌──────────── minute (0-59)
│ │ ┌────────── hour (0-23)
│ │ │ ┌──────── day of month (1-31)
│ │ │ │ ┌────── month (1-12)
│ │ │ │ │ ┌──── day of week (0-6, Sun=0)
│ │ │ │ │ │
* * * * * *

Common patterns:

Pattern Description
*/5 * * * * * Every 5 seconds
0 * * * * * Every minute
0 */5 * * * * Every 5 minutes
0 0 * * * * Every hour
0 0 9 * * * Daily at 9:00 AM
0 0 9 * * 1-5 Weekdays at 9:00 AM