Skip to main content
V2 data forms support JavaScript scripting through a sandboxed QuickJS runtime. Scripts can appear in three places: inline expressions within bindings, named functions in the scripts registry, and fully-described scriptModules with metadata. All script execution happens within a secure sandbox with configurable time limits and explicit permission controls. This guide covers how to write scripts, where to place them, how the QuickJS sandbox works, and how to debug scripts at runtime. For the full list of bridge methods available to scripts, see the Bridge API reference. Understanding the scripting model is key to building dynamic V2 forms. While simple forms can rely entirely on props and basic bindings, scripts unlock computed values, validation logic, conditional formatting, and custom event handling.

Inline Expressions in Bindings

The simplest form of scripting is an inline expression in a bindings entry. Each binding value is a JavaScript expression evaluated against the data context (ctx).
{
  "component": "card:label",
  "bindings": {
    "label": "ctx.dataObjects?.[0]?.path ?? 'No data'",
    "visible": "ctx.dataObjects?.length > 0"
  }
}
Binding expressions have access to the ctx variable, which contains the current data context. In a loop (for), the context also includes $item, $index, $parent, and $root.
Binding expressions should be pure and side-effect-free. They are re-evaluated whenever the data context changes. If you need to perform side effects (API calls, data mutations), use event handlers instead.

Expression Syntax

Expressions are standard JavaScript. They are wrapped in an immediately-invoked function by the runtime:
// Your binding expression:
ctx.dataObjects?.[0]?.path ?? 'No data'

// What the runtime evaluates:
(function() { const ctx = {...}; return (ctx.dataObjects?.[0]?.path ?? 'No data'); })()
You can use ternary operators, optional chaining, nullish coalescing, array methods, and template literals:
{
  "bindings": {
    "label": "`Total: ${ctx.dataObjects?.filter(o => o.path === 'lineItem').length ?? 0} items`",
    "backgroundColor": "ctx.$item?.status === 'error' ? '#fee2e2' : '#ffffff'"
  }
}

Named Scripts in the Scripts Registry

For reusable logic, register named scripts in the top-level scripts object. Each entry is a function that receives a context parameter and returns a value.
{
  "version": "2",
  "scripts": {
    "formatCurrency": "function(ctx) { const val = ctx.value; return val != null ? '$' + Number(val).toFixed(2) : '--'; }",
    "isOverdue": "function(ctx) { const due = new Date(ctx.dueDate); return due < new Date(); }",
    "itemCount": "function(ctx) { return ctx.dataObjects?.filter(o => o.path === 'lineItem').length ?? 0; }"
  },
  "nodes": [ ... ]
}

Referencing Named Scripts

Named scripts can be used in two ways: 1. In computed bindings: The computed object on a UINode maps prop names to script names. The script is called with the current data context and its return value becomes the prop value.
{
  "component": "card:label",
  "computed": {
    "label": "itemCount"
  }
}
2. In scriptRef event handlers: Use the scriptRef event type to call a named script when an event fires.
{
  "events": {
    "change": {
      "type": "scriptRef",
      "target": "validateAndSave"
    }
  }
}

ScriptModule with Metadata

For scripts that need documentation, type information, or debounce behavior, use scriptModules instead of the flat scripts registry. A ScriptModule wraps a script function with additional metadata.
{
  "scriptModules": {
    "computeSubtotal": {
      "source": "function(ctx) { return ctx.items.reduce((sum, item) => sum + (item.amount || 0), 0); }",
      "description": "Sums the amount attribute across all child line items",
      "inputs": {
        "items": "DataObject[] - line item data objects with an amount attribute"
      },
      "returns": "number - the total amount",
      "debounce": 200
    }
  }
}
FieldTypeDescription
sourcestringThe JavaScript function body
descriptionstringHuman-readable description
inputsRecord<string, string>Expected input types (documentation only, not enforced)
returnsstringReturn type description
debouncenumberDefault debounce in milliseconds
Script modules are referenced the same way as named scripts — through computed bindings or scriptRef event handlers.

QuickJS Sandbox

V2 form scripts run inside a QuickJS WebAssembly sandbox, not in the browser’s main JavaScript engine. This provides several security guarantees:

Security Model

  • No DOM access: Scripts cannot access document, window, or any browser APIs.
  • No network access: Scripts cannot make fetch or XMLHttpRequest calls directly. Use kodexa.http bridge methods instead.
  • No file system: Scripts cannot read or write files.
  • No eval or Function: Dynamic code generation is not available inside the sandbox.
  • Execution time limit: Each script evaluation is interrupted if it exceeds maxExecutionMs (default: 1000ms). Configure this in the bridge section.

What Is Available

Inside the QuickJS sandbox, scripts have access to:
  • Standard JavaScript: Variables, functions, operators, ternary, optional chaining, destructuring, template literals
  • ctx object: The data context with dataObjects, tagMetadataMap, and loop variables
  • kodexa.* bridge: The bridge API (when called from event handlers). See Bridge API
  • JSON: JSON.parse() and JSON.stringify()
  • Math: The full Math object
  • Array/String methods: All standard built-in methods

Execution Time Limits

The sandbox uses an interrupt handler that checks elapsed time between operations. If a script exceeds the configured limit, it is terminated and throws an error.
{
  "bridge": {
    "maxExecutionMs": 2000
  }
}
The default limit is 1000ms. For forms with heavy computation (large array processing, complex aggregations), consider increasing this value. However, long-running scripts will block the UI, so optimize your logic where possible.

Debugging Scripts

Using kodexa.log

The kodexa.log namespace provides logging methods that output to the browser’s developer console with a [DataFormV2] prefix:
{
  "events": {
    "change": {
      "type": "script",
      "target": "kodexa.log.debug('Value changed:', ctx.$item?.uuid, ctx.newValue)"
    }
  }
}
Three log levels are available:
MethodConsole MethodWhen to Use
kodexa.log.debug(...)console.debugDevelopment-time tracing
kodexa.log.warn(...)console.warnUnexpected but recoverable situations
kodexa.log.error(...)console.errorErrors that should be investigated

Common Script Errors

Script error: undefined is not a function This usually means you are trying to call a method that is not available in the QuickJS sandbox (e.g., fetch, setTimeout). Script error: interrupted The script exceeded maxExecutionMs. Look for infinite loops or expensive operations. Script “myScript” not found The scriptRef target does not match any key in scripts or scriptModules. Check for typos. Permission denied: data:write The script tried to call a bridge method that requires a permission not listed in bridge.permissions. Add the required permission to the form’s bridge configuration.

Common Patterns

Computed Display Values

{
  "scripts": {
    "formatDate": "function(ctx) { if (!ctx.value) return '--'; const d = new Date(ctx.value); return d.toLocaleDateString('en-US'); }",
    "statusBadge": "function(ctx) { const s = ctx.status; return s === 'approved' ? 'Approved' : s === 'pending' ? 'Pending Review' : 'Unknown'; }"
  }
}

Conditional Styling

{
  "component": "card:cardPanel",
  "bindings": {
    "backgroundColor": "ctx.$item?.hasExceptions ? '#fef2f2' : '#ffffff'"
  }
}

Aggregation Across Objects

{
  "scripts": {
    "grandTotal": "function(ctx) { return ctx.dataObjects?.filter(o => o.path === 'lineItem').reduce((sum, o) => { const amt = o.attributes?.find(a => a.path === 'lineItem/amount'); return sum + (Number(amt?.value) || 0); }, 0) ?? 0; }"
  }
}

Next Steps