Migration - Upgrades

How to Migrate ColdFusion Code to CFScript Syntax

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

See also  How to Use Version Control for Safe Migration

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




#greeting#, user ##url.id#

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;
}
}

Best practices:

  • 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).

9) Replace integration tags with scriptable APIs

  • 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.
See also  How to Handle Deprecated Tags During ColdFusion Upgrade

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.
See also  How to Move ColdFusion from Windows to Linux Servers

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 =





#profile.name# (#qUser.email#)

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.

About the author

Aaron Longnion

Aaron Longnion

Hey there! I'm Aaron Longnion — an Internet technologist, web software engineer, and ColdFusion expert with more than 24 years of experience. Over the years, I've had the privilege of working with some of the most exciting and fast-growing companies out there, including lynda.com, HomeAway, landsofamerica.com (CoStar Group), and Adobe.com.

I'm a full-stack developer at heart, but what really drives me is designing and building internet architectures that are highly scalable, cost-effective, and fault-tolerant — solutions built to handle rapid growth and stay ahead of the curve.