risto
05/28/2026, 8:34 PMwheels new blog
[ERROR] Error: Unknown command, file, or module: 'new'
- If it's a file, check the path and extension (.cfm, .cfs, .lucli)
- If it's a module, run 'lucli modules list' to see available modules
- Run 'lucli --help' to see available commands
wheels --version
Wheels Version: 4.0.1 (stable)
__ ___ _
\ \ / / |__ ___ ___| |___
\ \ /\ / /| '_ \ / _ \/ _ \ / __|
\ V V / | | | | __/ __/ \__ \
\_/\_/ |_| |_|\___\___|_|___/
<https://wheels.dev>Peter Amiri
05/28/2026, 8:36 PMGareth
06/02/2026, 10:41 PMPeter Amiri
06/03/2026, 3:09 AMangel
06/03/2026, 5:23 PMisHidden is returned as ishidden which breaks the consumer of the API (yes, I read the docs, not recommended to use Wheels for APIs). The only way to fix it that I've found is to create properties on the model via the property() function. I've tried using returnAs="structs" and returnAs="struct" which according to the documentation, should preserve case but it does not. Using renderText() does not work since the properties are all lower case coming out of the model. Is there a way to get the casing working like in CFWheels 2.5 via a configuration? I want to avoid modifying as much as the code as possible, thanks!Adam Brunt
06/08/2026, 9:33 AMtransaction argument is required but we have never supplied it - I am assuming it defaults to commit ?? But what I am really interested in is how long Wheels waits (if it does) for a lock to be released if some other process is updating the same model row that it is trying to update?
In particular, is there functionality in Wheels to automatically handle scenarios when model/adapters/Base.cfc throws errors such as "Deadlock found when trying to get lock; try restarting transaction" ?Peter Amiri
06/10/2026, 12:21 PM--no-* negations and every named-only invocation. wheels seed --environment=production quietly seeded development. All 24 commands now parse through one typed ArgSpec, and the lossy shim is gone.
Highlights:
β’ --no-sqlite / --no-routes / --no-test-db and named-only flags now actually work everywhere
β’ CLI failures exit non-zero β a typo'd subcommand or failed migration no longer looks green to CI
β’ wheels migrate / seed / reload / g admin refuse to attach to a different project's server (the wrong-database footgun)
β’ PostgreSQL/CockroachDB FK migrations work again (--belongsTo scaffolds crashed before), and Oracle 19c/21c DROP TABLE is fixed
β’ Framework helpers like env() can no longer be invoked as controller actions from a URL
β’ wheels new keeps generated secrets in .env, not in committed config
Upgrade: brew upgrade wheels / scoop update wheels / apt upgrade wheels / dnf upgrade wheels
Full write-up: https://blog.wheels.dev/posts/wheels-4-0-3-releasedristo
06/12/2026, 6:05 PMPeter Amiri
06/16/2026, 4:22 PMcreateTable returns a builder β nothing runs until you call .create() (or .change() for changeTable). The #1 silent footgun.
β’ The column helpers all speak columnNames (plural, comma-list to add several at once). primaryKey() is the one exception β it does NOT split on commas.
β’ timestamps() adds THREE columns: createdAt, updatedAt, AND deletedAt (the soft-delete marker). Don't hand-roll them.
β’ execute() takes only sql β no parameter binding. Inline SQL + NOW() for portable dates; `addRecord()`/`updateRecord()` when you need binding.
β’ Each migration runs in its own transaction; a failure stops the loop and leaves you consistent.
β’ The team part: migrate info flags orphan rows [?], migrate doctor gives a health report, and `forget`/`pretend --yes` reconcile a shared DB without rolling back the wrong way.
Shared-DB reconciliation has its own deep dive in the 4.0.2 release notes β this post is the get-started-and-stay-out-of-trouble version.Peter Amiri
06/17/2026, 4:18 AMcli/lucli/Module.cfc -- its public functions become tools by reflection. No port, no socket, no running dev server. The AI editor spawns wheels mcp wheels and speaks JSON-RPC 2.0 over stdio.
What the post covers:
β’ The reflection model: the CLI is the MCP server -- no second surface to drift
β’ Setup with one .mcp.json snippet (no wheels mcp setup -- that command doesn't exist; wheels mcp just prints the config and writes nothing)
β’ The 18-tool surface and exactly why console, `start`/`stop`, new, and friends are excluded
β’ A worked example: shipping a commenting feature end-to-end through Claude Code -- generate, migrate, test
β’ The deprecated /wheels/mcp HTTP endpoint and why stdio replaced it
β’ A bit of template drift we found while writing the post (the OpenCode template still pointed at the old HTTP endpoint with a literal {PORT})
The tools advertise bare names -- generate, migrate, test, destroy. Clients namespace them under the server, so Claude Code shows them as mcp__wheels__generate.
<https://blog.wheels.dev/posts/wheels-claude-stdio-mcp>Zain Ul Abideen
06/19/2026, 11:05 AMWheels applications (1.x, 2.x, and 3.x) to newer versions with less manual effort and reduced risk.
To make sure the tool solves real-world problems, Iβd love feedback from developers and organizations currently maintaining older Wheels applications.
As a thank you, anyone who participates and provides detailed feedback will receive free access to the migration tool once itβs ready.
A few questions:
1. What version of Wheels are you currently running?
2. How old is your application?
3. What ColdFusion/Lucee version are you using?
4. Have you ever attempted a Wheels upgrade? If yes, what challenges did you encounter?
5. What are the biggest concerns preventing an upgrade today?
β’ Breaking changes
β’ Lack of time/resources
β’ Third-party plugins
β’ Legacy code complexity
β’ Lack of testing
β’ Deployment risk
β’ Other
6. Approximately how large is your application?
β’ Number of models
β’ Number of controllers
β’ Estimated LOC (if known)
7. Do you have automated tests in place?
8. Are you using Docker, CI/CD, or other deployment automation?
9. Which migration tasks would you most like to automate?
10. If a migration tool could solve one major pain point for your team, what would it be?
My goal is to build tooling that helps identify upgrade blockers, automate repetitive migration tasks, and provide a safer upgrade path for legacy Wheels applications.
Your feedback will directly influence the roadmap and features of the migration tool.
Thank you for helping make legacy Wheels upgrades easier for the entire community!Peter Amiri
06/19/2026, 1:55 PM/wheels/info no longer leaking csrfCookieEncryptionSecretKey or datasource creds, fail-closed dev-UI + reload gates, a new trustProxyHeaders setting.
β’ Performance β lock-free `model()`/`controller()` warm path, memoized route lookups, schema-column cache, O(1) dispatch gate.
β’ wheels deploy grew up β ~20 Kamal-port fixes: fresh-host boot, all-or-nothing fleet lock, secret redaction, audit trail.
β’ Cross-engine β Adobe CF reload 500s on case-sensitive filesystems fixed, BoxLang null hardening, Oracle/SQL Server identity retrieval.
β’ Install it anywhere β `brew`/`scoop`/`apt`/`dnf`, now including arm64 Linux, install-tested daily.
If you grabbed 4.0.4: just upgrade to 4.0.5. Everything in 4.0.4 is in it.Peter Amiri
06/22/2026, 3:15 PMenum(property="status", values="draft,published,archived") registers a validation, three boolean checkers (isDraft, isPublished, isArchived), and three scopes (.draft(), .published(), .archived()). All parameterised.
β’ The chainable query builder β .where("col", value), .whereIn, .whereBetween, .orderBy, .limit, .get(). Auto-quoting + type validation on the way to SQL, so integer/float/boolean payloads can't sneak past the binding layer.
β’ Three features, one design β they all return deferred-query proxies that materialise into the same finder-argument struct on a terminal call. The chain reads top-to-bottom in the order you'd describe the query out loud.
Side effect of writing the post: framework fix (#2736) β whereIn / whereNotIn with empty arrays no longer emit malformed IN () SQL. whereIn(empty) now sets a flag the terminals honour (count β 0, exists β false, findAll β empty query, etc.); whereNotIn(empty) is a no-op (exclude-none = match-all). Same user-facing behaviour as Rails / Sequel / Django / Eloquent. Fourteen regression specs locked in.Peter Amiri
06/23/2026, 8:35 PMrenderSSE(data=..., event=..., retry=...) β one event per request, browser auto-reconnects. Spec-covered, engine-safe, holds no thread. The one to reach for.
β’ The streaming trio β initSSEStream() returns the raw writer, then sendSSEEvent / sendSSEComment (keep-alive heartbeat) / closeSSEStream for long-lived pushes.
β’ isSSERequest() β serve SSE to EventSource clients and HTML to everyone else from one action.
β’ The bounded streaming loop β timeout cap, writer.checkError() disconnect detection, heartbeats, sleep, try/finally close. CFML holds a worker thread for the whole connection, so this part is mandatory.
β’ The one real sharp edge: initSSEStream() is engine-fragile on BoxLang and not spec-covered end-to-end. Hand-test streaming per engine; renderSSE has no such caveat.
About 4 lines of controller code for server-to-client updates, no second protocol.Peter Amiri
06/26/2026, 5:27 AMhandle(request, next) returns a string; code before next() runs in, code after runs out.
β’ Why request is NOT the CFML request scope (it's a {params, route, pathInfo, method} struct) and the Adobe CF scope-shadow trap that follows.
β’ The lifecycle that changes everything: every middleware is a singleton shared across all concurrent requests for the app's lifetime β so any mutable state MUST be locked. Worked example with a cflock-guarded counter.
β’ Short-circuiting β return a string WITHOUT calling next() to skip the controller (how RateLimiter does its 429 and Cors does OPTIONS).
β’ Registration: global in config/settings.cfm vs route-scoped in config/routes.cfm, string-path vs object-instance, and why global always runs before route-scoped.
Three worked components: a request timer, a thread-safe audit logger, and a maintenance gate.Peter Amiri
06/26/2026, 5:51 PMconfig/services.cfm with injector() + the fluent di.map("x").to("...").asSingleton() chain (and yes β you create that file yourself; the scaffold ships none)
β’ The three lifetimes β transient (the DEFAULT, the one that bites), .asSingleton(), .asRequestScoped() β and when to reach for each
β’ Resolving anywhere with service("name") (it throws Wheels.DI.ServiceNotFound, it doesn't return null)
β’ Controller inject("a, b") in config() β use as this.a in actions, resolved per-request
β’ Constructor auto-wiring: init() param names that match registered mappings get filled in for free
β’ Sharp edges: singletons cached by mapping NAME not component path; auto-wiring only fires with empty initArguments; mapInstance() vs map() in plugins
A user-facing how-to β not the wheelsdi internals story (that's the earlier "From WireBox to wheelsdi" post).Peter Amiri
06/30/2026, 1:19 AMseedOnce() β the idempotent seed helper. Create a row only if a matching one doesn't already exist; safe to run a hundred times.
β’ The file convention: app/db/seeds.cfm (runs in every environment) then app/db/seeds/<env>.cfm (env-specific, sibling directory β NOT a hyphenated suffix).
β’ Why the whole run is one transaction that rolls back on ANY validation failure (#2973) β a half-applied seed run must never look fully-applied.
β’ Composite uniqueness, seeding from code via application.wheels.seeder, and the CLI: wheels seed, scaffold with wheels generate snippets seed-data.
β’ Sharp edges: it's a skip not an upsert, --generate is random non-idempotent junk, and wheels db:seed / wheels generate seed are not real commands.Peter Amiri
07/01/2026, 12:05 PMresources() β the 7 RESTful routes from one call (and why update is secretly TWO routes β PATCH and PUT)
β’ Nested resources: the callback form auto-closes for you; nested=true makes YOU call .end(). Mix them and you add an extra end() that pops an empty scope stack and throws.
β’ Route model binding β binding=true resolves <http://params.post|params.post> before your action runs and 404s a missing record, so the findByKey + not-found dance disappears
β’ Convention vs explicit binding diverge on a missing model class (skip vs re-throw); a missing record always 404s
β’ The traps: route order is load-bearing (wildcard LAST), and you can't name an action after a framework helper (model, redirectTo, the is* predicates) or it 404s
Standalone how-to with a full worked config/routes.cfm. If you've ever shipped /posts/999999 and gotten a stack trace instead of a 404, this is the post.Peter Amiri
07/03/2026, 5:15 AMcondition / unless / when β and why those are a fixed-grammar string parser, NOT Evaluate() (bad expressions silently skip in production)
β’ Custom validate() / validateOnCreate() / validateOnUpdate() β register a method name, fail by calling addError() (the return value is ignored β the #1 gotcha)
β’ enum() β one call that auto-registers validatesInclusionOf, generates isDraft()-style checkers, and creates per-value scopes
β’ Reading errors right: errorsOn() returns an array of {property, message, name} STRUCTS β loop and read .message, donβt print the struct
Plus a βsharp edgesβ section: scoped uniqueness isnβt a DB constraint, presence is skipped on columns with a DB default, confirmation errors land on the virtual passwordConfirmation property, and allowBlank defaults to false everywhere.Peter Amiri
07/03/2026, 5:15 AMobjectName is the variable name (a string), not the object. textField(objectName="user", property="email") emits name="user[email]" and round-trips into params.user.email.
β’ The real HTML5 single-input roster β emailField, urlField, numberField, telField, dateField, colorField, rangeField, searchField (+ textField with a type override). No timeField/dateTimeField/monthField/weekField exist.
β’ method="put" isn't real HTTP β startFormTag rewrites to POST + a hidden _method field, and auto-appends the CSRF token for non-GET.
β’ The unchecked-checkbox problem and how uncheckedValue (on by default, with the ($checkbox) companion) keeps the param from vanishing.
β’ *Tag helpers (flat params, for search/login/filters) vs object helpers (nested params).
β’ errorMessagesFor (a <ul> of everything) vs errorMessageOn (first message, inline).
β’ There's no standalone label() helper β labels come from the label= arg, and the input nests inside the label by default.
A worked, end-to-end create/edit form, plus a Sharp Edges section built from the behaviors that bite first.Peter Amiri
07/04/2026, 5:15 PMname arg, and when to override them
β’ The exact dynamic methods each one generates β author.posts(), author.postCount(), author.addPost(), author.deleteAllPosts(), and the rest β with a table per type
β’ dependent= and its five legal values (delete / deleteAll / remove / removeAll / false). There is NO `nullify`; FK-nulling is remove / removeAll. belongsTo has no dependent at all.
β’ nestedProperties β save a parent and its children from one form submit; autoSave defaults true, allowDelete defaults false
β’ Eager loading with include= to kill the N+1, why nested include="artist(genre)" is query-only, has-many-through via shortcut, and why a polymorphic belongsTo can't be eager-loaded
Plus a Sharp Edges section: getters return QUERIES not arrays, join-type defaults differ by association (belongsTo inner-joins and silently drops parentless rows), and mixed positional+named args error every time.Peter Amiri
07/06/2026, 3:02 PMcache= args do nothing on your laptop β caching is OFF in development by default (cacheActions/cachePages/cachePartials/cacheQueries all false in dev)
β’ Action caching with caches(), including static=true (whole page via cfcache, aborts before filters) and appendToKey to vary by user
β’ The TWO query caches everyone confuses β request-level dedupe (always on) vs findAll(cache=N) engine caching β and why reload=true only defeats one of them
β’ Partial + page caching via includePartial(cache=...) / renderView(cache=...)
β’ The uncomfortable truth about invalidation: there's no clearCache(key). clearCachableActions() only unregisters; real eviction = short times + ?reload=true
β’ Sharp edges: cache times are MINUTES not seconds, the 5000-item bounded cache silently drops new entries when full, static caching no-ops on engines without cfcache
A worked blog-home-page example ties it together β action cache as the outer skin, query + partial caches underneath.Peter Amiri
07/07/2026, 5:59 PMhasAttachment, no upload service, no params.file. And that's fine. The honest idiomatic flow is five steps, and this post walks every one end to end.
What it covers:
β’ The multipart trap β startFormTag does NOT auto-add enctype just because a fileField is on the form. Default is multipart=false. Forget multipart=true and the upload fails silently β no error, file posts as plain text. This eats more uploads than everything else combined.
β’ fileField vs fileFieldTag β bound (user[avatar]) vs unbound (document) field names, and why the name is your contract with the controller.
β’ The controller β native CFML cffile action="upload" (Wheels has zero upload wrapper), nameConflict="makeUnique", and copying the result onto params.user so the model can validate it.
β’ Validation β validatesFormatOf over the stored string + a custom validate() callback for size/MIME, because validators can't see multipart uploads.
β’ sendFile β the one direction with a real Wheels helper, with built-in path-traversal protection.
Plus a "Sharp edges" section of real, source-confirmed gotchas.Peter Amiri
07/09/2026, 1:21 AMsendEmail is the whole mailer β no Mailer class, no ActionMailer. One controller helper that renders a view and sends it.
β’ The arg-routing rule: a cfmail attribute goes to the email, everything else becomes a view variable. Get the name wrong and your reply-to silently becomes template data.
β’ Email template resolution β bare name vs. leading-slash, and why the slash matters.
β’ Multipart text+html and the <-counting heuristic that flips your parts if your text body has code in it. detectMultipart=false fixes it.
β’ Attachments, and deliver=false as the preview/test path.
β’ The big one: the bare sendEmail() inside a Job's perform() that the docs show does not work β a Job is a plain component with no controller mixins. The fix is controller("Mailer").sendEmail(...). Full background-delivery example included.risto
07/14/2026, 2:19 AMgknight
07/17/2026, 12:32 AMPeter Amiri
07/22/2026, 12:01 PMPeter Amiri
08/21/2026, 2:31 PMwheels generate auth, an authorization policy layer, pluggable storage disks, and a query builder that finally lets select() start a chain. Also lands the mixin-plan cache that was making 4.0.x feel slower than 2.x.
Write-up: blog.wheels.dev/posts/wheels-4-0-6-released Discussion: github.com/wheels-dev/wheels/discussions/3390
brew upgrade wheels
# or scoop / apt / dnf
wheels --version # Wheels Version: 4.0.6Peter Amiri
09/28/2026, 6:22 PMoffset(), stricter CLI arguments, wheels test exit codes
Upgrade: brew upgrade wheels (or scoop/apt/dnf), then wheels upgrade apply. Copy the reload-password header block into your `public/Application.cfc`; details in the post.
π blog.wheels.dev/blog/wheels-4-1-1-released
_apt users: if wheels fails with UnsupportedClassVersionError, run unset JAVA_HOME (it needs Java 21). The fix is in 4.1.2._Rodney
09/28/2026, 6:54 PM