Skip to content

docs/cli: wheels jobs ... worker CLI is advertised (observability guide + root CLAUDE.md) but does not exist #3090

Closed

Description

@bpamiri

Summary

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)

  1. 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
  2. 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).

Activity

  1. wheels-bot commented on Jun 12, 2026

    @wheels-bot
    Contributor

    Wheels Bot — Triage

    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-bot:triage:3090 -->
    <!-- wheels-bot:triage-class:framework-design -->

  2. wheels-bot commented on Jun 12, 2026

    @wheels-bot
    Contributor

    Wheels Bot — Cross-Framework Research

    Question

    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
    Laravel queue:<verb> namespace queue:work/retry/failed/flush/forget/clear/prune-failed/monitor Cleanest verb set; but purge splits 4 ways; no single status; monitor only emits an event --tries/--backoff/--stop-when-empty/--max-jobs; retry by UUID; Horizon = Redis dashboard
    Django manage.py <noun_verb> Core db_worker (run only); django-rq rqworker/rqstats; Celery worker/inspect/purge Cohesive under manage.py; core Tasks ships no worker; Celery powerful but heavy --burst/--batch drain-and-exit; --json stats; retry is task-config not a CLI verb
    Phoenix / Oban runtime API + Oban Web; mix domain.action Workers run in-process (BEAM supervision tree) — no worker CLI; monitor = LiveView dashboard Free for BEAM; assumes an always-on process tree CFML lacks States available/executing/retryable/discarded; delete_all_jobs by state+age = purge
    Spring Batch / Boot jar + spring.batch.job.name; JobOperator words Launch via jar param; ops are flags/words (-restart/start/stop/abandon); Actuator HTTP = status Verb CLI deliberately absent; status = Java API or /actuator JSON restart only FAILED/STOPPED; recover for zombies; async stop must be polled
    Sidekiq (+1, reference job processor) sidekiq + sidekiqmon; *Set#clear/retry sidekiq worker; sidekiqmon = zero-config status CLI; retry/purge via Ruby API + web sidekiqmon is the model for jobs status; retry/purge have no CLI verb Exp. backoff (~25 retries/20d); Dead "morgue" set; empty queue counts default 0

    +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/monitor CLI 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). Not vendor/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.

    Sources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions