From hypervibe
Add a scheduled task (CRON) to an existing Next.js project. Creates a protected /api/cron/<task-name> route and registers the schedule on the right clock - by default the unified shared Hypervibe worker (hypervibe-jobs, precise to the minute, zero extra Cloudflare slot), a dedicated Cloudflare Worker only when the task needs isolated resources, or a GitHub Action as the no-Cloudflare fallback. Can be called by /bootstrap, by /add-automation, or standalone.
How this skill is triggered — by the user, by Claude, or both
Slash command
/hypervibe:add-cron [description of what the cron should do][description of what the cron should do]The summary Claude sees in its skill listing — used to decide when to auto-load this skill
```bash
wrangler (do this BEFORE any other wrangler command in this skill)eval "$(node "${CLAUDE_SKILL_DIR}/../../scripts/wrangler-env-init.mjs")"
This line loads CLOUDFLARE_API_TOKEN from User scope (Windows registry / shell rc on Mac/Linux) if it is not already in process.env, and adds the pnpm bin to PATH (for bash sessions where pnpm setup has not yet propagated). Without it, wrangler fails with "command not found" on Mac (Spotlight), or may use a different Cloudflare account than the one the user expects.
You add a scheduled task to the current Next.js project. You decide yourself which clock is best, based on the nature of the task. You ask the user NOTHING about this choice - you act, then you explain in 1 sentence what you did in the final summary.
The users of this plugin may be non-technical. In EVERYTHING you show to the user:
Clock → fetch() → Vercel /api/cron/<task-name> (protected by CRON_SECRET)
The business logic always lives in Next.js (/api/cron/<task-name>/route.ts), protected by a CRON_SECRET bearer. The clock only pings the endpoint at the desired time. What changes between the options is solely who presses the button.
The account-wide hypervibe-jobs worker, shared across ALL the user's projects and roles (scheduled pings, database backups, quota watch). Lives in a git-versioned local repo (~/.hypervibe-jobs/), ticks every minute, and consumes ONE Cloudflare cron slot in total no matter how many tasks and projects use it. Precision: to the minute. This is where virtually every scheduled task belongs.
A Cloudflare Worker created specifically for this task. Only justified when the task itself needs an isolated Cloudflare binding (its own R2 / KV / D1 / Durable Object) or a secret that must NOT coexist with other projects' secrets. Consumes 1 of the 5 free cron slots on the account.
A YAML workflow in the project's GitHub repo. Used ONLY when Cloudflare is not configured on the machine and the user does not want to configure it. Free and unlimited, but delays of 30-60 min are possible during peak load.
This skill reads the Cloudflare token from the vault → first, make sure it is unlocked (follow _ensure-vault): node "${CLAUDE_SKILL_DIR}/../../scripts/vault/vault.mjs" status → if locked/expired, run launch.mjs unlock; if the vault does not exist, delegate to _add-keyring. (Not needed if you already know Cloudflare is unavailable and the cron will go to a GitHub Action.)
Make sure the unified shared worker is provisioned (idempotent, fast when already there):
eval "$(node "${CLAUDE_SKILL_DIR}/../../scripts/wrangler-env-init.mjs")"
result=$(node "${CLAUDE_SKILL_DIR}/../../scripts/shared-worker/ensure.mjs")
Parse the JSON:
ok=true → store CF_OK=true, plus WORKER_DIR, WORKER_URL, and the current jobs count. status is created (first time - one sentence to the user: "I set up your shared clock, a single mechanism that will serve all your projects") or already_present (silent).ok=false → Cloudflare is not usable on this machine (wrangler missing, token missing/locked). Store CF_OK=false. Do NOT abort: the GitHub clock can still take the task (Step 4 will force it). Mention /start as the way to enable Cloudflare later.Briefly verify that the user's need is really a cron:
Good candidate:
Bad candidate (→ suggest /add-automation instead):
If no red flag, continue silently.
If the user passed a description as an argument ($ARGUMENTS not empty), use it directly. Do not ask.
Otherwise, ask:
What will this scheduled task be used for?
Describe in one sentence what it should do, e.g.:
- "send a weekly SEO report by email"
- "reset user quotas at midnight"
- "sync Brevo contacts every hour"
- "clean up temporary files every night"
Capture the answer in TASK_DESCRIPTION.
Invoke _detect-project-root to retrieve PROJECT_NAME, WEB_DIR, IS_NEXTJS, IS_MONOREPO. Abort if IS_NEXTJS=no.
When do you want this task to run?
Say it in your own words, for example: "every day at 9am", "every Monday morning", "every hour", "on the 1st of the month at midnight".
Convert it into a 5-field UTC cron expression (the user thinks in local time, it is up to you to convert). Store it in CRON_EXPR and keep the human-readable version in CRON_HUMAN.
Bash: date -u +%H
# If you need to convert a local time to UTC
Also ask:
Give a short name for this task, so we can recognize it easily later. For example:
rapport-hebdo,sync-clients,nettoyage.
Store it in TASK_NAME (kebab-case ASCII).
You ask the user nothing. The logic is now simple:
If the description explicitly mentions a need for an isolated Cloudflare R2 / KV / D1 / Durable Object, or a secret that must NOT be shared with other projects on the same account, store NEEDS_DEDICATED_CF=true. Otherwise false. (This is rare: a plain "ping my site on a schedule" task NEVER needs this.)
| Case | CF_OK | NEEDS_DEDICATED_CF | → CHOICE |
|---|---|---|---|
| 1 | true | false | shared (the default for virtually everything) |
| 2 | true | true | cf-dedicated - but first check a free slot exists: run node "${CLAUDE_SKILL_DIR}/../../scripts/count-cf-cron-slots.mjs"; if cfFree = 0, fall back to shared and note in the summary that the isolated spot was not possible |
| 3 | false | * | gh (no Cloudflare on this machine) |
Also build REASON (1 non-tech sentence) for the final summary:
shared: "I put it on your shared clock: precise to the minute, it serves all your projects at zero extra cost"cf-dedicated: "I gave it its own dedicated clock because this task needs its own isolated storage"gh: "Cloudflare is not set up on this machine, so I used the GitHub clock - free and unlimited, but it can run 30-60 minutes late. If that ever matters, run /start to enable Cloudflare and tell me to move the task."If CHOICE=gh AND the task smells timing-critical (frequency > 1x/hour, "exactly at midnight", "reset", user-visible consequence when late), be honest in the final summary about the concrete impact of a possible delay.
Check whether CRON_SECRET already exists in .env:
grep -q "^CRON_SECRET=" .env 2>/dev/null && echo "exists" || echo "missing"
Invoke _generate-secret with format=hex, length=32. Capture the value.
Invoke _push-env-vars with:
CRON_SECRET=<value>Read the value from .env for the following steps.
CHOICECHOICE=shared → unified shared worker (one command does everything)WEB_DIR_FLAG=""
[ "$IS_MONOREPO" = "yes" ] && WEB_DIR_FLAG="--web-dir apps/web"
[ "$IS_MONOREPO" = "no" ] && WEB_DIR_FLAG="--web-dir ."
result=$(CRON_SECRET_VALUE="<CRON_SECRET>" node "${CLAUDE_SKILL_DIR}/../../scripts/shared-worker/register.mjs" \
--kind ping \
--task-name "<TASK_NAME>" \
--cron "<CRON_EXPR>" \
--app-url "<NEXT_PUBLIC_APP_URL>" \
--project-name "<PROJECT_NAME>" \
$WEB_DIR_FLAG \
--put-secrets)
This single call: creates the protected Next.js route (if absent), registers the task in the versioned registry, commits the change, uploads the project's secret to the shared worker (first time only), and redeploys. Parse the JSON: ok, action (added/replaced), job (the name of the entry in the shared registry, normally <PROJECT_NAME>-<TASK_NAME> - store it as JOB_NAME, the management commands below use it), routeCreated, missingSecrets (should be empty; if not, follow its nextSteps).
CHOICE=cf-dedicated → dedicated Cloudflare WorkerInvoke _setup-wrangler. Then:
node "${CLAUDE_SKILL_DIR}/../../scripts/setup-cron-worker.mjs" \
--task-name "<TASK_NAME>" \
--cron-expr "<CRON_EXPR>" \
--app-url "<NEXT_PUBLIC_APP_URL>" \
--project-name "<PROJECT_NAME>"
Add --web-dir apps/web if monorepo.
Upload the secret + deploy:
cd cron-workers/<TASK_NAME>
echo "<CRON_SECRET>" | wrangler secret put CRON_SECRET
wrangler deploy
cd ../..
CHOICE=gh → GitHub ActionCreate <WEB_DIR>/src/app/api/cron/<TASK_NAME>/route.ts (route protected by CRON_SECRET):
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
const auth = req.headers.get("authorization");
if (auth !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// YOUR CRON LOGIC HERE - described as: <TASK_DESCRIPTION>
return NextResponse.json({ success: true, timestamp: new Date().toISOString() });
}
export async function GET(req: NextRequest) {
return POST(req);
}
gh repo view --json nameWithOwner -q .nameWithOwner
If absent → abort, ask the user to push the project to GitHub first.
gh secret set CRON_SECRET --body "<CRON_SECRET>"
gh secret set CRON_APP_URL --body "<NEXT_PUBLIC_APP_URL>"
Write .github/workflows/cron-<TASK_NAME>.yml:
name: Cron - <TASK_NAME>
on:
schedule:
- cron: "<CRON_EXPR>"
workflow_dispatch:
jobs:
trigger:
runs-on: ubuntu-latest
steps:
- name: Ping /api/cron/<TASK_NAME>
env:
CRON_SECRET: ${{ secrets.CRON_SECRET }}
CRON_APP_URL: ${{ secrets.CRON_APP_URL }}
run: |
curl -fsSL -X POST \
-H "Authorization: Bearer $CRON_SECRET" \
"$CRON_APP_URL/api/cron/<TASK_NAME>"
Check .github/workflows/keepalive.yml. If it does not exist, create it:
name: Keepalive
on:
schedule:
- cron: "0 8 1 * *"
workflow_dispatch:
jobs:
keepalive:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit --allow-empty -m "chore: keepalive"
git push
git add .github/workflows/ <WEB_DIR>/src/app/api/cron/<TASK_NAME>/
git commit -m "feat: add scheduled task <TASK_NAME> via GitHub Action"
git push
Invoke _update-claude-md with:
custom heading: ## CronFor a shared worker cron:
- **<TASK_NAME>** (shared hypervibe-jobs worker) - `<CRON_EXPR>` (<CRON_HUMAN>) → registered as `<JOB_NAME>` in `~/.hypervibe-jobs/jobs.js` (git-versioned) → calls `/api/cron/<TASK_NAME>`
For a dedicated CF Worker cron:
- **<TASK_NAME>** (dedicated Cloudflare Worker) - `<CRON_EXPR>` (<CRON_HUMAN>) → `cron-workers/<TASK_NAME>/` → calls `/api/cron/<TASK_NAME>`
For a GitHub Action cron:
- **<TASK_NAME>** (GitHub Action) - `<CRON_EXPR>` (<CRON_HUMAN>) → `.github/workflows/cron-<TASK_NAME>.yml` → calls `/api/cron/<TASK_NAME>`
Add as the section intro (created only once):
Scheduled tasks. A clock pings the `/api/cron/<name>` endpoint on the Next.js side, protected by `CRON_SECRET`. The business logic lives in Next.js. 3 possible clocks:
- **Shared hypervibe-jobs worker** (default): one account-wide Cloudflare worker for all projects (pings, backups, quota watch), registry git-versioned in `~/.hypervibe-jobs/`, timing to the minute, 1 CF slot total
- **Dedicated Cloudflare Worker**: only when the task needs isolated resources (own R2/KV/D1), 1 CF slot per task
- **GitHub Action**: fallback without Cloudflare, best-effort (±30-60 min), automatic monthly keepalive
And env-vars:
- \CRON_SECRET` - Bearer token used to authenticate cron clock against `/api/cron/``Choose the right block according to CHOICE, incorporating REASON in non-tech language.
shared✅ Your task <TASK_NAME> is in place
It will trigger <CRON_HUMAN>.
<If the shared clock was created on this occasion: "I set up your shared clock for the first time: a single mechanism, versioned and kept safe on your computer, that will serve all your current and future projects at no extra cost.">
For now it does nothing concrete - I prepared the file where you will write what it should do (<TASK_DESCRIPTION>). Tell me what it should run and I'll code the logic for you.
You can also ask me at any time:
- "run the task right now to test it"
- "show me the latest triggers"
- "change the schedule to X"
- "delete this task"
cf-dedicated✅ Your task <TASK_NAME> is in place
It will trigger <CRON_HUMAN>.
For now it does nothing concrete - I prepared the file where you will write what it should do (<TASK_DESCRIPTION>). Tell me what it should run and I'll code the logic for you.
You can also ask me at any time: "run the task right now to test it", "show me the latest triggers", "change the schedule to X", "delete this task".
gh✅ Your task <TASK_NAME> is in place
It will trigger <CRON_HUMAN>.
A small reminder: GitHub can be 30 to 60 min late<, concrete consequence for this task if relevant>. I also added (if not already done) a tiny invisible task that runs once a month to prevent GitHub from disabling the clock if you don't touch the project for 60 days.
If the delay ever becomes a problem, run
/startto enable Cloudflare on this machine, then tell me "move this task to my shared clock" and I'll migrate it.For now it does nothing concrete - I prepared the file where you will write what it should do (<TASK_DESCRIPTION>). Tell me what it should run and I'll code the logic for you.
On the shared clock, the registry job name is JOB_NAME = <PROJECT_NAME>-<TASK_NAME> (tasks registered by older plugin versions may be listed under <TASK_NAME> alone - when in doubt, --list shows the exact names).
ADMIN=$(node "${CLAUDE_SKILL_DIR}/../../scripts/_read-user-env.mjs" HYPERVIBE_JOBS_ADMIN_TOKEN)
curl -s -X POST -H "Authorization: Bearer $ADMIN" "<WORKER_URL>/trigger?name=<JOB_NAME>"
(For a GitHub clock: gh workflow run cron-<TASK_NAME>.yml. For a dedicated clock: curl the /api/cron/<TASK_NAME> route directly with the project's CRON_SECRET.)node "${CLAUDE_SKILL_DIR}/../../scripts/shared-worker/register.mjs" --list (+ .github/workflows/cron-*.yml + cron-workers/*/ for the other clocks). Present them in plain language.--cron (same project + same task name = update in place).node "${CLAUDE_SKILL_DIR}/../../scripts/shared-worker/register.mjs" --remove --name <JOB_NAME>. Also offer to delete the now-unused /api/cron/<TASK_NAME> route.CHOICE. Not before - the automatic decision is the default.npx claudepluginhub flavien-ia/hypervibe-harness --plugin hypervibeAdd an automation - scheduled task, background process, long-running worker, webhook handler, heavy computation, or a personal recurring AI mission. Acts as a smart orchestrator: discovery phase to understand the user's actual need, infers whether the job belongs to the APP (runs on the app's infrastructure: cron via /add-cron, Cloudflare Worker, or Render Background Worker) or to the OPERATOR (a Claude routine on the user's own account via _create-routine), recommends with plain-words reasoning, and delegates to the right sub-skill after validation. Optionally converts the project to Turborepo when a dedicated worker is needed.
Guides cron job scheduler setup, configuration, and best practices for backend systems. Generates production-ready cron configurations.