Upgrade Lucee 7.1 to 8.0: Checklist for AI Coding Agents
Upgrade Lucee 7.1 to 8.0: Checklist for AI Coding Agents
Purpose: a checklist an AI coding agent can follow to upgrade an application or server from Lucee 7.1 to 8.0. It says what to search for, what to change, how to verify the result and what to leave alone. Humans should read Breaking Changes between Lucee 7.1 and 8.0, which explains the background.
Rules for the agent:
- Work through the steps in order. They are sorted by impact.
- Only change what a step tells you to change. If a search finds nothing, move on.
- Show the user every change to
.CFConfig.json, environment files and Dockerfiles before you apply it. - Search commands use ripgrep (
rg). Withgrep, usegrep -rnEwith the same pattern. JSON checks usejq.
0. Find the files
# Lucee server / web config (the file is named .CFConfig.json or config.json)
rg --files --hidden -g '.CFConfig.json' -g 'config.json' -g '!node_modules'
# environment and startup files
rg --files --hidden -g 'Dockerfile*' -g '*compose*.y*ml' -g '.env*' -g '*.env' -g 'setenv.sh' -g 'setenv.bat' -g '*.properties' -g '!node_modules'
# CFML code
rg --files -g '*.cfc' -g '*.cfm' -g '*.cfml'
Only treat config.json as Lucee config if it sits in a Lucee context folder (lucee-server/context/ or WEB-INF/lucee/) or contains Lucee keys such as "inspectTemplate" or "extensions".
1. Prerequisites and environment checks
1.1 Java 21 or newer (blocker)
Lucee 8.0 does not start on Java 11 or 17.
java -version
rg -n -i '^\s*FROM\s' -g 'Dockerfile*'
rg -n -i '(JAVA_HOME|jdk|jre|temurin|openjdk|corretto|zulu)[^0-9\n]*(8|11|17)\b' -g 'Dockerfile*' -g '*compose*.y*ml' -g 'setenv.*' -g '*.env' -g '.env*'
Fix: switch the JVM or base image to Java 21 (LTS) or newer.
# before
FROM eclipse-temurin:17-jre
# after
FROM eclipse-temurin:21-jre
The servlet container must still be Jakarta based (Tomcat 10.1+, Jetty 11+), the same as for 7.x. Do not change it.
1.2 Extension setup (only if extensions are controlled explicitly)
In 8.0, MarkdownToHTML() and smb:// resources come from extensions. They are bundled and installed automatically, unless the setup controls extensions explicitly.
rg -n -i 'LUCEE_EXTENSIONS(_INSTALL|_CONFIG_ONLY)?\b|lucee\.extensions(\.install|\.config\.only)?\b'
rg -n -i 'markdownToHTML\s*\(|smb://' -g '*.cfc' -g '*.cfm' -g '*.cfml'
You only need to act if both searches find something, and the first one shows lucee.extensions.install=false, lucee.extensions.config.only=true, or a fixed LUCEE_EXTENSIONS / extensions list. In that case, add the extension that the code uses to that list, in the same format as the existing entries:
| Used in code | Extension | ID | Bundled version |
|---|---|---|---|
MarkdownToHTML() |
org.lucee:markdown-extension |
3AEDA748-F62B-42E3-8E8DC5AE0DDABE09 |
1.0.0.2-RC |
smb:// |
org.lucee:smb-extension |
A35C8501-FFBB-43EA-975C6883C92A7D5E |
1.0.0.3-RC |
Lucee light builds also need these added this way.
1.3 Maven repositories must be reachable
Lucee 8.0 downloads artifacts (extensions, libraries, and Janino if Java code is compiled at runtime on a JRE) from Maven repositories. Check that the server can reach the defaults:
for u in https://repo1.maven.org/maven2/ https://maven-central.storage-download.googleapis.com/maven2/ https://maven.lucee-services.com/; do
curl -s -o /dev/null -w "%{http_code} $u\n" --max-time 10 "$u"
done
If they are blocked (firewall, offline), either allow them or point Lucee to an internal mirror with maven → repository in .CFConfig.json. That setting replaces the default list, so the mirror has to proxy Maven Central too:
"maven": {
"repository": [ "https://nexus.example.com/repository/maven-public/" ]
}
2. Move top-level keys into their section in .CFConfig.json (high impact, silent)
Lucee 8.0 only reads these keys inside their section. If they are left at the top level, there is no error and no warning. Lucee ignores them and uses the default.
Detect them (run once per config file):
jq -r 'keys[] | select(test("^(debugging(Show)?(Database|Exception|Template|Dump|Tracing|Trace|Timer|ImplicitAccess|ImplicitVariableAccess|QueryUsage|Thread)|show(Debug|Doc|Reference|Metrics?|Tests?)|doc|documentation|reference|metrics?|test|cacheDefault(Object|Query|Template|Resource|Function|Include|File|HTTP|Webservice)|updateProxy(Host|Port|Username|Password))$"; "i"))' .CFConfig.json
Fix: move each key found to its new location. Keep the value as it is.
| Top-level key | Move to |
|---|---|
debuggingDatabase, debuggingShowDatabase |
monitoring.debuggingDatabase |
debuggingException, debuggingShowException |
monitoring.debuggingException |
debuggingTemplate, debuggingShowTemplate |
monitoring.debuggingTemplate |
debuggingDump, debuggingShowDump |
monitoring.debuggingDump |
debuggingTracing, debuggingShowTracing, debuggingShowTrace |
monitoring.debuggingTracing |
debuggingTimer, debuggingShowTimer |
monitoring.debuggingTimer |
debuggingImplicitAccess, debuggingImplicitVariableAccess, debuggingShowImplicitAccess |
monitoring.debuggingImplicitAccess |
debuggingQueryUsage, debuggingShowQueryUsage |
monitoring.debuggingQueryUsage |
debuggingThread, debuggingShowThread |
monitoring.debuggingThread |
showDebug |
monitoring.showDebug |
showDoc, doc, documentation, showReference, reference |
monitoring.showDoc |
showMetric, showMetrics, metric, metrics |
monitoring.showMetric |
showTest, showTests, test |
monitoring.showTest |
cacheDefaultObject … cacheDefaultWebservice |
cache.defaultObject … cache.defaultWebservice (drop the cacheDefault prefix, keep the rest, e.g. cacheDefaultHTTP → cache.defaultHTTP) |
updateProxyHost |
proxy.server |
updateProxyPort |
proxy.port |
updateProxyUsername |
proxy.username |
updateProxyPassword |
proxy.password |
Before:
{
"debuggingTemplate": true,
"showDebug": true,
"cacheDefaultQuery": "myQueryCache",
"updateProxyHost": "proxy.example.com",
"updateProxyPort": 8080
}
After:
{
"monitoring": { "debuggingTemplate": true, "showDebug": true },
"cache": { "defaultQuery": "myQueryCache" },
"proxy": { "server": "proxy.example.com", "port": 8080 }
}
If monitoring, cache or proxy already exists, merge the keys into it. Do not create a second key with the same name. If a key exists both at the top level and in the section, keep the value from the section and remove the top-level one. Ask the user if the two values differ.
Leave these at the top level (they are not moved): debuggingLogOutput, debuggingMaxRecordsLogged, debugTemplates, showVersion.
3. Extension providers (high impact for custom providers)
rg -n -A5 '"extensionProviders"' -g '.CFConfig.json' -g 'config.json'
extensionProviders now takes Maven group IDs. URL entries (http://…, https://…, for example https://extension.lucee.org or https://www.forgebox.io) are ignored.
// before
"extensionProviders": [ "https://extension.lucee.org", "https://www.forgebox.io" ]
// after
"extensionProviders": [ "org.lucee" ]
For a custom provider, ask the user for the Maven group ID of their extensions. Do not invent one.
4. parallel=true in iteration functions (medium impact, behaviour change)
In 8.0 on Java 21+, parallel=true runs on virtual threads with no concurrency limit. 7.1 used at most 20 platform threads.
rg -n -i '\b(array|struct|query|list|collection)?(each|map|filter|some|every)\s*\(' -g '*.cfc' -g '*.cfm' -g '*.cfml' | rg -i '\btrue\b|parallel'
rg -n -i '\.(each|map|filter|some|every)\s*\(' -g '*.cfc' -g '*.cfm' -g '*.cfml' | rg -i '\btrue\b|parallel'
Review each hit. You only need to act when the closure uses a limited resource (database queries, HTTP calls to a rate-limited service, file handles). In that case, make the limit explicit:
// before (7.1: at most 20 platform threads)
arrayEach( ids, function( id ) { queryExecute( "..." ); }, true );
// after (same behaviour as 7.1)
arrayEach( ids, function( id ) { queryExecute( "..." ); }, "thread", 20 );
The argument order is: collection, closure, parallel, maxConcurrency. Do not mix positional and named arguments in one call. Lucee rejects that. Leave pure CPU or in-memory closures as they are. To switch parallel=true back to platform threads server-wide, the user can set LUCEE_ALLOW_VIRTUAL_THREADS=false. Suggest it, but do not set it yourself.
5. Administrator.cfc / <cfadmin> scripts (medium impact, silent)
Only relevant if the code automates the Lucee Administrator.
rg -n -i 'action\s*=\s*["'"'"'](get|update)(LoginSettings|QueueSetting|CustomTagSetting|DebugSetting)["'"'"']|\.(get|update)(LoginSettings|QueueSetting|CustomTagSetting|DebugSetting)\s*\(' -g '*.cfc' -g '*.cfm' -g '*.cfml'
rg -n -i 'action\s*=\s*["'"'"'](getRHExtensionProviders|updateRHExtensionProvider|updateExtensionProvider|removeRHExtensionProvider|removeExtensionProvider|getDefaultPassword|updateDefaultPassword|removeDefaultPassword|getAdminSyncClass|updateAdminSyncClass)["'"'"']' -g '*.cfc' -g '*.cfm' -g '*.cfml'
Rename the arguments/attributes. Old names are ignored without an error. Administrator.cfc keeps the current value, and <cfadmin> saves the default.
| Old | New |
|---|---|
rememberMe, captcha, delay |
loginRememberme, loginCaptcha, loginDelay |
database, queryUsage, exception, tracing, dump, timer, implicitAccess, thread |
debuggingDatabase, debuggingQueryUsage, debuggingException, debuggingTracing, debuggingDump, debuggingTimer, debuggingImplicitAccess, debuggingThread |
maxLogs |
debuggingMaxRecordsLogged |
max, timeout, enable (request queue) |
requestQueueMax, requestQueueTimeout, requestQueueEnable |
deepSearch, localSearch, customTagPathCache, extensions (custom tags) |
customTagDeepSearch, customTagLocalSearch, customTagUseCachePath, customTagExtensions |
// before
admin action="updateQueueSetting" type="server" password=pw max=100 timeout=0 enable=true;
// after
admin action="updateQueueSetting" type="server" password=pw requestQueueMax=100 requestQueueTimeout=0 requestQueueEnable=true;
The read actions getLoginSettings, getQueueSetting, getCustomTagSetting and getDebugSetting also return the new names. Update code that reads the result (e.g. result.max → result.requestQueueMax, result.maxLogs → result.debuggingMaxRecordsLogged).
<cfadmin action="updateDebug"> still accepts the old debug option names. You can rename them there, but you don't have to.
Removed actions (second search): replace getRHExtensionProviders / updateRHExtensionProvider / updateExtensionProvider / removeRHExtensionProvider / removeExtensionProvider with getExtensionGroups / updateExtensionGroups / removeExtensionGroups (Maven group IDs, see step 3). getDefaultPassword, updateDefaultPassword, removeDefaultPassword, getAdminSyncClass and updateAdminSyncClass have no replacement. Report these to the user instead of deleting the code.
6. Renamed environment variables / system properties (low impact)
rg -n -i 'LUCEE_APPLICATION_(LISTENER|MODE)|LUCEE_DEBUGGING_OPTIONS|LUCEE_MAVEN_DEFAULT_REPOSITORIES|lucee\.application\.(listener|mode)|lucee\.debugging\.options|lucee\.maven\.default\.repositories|lucee\.requesttimeout\.(memory|cpu|concurrentrequest)threshold'
The old names still work as aliases, so this is optional cleanup. If you rename, use:
| Old | New |
|---|---|
LUCEE_APPLICATION_LISTENER / lucee.application.listener |
LUCEE_LISTENER_TYPE / lucee.listener.type |
LUCEE_APPLICATION_MODE / lucee.application.mode |
LUCEE_LISTENER_MODE / lucee.listener.mode |
LUCEE_DEBUGGING_OPTIONS=template,database |
LUCEE_MONITORING_DEBUGGINGTEMPLATE=true, LUCEE_MONITORING_DEBUGGINGDATABASE=true (one per option) |
LUCEE_MAVEN_DEFAULT_REPOSITORIES |
maven.repository in .CFConfig.json (replaces the defaults, so include Maven Central, see 1.3) |
-Dlucee.requesttimeout.memorythreshold etc. |
-Dlucee.requestTimeout.memorythreshold etc. (env vars LUCEE_REQUESTTIMEOUT_* unchanged) |
Also check for other LUCEE_* variables. In 8.0, every config setting can be set through an environment variable or system property, and that value wins over .CFConfig.json:
rg -n -o 'LUCEE_[A-Z0-9_]+' -g 'Dockerfile*' -g '*compose*.y*ml' -g '*.env' -g '.env*' -g 'setenv.*' | sort -u
List them for the user, especially ones that seem to conflict with .CFConfig.json. Do not delete them on your own.
7. Smaller code changes (low impact)
7.1 LuceeExtension( download=... )
rg -n -i 'LuceeExtension\s*\(' -g '*.cfc' -g '*.cfm' -g '*.cfml'
If the call passes download, rename that argument to detailed. There is no alias.
7.2 Key order of application, server, session
rg -n -i 'for\s*\(\s*(var\s+)?\w+\s+in\s+(application|server|session)\b|structKey(List|Array)\s*\(\s*(application|server|session)\b|serializeJSON\s*\(\s*(application|server|session)\b' -g '*.cfc' -g '*.cfm' -g '*.cfml'
These scopes no longer keep insertion order. Change code only if it depends on the order, by sorting the keys or copying into structNew( "ordered" ) first.
7.3 PDF generation
rg -n -i '<cfdocument|\bdocument\s*\(|this\.pdf\.type|\btype\s*=\s*["'"'"'](classic|modern|pd4ml|fs)["'"'"']' -g '*.cfc' -g '*.cfm' -g '*.cfml'
The bundled PDF extension 3.0 uses a new rendering engine (OpenHTMLToPDF), and PD4ML ("classic") was removed. type and this.pdf.type are ignored now, so you may remove them. Tell the user to compare the generated PDFs with the 7.1 output. Do not rewrite layouts on your own.
7.4 Other
- If a client or proxy relies on
Connection: closeafter<cflocation>, REST error responses or<cfflush interval>, enable thecloseConnectionsetting. Otherwise do nothing. - If the server only has XML config (
lucee-server.xml/lucee-web.xml.cfm) and no.CFConfig.json, note that 8.0 no longer converts XML config at startup. Convert it on 7.1 first (or withConfigTranslate()).
8. Verify
-
Start Lucee 8.0 once with config validation:
LUCEE_CONFIG_VALIDATE=true # or -Dlucee.config.validate=trueThis loads every setting at startup instead of on first use, so invalid values show up right away. In 8.0, a value that cannot be parsed (for example an invalid timespan) makes the setting fail when it is used. 7.1 quietly used the default instead.
-
Check the server logs (
lucee-server/context/logs/) and the console output (stdout/stderr, e.g.catalina.out) for configuration errors, and for failed extension installs or Maven downloads. -
Check that the moved settings are active: open the Administrator or call
<cfadmin action="getDebug" returnVariable="d">(returnsdebuggingTemplate,debuggingDatabase, …) and compare with the 7.1 values. Remember that keys left at the top level (checklist step 2) produce no log entry. You can only see them by comparing values. -
Check that settings defined through environment variables appear as read-only in the Administrator. That shows which values come from the environment.
-
Run the application's test suite. Exercise
parallelcode paths, PDF generation, and any<cfadmin>automation.
Remove LUCEE_CONFIG_VALIDATE again after the check if the user does not want it permanently.
Do not change
Stop agents from over-editing. These do not need changes for 8.0:
- The
.CFConfig.jsonformat, file name and location. Do not rename keys other than those in step 2, and do not reformat or reorder the file. - Keys that are already inside
monitoring,cacheorproxy. Old names there still work (e.g.cacheDefaultQueryinsidecache). parallel="thread"/parallel="virtual"calls, andmaxThreads/maxThreadCountarguments (still accepted as aliases ofmaxConcurrency).parallel=false: still means sequential.<cfthread>code: the newvirtualattribute is opt-in.MarkdownToHTML()andsmb://code: they keep working, the extensions are bundled (see 1.2 for the one exception).CreateULID()and HTML parsing (htmlParse): still in core.- Mail, FTP and Scheduler Classic: still bundled extensions, as in 7.1.
- Javax vs Jakarta imports or servlet container: unchanged since 7.0.
- Single mode / multi mode settings: unchanged.
lucee.maven.download.policy.*settings: still work.- Old environment variable names in step 6: they still work. Rename only if the user wants to clean up.
See also
- Breaking Changes between Lucee 7.1 and 8.0
- Environment Variables / System Properties for Lucee
- Extension Installation
- Virtual Threads