You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
docs/cli: wheels jobs ... worker CLI is advertised (observability guide + root CLAUDE.md) but does not exist #3090
deployment/observability-and-logging.mdx ("wheels jobs status reports pending, processing, and failed counts per queue") and the root CLAUDE.md "Background Jobs / Worker CLI" block (wheels jobs work|status|retry|purge|monitor) document a jobs subcommand that isn't in cli/lucli/Module.cfc:
$ wheels jobs status
[ERROR] Component [modules.wheels.Module] has no function with name [jobs]
The framework side exists (vendor/wheels/Job.cfc — processQueue()/queueStats()), so apps can only drive queues programmatically or via HTTP today. The current state ships copy-pasteable commands that error on every install.
Repro
Any wheels CLI 4.0.3 install: wheels jobs status → the error above.
Proposed direction (pick one)
Implement the jobs module command — thin wrapper over wheels.Job processQueue/queueStats via the running app, mirroring how wheels test/wheels migrate reach the server; or
De-document: fix the observability guide's alerting bullet (point at (new wheels.Job()).queueStats()), the background-jobs guide if affected, and the root CLAUDE.md Worker CLI block.
Acceptance
Either wheels jobs status works (with CLI specs), or no shipped doc/CLAUDE.md advertises wheels jobs ....
Reported by the guide-behavioral-audit P1 batch 2 (work item p1-16-deploy, claim obs-09; wheels CLI 4.0.3).
Classified as framework-design (API/feature design question).
The report is accurate — jobs is absent from cli/lucli/Module.cfc, while the observability guide and root CLAUDE.md (Worker CLI block) advertise wheels jobs work|status|retry|purge|monitor, so every copy-pasted invocation errors. But resolving it requires a decision the issue itself frames as "pick one": implement the jobs CLI command as a thin wrapper over wheels.Job (processQueue/queueStats), or de-document the advertised surface. Build-the-feature vs. remove-the-docs is a design decision, not a defect fix — and the docs outcome depends on which path is chosen.
Cross-framework research will follow shortly — Rails, Laravel, Django, and other modern frameworks will be consulted before any path forward is proposed.
wheels jobs work|status|retry|purge|monitor is advertised by the observability guide and the root CLAUDE.md Worker-CLI block, but no jobs function exists in cli/lucli/Module.cfc, so every copy-pasted invocation errors (Component [modules.wheels.Module] has no function with name [jobs]). The framework runtime already exists (vendor/wheels/Job.cfc ships processQueue(), queueStats(), retryFailed(), purgeCompleted()). Design question: what should the CLI surface be, and is "implement the thin wrapper" or "de-document" the right call?
How other frameworks handle this
Framework
Naming
Surface
Trade-offs
Edge cases
Rails 8 (Solid Queue + Mission Control)
bin/jobs; console jobs.failed.retry_all
Worker = bin/jobs; status/retry/purge via console API + web dashboard
Simple worker entry; no status/retry/purge CLI verbs — split across console + browser
Declarative retry_on; auto-prune hourly; graceful TERM re-queues in-flight
+1 justification: Sidekiq is the de-facto reference for job-queue tooling; its sidekiqmon is the clearest precedent for a read-only jobs status command — the exact gap Wheels is filling.
Dominant pattern
Two consensuses, one split. (1) The worker is a long-lived foreground CLI process — Rails bin/jobs, Laravel queue:work, Django db_worker/rqworker, Sidekiq sidekiq, Celery worker all agree; Phoenix (in-process supervision) and Spring (jar-launch) diverge only because their runtimes differ, not because a worker CLI is wrong — and CFML, like PHP/Python/Ruby, has no always-on supervised process, so the worker-CLI model fits. (2) Drain-and-exit levers (--once/--stop-when-empty/--max-jobs) are near-universal for CI/Docker/cron. The split is monitoring: a status/monitorCLI is the minority (Sidekiq sidekiqmon, django-rq rqstats --json); most push it to a web dashboard (Mission Control, Horizon, Flower, Oban Web, Actuator). Since Wheels already ships queueStats() and advertised a status verb, the Sidekiq/rqstats precedent makes a terminal status clean. retry/purge as CLI verbs are rarest (Laravel is the main precedent) — but retryFailed()/purgeCompleted() already exist, so the verbs are thin wrappers, not new machinery.
Recommended path for Wheels
Implement the thin wrapper (issue option 1), not de-document. Every advertised verb maps one-to-one onto an existing Job.cfc primitive, and the command is structurally identical to the existing wheels migrate <verb> / wheels packages <verb> dispatchers — a verb router that reaches the running app exactly like runMigration()/runTests(). De-documenting would remove a feature users already expect and that is ~90% built on the framework side. The wheels <noun> <verb> naming already matches Wheels conventions, so no adaptation is needed.
// cli/lucli/Module.cfc - new public dispatcher, mirrors migrate()/test()
public string function jobs() {
var args = new services.ArgSpec().toArgv(structuredArgs(arguments));
var action = arrayLen(args) ? lCase(args[1]) : "status";
switch (action) {
case "work": return runJobsWork(args); // long-running poll loop over processQueue()
case "status": return runJobsStatus(args); // queueStats() snapshot; table or --format=json
case "retry": return runJobsRetry(args); // retryFailed()
case "purge": return runJobsPurge(args); // purgeCompleted()
case "monitor": return runJobsMonitor(args); // status re-rendered on --interval
default:
out("Usage: wheels jobs [work|status|retry|purge|monitor]", "red");
throw(type="Wheels.InvalidArguments", message="Unknown jobs action");
}
}
// Helpers reach the running app like runMigration()/runTests(), then call the EXISTING
// Job.cfc primitives with named args (anti-pattern 1):
// status -> (new wheels.Job()).queueStats(queue=opts.queue) // pending/processing/failed per queue
// work -> loop { (new wheels.Job()).processQueue(queue=opts.queue, limit=opts.limit) }
// exit on SIGINT / --once / --stop-when-empty / --max-jobs (drain-and-exit)
// retry -> (new wheels.Job()).retryFailed(queue=opts.queue)
// purge -> (new wheels.Job()).purgeCompleted(days=opts.olderThan, queue=opts.queue)
// monitor-> re-run status every opts.interval seconds (rqstats / sidekiqmon style)
Usage (already in the docs that triggered this issue):
wheels jobs work --queue=mailers --interval=3 --stop-when-empty
wheels jobs status --format=json
wheels jobs purge --completed --older-than=30
Lives in: cli/lucli/Module.cfc (dispatcher + runJobs* helpers). Notvendor/wheels/** — the runtime primitives are already there.
Composes with: the wheels.Job runtime API (processQueue/queueStats/retryFailed/purgeCompleted), the auto-created wheels_jobs table, and the mcpHiddenTools() convention (expose status/retry/purge as the wheels_jobs MCP tool; hide long-running work like start/stop are hidden).
Cross-engine notes
Clean. The dispatcher runs on the single LuCLI runtime — no Lucee/Adobe/BoxLang split in the CLI layer. The Job.cfc primitives it calls already run cross-engine and are covered by the framework suite. Keep the worker loop free of struct.map() / obj["key"]() / array-by-value traps (none needed). queue is safe as a var/arg name — not a CFML reserved scope (anti-pattern 11 covers url/form/request/etc.).
Open questions
work daemon model: confirm a foreground process supervised by systemd/Docker (Rails/Laravel/Sidekiq consensus) with --once/--stop-when-empty/--max-jobs exit levers, vs. self-daemonizing. Foreground recommended.
purge scope:Job.cfc exposes only purgeCompleted(). To honor --failed, add a sibling purgeFailed() (one method), or scope the first cut to completed-only and document it. Never default to nuking everything.
monitor: ship a terminal refresh of status now (django-rq rqstats --interval, sidekiqmon), leaving a real-time web dashboard to a future package? Recommended: terminal refresh now.
MCP exposure: add jobs work to mcpHiddenTools() (long-running, like start/stop) while status/retry/purge surface as wheels_jobs?
Confidence: high
The worker-CLI pattern is a clear cross-framework consensus, every advertised verb already has a matching Job.cfc primitive, the surface mirrors the existing wheels migrate <verb> dispatcher with no convention/anti-pattern conflict, and the CLI layer is cross-engine-clean — the open questions are implementation scoping, not design forks.
Activity
wheels-bot commentedon Jun 12, 2026
Wheels Bot — Triage
Classified as framework-design (API/feature design question).
The report is accurate —
jobsis absent fromcli/lucli/Module.cfc, while the observability guide and rootCLAUDE.md(Worker CLI block) advertisewheels jobs work|status|retry|purge|monitor, so every copy-pasted invocation errors. But resolving it requires a decision the issue itself frames as "pick one": implement thejobsCLI command as a thin wrapper overwheels.Job(processQueue/queueStats), or de-document the advertised surface. Build-the-feature vs. remove-the-docs is a design decision, not a defect fix — and the docs outcome depends on which path is chosen.Cross-framework research will follow shortly — Rails, Laravel, Django, and other modern frameworks will be consulted before any path forward is proposed.
<!-- wheels-bot:triage:3090 -->
<!-- wheels-bot:triage-class:framework-design -->
wheels-bot commentedon Jun 12, 2026
Wheels Bot — Cross-Framework Research
Question
wheels jobs work|status|retry|purge|monitoris advertised by the observability guide and the rootCLAUDE.mdWorker-CLI block, but nojobsfunction exists incli/lucli/Module.cfc, so every copy-pasted invocation errors (Component [modules.wheels.Module] has no function with name [jobs]). The framework runtime already exists (vendor/wheels/Job.cfcshipsprocessQueue(),queueStats(),retryFailed(),purgeCompleted()). Design question: what should the CLI surface be, and is "implement the thin wrapper" or "de-document" the right call?How other frameworks handle this
bin/jobs; consolejobs.failed.retry_allbin/jobs; status/retry/purge via console API + web dashboardretry_on; auto-prune hourly; graceful TERM re-queues in-flightqueue:<verb>namespacequeue:work/retry/failed/flush/forget/clear/prune-failed/monitorstatus;monitoronly emits an event--tries/--backoff/--stop-when-empty/--max-jobs; retry by UUID; Horizon = Redis dashboardmanage.py <noun_verb>db_worker(run only); django-rqrqworker/rqstats; Celeryworker/inspect/purge--burst/--batchdrain-and-exit;--jsonstats; retry is task-config not a CLI verbmix domain.actionavailable/executing/retryable/discarded;delete_all_jobsby state+age = purgespring.batch.job.name;JobOperatorwords-restart/start/stop/abandon); Actuator HTTP = status/actuatorJSONrestartonly FAILED/STOPPED;recoverfor zombies; async stop must be polledsidekiq+sidekiqmon;*Set#clear/retrysidekiqworker;sidekiqmon= zero-config status CLI; retry/purge via Ruby API + websidekiqmonis the model forjobs status; retry/purge have no CLI verb+1 justification: Sidekiq is the de-facto reference for job-queue tooling; its
sidekiqmonis the clearest precedent for a read-onlyjobs statuscommand — the exact gap Wheels is filling.Dominant pattern
Two consensuses, one split. (1) The worker is a long-lived foreground CLI process — Rails
bin/jobs, Laravelqueue:work, Djangodb_worker/rqworker, Sidekiqsidekiq, Celeryworkerall agree; Phoenix (in-process supervision) and Spring (jar-launch) diverge only because their runtimes differ, not because a worker CLI is wrong — and CFML, like PHP/Python/Ruby, has no always-on supervised process, so the worker-CLI model fits. (2) Drain-and-exit levers (--once/--stop-when-empty/--max-jobs) are near-universal for CI/Docker/cron. The split is monitoring: astatus/monitorCLI is the minority (Sidekiqsidekiqmon, django-rqrqstats --json); most push it to a web dashboard (Mission Control, Horizon, Flower, Oban Web, Actuator). Since Wheels already shipsqueueStats()and advertised astatusverb, the Sidekiq/rqstatsprecedent makes a terminalstatusclean.retry/purgeas CLI verbs are rarest (Laravel is the main precedent) — butretryFailed()/purgeCompleted()already exist, so the verbs are thin wrappers, not new machinery.Recommended path for Wheels
Implement the thin wrapper (issue option 1), not de-document. Every advertised verb maps one-to-one onto an existing
Job.cfcprimitive, and the command is structurally identical to the existingwheels migrate <verb>/wheels packages <verb>dispatchers — a verb router that reaches the running app exactly likerunMigration()/runTests(). De-documenting would remove a feature users already expect and that is ~90% built on the framework side. Thewheels <noun> <verb>naming already matches Wheels conventions, so no adaptation is needed.// cli/lucli/Module.cfc - new public dispatcher, mirrors migrate()/test() public string function jobs() { var args = new services.ArgSpec().toArgv(structuredArgs(arguments)); var action = arrayLen(args) ? lCase(args[1]) : "status"; switch (action) { case "work": return runJobsWork(args); // long-running poll loop over processQueue() case "status": return runJobsStatus(args); // queueStats() snapshot; table or --format=json case "retry": return runJobsRetry(args); // retryFailed() case "purge": return runJobsPurge(args); // purgeCompleted() case "monitor": return runJobsMonitor(args); // status re-rendered on --interval default: out("Usage: wheels jobs [work|status|retry|purge|monitor]", "red"); throw(type="Wheels.InvalidArguments", message="Unknown jobs action"); } } // Helpers reach the running app like runMigration()/runTests(), then call the EXISTING // Job.cfc primitives with named args (anti-pattern 1): // status -> (new wheels.Job()).queueStats(queue=opts.queue) // pending/processing/failed per queue // work -> loop { (new wheels.Job()).processQueue(queue=opts.queue, limit=opts.limit) } // exit on SIGINT / --once / --stop-when-empty / --max-jobs (drain-and-exit) // retry -> (new wheels.Job()).retryFailed(queue=opts.queue) // purge -> (new wheels.Job()).purgeCompleted(days=opts.olderThan, queue=opts.queue) // monitor-> re-run status every opts.interval seconds (rqstats / sidekiqmon style)Usage (already in the docs that triggered this issue):
Lives in:
cli/lucli/Module.cfc(dispatcher +runJobs*helpers). Notvendor/wheels/**— the runtime primitives are already there.Composes with: the
wheels.Jobruntime API (processQueue/queueStats/retryFailed/purgeCompleted), the auto-createdwheels_jobstable, and themcpHiddenTools()convention (exposestatus/retry/purgeas thewheels_jobsMCP tool; hide long-runningworklikestart/stopare hidden).Cross-engine notes
Clean. The dispatcher runs on the single LuCLI runtime — no Lucee/Adobe/BoxLang split in the CLI layer. The
Job.cfcprimitives it calls already run cross-engine and are covered by the framework suite. Keep the worker loop free ofstruct.map()/obj["key"]()/ array-by-value traps (none needed).queueis safe as a var/arg name — not a CFML reserved scope (anti-pattern 11 coversurl/form/request/etc.).Open questions
workdaemon model: confirm a foreground process supervised by systemd/Docker (Rails/Laravel/Sidekiq consensus) with--once/--stop-when-empty/--max-jobsexit levers, vs. self-daemonizing. Foreground recommended.purgescope:Job.cfcexposes onlypurgeCompleted(). To honor--failed, add a siblingpurgeFailed()(one method), or scope the first cut to completed-only and document it. Never default to nuking everything.monitor: ship a terminal refresh ofstatusnow (django-rqrqstats --interval,sidekiqmon), leaving a real-time web dashboard to a future package? Recommended: terminal refresh now.jobs worktomcpHiddenTools()(long-running, likestart/stop) whilestatus/retry/purgesurface aswheels_jobs?Confidence:
highThe worker-CLI pattern is a clear cross-framework consensus, every advertised verb already has a matching
Job.cfcprimitive, the surface mirrors the existingwheels migrate <verb>dispatcher with no convention/anti-pattern conflict, and the CLI layer is cross-engine-clean — the open questions are implementation scoping, not design forks.Sources
docs: correct five stale CLAUDE.md claims from the guide behavioral a…
docs(web/guides): fix basics + digging-deeper guide audit findings (g…
docs(web/guides): correct start-here guide drift from the 2026-06 beh…