Ortus Solutions today announces the release of BoxLang 1.19.0, the software productivity platform for building, modernizing and running applications, with developers and AI agents working together. This release is about observability: built-in code coverage and execution profiling, the missing post-invocation events for BIFs, and new query and transaction events, and it pairs with the new BxLens beta, the debug bar and ops console that exposes hotspots and bytecode profilers. It also adds your own global settings in boxlang.json, static method interception, raw binary HTTP streaming and a lot of hardening across JDBC, Java interop and CFML compatibility.
45 issues landed: 6 new features, 18 improvements, 20 bugs and 1 task. π
Why observability, and why now
Developers and AI agents share one problem: they can only fix what they can see. A human wants to know which code ran and where the time went. An agent building and verifying an app needs the same signals, in a form it can consume. 1.19 gives the runtime much better answers to "what just happened?" and keeps the cost at zero when you are not asking.
π Code coverage and execution profiling
BoxLang can now record which code ran, how often, and where the time went while your code executes, with no changes to that code.
- Coverage. Per file, whether each executable span ran (covered or missed) and how many times.
- Timing. Accumulated self time per span, so hot code is easy to find.
- Fine grained. Branches, short-circuit operators, ternaries, function and closure bodies and class code are tracked independently, so you can see which branch actually ran.
- Aggregation. Covered, executable and total counts, and a coverage percentage, across files.
- Exports. Built-in export to JSON (per line or per span) and SonarQube generic coverage XML for CI, plus HTML reports for line coverage, spans and a span heatmap.
- Off by default. Nothing is instrumented unless you turn it on.
Capture is a single flag:
{
"codeProfilerEnabled": true
}
The collected data is exposed through the runtime's CodeProfilerService, which coverage reporters and CI exporters consume. BxLens uses the same engine for its Bytecode profiler, covered below.
π BIF interception points, fixed
postBIFInvocation was declared but never announced, and onBIFInvocation fired twice per call. BIF invocation now mirrors user-defined functions:
| Point | When | Data |
|---|---|---|
onBIFInvocation | Once, before the call | context, arguments, bif and name |
postBIFInvocation | After a successful call | The same data plus result and elapsedNanos. Listeners may override result |
onBIFException | New. When the BIF throws | exception and elapsedNanos. The exception is rethrown unchanged |
With no listeners registered there is no allocation and no clock read on the hot path. Interceptors announced without a context (BIF, query and transaction events) now also run on the current request when there is one, so they see its settings and no longer rebuild the runtime config on every read. See Life-cycle Events.
ποΈ Query and transaction observability
onQueryExecuteErroris announced when a query fails, withpendingQuery,context,exceptionand the elapsedexecutionTimein milliseconds. The original exception is always rethrown unchanged, even if a listener throws.- Cached results are visible. Results served from the query cache now announce
preQueryExecuteandpostQueryExecutewithcached=trueand anexecutionTimeof0.preQueryExecutenow includesdbtypeandcached. onTransactionReleaseSavepointjoins the transaction lifecycle events.- Better stored procedure logs. With
debug=true, logs show which variables and types were created from whichOUTparameter names.
Savepoint release
The new transactionReleaseSavepoint( name ) BIF and the releasesavepoint action of bx:transaction discard a savepoint you no longer need. Release is optional cleanup. It does not commit or roll back data, it frees driver resources in long transactions, and it lets you reuse savepoint names. Drivers that do not support release raise a DatabaseException.
transaction {
for ( var item in batch ) {
var recoveryPoint = "item_" & createUUID().replace( "-", "", "all" )
transactionSetSavepoint( recoveryPoint )
try {
importItem( item )
} catch ( any error ) {
transactionRollback( recoveryPoint )
recordImportFailure( item, error )
}
transactionReleaseSavepoint( recoveryPoint )
}
}
Connection pool and abort fixes
- JDBC pool leak fixed (BL-2763). Connections acquired inside a transaction for another datasource or with credential overrides, stored procedure calls that fail during setup, and connections still open when a request ends are now returned to the pool. A failure while closing one transaction or in one shutdown listener no longer stops the remaining cleanup.
- Transactions roll back on
abort. A database transaction is now rolled back whenabortruns inside it, matching Adobe ColdFusion and Lucee. - Clearer datasource names (BL-2770). On-the-fly datasources created by overriding credentials are now named
{datasource}_user_{username}_{hash}. - Nested database exceptions (BL-2743). A cause that was already set no longer makes the exception chain throw. It is stored as a suppressed exception and still shows in the stack trace.
π BxLens: the debug bar and ops console for BoxLang (beta)
The runtime now emits the signals. BxLens is how you see them. It is the debug bar and ops console for BoxLang, it needs BoxLang 1.18 or newer and Java 21, and it is currently in beta. Beyond the bar and the console, the beta exposes Hot spots and a Bytecode profiler, so you can go from "this request is slow" to "this is the exact expression that is slow." The Bytecode profiler is built on the code profiler that ships in BoxLang 1.19, and no other BoxLang tool shows the time of every line of your code like it.
BxLens has two surfaces, and both ship off by default:
- The bar is a strip docked on every HTML page. It shows what that one request did: timeline, queries, HTTP calls, exceptions, timers and scopes.
- The console is a password protected page for the whole server: requests, errors, queries, datasources, caches, logs, tasks, the JVM and threads.

The bar on a page: a waterfall of templates, queries and HTTP calls for one request.

The console overview: the health of the server at a glance.

Queries: the slowest and the most repeated statements.
What you get:
- A full waterfall for one request: templates, queries, HTTP calls and transactions, with repeated queries (N+1) and the line that ran them
- Errors grouped into issues you can resolve or ignore
- Outgoing HTTP calls by host and endpoint
- The JVM: memory, garbage collection and deadlock detection
- ColdBox debugging: events, views, routes, modules and findings
- Hot spots and the Bytecode profiler (BoxLang+) for finding the slow expression
- AI observability for agents, models, MCP servers, tokens and cost
- Lensy, the assistant that answers in plain words and asks before it changes anything (BoxLang+)
BxLens is freeware with limits. The debug bar and the basics of the console are free for one server, in production too. BoxLang+ lifts the limits and adds history, alerts, many servers, AI observability and Lensy.
π₯ Hot spots: where did the time go?
A slow request has a time, and the question is where it went. While a request runs, BxLens looks at what it is doing about ten times a second and shows the result as one picture, widest first. The widest bar is where to start.
- Your code is a template, function or class of your application, with the line.
- Database is the statement that was running, with its values hidden.
- Outgoing calls are the HTTP calls that were running, with host and path.
- Waiting is a lock, a sleep or a wait for another thread.
- BoxLang and server is the runtime and web server, hidden by default so their time goes to the code that called them.

Hot spots of one slow request: your code, the database, outgoing calls and waits, widest first.
You can open it three ways:
- A finished request: every request that runs longer than one second gets a Hot spots section
- A running request: pick it on the In flight page and watch the picture build live
- A URL: merge the pictures of every run of a URL, across every server that shares the store

The merged Hot spots of a URL, with the slowest runs underneath.
Rules let you look at more than the slow ones. Add a rule for a path and method, or press Watch this URL for 10 minutes, an hour or a day and the rule removes itself. You can also ask Lensy to read the picture, or share it by mail or to a chat channel.

It is a sample, not a measurement, so trust the wide bars. Hot spots is off until you switch it on, pauses itself when the server CPU is high, and never looks at /health* or the console itself. It is a BoxLang+ feature. Read Hot spots and the Find and fix a slow page guide, or watch the walkthrough.
𧬠Bytecode profiler: every expression, counted and timed
Hot spots tells you which function and line took the time. The Bytecode profiler tells you which expression inside it, how many times it ran, and how long each run took, and paints it on your own source. It uses the code profiler built into BoxLang 1.19.

A heatmap on your real source. The colour bar runs from red (most time) to blue (little), and hot expressions show how many times they ran.
- Sessions. Record the whole server for 5 to 60 minutes, started by hand. A session always ends by itself, and only one runs at a time.
- Rules. For a problem you cannot reproduce, add a rule: "when this request arrives, record 10 minutes."
- Two views. Files shows the templates and classes that ran with their share of the time. Expressions shows the widest expressions, how many times they ran, and the time per run.
- Jump from Hot spots. A row that is your own code has an Expressions button that opens the whole function with its heat.
- Ask Lensy or share. Lensy reads the files and widest expressions and says what to change first. Share a session by mail or to a channel.

A session: the files that ran and the time in your code.
Turn it on with the flag from the 1.19 profiling engine, set before the deploy so that templates compiled while it is on carry the counters, then switch on the collector in BxLens:
{
"codeProfilerEnabled": true
}
A few honest notes. Recording covers the whole server, so pages run slower while a session runs. The times include a small cost for every expression, so use them as a ranking and trust the counts. Templates precompiled with boxlang compile had no source to paint on in the BxLens tests. Nothing is recorded outside a session, and it needs BoxLang 1.19 or newer. It is a BoxLang+ feature. Read the Bytecode profiler docs or watch the walkthrough.
Turn the bar on in boxlang.json:
{
"modules": {
"bxLens": {
"settings": {
"bar": { "enabled": true }
}
}
}
}
Install it with the BoxLang binary or CommandBox:
# BoxLang binary
bx-module install bx-lens
# CommandBox
box install bx-lens
Watch the trailer:
More screenshots and nine narrated walkthroughs are in the BxLens gallery and walkthroughs. Want to try it on real problems? Run the BxLens demo app.
ποΈ Your own global settings: runtimeSettings
Store your own named values in boxlang.json and override them per application. BoxLang loads and serves the struct. It never interprets it.
{
"runtimeSettings": {
"apiUrl": "${Setting: env.API_URL:http: //localhost:8080 not found}",
"stripeKey": "bxsecret:AbC123...",
"features": { "beta": true, "regions": [ "us", "eu" ] }
}
}
- Any value type. Strings, numbers, booleans, structs and arrays.
- Environment variables and secrets.
${Setting: env.VAR not found},${Setting: env.VAR:default not found}andbxsecret:values resolve at any depth, including inside arrays. - Application overrides.
this.runtimeSettingsinApplication.bxis deep merged over the global values and the application wins by key. Arrays and scalars are replaced, not merged. The application layer re-resolves placeholders and secrets on every request. - Read only. Returned structs and arrays are deeply unmodifiable.
// Application.bx
class {
this.name = "myApp"
this.runtimeSettings = {
features : { level : 7 }
}
}
getRuntimeSetting( "features.level" ) // 7, from the application
getRuntimeSetting( "features.beta" ) // true, from boxlang.json
getRuntimeSetting( "features.regions.1" ) // "us", array indexes are 1-based
getRuntimeSetting( "timeout", 30 ) // default when missing
getRuntimeSetting() // the whole merged struct
A missing key with no default throws, the same convention as getSystemSetting(). Read the Runtime Settings guide.
π§© Static method interception: onMissingStaticMethod()
Classes have always had onMissingMethod() for instance calls. Static calls to a missing method used to throw a KeyNotFoundException. A class can now handle them, which enables dynamic static APIs such as User::findByEmail( "a@b.com" ).
class {
static function onMissingStaticMethod( string missingMethodName, struct missingMethodArguments ) {
return "called #missingMethodName#"
}
}
MyClass::anything( 1, 2 ) // "called anything"
- It runs only when the static method is missing, so normal static calls cost nothing extra.
- It works when declared in the class and when a module adds the function to a class's static scope at runtime.
- A handler can cache by putting a function into the static scope under the missing name, so later calls are normal static calls.
- It also works for classes that extend a Java class. A call goes to the Java parent only when the parent has a matching method.
Two related fixes: Foo::counter = 42 now assigns a static variable from outside the class, and static[ name ] = value inside a function is no longer parsed as an array destructuring declaration. See Static Constructs.
π‘ Raw binary HTTP streaming: onBinaryChunk
http().onChunk() decodes the body as text and splits it on newlines, which corrupts binary streams such as audio. The new onBinaryChunk callback receives raw bytes per network read.
http( url, "POST" )
.header( "Authorization", "Bearer #key#" )
.body( payload )
.timeout( 30 )
.onBinaryChunk( ( bytes, info ) => {
sendToClient( bytes )
return !clientGone // an explicit false stops the stream and closes the connection
} )
.onError( ( e, result ) => handleError( e ) )
.send()
infocarrieschunkNumber,totalBytes,headers(first chunk only),resultandhttpClient.- Stop on
false. Only an explicitfalsestops the stream. The response closes immediately and the result is flagged as not completed, not as an error. - Idle timeout for streaming. In binary and SSE modes
timeoutbecomes the longest wait for headers or between received bytes. A stalled stream is reported as a timeout error (408). - Errors. In binary and SSE modes a non-2xx status skips the chunk callback and is reported as
HTTP <status>: <body>throughonErrorand the result struct. - SSE gets the same treatment, including stop-on-false, idle timeout and
Content-Encodingdecoding.
Also available as an attribute on bx:http, and fully backward compatible. See HTTP Calls and Server-Sent Events.
π generateKeyPair()
A new BIF next to generateSecretKey() for asymmetric key pairs, with no Java interop needed.
pair = generateKeyPair( "Ed25519" )
pair.publicPem
pair.privatePem
- Algorithms:
Ed25519,Ed448,X25519,X448,RSA(2048, 3072, 4096) andEC(secp256r1,secp384r1,secp521r1). - Returns a struct with
algorithm,privateKey(base64 PKCS8 DER),publicKey(base64 X.509 SPKI DER),privatePemandpublicPem. - Randomness comes from the JDK
SecureRandom, supplemented with the runtime's global secret seed.
β Extending Java classes
A BoxLang class that uses extends="java:..." is now much more capable:
- Call super-class methods that need argument coercion, such as
add()on a class extendingjava.util.ArrayList(BL-2771) - Override protected Java methods (BL-2772)
- Extend Java classes supplied by a module or a runtime JAR (BL-2773)
β‘ Performance
- Class resolution no longer lists every directory on the path for each lookup miss. On case-sensitive file systems the cost grew with the number of class files, and it is no longer quadratic (BL-2747).
replace()andreplaceNoCase()were refactored to avoid allocating substrings (BL-2756).- Case-insensitive searches compare in place instead of lower-casing whole strings. That speeds up
findNoCase(),replaceNoCase(), theCONTAINSoperator,stringStartsWithNoCase()andstringEndsWithNoCase()(BL-2765). - Date compares no longer rebuild the whole runtime config struct (BL-2758).
- Debug mode caches source snippets for exception contexts in a new
bxCodeSnippetscache, so repeated errors no longer re-read source files (BL-2764).
π οΈ Language, tooling and CFML compatibility
print()andprintln()accept any number of arguments.print( "Age:", 21 )outputsAge: 21.duplicate()handles JavaList,MapandSetinstances directly, honorsdeep=false, and is faster.- Java class dumps now show public instance and static methods in separate sections.
systemCacheClear( "classfiles" )clears compiled class files cached on disk.- BigDecimal and BigInteger casters accept strings with leading whitespace such as
" 1". cftranspile: docblock@returnno longer becomes an invalid annotation, script components without abx:prefix now compile, tag-form<cffunction>in.cfmtemplates is emitted asbx:functioninstead of being silently dropped, and the Lucee argument aliases ofinvoke()are mapped to BoxLang's.- Fixed: a
VerifyErroron scripts withimportplustry/catch, a parser error withnot listFindNoCase()in a struct literal,return +expr;,finallyskipped whenabortran inside atrywith acatch, andcf_custom tags losing theirtemplateandnameattributes. - Platform: environment variables are now imported on Windows with
boxlang.bat, and MiniServergetRequestBody()no longer returns an empty string when the body arrives after the headers. - Modules: bx-orm fixes for
generator="identity"on PostgreSQL reserved-word tables and forfieldtype="collection", and bx-image now rejects oversized images from the header before decoding them.
π Security
- Jackson updated to 2.21.7, resolving CVE-2026-89425
- Mitigation for CVE-2026-102554. The core now depends on
javaparser-coreonly, so Guava, which BoxLang never used, is no longer bundled - jsoup updated to 1.23.2 (CVE-2026-75140)
β‘ Migration notes
Read these before upgrading:
- Thread scope and unscoped lookup (BL-1792). Inside a
threadbody, an unscoped name no longer falls back to the thread's metadata struct.foono longer resolves afterthread.foo = "bar", so usethread.foo. An unscoped assignment such asstatus = "x"now goes into the local scope. abortinside a transaction. The open transaction is now rolled back, so writes made before theabortare no longer committed.onBIFInvocation. It now fires once per call, not twice. If you worked around the duplicate, remove the workaround.duplicate()of Java collections. Shallow duplicates of JavaList,MapandSetinstances are now really shallow. Before, they were always deep copies.- Date strings in comparisons. A date string compared inside a request is now read in the request timezone.
- Credential-override datasource names. On-the-fly datasources are named
{datasource}_user_{username}_{hash}instead ofonthefly_{hash}. - Java integrators. The
Compareand comparison operator methods without a context are deprecated in favor of the context-taking overloads.
Get started
Grab BoxLang 1.19.0 from boxlang.io and read the full release notes for every ticket.
Resources
- Release notes: BoxLang 1.19.0
- BoxLang: boxlang.io | BoxLang docs
- BxLens (beta): bxlens.boxlang.io | Gallery | Demo app
- Community: Ortus Community Slack
- Professional support and consulting: ortussolutions.com/services
Build it, run it, and now see exactly what it did.
Add Your Comment