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

@flow-state-dev/scheduled

v0.2.0

Published

Scheduled-actions transport for flow-state-dev.

Readme

@flow-state-dev/scheduled

Scheduled-actions transport adapter for @flow-state-dev/engine.

Mounts a single dispatch endpoint per flow:

POST /api/flows/:kind/schedules/:scheduleId/dispatch
GET  /api/flows/:kind/schedules

The framework owns the dispatch contract, validation, two-phase auth, and provenance. The host runs the scheduler (Vercel Cron, Cloud Scheduler, EventBridge, GitHub Actions, node-cron).

Install

pnpm add @flow-state-dev/scheduled

Mount the adapter

import { createFlowApiRouter } from "@flow-state-dev/engine";
import { createScheduledTransportAdapter } from "@flow-state-dev/scheduled";

const router = createFlowApiRouter({
  registry,
  stores,
  adapters: [createScheduledTransportAdapter()]
});

Static schedules

import { defineFlow } from "@flow-state-dev/core";
import { createBearerSecretPrincipalResolver } from "@flow-state-dev/engine";

export const billing = defineFlow({
  kind: "billing",
  authentication: {
    resolvePrincipal: createBearerSecretPrincipalResolver({
      secret: process.env.FSDEV_SCHEDULER_SECRET!,
      principal: { userId: "system" }
    }),
    requireUser: true
  },
  schedules: {
    static: {
      "monthly-invoices": {
        cron: "0 0 1 * *",
        block: generateMonthlyInvoices
      }
    }
  }
});

A static schedule carries its handler block inline (the shared action core), not a name pointing into flow.actions. Same model the webhook transport uses. defineScheduleBinding (exported from @flow-state-dev/core) is the optional typed constructor. A scheduled handler has no HTTP or MCP caller surface; declare a block in both schedules.static and flow.actions (same reference) to expose it both ways.

Dynamic schedules (per-user reminders, agent-created follow-ups)

A persisted schedule row can't hold a block, so it stores a kind discriminator string instead. The resolver maps kind → block through a required blocks map.

import {
  createResourceCollectionScheduleResolver,
  type ScheduleResourceState
} from "@flow-state-dev/scheduled";
import { defineResourceCollection } from "@flow-state-dev/core";
import { z } from "zod";

const userSchedules = defineResourceCollection<ScheduleResourceState>({
  pattern: "schedules/*",
  scope: "user",
  stateSchema: z.object({
    cron: z.string(),
    kind: z.string(),          // handler discriminator, not a flow-action name
    input: z.unknown().optional(),
    timezone: z.string().optional(),
    onOverlap: z.enum(["skip", "allow"]).optional(),
    description: z.string().optional(),
    enabled: z.boolean().default(true)
  })
});

defineFlow({
  kind: "reminders",
  user: { resources: { schedules: userSchedules } },
  schedules: {
    resolve: createResourceCollectionScheduleResolver({
      collection: userSchedules,
      blocks: { sendDigest, sendReminder }   // persisted `kind` → block
    })
  }
});

The default URL convention is <userId>/<collectionKey>. Override with parseId for richer compositions. A row whose kind isn't in the blocks map resolves to null (404).

Durable dynamic schedules don't recover across crashes. A dynamic schedule's action core is produced by the resolver at dispatch time and carried on the dispatch envelope, never persisted (a block can't be serialized). So a durable dynamic schedule mid-run when the process crashes has no persisted coordinate to re-resolve its handler from, and the run is dropped. Static schedules recover normally — their handler is reachable from a stable coordinate. Make a durable scheduled action static if it must survive a crash.

Source and metadata

Every scheduled-driven request carries source: "scheduled" and a namespaced metadata.schedule:

  • metadata.schedule.scheduleId — the dispatch URL id
  • metadata.schedule.origin"static" or "dynamic"
  • metadata.schedule.cron, metadata.schedule.nominalFireTime, metadata.schedule.dispatchedAt, metadata.schedule.timezone

The dispatched request's action field is the handler block's name (provenance only — a scheduled handler is never reachable through the action endpoint).

Schedule index

ScheduleIndex is an opt-in adapter interface that lets a polling cron tick find due dynamic schedules in one query, instead of scanning every user's schedule collection. Store packages implement it (createPostgresScheduleIndex, createSQLiteScheduleIndex); custom backends implement the three-method interface directly.

The contract is at-most-once: claimDue atomically advances rows before returning them, so a dispatch that fails after advance is dropped, not retried.

defineScheduleCollection

defineScheduleCollection({ pattern, index }) wraps defineResourceCollection with the schedule state schema and mirrors every create/update/delete into the supplied index using lifecycle hooks. Omit index and the collection still works — no mirroring, no hooks.

import { defineScheduleCollection } from "@flow-state-dev/scheduled";
import { createSQLiteScheduleIndex } from "@flow-state-dev/store-sqlite";

const index = createSQLiteScheduleIndex(db);

export const schedules = defineScheduleCollection({
  pattern: "schedules/*",
  index
});

Rows with enabled: false are removed from the index, so toggling a schedule off stops it firing without deleting the underlying record.

Conformance suite

@flow-state-dev/scheduled/testing exports createScheduleIndexConformanceTests for new ScheduleIndex implementations:

import { describe } from "vitest";
import { createScheduleIndexConformanceTests } from "@flow-state-dev/scheduled/testing";

createScheduleIndexConformanceTests("my-backend", {
  createIndex: () => /* fresh empty index */,
  cleanup: (idx) => /* tear down */
});

Covers upsert idempotence, atomic claim+advance, no-op remove, bad-cron skip, and the limit parameter. The Postgres and SQLite adapters both run this suite against their backends.

See the schedule index reference for the full interface and contract.

See also