Component Functions
Component functions are custom JavaScript expressions that run in a secure serverless sandbox. They allow you to add logic to any component — data transformations, external API calls, calculations, or orchestrating operations across the platform.
Overview
Each component can have one or more functions defined in its settings. A function consists of a name and a JavaScript expression that gets executed on demand. Functions run with full access to the platform's runtime libraries, enabling you to interact with other components, call external APIs, trigger workflows, and more.
Key characteristics:
- Functions run in a secure sandbox with full async/await support
- Can call other functions on the same component or standalone functions
- Extension functions are namespaced with dot notation to avoid collisions
Function Structure
A function is defined with two properties:
| Property | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Unique identifier within the component |
expr |
string |
Yes | JavaScript expression to execute |
Example Definition
{
"functions": [
{
"name": "calculate_total",
"expr": "const items = $input.items || []; const total = items.reduce((sum, i) => sum + (i.price * i.quantity), 0); return { total, item_count: items.length }"
},
{
"name": "enrich_customer",
"expr": "const { Component } = $sdk.version('0.9'); const customer = await new Component('customers').get($input.customer_id); return { name: customer.data.name, email: customer.data.email }"
}
]
}
Calling Functions
API
Request body:
{
"data": {
"items": [
{ "name": "Widget", "price": 10, "quantity": 3 },
{ "name": "Gadget", "price": 25, "quantity": 1 }
]
}
}
Response:
The function's return value is sent directly as the response body.
Options
You can pass additional options alongside the input data:
{
"data": { "order_id": "ORD-001" },
"options": {
"expression": "return { override: true }",
"config": { "custom_key": "value" },
"include_logs": true
}
}
| Option | Type | Description |
|---|---|---|
expression |
string |
Override the stored expression with a custom one for this execution |
config |
object |
Override the component config for this execution |
include_logs |
boolean |
Include captured console.log output in the response |
When include_logs is true, the response wraps the result:
SDK
import { Component } from "@ptkl/sdk"
const orders = new Component("orders")
const result = await orders.function("calculate_total", {
items: [
{ name: "Widget", price: 10, quantity: 3 },
{ name: "Gadget", price: 25, quantity: 1 }
]
})
From Another Function
Within a function's runtime, you can call other functions on any component:
const { Component } = $sdk.version('0.9')
const orders = new Component("orders")
const result = await orders.function("calculate_total", {
items: $input.items
})
Functions defined on the same component are also available as shared functions — you can call them directly by name without going through the SDK:
// If the component has functions "calculate_total" and "apply_discount",
// each can call the other directly by name:
const total = await calculate_total($input)
const discounted = await apply_discount({ total, discount: 0.1 })
Execution Context
Every function receives a rich execution context with access to input data, environment configuration, and the platform SDK.
Input & Configuration
| Variable | Type | Description |
|---|---|---|
$input |
object |
The data payload passed to the function |
$config |
object |
The component's configuration for the current environment |
$env |
object |
Component-level environment variables |
$globalEnv |
object |
Project-level global environment variables |
$currentEnv |
string |
Current environment name (e.g. "dev", "live") |
$initiator |
object |
The user or service that invoked the function |
$sdk |
object |
Protokol SDK factory — see SDK below |
// Access the input data
const orderId = $input.order_id
// Read component config
const apiKey = $config.api_key
// Check environment
if ($currentEnv === "live") {
// production-only logic
}
// Access global env vars
const webhookSecret = $globalEnv.WEBHOOK_SECRET
SDK
The Protokol SDK is available as $sdk, a factory class that returns a versioned SDK instance:
const { Component } = $sdk.version('0.9')
const products = new Component("products")
const { models } = await products.find({ $adv: { status: "active" } })
When comparing date fields in $adv, both plain date strings and $date casting work:
const { Component } = $sdk.version('0.9')
const orders = new Component("orders")
// Both forms work:
const { models } = await orders.find({
$adv: { updated_at: { $lt: "2025-01-01" } }
})
// or with explicit $date:
const { models } = await orders.find({
$adv: { updated_at: { $lt: { $date: "2025-01-01" } } }
})
In $aggregate pipelines, you must use $date:
const { models } = await orders.find({
$aggregate: [{ $match: { updated_at: { $gte: { $date: "2025-01-01T00:00:00Z" } } } }]
})
Note
The SDK inside functions uses the service context of the current project and environment. Authentication is handled automatically.
Utility Libraries
The following libraries are pre-loaded and available as globals:
| Global | Library | Description |
|---|---|---|
moment |
Moment.js | Date/time manipulation |
_ |
Lodash | Utility functions |
handlebars |
Handlebars | Template engine |
Buffer |
Node.js Buffer | Binary data operations |
// Group items by category
const grouped = _.groupBy($input.items, "category")
// Format a date
const formatted = moment($input.date).format("MMMM Do YYYY")
// Render a template
const template = handlebars.compile("Hello, {{name}}!")
const greeting = template({ name: $input.customer_name })
Security & Encoding
| Global | Description |
|---|---|
bcrypt |
Password hashing — genSalt, hash, compare |
crypto |
Cryptographic operations — hash, hmac, encrypt, decrypt, sign, verify |
jwt |
JSON Web Tokens — sign, verify, decode |
// Hash a password
const salt = bcrypt.genSalt(10)
const hashed = bcrypt.hash($input.password, salt)
// Create a JWT
const token = jwt.sign(
{ user_id: $input.user_id, role: "admin" },
$config.jwt_secret,
{ expiresIn: "1h" }
)
// SHA-256 hash
const checksum = crypto.hash(JSON.stringify($input.data))
IDL Validation
When a function has an IDL entry under idl.functions, the platform automatically validates both input and output:
- Input validation —
$inputis validated againstidl.functions[name].inputbefore the expression executes. If the caller passes data that doesn't match the declared input shape, the call is rejected immediately with a structured error — the expression never runs. - Output validation — the function's return value is validated against
idl.functions[name].outputafter execution. If the returned value doesn't match, the call fails with a validation error.
Validation is open (lenient) — extra fields are accepted, only missing required fields and type mismatches cause failures.
If a function has no IDL entry, validation is skipped entirely — the function behaves as before.
{
"error": "IDL_VALIDATION_FAILED",
"field": "items[0].sku",
"expected": "string",
"received": "number"
}
See IDL — Function Signatures for how to declare input and output types.
Debugging
console.log
All console.log() calls are captured and can be returned with the response when include_logs: true is set in the request options.
console.log("Processing order:", $input.order_id)
console.log("Items count:", $input.items.length)
return { processed: true }
debug
Add structured debug data that appears in execution logs:
Error Handling
RuntimeError
Throw a RuntimeError to return a structured error with additional data:
if (!$input.order_id) {
throw new RuntimeError("Missing order_id", {
field: "order_id",
code: "VALIDATION_ERROR"
})
}
RuntimeError supports chaining:
throw new RuntimeError("Payment failed")
.addData("provider", "stripe")
.addData("error_code", "card_declined")
Standard Errors
Throw a standard Error for simple failure cases:
Errors are returned with HTTP 500 and include the message, any attached data, and any output produced before the error.
Extension Functions
Functions defined within a component extension follow the same structure and have access to the same execution context. The key difference is naming — extension functions are namespaced with dot notation.
Calling Extension Functions
Extension functions are invoked using extensionName.functionName:
Extension functions are only available when the extension is active (is_active: true). If the extension is deactivated, its functions return a not-found error.
Resolution Order
When a function is called:
- If the name starts with
$.— the$prefix is stripped and the platform searches the component's own functions first, then all active extensions in install order. The last-installed active extension wins if multiple extensions define the same function name. - If the name contains a dot (
.) — the platform looks for a matching extension function usingextensionName.functionName - Otherwise — it searches the component's own functions array
Use $.functionName when you don't care which extension provides the function:
Use extensionName.functionName when you need to target a specific extension:
Permissions
Function execution requires the user to have one of:
functions— general permission to execute any function on the componentfunction::{name}— permission to execute a specific function by name
Execution Environment
| Property | Value |
|---|---|
| Memory limit | 128 MB |
| Timeout | 30 seconds |
| Async support | Full async/await |
| Call depth | Maximum 5 nested function calls |
Warning
Functions run in an isolated sandbox. Node.js system modules (e.g. fs, path, net) are not available. Use the SDK and provided libraries instead.