anolilab/lunora/lunora-setup-scheduler
Claude Code and agent skill by anolilab/lunora
- community
Checking that the SKILL.md file is still in the repository.
Schedules deferred and recurring work in a Lunora app. Covers `ctx.scheduler.runAfter` / `runAt` (delayed dispatch of a function or workflow), cron jobs in `lunora/crons.ts` via `lunora registry add crons` and the `cronJobs()` builder, the `SchedulerDO` / `SCHEDULER` binding and `LUNORA_ORIGIN_URL`, retries and dead-lettering, and `createWorkpool` for bounded concurrency. Use when the user wants to "run this later", "send a reminder in 3 days", "run every night / every 5 minutes", add a cron, c…
Install
npx vskill@latest install anolilab/lunora/lunora-setup-schedulerSKILL.md
Lunora Setup Scheduler
Lunora schedules work in two ways, both implemented in @lunora/scheduler:
- Deferred dispatch:
ctx.scheduler.runAfter/runAtrun a function (or
start a workflow) later. Jobs are stored in the SchedulerDO Durable Object (binding SCHEDULER), which owns the alarm, the retries and the dead-letter list.
- Recurring jobs (crons): declared in
lunora/crons.tswithcronJobs().
Codegen compiles them into Cloudflare Cron Triggers, and the Worker's scheduled() handler dispatches them directly. They don't go through the SchedulerDO.
When Not to Use
- The project has no Lunora backend yet: use
lunora-quickstartfirst. - You only need to react to data changes: use a reactive
query/
subscription (lunora-realtime).
- Multi-step orchestration with sleeps and waits belongs in a durable workflow.
A scheduler or cron can start one, but don't build step logic on top of jobs.
Deferred dispatch: runAfter / runAt
Wire it once
Codegen needs @lunora/scheduler installed as soon as ctx.scheduler is used, because the generated _generated/app.ts imports createScheduler. Once the dependency is present, codegen emits _generated/scheduler.ts (re-exporting SchedulerDO) and a .scheduler() method on the app builder.
- Vite-first apps (generated worker entry): the plugin forwards
SchedulerDO and calls .scheduler() for you.
- Hand-written worker entry (
src/server.tswithdefineApp): wire it yourself.
wrangler.jsonc's SCHEDULER binding is reconciled from this export:
The SchedulerDO calls back into the Worker at LUNORA_ORIGIN_URL. It reads that value from its own env and never from the request, so a caller can't point dispatch at an arbitrary URL. Without it, every runAfter/runAt fails with ORIGIN_NOT_CONFIGURED, and nothing provisions it for you:
- dev:
LUNORA_ORIGIN_URL="http://localhost:5173"in.dev.vars(your dev
server's URL)
- prod:
vars.LUNORA_ORIGIN_URLinwrangler.jsonc, or
wrangler secret put LUNORA_ORIGIN_URL (per --env)
lunora doctor warns when the scheduler binding exists and the variable is unset.
Schedule from a mutation or action
- The full
ctx.schedulersurface isrunAfter(delayMs, target, args?),
runAt(timestampMs, target, args?) (epoch ms), cancel(id), get(id), and list(). It is not available in a query, because queries are deterministic and re-run.
targetis a generatedinternal.*/api.*mutation or action reference, a
"file:fn" string, or a workflows.<name> / agents.<name> reference. A workflow or agent reference starts a fresh instance when the job fires, with args as its params. Scheduled jobs run with no end-user identity, so target internal.* functions.
- Inside a mutation, scheduling is transactional. The call is buffered and
dispatched only after the mutation commits. If the mutation rolls back (OCC conflict, validation error), the job is dropped too. The returned id is final immediately, so you can store it on the row you're writing and cancel it later.
- Retries use the DO defaults: 5 retries, exponential backoff from 30 s. A job
that exhausts them is dead-lettered (visible in the studio), not dropped. To set a per-job retry policy ({ maxAttempts, backoff: "exponential" | "linear", baseMs, maxMs }) or a shardKey, build your own client with createScheduler({ namespace }) from @lunora/scheduler. Its runAfter/runAt take a fourth RunOptions argument and add dead() / deadRetry(id).
Recurring jobs: crons
Step 1: Add the starter
This adds @lunora/server + @lunora/scheduler to package.json and copies two files you own: lunora/crons.ts (a cronJobs() registry) and lunora/crons/jobs.ts (an example internalMutation, internal.crons.jobs.run).
Step 2: Declare jobs
- Names must be non-empty string literals, unique across the project.
- Targets must be static property accesses (
internal.<…path>.<fn>, one segment
per folder and file, e.g. internal.crons.jobs.run) or a workflows.<name> / agents.<name> reference. Codegen resolves them from the AST, so a computed reference fails codegen. args is optional and must be a literal.
- Schedules are UTC and validated when the job is defined.
intervaltakes exactly one of
{ minutes } (must divide 60) or { hours } (must divide 24). { seconds } is rejected because Cron Triggers have a one-minute floor. For sub-minute work, use runAfter.
- Use
hourlywith an offset minute to spread jobs away from:00.
Step 3: Regenerate
Codegen emits lunora/_generated/crons.ts (LUNORA_CRON_TRIGGERS plus the LUNORA_CRONS dispatcher map, which defineApp wires into scheduled()) and syncs triggers.crons in wrangler.jsonc. Don't hand-edit that array.
Verify: the studio's scheduled-jobs view lists each cron and has a "Run now" action, which calls the same dispatch path as a real tick.
Bounded concurrency: workpool (optional)
A workpool is a named pool inside the same SchedulerDO. It needs no extra binding. Build it in Worker code that has the raw env:
The DO dispatches at most maxConcurrency jobs at once and queues the rest durably. One alarm drain keeps at most six dispatches in flight (the DO connection limit), so a long-running job holds a slot for its whole duration. For individually long jobs (LLM calls, exports), prefer createQueueWorkpool, which is backed by Cloudflare Queues. You give up the hard cap, per-job cancel and per-job status in exchange.
Common Pitfalls
- `ORIGIN_NOT_CONFIGURED`:
LUNORA_ORIGIN_URLis unset for theSchedulerDO
(see above).
- `SchedulerDO` not exported from a hand-written worker entry: wrangler
only binds classes the entry exports, so SCHEDULER is never provisioned.
- Dynamic `name` or target in `cronJobs()`: codegen cannot discover it.
- Editing crons without re-running `lunora codegen`:
triggers.cronsand the
dispatcher map are generated output and go stale.
- Non-idempotent handlers: a failed job is retried, and a slow cron tick
can overlap the next one.
Security
- Security scanPassed, 96 of 100Pattern checks on the skill's files.
- Model reviewNot runA model review is scheduled for the next rescan.
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingpackages/cli/skills/lunora-setup-scheduler/SKILL.md
Popularity
- 7-day trend score
- 5
Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/anolilab/lunora/lunora-setup-scheduler)