Skip to content

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

POST /v4/system/component/{ref}/function/{name}

Request body:

{
  "data": {
    "items": [
      { "name": "Widget", "price": 10, "quantity": 3 },
      { "name": "Gadget", "price": 25, "quantity": 1 }
    ]
  }
}

Response:

{
  "total": 55,
  "item_count": 2
}

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:

{
  "response": { "total": 55 },
  "logs": ["Processing 2 items...", "Total calculated: 55"]
}

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$input is validated against idl.functions[name].input before 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].output after 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:

debug.add({ step: "validation", valid: true })
debug.add({ step: "calculation", total: 55 })

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:

throw new Error("Insufficient inventory")

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:

POST /v4/system/component/orders/function/shipping_tracker.get_status
{
  "data": { "tracking_number": "1234567890" }
}
import { Component } from "@ptkl/sdk"

const orders = new Component("orders")
await orders.function("shipping_tracker.get_status", {
  tracking_number: "1234567890"
})

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:

  1. 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.
  2. If the name contains a dot (.) — the platform looks for a matching extension function using extensionName.functionName
  3. Otherwise — it searches the component's own functions array

Use $.functionName when you don't care which extension provides the function:

const orders = new Component("orders")
await orders.function("$.calculate", { amount: 100 })
POST /v4/system/component/orders/function/$.calculate

Use extensionName.functionName when you need to target a specific extension:

await orders.function("billing.calculate", { amount: 100 })

Permissions

Function execution requires the user to have one of:

  • functions — general permission to execute any function on the component
  • function::{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.