Why move from tag-based CFML to CFScript Syntax
Migrating tag-heavy ColdFusion templates to CFScript yields cleaner, more maintainable, and testable code. Script-based CFML reads closer to modern languages, improves IDE tooling and linting, and makes Refactoring and Unit testing with frameworks like TestBox more straightforward. It also eases Onboarding for developers familiar with JavaScript, Java, or C#. Beyond readability, CFScript improves scoping discipline, promotes functional patterns (closures, array/struct member functions), and simplifies reuse in components (CFCs).
Prerequisites / Before You Start
- Backups
- Create complete backups of your application code, coldfusion.xml/Lucee server.xml, datasources, Scheduled tasks, and Application.cfm/Application.cfc.
- Snapshot your database(s) if possible or ensure point-in-time recovery.
- Version planning
- Decide your CFML engine: Adobe ColdFusion (ACF) or Lucee.
- Select a target engine version and compatibility mode.
- Dependencies and integrations
- Tooling
- Install CommandBox for local servers and Automation.
- Add linters/formatters: CFLint, cfformat, and a CFML language server (VS Code CFML, SublimeCFML).
- Testing and observability
- Add or expand TestBox tests (unit/Integration).
- Enable request and application logs, and optionally a profiler/monitor (FusionReactor, SeeFusion).
- Coding Standards and strategy
- Agree a style guide for CFScript (scoping, naming, Error handling, member functions).
- Decide incremental vs. big-bang Migration; prefer incremental.
- Staging environment
- Prepare a test/staging server mirroring production.
- Enable robust exception info (only on non-prod).
Supported Features by engine/version (Quick reference)
| Topic | ACF 2016+ | ACF 2018+ | ACF 2021+ | Lucee 5.3+ |
|---|---|---|---|---|
| queryExecute() | Yes | Yes | Yes | Yes |
| Tag islands in script | Limited | Improved | Improved | Strong |
| Member functions (array/struct) | Yes | Yes | Yes | Yes |
| Arrow/closures | Yes (closures) | Yes | Yes | Yes |
| Script equivalents for cfhttp/cfmail | Yes (service objects) | Yes | Yes | Yes (plus tag islands) |
| Non-null/Null support | Limited | Better | Better | Strong |
Note: “Tag islands” allow tags inside cfscript blocks. Support differs between engines; test before relying on them.
Step-by-Step Migration guide
1) Inventory and categorize your CFML
- Tag-heavy pages: .cfm templates with cfset/cfif/cfoutput/cfinclude.
- Business logic in tags: cfquery, cfloop (query), cftransaction, cfstoredproc.
- Integration tags: cfhttp, cfmail, cffile, cfdirectory, cfpdf, cfdocument, cfimage.
- Application scaffolding: Application.cfm vs Application.cfc, cflogin, Session management.
Prioritize foundational files (Application.cfm → Application.cfc) and high-traffic templates, then move outward to edge features (PDF, image).
2) Establish Application.cfc in CFScript
If you still rely on Application.cfm, move to Application.cfc in script.
Example:
cfc
component {
this.name = “MyApp”;
this.sessionManagement = true;
this.datasource = “MyDSN”;
function onApplicationStart() {
application.startedAt = now();
return true;
}
function onSessionStart() {
session.init = true;
}
function onRequestStart(string targetPage) {
// Security headers
cfheader(name="X-Frame-Options", value="SAMEORIGIN");
return true;
}
function onRequest(string targetPage) {
include arguments.targetPage;
}
function onError(any exception, string eventName) {
writeLog(text="Error: #exception.message#", file="app");
}
}
Key points:
- Prefer a single entry point with onRequest.
- Move per-request includes to script-style include.
- Centralize datasource and Security headers here if practical.
3) Replace cfset/cfparam/cfif/cfoutput with script equivalents
- cfset → direct assignment
- cfif/cfelseif/cfelse → if/else if/else
- cfparam → param statement
- cfoutput → writeOutput() (use sparingly; avoid building HTML with string concatenation when using frameworks like ColdBox)
Example conversion:
Tag form:
cfm
Script:
cfscript
param name=”url.id” default=0 type=”numeric”;
greeting = “Hello”;
if (url.id > 0) {
writeOutput(greeting & “, user ” & url.id);
}
Tip: Avoid mixing output and logic. Consider returning data from functions and letting views format it.
4) Migrate cfquery to queryExecute() or Query objects
Tag form:
cfm
SELECT id, email
FROM Users
WHERE id =
Script with positional parameters:
cfscript
qUser = queryExecute(
“SELECT id, email FROM Users WHERE id = ?”,
[ { value=url.id, cfsqltype=”cf_sql_integer” } ],
{ datasource=”MyDSN” }
);
Script with named parameters:
cfscript
qUser = queryExecute(
“SELECT id, email FROM Users WHERE id = :id”,
{ id = { value=url.id, cfsqltype=”cf_sql_integer” } },
{ datasource=”MyDSN” }
);
Working with the result:
cfscript
if (qUser.recordCount == 1) {
userEmail = qUser.email[1];
}
Transactions:
cfscript
transaction {
queryExecute(“UPDATE Users SET last_login = NOW() WHERE id = :id”, { id=url.id }, { datasource=”MyDSN” });
}
Stored procedures:
- Prefer queryExecute where possible.
- For complex cfstoredproc use cases, retain a tag island or use engine-specific storedproc utilities (Lucee has storedproc() function; ACF typically uses cfstoredproc—wrap it in a tag island when necessary).
5) Convert cfloop to for/while/iterator patterns
- Array/list loops:
cfscript
for (item in myArray) {
// …
}
for (i=1; i <= listLen(csv); i++) {
part = listGetAt(csv, i);
}
- Query loops:
cfscript
for (row=1; row <= q.recordCount; row++) {
writeOutput(q.email[row] & “
“);
}
// Or use each()/map() in Lucee and ACF modern versions
q.each(function(row) {
writeOutput(row.email & “
“);
});
6) Replace cfinclude with script include
Tag:
cfm
Script:
cfscript
include “/views/header.cfm”;
Note: Includes execute in the current scope. Use modules/components for reusability rather than many includes.
7) Convert cffunction/cfcomponent to CFScript CFCs
Tag:
cfm
Script:
cfc
component accessors=true {
public numeric function sum(required numeric a, required numeric b) {
var total = arguments.a + arguments.b; // or local.total
return total;
}
}
- Use local scope (var) inside functions to prevent scope leakage.
- Prefer returnType and argument types for clarity and tooling.
- Add access modifiers: public, package, private, remote.
8) Error handling: cftry/cfcatch → try/catch/finally
Tag:
cfm
Script:
cfscript
try {
risky = 100 / val(form.divisor);
} catch (any e) {
writeLog(text=”Error: #e.message#”, file=”app”);
} finally {
// optional cleanup
}
Use specific types when possible (database, expression, application).
- cfhttp → HTTP service object
cfscript
httpService = new http();
httpService.setMethod(“get”);
httpService.setUrl(“https://api.example.com/users/1“);
result = httpService.send();
if (result.getStatusCode() == 200) {
data = deserializeJSON(result.getPrefix().fileContent);
}
-
cfmail → Mail service object
cfscript
mailSvc = new mail();
mailSvc.setTo(“user@example.com”);
mailSvc.setFrom(“no-reply@example.com”);
mailSvc.setSubject(“Welcome”);
mailSvc.setType(“html”);
mailSvc.setBody(“Hello!
“);
mailSvc.send(); -
cffile/cfdirectory → built-in functions
cfscript
fileWrite(expandPath(“/tmp/out.txt”), “Hello”);
content = fileRead(expandPath(“/tmp/out.txt”));
dirs = directoryList(expandPath(“/var/www”), true, “path”); -
cflock → lock block
cfscript
lock name=”cfg” timeout=5 type=”exclusive” {
application.counter = (application.counter ?: 0) + 1;
} -
cftransaction → transaction block (shown above)
-
cfimage/cfpdf/cfdocument/cfchart
- Prefer dedicated libraries/services if needed.
- If no clean script equivalent exists in your engine, use tag islands for those sections or wrap functionality inside helper CFCs that internally use tags.
Example tag island (engine-dependent):
cfscript
// Only where supported
cfpdf(action=”merge”, source=”/tmp/a.pdf,/tmp/b.pdf”, destination=”/tmp/out.pdf”);
Or Encapsulate:
cfc
component {
public void function mergePDFs(required array files, required string outPath) {
// inside this method, use cfpdf tag
cfpdf(action=”merge”, source=arrayToList(arguments.files), destination=arguments.outPath);
}
}
10) Adopt CFScript idioms: member functions, closures, and functional operations
- Arrays and structs have rich member functions:
cfscript
names = [“Ana”,”Bo”,”Carmen”,”Dee”];
short = names.filter(function(n){ return len(n) <= 3; }).map(function(n){ return uCase(n); });
settings = { debug:false, theme:”light” };
keys = settings.keyArray();
- Closures enable callbacks for map/filter/reduce. Use them to replace verbose tag loops.
11) Scoping and security adjustments
- Use local (var) scope inside functions.
- Avoid relying on implicit scopes; prefer explicit variables., arguments., local., request., session., application., server.
- Migrate cfparam usage to guard inputs early.
- Replace evaluate() with safer alternatives (structFind, indirect references) and parameterized queries (queryParam via queryExecute parameters).
12) Incremental cutover and tag islands strategy
- Convert low-risk templates first.
- Isolate complex tags (cfdocument/cfpdf/cfimage) within helper components. Gradually refactor or retain tag islands where the script equivalent is limited.
- Keep the app running with mixed templates during transition.
Risks, Common Issues, and How to Avoid Them
- Scope leaks After migration
- Risk: Removing cfset/var leads to variables leaking into broader scope.
- Avoidance: Always declare local variables with var or local., and enable lint rules to flag missing var.
- Output differences
- Risk: Tag-based cfoutput behavior differs from writeOutput. Unintended whitespace or missing output.
- Avoidance: Centralize rendering logic; avoid implicit output. Verify layout pages carefully.
- Query parameterization regressions
- Risk: Swapping cfqueryparam for queryExecute parameters incorrectly.
- Avoidance: Use positional or named parameters consistently and always specify cfsqltype. Add tests for SQL queries.
- Engine-specific behavior
- Risk: Tag islands or service object APIs differ between ACF and Lucee.
- Avoidance: Keep engine-specific adapters. Test on the target engine and version configured the same as production.
- Date/time and locale formatting
- Risk: Changes to LSDateFormat/LSParseDateTime or default locales alter output.
- Avoidance: Specify locale/timezone explicitly where necessary and test critical formatting.
- Null handling and empty strings
- Risk: Differences in null support cause NPE-like errors in script.
- Avoidance: Normalize inputs, use isNull(), Default values, and safe Navigation where available.
- Legacy features and Deprecated tags
- Risk: Old Custom tags or deprecated attributes lack script equivalents.
- Avoidance: Wrap in CFCs, or replace with libraries. Maintain a compatibility layer temporarily.
Post-Migration Checklist
- Functional validation
- Run all TestBox suites. Add tests for critical paths missed previously.
- Verify Authentication/authorization flows, session persistence, and CSRF protections.
- Database checks
- Confirm all queries run with proper parameterization and return expected rowcounts.
- Validate transactions and rollback behavior under failure scenarios.
- Rendering and output
- Compare key pages’ HTML source to Pre-migration snapshots.
- Validate PDFs, images, charts where applicable, both visually and via size/checksums.
- Performance and resource usage
- Load test main endpoints; compare response times and memory usage to baseline.
- Check thread and pool metrics where concurrency (lock/thread/transaction) is used.
- Logging and error handling
- Ensure logs contain meaningful messages and stack traces.
- Trigger sample exceptions to validate onError, try/catch, and alerts.
- Security
- Confirm secure headers, input validation (param), and proper encoding in outputs.
- Re-run vulnerability scans and static analysis.
- Deployment readiness
- Confirm Application.cfc configs (datasource, mappings, customtag paths) in all environments.
- Ensure Scheduled tasks and CF/Lucee admin settings match expectations.
- Code quality
- Run CFLint and cfformat across the codebase.
- Adopt pre-commit hooks to enforce Standards.
Practical Conversion Reference
Common tag-to-script mappings
| Tag | Script alternative |
|---|---|
| cfset | variable = expression; |
| cfif/cfelseif/cfelse | if / else if / else |
| cfparam | param name=”scope.var” default=… type=”…” |
| cfoutput | writeOutput(string) |
| cfinclude | include “path.cfm”; |
| cfquery | queryExecute(sql, params, options) |
| cflock | lock name=”…” type=”…” { … } |
| cftransaction | transaction { … } |
| cfhttp | http = new http(); http.set…(); http.send(); |
| cfmail | mail = new mail(); mail.set…(); mail.send(); |
| cffile/cfdirectory | fileRead/fileWrite/directoryList |
| cfloop | for/while/for-in, array/struct member functions |
| cftry/cfcatch | try/catch/finally |
Tip: For tags without clean script equivalents (cfpdf, cfdocument), either use helper components that contain tags, or rely on tag islands if your engine supports them.
Example: Converting a page end-to-end
Before (tag-based):
cfm
SELECT id, email FROM Users WHERE id =
After (CFScript):
cfscript
param name=”url.id” default=0 type=”numeric”;
qUser = queryExecute(
“SELECT id, email FROM Users WHERE id = :id”,
{ id = { value=url.id, cfsqltype=”cf_sql_integer” } },
{ datasource=”MyDSN” }
);
if (qUser.recordCount) {
httpService = new http();
httpService.setMethod(“get”);
httpService.setUrl(“https://api.example.com/profile/” & qUser.id[1]);
res = httpService.send();
if (res.getStatusCode() == 200) {
profile = deserializeJSON(res.getPrefix().fileContent);
writeOutput(profile.name & " (" & qUser.email[1] & ")");
}
}
Automation and Tooling Tips
- Use CommandBox to run local servers and scripts:
- box start cfengine=adobe@2021.0.9
- box start cfengine=lucee@5.3.10
- Auto-format script
- box install commandbox-cfformat
- box cfformat run src=./src
- Lint for issues
- CFLint and CFML Language Server to highlight missing var, unused vars, or risky scopes.
- Reference resources
- cfdocs.org for script examples and member functions.
- Engine release notes for script coverage and differences.
Performance and Refactoring Opportunities
- Replace repetitive list loops with array/struct functions: map/filter/reduce for cleaner, faster code.
- Use queryExecute with named params to self-document SQL and reduce errors.
- Reduce template includes; move logic to CFCs with clear interfaces.
- Adopt caching (cachePut/cacheGet, query caching options) thoughtfully in script.
- Leverage asynchronous patterns with cfthread equivalents where needed, wrapped in robust try/catch.
Validation Steps for Production Cutover
- Dry-run Deployment to a staging environment identical to production.
- Run a full regression test and smoke test critical transactions.
- Monitor logs and application metrics for 24–48 hours post-release.
- Keep a rollback plan: branch/tag in VCS and database rollback capability.
- Communicate changes to the team; share a quick-reference guide of new CFScript conventions adopted.
FAQ
What is the fastest way to start converting without breaking everything?
Begin by migrating Application.cfm to a CFScript-based Application.cfc, then convert small, low-risk templates. Wrap complex tags (cfpdf, cfdocument) in helper CFCs or use tag islands temporarily. Use automated tests and linters to catch regressions early.
Do I have to replace every tag like cfhttp or cfmail right away?
No. Prioritize core logic (cfset, cfif, cfquery, cfloop) first. For cfhttp/cfmail, engines offer scriptable service objects, but you can keep tag islands during transition. Plan a second pass to replace them with clean script.
How should I handle cfqueryparam when moving to queryExecute?
Use either positional parameters or named parameters with explicit cfsqltype. Example for named params:
q = queryExecute(“SELECT * FROM t WHERE id = :id”, { id={ value=url.id, cfsqltype=”cf_sql_integer” } }, { datasource=”MyDSN” });
Are there differences between Adobe ColdFusion and Lucee I should worry about?
Yes. Tag island support, some service object APIs, null handling, and certain script-only features vary. Test on your target engine/version, and abstract engine-specific code behind small adapter functions or components.
