๐Ÿ”ท Core

boxlang-runtime-google-cloud-functions

Use this skill when building, testing, or deploying BoxLang applications on Google Cloud Functions Gen 2, including the handlers/ routing convention, manifest.json, FunctionRunner entry point, environment variables, local development with the GCF invoker, and the boxlang-starter-google-functions project.

$ npx skills add ortus-boxlang/skills/boxlang-developer/runtime-google-cloud-functions
$ coldbox ai skills install ortus-boxlang/skills/boxlang-developer/runtime-google-cloud-functions
๐Ÿ”— https://skills.boxlang.io/skills/raw/ortus-boxlang/skills/boxlang-developer~runtime-google-cloud-functions

BoxLang on Google Cloud Functions

BoxLang is the AI-native software productivity platform for building, modernizing and running applications, with developers and AI agents working together.

Overview

BoxLang runs on Google Cloud Functions Gen 2 via the Java 21 runtime. The FunctionRunner bridge handles HTTP request mapping, handler routing, class caching, and response serialization - so you focus on writing BoxLang handler files.

Starter template: https://github.com/ortus-boxlang/boxlang-starter-google-functions

Runtime entry point:

ortus.boxlang.runtime.gcp.FunctionRunner

Architecture

HTTP Request
    โ†’ GCF Gen 2 (java21 runtime)
    โ†’ FunctionRunner (HttpFunction)
    โ†’ RequestMapper (HttpRequest โ†’ BoxLang event struct)
    โ†’ Route resolution (manifest.json / handlers/ scan โ†’ .bx file)
    โ†’ Handler compilation + class cache (warm invocations)
    โ†’ Method dispatch (run() or x-bx-function header)
    โ†’ BoxLang handler execution
    โ†’ ResponseMapper (response struct โ†’ HttpResponse)

Starter Project

git clone https://github.com/ortus-boxlang/boxlang-starter-google-functions.git
cd boxlang-starter-google-functions

# Verify environment
./gradlew clean test

Lambda.bx Convention

Lambda.bx at the project root is the default handler - it runs for / and for any URI that doesn't match a registered route:

// src/main/bx/Lambda.bx
class {

    function run( event, context, response ){
        response.statusCode = 200
        response.headers    = { "Content-Type": "application/json" }
        response.body       = jsonSerialize({
            message: "Hello from BoxLang!",
            input:   event
        })
    }

    // Alternative method (call with x-bx-function header)
    function anotherLambda( event, context, response ){
        return "Hola!!"
    }

}

Application Lifecycle (Application.bx)

Place Application.bx next to Lambda.bx at the project root for lifecycle hooks. It fires for every request, whether it's served by Lambda.bx or by a routed handler under handlers/, and is never itself a URI-routing target.

class {
    this.name = "MyFunction"

    boolean function onApplicationStart(){
        application.db = createObject("component","DataService").init()
        return true
    }

    boolean function onRequestStart( required string targetPage ){
        return true
    }
}

Wrapping responses and handling errors

run() and every request lifecycle hook (onRequestStart, onRequestEnd, onError, onAbort) receive the same response struct as their last argument. The value a handler returns is stored in response.body before onRequestEnd runs, so a hook can wrap or replace it:

class {
    function onRequestEnd( target, event, context, response ){
        response.body = { ok: true, data: response.body }
    }

    function onError( exception, eventName, event, context, response ){
        response.body = { ok: false, error: exception.message }
        // response.statusCode = 404   // override the default 500
    }
}

Hook arguments: onRequestStart( target, event, context, response ), onRequestEnd( target, event, context, response ), onError( exception, eventName, event, context, response ), onAbort( target, event, context, response ). onApplicationStart and the session hooks are fired by BoxLang itself and receive no request data.

Rules to remember:

  • onRequestEnd runs before onError. On a failure onRequestEnd wraps the empty body first, then onError overwrites it, so onError has the last word.
  • A handled error defaults the status to 500 unless onError sets response.statusCode. It used to be 200.
  • If Application.bx defines onError, the error counts as handled whatever the hook returns. To fail the invocation, rethrow from onError or do not define it.
  • Hooks that do not declare the extra response argument keep working.

URI Routing with handlers/

Every request goes to Lambda.bx by default. To route requests to a different class based on the URL path, add it under src/main/bx/handlers/ instead of the project root:

// src/main/bx/handlers/Products.bx
class {
    function run( event, context, response ){
        response.statusCode = 200
        response.body = {
            "error": false,
            "data": [ "Product A", "Product B" ]
        }
    }
}
Incoming URIHandler File
/productshandlers/Products.bx
/api/testhandlers/api/Test.bx (nested)
/user-profileshandlers/UserProfiles.bx (hyphens map to PascalCase)
/ or anything unmatchedLambda.bx (the default handler)

Folders can be nested and use any case you like - only the leaf .bx filename needs to be PascalCase. Only files under handlers/ are ever routable. Application.bx, Lambda.bx, and anything else at the project root can never be reached this way, no matter what path or x-bx-function header a client sends.

manifest.json: the routing table

./gradlew generateManifest scans handlers/ and writes src/main/bx/manifest.json - the build-time routing table the runtime reads once at cold start. It's wired via dependsOn into test, runFunction, and buildLambdaZip, so it can never silently drift. It's gitignored - never hand-edited or committed.

{
	"manifestVersion": 1,
	"defaultHandler": { "file": "Lambda.bx", "method": "run" },
	"handlers": {
		"products": { "file": "handlers/Products.bx" },
		"api/test": { "file": "handlers/api/Test.bx" }
	},
	"reserved": ["Application.bx", "Lambda.bx"]
}

The runtime enforces reserved and defaultHandler, not just documents them: a manifest can never route to a reserved file, and defaultHandler.file/ method is honored as the fallback handler when present.

If manifest.json is missing or invalid, the runtime falls back to scanning handlers/ directly, and if that directory doesn't exist either, to scanning the function root for backward compatibility with pre-handlers/ deployments - gated behind BOXLANG_ENABLE_ROOT_SCAN (default true; set to false to restrict that last-resort scenario to the default handler only).


Calling Specific Methods (Header Routing)

Use the x-bx-function header to call a specific method on the resolved handler:

curl http://localhost:9099/products                                  # run()
curl -H "x-bx-function: createOrder" -X POST http://localhost:9099/products \
    -H "Content-Type: application/json" \
    -d '{"productId": 1, "qty": 2}'

Only a public/remote method you declared is reachable this way.


Environment Variables

VariableDescription
BOXLANG_GCP_ROOTRoot directory for .bx files. Default: /workspace
BOXLANG_GCP_CLASSOverride the default handler path
BOXLANG_GCP_DEBUGMODEEnable verbose logging and disable class caching
BOXLANG_GCP_CONFIGPath to a custom boxlang.json configuration
BOXLANG_ENABLE_ROOT_SCANAllow the legacy root-directory routing fallback (see URI Routing above). Default: true. Shared across every BoxLang serverless runtime (AWS/GCP/Azure).
K_SERVICE / K_REVISION / GOOGLE_CLOUD_PROJECTSet automatically by GCF

Local Development

./gradlew runFunction                        # start local server (port 9099)
./gradlew runFunction -PtestPort=8080         # custom port
./gradlew runFunction -PdebugMode=true        # disable class cache for hot reload
curl http://localhost:9099/
curl -X POST http://localhost:9099/products -H "Content-Type: application/json" -d '{"qty":2}'
curl -H "x-bx-function: anotherLambda" http://localhost:9099/

Cold Start, Warm Start, Debug Mode

ModeBehavior
Cold startRuntime initializes; first request is slower
Warm invocationCompiled handler class reused - fast
Debug mode ONClass cache disabled; edits picked up immediately
Debug mode OFF (production)Maximum performance via class caching

Deploying to GCP

gcloud auth login
gcloud config set project YOUR_PROJECT_ID

./gradlew clean shadowJar buildLambdaZip

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.


Portability with AWS Lambda and Azure Functions

Handler patterns are intentionally identical across all three BoxLang serverless runtimes - the same handlers/ convention, manifest.json schema, run(event, context, response) contract, and x-bx-function header dispatch. .bx code moves between providers unmodified.

FeatureGCFAWS LambdaAzure Functions
Runner classFunctionRunnerLambdaRunnerAzureFunctionRunner
Handler methodrun()run()run()
Method routing headerx-bx-functionx-bx-functionx-bx-function
Debug mode env varBOXLANG_GCP_DEBUGMODEBOXLANG_LAMBDA_DEBUGMODEBOXLANG_AZURE_DEBUGMODE
Root-scan opt-outBOXLANG_ENABLE_ROOT_SCANBOXLANG_ENABLE_ROOT_SCANBOXLANG_ENABLE_ROOT_SCAN

Production Checklist

  •  BOXLANG_GCP_DEBUGMODE=false in production
  •  Errors are shaped in onError (set response.statusCode when 500 is not right); remember onError always counts as handled
  •  Routing convention adopted: handlers live under handlers/, manifest.json regenerated via generateManifest (wired into buildLambdaZip)
  •  BOXLANG_ENABLE_ROOT_SCAN=false once you've fully migrated to handlers/
  •  Secrets injected as environment variables (not in code)
  •  Memory: 512MB minimum; 1GB+ for database-heavy functions
  •  Integration tests passing: ./gradlew clean test
  •  Deployment ZIP built fresh before each deploy: ./gradlew shadowJar buildLambdaZip