Blog

Luis Majano

April 09, 2026

Spread the word


Share your thoughts

We just shipped the BoxLang Google Cloud Functions Runtime โ€” and it brings the same write-once-run-anywhere serverless experience you already know from our AWS Lambda runtime, now running natively on Google Cloud Functions Gen2.

The big idea is simple: the same .bx handler file you deploy to AWS Lambda works on GCF without modification. One codebase. Two clouds. Zero rewrites.

The Architecture

The GCF experience is intentionally split into two projects:

Your business logic lives entirely in BoxLang files under src/main/bx. Java is only the serverless bridge โ€” you never touch it.

The runtime entry point is ortus.boxlang.runtime.gcp.FunctionRunner, which implements Google Cloud's HttpFunction interface. On cold start the BoxLang runtime initializes once. Compiled .bx classes are cached internally and reused across all warm invocations โ€” you pay the compilation cost exactly once.

Documentation

Of course, everything is fully documented: https://boxlang.ortusbooks.com/getting-started/running-boxlang/google-cloud-functions

FIU Collaboration

This runtime was built in collaboration with Florida International University (FIU) and a talented team of students who contributed to making BoxLang on Google Cloud Functions a reality. We are incredibly grateful for their hard work and dedication to the BoxLang ecosystem. ๐ŸŽ“


Writing Handlers

Handlers are plain BoxLang classes that expose a run() method:

class {

    function run( event, context, response ) {
        response.statusCode = 200
        response.body = {
            "error"    : false,
            "messages" : [],
            "data"     : "Hello from BoxLang on GCF!"
        }
    }

}

Every handler method receives three arguments:

ArgumentTypeDescription
eventStructAll incoming request data โ€” method, path, headers, body, query
contextStructGCF metadata โ€” function name, revision, project ID, request ID
responseStructMutable response struct โ€” statusCode, headers, body, cookies

Returning a struct or array from your method JSON-serializes it automatically. Returning a plain string writes it verbatim. Or set response.body directly for explicit control.

A single handler class can expose multiple methods:

class {

    function run( event, context, response ) {
        response.statusCode = 200
        response.body = { "resource": "customers", "path": event.path }
    }

    function findById( event, context, response ) {
        response.statusCode = 200
        response.body = { "action": "findById", "path": event.path }
    }

}

Two Layers of Routing

The runtime gives you two independent dispatch layers, which together let you build clean multi-resource APIs from a single function deployment.

Layer 1 โ€” URI โ†’ Handler Class

The first URI path segment is converted to PascalCase and matched against .bx files in your handler root. If no match is found, Lambda.bx is the fallback:

Request URIResolved Handler
/Lambda.bx
/customersCustomers.bx
/customers/123Customers.bx
/productsProducts.bx
/user-profilesUserProfiles.bx
/unknownLambda.bx

To add a route, just drop a PascalCase .bx file in src/main/bx. No config. No routing tables. Convention does the work.

src/main/bx/
  Application.bx
  Lambda.bx         โ† fallback + root route
  Customers.bx      โ† handles /customers/**
  Products.bx       โ† handles /products/**
  UserProfiles.bx   โ† handles /user-profiles/**

Layer 2 โ€” x-bx-function โ†’ Method

Once the handler class is resolved, you can route to any method in it using the x-bx-function request header. Without the header, run() is called by default:

# Calls Lambda.bx::run()
curl http://localhost:9099/

# Calls Lambda.bx::anotherLambda()
curl -H "x-bx-function: anotherLambda" http://localhost:9099/

# Calls Customers.bx::findById()
curl -H "x-bx-function: findById" http://localhost:9099/customers/123

URI path selects the class. x-bx-function selects the method. Two clean axes of dispatch.


The Event Struct โ€” Portable By Design ๐ŸŽฏ

This is the part worth paying attention to. The event struct your handler receives mirrors the AWS API Gateway v2.0 HTTP event format โ€” intentionally:

{
    method                : "GET",
    path                  : "/products/42",
    rawPath               : "/products/42",
    headers               : { "content-type": "application/json" },
    queryStringParameters : { page: "1" },
    body                  : "",
    requestContext        : {
        http : {
            method : "GET",
            path   : "/products/42"
        }
    }
}

If you're already running BoxLang on AWS Lambda, your handlers drop into GCF with zero changes. Same struct shape. Same handler signature. Same return conventions.


Run Locally in Seconds

Clone the starter and you're running in three commands:

git clone https://github.com/ortus-boxlang/boxlang-starter-google-functions.git
cd boxlang-starter-google-functions
./gradlew clean test
./gradlew runFunction

Expected output:

================================================================
 BoxLang GCF Function Invoker
 Listening on  : http://localhost:9099
 Function root : .../src/main/bx
 Debug mode    : true
 Press Ctrl+C to stop.
================================================================

The runFunction task uses the official Google Functions Java Invoker under the hood and wires it to FunctionRunner automatically. Local overrides are available as Gradle flags:

./gradlew runFunction -PtestPort=8080       # custom port
./gradlew runFunction -PdebugMode=true      # verbose logging + live reload
./gradlew runFunction -PfunctionRoot=/path  # custom handler directory

Enable debugMode and .bx file changes are picked up live โ€” no JAR rebuild, no restart. Never enable debug mode in production โ€” it disables class caching.

Smoke Testing Locally

# Default handler
curl http://localhost:9099/

# POST with JSON body
curl -X POST http://localhost:9099/ \
  -H "Content-Type: application/json" \
  -d @workbench/sampleRequests/event-local.json

# Method routing
curl -H "x-bx-function: anotherLambda" http://localhost:9099/

Testing

The starter ships with a full integration test suite using JUnit and Google Truth. Tests run against real GCF mock HTTP objects โ€” no mocking frameworks:

Test ClassCoverage
FunctionRunnerTestRequest lifecycle, URI routing, method routing, JSON responses, concurrency
MockHttpRequestFluent test request builder
MockHttpResponseCaptures status, body, and headers
./gradlew test

# Run just the integration suite
./gradlew test --tests "com.myproject.FunctionRunnerTest"

# Open the HTML report
open build/reports/tests/test/index.html

Build and Deploy

Build the Deployable Package

./gradlew clean shadowJar buildLambdaZip

This produces a deployable ZIP at:

build/distributions/boxlang-google-function-project-<version>.zip

The ZIP contains everything GCF needs โ€” .bx handlers at the package root, boxlang.json, boxlang_modules/, and the runtime JAR with all dependencies in lib/.

Deploy to Google Cloud Functions Gen2

gcloud auth login
gcloud config set project YOUR_PROJECT_ID

gcloud functions deploy YOUR_FUNCTION_NAME \
  --gen2 \
  --runtime=java21 \
  --region=us-central1 \
  --entry-point=ortus.boxlang.runtime.gcp.FunctionRunner \
  --trigger-http \
  --allow-unauthenticated \
  --source=build/distributions/boxlang-google-function-project-1.0.0.zip

If you changed version in gradle.properties, update the ZIP filename in --source accordingly.


Environment Variables

VariableDescription
BOXLANG_GCP_ROOTRoot directory for .bx handlers
BOXLANG_GCP_CLASSOverride default handler path
BOXLANG_GCP_DEBUGMODEEnable verbose logging and disable class caching
BOXLANG_GCP_CONFIGCustom boxlang.json path
K_SERVICEFunction name (set automatically by GCF Gen2)
K_REVISIONFunction revision (set automatically by GCF Gen2)
GOOGLE_CLOUD_PROJECTProject ID (set automatically by GCF Gen2)

BoxLang Modules Work Too

Any BoxLang module works with this runtime. Include it in your deployment ZIP under boxlang_modules/ and point BOXLANG_GCP_CONFIG at your boxlang.json. The entire module ecosystem โ€” database, caching, AI, and more โ€” is available inside your serverless functions.


Get Started

๐Ÿš€ Starter Template: github.com/ortus-boxlang/boxlang-starter-google-functions

๐Ÿ“ฆ Runtime Source: github.com/ortus-boxlang/boxlang-google-functions

๐Ÿ“š BoxLang Docs: boxlang.ortusbooks.com


BoxLang is built to run everywhere โ€” web servers, AWS Lambda, Google Cloud Functions, WebAssembly, mobile, and more. Every new runtime we ship gets you closer to a world where your application logic outlives any single cloud vendor's pricing model.

Write once. Run anywhere. Ship faster. โšก

Add Your Comment

Recent Entries

Introducing bx-jwt: Enterprise-Grade JSON Web Tokens for BoxLang ๐Ÿ”

Introducing bx-jwt: Enterprise-Grade JSON Web Tokens for BoxLang ๐Ÿ”

JWT authentication is everywhere. But rolling it correctly โ€” with proper algorithm enforcement, key management, clock skew handling, JWE encryption, and zero security footguns โ€” is anything but trivial. Today, we're shipping bx-jwt, a production-ready JWT/JWE module for BoxLang that handles all of it out of the box, so you can focus on building, not fighting cryptography.

Luis Majano
Luis Majano
May 22, 2026
What โ€œModernize or Dieโ€ Really Means in 2026

What โ€œModernize or Dieโ€ Really Means in 2026

โ€œModernize or Dieโ€ is not about forcing teams into MVC, chasing trends, or rewriting every CFML application from scratch. It means making sure your applications, teams, and processes can survive the future: easier to maintain, test, secure, deploy, document, hire for, and evolve. In 2026, modernization is less about adopting the newest pattern and more about reducing business risk, protecting the value already built into your systems, and ensuring CFML applications remain credible, sustai...

Cristobal Escobar
Cristobal Escobar
May 22, 2026