Skip to content

Function Node

Overview

The Function node executes custom JavaScript code to transform data, perform calculations, or implement custom logic. This is ideal for data manipulation tasks that don’t require AI reasoning or external tool calls.

In tool workflows (kind: tool), the Function node is the preferred node for custom logic. Tool workflows compile to Step Functions Express and run synchronously, so Bash nodes are not allowed there — a Function node runs as a plain synchronous invoke that fits the tool call window. In kind: standard workflows either node works.

When to Use

Use a Function node when you need to:

  • Transform data formats (e.g., convert arrays, restructure objects)
  • Perform calculations or aggregations
  • Filter or map over collections
  • Format strings or dates
  • Validate data structure
  • Implement custom business logic
  • Parse or stringify JSON

Configuration

Code Editor

Code: Write JavaScript that will be executed

  • Full ES2021 JavaScript syntax support
  • Access to input data via payload variable
  • Return value becomes the node’s output
  • async/await is supported — the code runs inside an async function, and the injected hutly.* methods return promises you await

Available Variables

  • payload: The complete workflow execution context
    • payload.results.<nodeId> - Outputs from previous nodes
    • payload.trigger - Data from the workflow trigger
    • payload.intelliaContext - Workflow metadata
  • hutly: The injected backend API (vault, memory, HTTP, moment, askAI) — see The hutly API below
  • console: console.log / console.error for debugging (surfaced in the execution logs)
  • Buffer and standard ES2021 globals (JSON, Math, Date, Promise, Intl, …) are available. Node-only globals such as process, setTimeout, require, fetch, and URL are not — the code runs in a restricted sandbox.

Return Value

The function must return a value:

  • Return an object to create structured output
  • Return a primitive (string, number, boolean) for simple values
  • Return null or undefined for no output
  • Returned value is available at $.results.<nodeId>

The hutly API

Function nodes run with a hutly object in scope that exposes the backend capabilities the sandbox is allowed to use. All methods are async — await them. Nothing else reaches the backend (there is no fetch/require); this surface is the whole contract, and it’s checked at validation time (see Validation).

Member Signature Notes
hutly.vault.getItem(vaultItemId) Promise<VaultItem> Read a vault secret/item by id.
hutly.vault.getForApp(appName) Promise<VaultItem> Read the vault item bound to an app/integration.
hutly.memory.get(agentId, userId, scope, key) Promise<record> scope is "organization" or "user".
hutly.memory.set(agentId, userId, scope, key, value) Promise<record> Upsert a memory record.
hutly.memory.list(agentId, userId, scope, nextToken?) Promise<{ records, nextToken }> Paginated.
hutly.memory.delete(agentId, userId, scope, key) Promise<void>
hutly.http.get/post/put/delete/patch/request(...) Promise<AxiosResponse> Outbound HTTP (axios), 30s timeout. Read the body from res.data.
hutly.moment moment The moment-timezone factory — hutly.moment().tz("Australia/Sydney").format(...).
hutly.askAI(question, responseSchema, model?) Promise<object> One-shot LLM call returning JSON matching responseSchema. model is "gpt5.1" (default) or "gpt4.1". Consumes credits.
// Read a secret, call an external API through the host, and shape the result.
const key = await hutly.vault.getForApp("stripe");
const res = await hutly.http.get("https://api.example.com/orders", {
  headers: { Authorization: `Bearer ${key.value}` },
});
return { count: res.data.orders.length };

Validation

A function node’s code is checked when you hutly validate / deploy the workflow, and a broken node blocks the deploy:

  • Syntax errors and references to undefined variables (no-undef) are rejected.
  • Every hutly.* access is checked against the surface above — a typo like hutly.valut.getItem or a nonexistent method fails validation instead of blowing up mid-run.

Examples

Example: Format Date

Node ID: formatDate

Code:

const timestamp = payload.results.trigger.timestamp;
const date = new Date(timestamp);

return {
  formatted: date.toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }),
  iso: date.toISOString()
};

Output:

{
  "formatted": "December 22, 2025",
  "iso": "2025-12-22T02:00:00.000Z"
}

Access: $.results.formatDate.formatted

Example: Calculate Total

Node ID: calculateTotal

Code:

const items = payload.results.fetchItems.items;

const subtotal = items.reduce((sum, item) => {
  return sum + (item.price * item.quantity);
}, 0);

const tax = subtotal * 0.1;
const total = subtotal + tax;

return {
  subtotal: subtotal.toFixed(2),
  tax: tax.toFixed(2),
  total: total.toFixed(2),
  itemCount: items.length
};

Output:

{
  "subtotal": "145.50",
  "tax": "14.55",
  "total": "160.05",
  "itemCount": 3
}

Example: Filter Array

Node ID: filterActive

Code:

const users = payload.results.fetchUsers.data;

const activeUsers = users.filter(user => user.status === 'active');

return {
  activeUsers: activeUsers,
  count: activeUsers.length,
  filtered: users.length - activeUsers.length
};

Example: Transform Data Structure

Node ID: transformToMap

Code:

const products = payload.results.getProducts.items;

// Convert array to object keyed by product ID
const productMap = {};
products.forEach(product => {
  productMap[product.id] = {
    name: product.name,
    price: product.price,
    inStock: product.inventory > 0
  };
});

return {
  products: productMap,
  total: products.length
};

Output:

{
  "products": {
    "prod_123": {
      "name": "Widget",
      "price": 29.99,
      "inStock": true
    },
    "prod_456": {
      "name": "Gadget",
      "price": 49.99,
      "inStock": false
    }
  },
  "total": 2
}

Example: String Manipulation

Node ID: formatAddress

Code:

const address = payload.results.validateAddress.data;

const parts = [
  address.street,
  address.city,
  address.state,
  address.zip
].filter(Boolean);

return {
  oneLine: parts.join(', '),
  multiLine: parts.join('\n'),
  components: {
    street: address.street || '',
    city: address.city || '',
    state: address.state || '',
    zip: address.zip || ''
  }
};

Example: Conditional Logic

Node ID: determinePriority

Code:

const order = payload.results.trigger.order;

let priority = 'normal';
let rushFee = 0;

if (order.total > 1000) {
  priority = 'high';
} else if (order.requestedDate) {
  const daysUntilNeeded = Math.floor(
    (new Date(order.requestedDate) - new Date()) / (1000 * 60 * 60 * 24)
  );

  if (daysUntilNeeded < 3) {
    priority = 'rush';
    rushFee = 50;
  }
}

return {
  priority: priority,
  rushFee: rushFee,
  estimatedDelivery: priority === 'rush' ? '1-2 days' : '5-7 days'
};

Best Practices

  • Keep it Simple: Use Function nodes for straightforward transformations
  • Error Handling: Wrap risky operations in try-catch blocks
  • Return Early: Return immediately if conditions aren’t met
  • Null Safety: Check for null/undefined before accessing properties
  • Pure Functions: Avoid side effects; only transform input to output
  • Document Complex Logic: Use comments for non-obvious transformations
  • Test Thoroughly: Verify edge cases like empty arrays, null values

Limitations

  • No arbitrary libraries or require/import: only built-in ES2021 globals plus the injected hutly/payload/console/Buffer. Reach the backend through hutly.*, not require.
  • Network only via hutly.http: there is no bare fetch — outbound HTTP goes through hutly.http.* (host-mediated).
  • No file system, no timers: process, setTimeout, and the filesystem are not available.
  • Execution Timeout: functions must complete within the execution timeout.
  • Memory Constraints: large data transformations may hit memory limits.

Common Patterns

Safe Property Access

// Safely access nested properties
const email = payload.results.user?.data?.contact?.email || 'no-email@example.com';

return { email };

Array Operations

const items = payload.results.fetchItems.data || [];

return {
  // Map
  names: items.map(item => item.name),

  // Filter
  expensive: items.filter(item => item.price > 100),

  // Find
  firstMatch: items.find(item => item.category === 'electronics'),

  // Some/Every
  hasExpensive: items.some(item => item.price > 100),
  allInStock: items.every(item => item.stock > 0)
};

Object Operations

const data = payload.results.apiResponse.data;

return {
  // Merge objects
  merged: { ...data, timestamp: Date.now() },

  // Pick properties
  summary: {
    id: data.id,
    name: data.name,
    status: data.status
  },

  // Transform keys
  uppercase: Object.fromEntries(
    Object.entries(data).map(([k, v]) => [k.toUpperCase(), v])
  )
};