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

@corbits/cron

v0.2.1

Published

Cron schedules for an Interchange hub: grant-gated schedule routes plus a ticker that turns a due row into run-trigger mail

Readme

@corbits/cron

A Corbits hub module that wakes Interchange agents on five-field UTC cron schedules, mounted as @intx/hub-api routes on the hub (Interchange's multi-tenant control plane) and stored in its Postgres. A ticker mails each due schedule to its agent's current run, the running instance of the agent's workflow definition.

Schedules support create, list, pause, resume and delete. There is no edit: delete and recreate. An expression must be able to fire (no Feb 30), at most 256 characters and 64 comma clauses.

Why @corbits/cron?

  1. Schedules follow the agent, not the run. A schedule names an agent by its definition name, so it survives redeploys. It waits while the agent has no current run and stops for good when the agent is deleted.
  2. Mounts like any hub route. createCronRoutes returns a Hono<TenantEnv> sub-app. Every route runs through the hub's requireGrant, the check that the calling principal (a user or agent account) holds a grant (a permission on a resource).
  3. Safe with several hub replicas. Tickers claim due rows with FOR UPDATE SKIP LOCKED, so no schedule fires twice. Migrations are idempotent and run under an advisory lock on every boot.

Install

bun add @corbits/cron @intx/authz @intx/db @intx/hub-api drizzle-orm hono postgres

Runs on Node >= 24.

Quickstart

import { nextCronFireAfter } from "@corbits/cron";

console.log(nextCronFireAfter("0 9 * * *", new Date()));

Prints the next 09:00 UTC, the next time a schedule with that expression fires.

Where it fits

Interchange runs AI agents as principals with their own identity, permissions and credentials. Its hub holds tenants, principals, grants and workflow runs.

  • Runs in: the hub, as routes on its Hono app, a ticker in its process and a table in its Postgres.
  • Plugs into: @intx/hub-api routes and grants, and @intx/db (its DBConfig, its tenant table as FK target, and its workflow definitions and runs).
  • Pairs with: @corbits/webhooks, whose run-trigger deliverer sends the mail, and the other Corbits hub modules, @corbits/mailbox and @corbits/artifacts.

Reference

createCronRoutes(deps)

| deps | Type | What the host provides | | -------------- | -------------- | -------------------------------------------- | | db | CronDb | The hub's drizzle handle. | | requireGrant | RequireGrant | From @intx/hub-api's createRequireGrant. |

Routes

Paths are relative to where the host mounts the sub-app.

| Method | Path | Grant | Purpose | | ------ | ------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | GET | / | cron-schedule:* read | { schedules: [...] } for the caller's tenant. | | POST | / | cron-schedule:* create | Create from { expression, definitionName, subject, body }. 201 { schedule }; 400 invalid_body, invalid_expression or unknown_definition. | | POST | /:id/pause | cron-schedule:<id> manage | Stop the ticker firing it. { schedule } with enabled: false; 404 not_found. | | POST | /:id/resume | cron-schedule:<id> manage | Fire again from the next matching minute after now. { schedule } with enabled: true; 404 not_found. | | DELETE | /:id | cron-schedule:<id> manage | Delete one schedule. { ok: true }; 404 not_found when it is not in the caller's tenant. |

createCronTicker(opts)

Returns { start(), stop() }.

Delivery is at most once per due minute. Each tick first commits lastFiredAt on every due row it claims, then calls deliver outside the row lock. A process that dies mid-batch, or a deliver that throws, drops that minute's fire rather than repeating it; the schedule fires again at its next matching minute. Stop and waiting callbacks run after their change commits.

| opts | Type | Purpose | | ------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------- | | db | CronDb | The hub's drizzle handle. | | deliver | DeliverCronMail | Sends one due schedule's mail. createRunTriggerCronDeliver(deliverer) builds it. | | intervalMs | number (optional) | Poll period. Defaults to 60 000. | | onTickError | (error) => void (optional) | A tick failed before delivering, such as a lost connection. The next tick retries. | | onDeliveryError | (error, schedule) => void (optional) | One delivery failed. | | onScheduleWaiting | (schedule) => void (optional) | A schedule started waiting for its agent's next run. | | onScheduleStopped | (schedule) => void (optional) | A schedule stopped: reason is agent_deleted, or invalid_expression for a row that can never fire. |

A deliverer throws an error carrying RUN_GRANTS_NOT_ROUTABLE or RUN_MAIL_NOT_ROUTABLE (checked with isUnroutableRunTrigger) when a run's address is dead. If that run's sidecar is already released, the ticker marks the run failed so the schedule waits for the agent's next run.

runCronMigrations(dbConfig, { schema })

From @corbits/cron/migrations. Takes the same arguments as @intx/db's runMigrations. schema holds the host's tenant table, which must exist first. The schedule table always lives in the cron schema. Call it on every boot.

Other exports

| Export | Use | | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | isValidCronExpression(expression) | Syntax, range and cap check. POST / also requires a possible fire. | | nextCronFireAfter(expression, after) | The next matching UTC minute after after. | | isUnroutableRunTrigger(error), RUN_GRANTS_NOT_ROUTABLE, RUN_MAIL_NOT_ROUTABLE | The dead-address error contract a deliverer follows. | | CronRoutesDeps, CreateCronTickerOpts, CronTicker, CronDb, DeliverCronMail, RunTriggerDeliverer, UnroutableRunTrigger | Types for the above. |

Using with Interchange

Run the migrations on every hub boot and build the routes:

import { timeWindowEvaluator } from "@intx/authz";
import { createDB, createGrantStore, runMigrations } from "@intx/db";
import { createRequireGrant } from "@intx/hub-api";
import { createCronRoutes } from "@corbits/cron";
import { runCronMigrations } from "@corbits/cron/migrations";

const dbConfig = {
  host: "localhost",
  port: 5432,
  user: "postgres",
  password: "postgres",
  database: "interchange",
};
await runMigrations(dbConfig, { schema: "public" });
await runCronMigrations(dbConfig, { schema: "public" });
const { db } = createDB(dbConfig);

export const cronRoutes = createCronRoutes({
  db,
  requireGrant: createRequireGrant({
    grantStore: createGrantStore(db),
    conditionRegistry: { time_window: timeWindowEvaluator },
  }),
});

Mount cronRoutes at /cron under /api/tenants/:tenantId, behind the hub's auth and tenant middleware. Grant each principal cron-schedule:* with read and create to list and create, and manage (on cron-schedule:* or one cron-schedule:<id>) to delete. POST /cron with { "expression": "0 9 * * *", "definitionName": "daily-report", "subject": "Run", "body": "go" } then mails the daily-report agent every day at 09:00 UTC.

Start one ticker per hub process with createCronTicker({ db, deliver: createRunTriggerCronDeliver(deliverer) }), where deliverer is the hub's run-trigger deliverer from @corbits/webhooks' createRunTriggerDeliverer. Call ticker.stop() on shutdown. At fire time the ticker mails <run id>@<tenant domain> for the agent's current run, so the agent's workflow needs a mail trigger. Replicas share the table safely.

Upgrading from 0.1

  • mountCron(app, opts) is replaced by app.route(path, createCronRoutes({ db, requireGrant })). Routes are no longer at /api/tenants/:tenantId/cron, but wherever the host mounts them.
  • requireTenantMember is gone. Every route checks a cron-schedule grant, so principals need those grants before calling it, or they get 403.
  • applyCronMigrations(url, { tenantSchema }) is now runCronMigrations(dbConfig, { schema }), imported from @corbits/cron/migrations. Pass the old tenantSchema value as schema.
  • cronScheduleTable, CRON_FIELD_RANGES, cronExpressionCanFire, isValidTimeZone, zonedParts, MAX_LOOKAHEAD_MINUTES, resolveLiveDeployment, definitionExists, failStaleAnchorRun and the types CronField, ZonedParts, LiveDeployment, MountCronOpts, RequireTenantMember are no longer exported.
  • @intx/db, @intx/hub-api, drizzle-orm, hono and postgres are now peers.
  • createCronTicker's intervalMs is optional and defaults to one minute.
  • Existing databases upgrade on first boot with no manual step. Every 0.1.0 schedule keeps its state and keeps firing.

License

LGPL-2.1-only. See LICENSE.