Code Node - Custom Code Execution
The Code node is one of the most powerful features in Circuitry, allowing you to write custom code within your visual workflows. Perfect for data transformations, calculations, and custom logic that goes beyond standard nodes.
JavaScript, TypeScript, Python, Go, Rust, C, C++ and Zig are all supported, and each step chooses where it runs — on this device, or on a computer of your own.
🚀 Overview
The Code node bridges the gap between no-code and full programming flexibility. While Circuitry is designed to be no-code first, sometimes you need that extra bit of custom logic - and that's where the Code node shines.
Key Benefits
- JavaScript, TypeScript, Python, Go, Rust, C, C++ and Zig: write a step in whichever of them suits the job
- Choose where each step runs: on this device, or on your own computer
- Access to Input Data: Full access to data from previous nodes
- Template Variables: Use
{{variables}}within your code - No External Dependencies: Runs securely in an isolated environment
- Instant Testing: Test your code with sample data before executing
TypeScript steps
Pick TypeScript in the language dropdown and write the step as you would in any TypeScript file — interfaces, type annotations, generics. The types are removed the moment the step runs, so what executes is your code as JavaScript, in the same place and at the same speed.
Everything a JavaScript step can do, a TypeScript step does too: it runs on this device with nothing connected, it runs offline, it works on every plan, and a workflow started by a webhook can run it. There is nothing to build and nothing to install.
Three things worth knowing:
- Types are removed, not checked. Circuitry does not stop a step from running because a type is wrong — the annotations are there for you and for your editor. A mistake in the code itself still shows up as an error when the step runs, exactly as in JavaScript.
- Breakpoints land on the lines you wrote. Types are blanked out in place rather than deleted, so line 15 stays line 15 and a breakpoint set beside your
constpauses on it. See Debugging workflows. - A few TypeScript features don't fit a single step.
enum,namespace, constructor parameter properties andimport x = require(...)are the parts of TypeScript that build something at runtime rather than just describing a shape, so they can't simply be removed. A step using one says so and names the line. A plain object works in place of anenum(const Color = { Red: 'red' }), and assigning in the constructor body replaces a parameter property. Everything else — interfaces, type aliases, generics,as,satisfies,import type— is fine.
Go, Rust, C, C++ and Zig steps
Go, Rust, C, C++ and Zig steps are compiled rather than interpreted, which makes them fast and lets you write a step in the same language you already build in. That difference shapes how you write them.
You write ordinary Go, Rust, C, C++ or Zig. Your code is a file of its own, so the language's entire standard library is available — bring in what you need with a normal import or #include, exactly as you would in any other project. Third-party packages are not available: there is no go.mod, Cargo.toml or package manager, so a step depends only on the language itself.
Writing a Go step
Write one function called Run. The package line and main are supplied; everything else is yours, imports included.
import (
"fmt"
"strings"
)
func Run(input any) (any, error) {
m, _ := input.(map[string]any)
name, ok := m["name"].(string)
if !ok {
return nil, fmt.Errorf("expected a name")
}
fmt.Println("greeting", name)
return map[string]any{"greeting": "Hello " + strings.TrimSpace(name)}, nil
}
inputis the value from the previous step. Type-assert it before use, and check the result — an unchecked assertion will stop the step.- Numbers arrive as
float64, neverint, because they come from JSON. env("NAME")reads one of your environment variables, returning an empty string if it is not set.os.Getenvwill not reach them.- Return your result directly. Whatever you return becomes the step's output. Return an error to fail the step.
fmt.Printlnoutput appears in the step's log.
Writing a Rust step
Write one function called run. Add your own use lines for anything you need.
use std::collections::BTreeSet;
pub fn run(input: Json) -> Result<Json, String> {
let name = input
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| "expected a name".to_string())?;
let letters: BTreeSet<char> = name.chars().collect();
println!("greeting {}", name);
Ok(Json::obj(vec![
("greeting", Json::from(format!("Hello {}", name))),
("distinct_letters", Json::from(letters.len())),
]))
}
Reading JSON is already built in. Rust's standard library has no JSON support, so the Rust code step provides a Json type for it — you do not add a crate, and you should not try to bring in serde or serde_json, which are not available. Json is simply there:
| To do this | Write |
|---|---|
| Read a field | input.get("name") |
| Read an array item | input.at(0) |
| Convert a value | .as_str(), .as_f64(), .as_i64(), .as_bool(), .as_array() |
| Build a value | Json::from(42), Json::from("text"), Json::from(true) |
| Build an object | Json::obj(vec![("key", Json::from(1))]) |
| Build a list | Json::arr(vec![Json::from(1), Json::from(2)]) |
- Avoid
unwrap()on anything frominput. A panic stops the step outright — there is no catching it. Preferand_thenwithok_or_else, as above, and returnErrto fail with a message of your own. If a step does panic, Circuitry shows the reason and the line it happened on. - Numbers are
f64. Use.as_i64()when you want a whole number. env("NAME")reads one of your environment variables and returns an emptyStringwhen unset.std::env::varwill not reach them.- Whatever you return is the step's output, wrapped in
Ok(...). println!output appears in the step's log.
Use input directly, not template variables
Template variables like {{input.name}} are for assistant prompts and other text fields — places with no code to hold a value. Inside a code step you already have the data in scope, so reach for it directly. That is true in every language, and it reads better:
func Run(input any) (any, error) {
m, _ := input.(map[string]any)
name := m["name"] // instead of {{input.name}}
key := env("API_KEY") // instead of {{env.API_KEY}}
return map[string]any{"name": name, "hasKey": key != ""}, nil
}
In Go, Rust, C, C++ and Zig this is also enforced: a compiled step is reused until you change its code, so a value pasted into the source would make it different code every run and rebuild every time. Circuitry refuses it and tells you to use input instead.
Writing a C step
Write one function called run. There is no main.
#include <ctype.h>
Json *run(Json *input) {
const char *name = json_str(json_get(input, "name"), "");
if (strlen(name) == 0) {
return json_error("expected a name");
}
int letters = 0;
for (const char *p = name; *p; p++) {
if (isalpha((unsigned char)*p)) letters++;
}
printf("greeting %s\n", name);
Json *out = json_new_obj();
json_set(out, "greeting", json_from_str("hello"));
json_set(out, "letters", json_from_num((double)letters));
return out;
}
Reading JSON is already built in, as a Json type — C's standard library has none, and there is no package manager to add one, so do not reach for cJSON or Jansson.
| To do this | Write |
|---|---|
| Read a field | json_get(input, "name") |
| Read an array item | json_at(items, 0) |
| Convert a value | json_str(v, ""), json_num(v, 0), json_bool(v, 0) |
| Count entries | json_length(v) |
| Build a value | json_from_str("text"), json_from_num(42), json_from_bool(1) |
| Build an object | json_new_obj(), then json_set(obj, "key", value) |
| Build a list | json_new_arr(), then json_push(arr, value) |
- Never call
free. Memory is released when the step finishes, so there is no cleanup to write and freeing something yourself is a bug. - The accessors take a fallback and never crash on a missing or wrong-typed field, so check explicitly when a value is required.
- Six headers are already included —
stddef.h,stdint.h,stdlib.h,string.h,stdio.handmath.h. Including any of them again is harmless. Add your own#includefor anything else. - Numbers are
double. Cast when you want a whole number. strlencounts bytes, not characters — accented and emoji text counts more than you might expect.env("NAME")reads one of your environment variables and returns""when unset, neverNULL.getenvwill not reach them.- Fail the step by returning
json_error("message"). printfoutput appears in the step's log.
Writing a step in C++
Write one function called run. C++20, and there is no main.
#include <algorithm>
Json run(Json input) {
std::string name = input["name"].as_string("");
if (name.empty()) {
return Json::error("expected a name");
}
std::printf("greeting %s\n", name.c_str());
std::string upper = name;
std::transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
Json out = Json::object();
out.set("greeting", Json("hello " + name));
out.set("upper", Json(upper));
return out;
}
throw does not compile. This is the one thing to know before writing C++ here: the target has no unwinder, so the standard library is built without exception support. Avoid throw, try/catch, and library calls that throw (std::stoi, .at()). Fail the step by returning Json::error("message").
Reading JSON is already built in, as a Json class — the standard library has none, and there is no package manager, so do not reach for nlohmann/json.
| To do this | Write |
|---|---|
| Read a field | input["name"] |
| Read a nested field | input["body"]["user"]["name"] — missing keys give a null Json, so this never crashes |
| Read an array item | items[0] |
| Convert a value | .as_string(""), .as_number(0), .as_int(0), .as_bool(false) |
| Loop an array | for (const Json &item : items.items()) |
| Count / test | .size(), .has("key"), .is_null() |
| Build a value | Json(42), Json("text"), Json(true) |
| Build an object | Json::object(), then .set("key", value) |
| Build a list | Json::array(), then .push(value) |
- Seven headers are already included —
string,vector,utility,cstdio,cstdlib,cstringandcmath. Add your own#includefor anything else. - Prefer
printfto iostreams. Including<iostream>or<sstream>adds a couple of megabytes to the build for no benefit here. env("NAME")returns astd::string, empty when unset.std::getenvwill not reach your variables.
Writing a Zig step
Write one function called run. You do not write imports — std, Json and c (the Circuitry helpers) are already in scope.
pub fn run(input: Json) anyerror!Json {
const name = c.asStr(c.get(input, "name"), "");
if (name.len == 0) return c.stepError("expected a name");
std.debug.print("greeting {s}\n", .{name});
var seen = std.AutoHashMap(u8, void).init(c.allocator);
for (name) |ch| try seen.put(ch, {});
var out = c.newObject();
c.set(&out, "greeting", c.strf("hello {s}", .{name}));
c.set(&out, "distinct", c.int(@intCast(seen.count())));
return out;
}
If you do not read input, discard it explicitly. Zig treats an unused parameter as an error, not a warning, so a step that ignores its input must start with:
_ = input;
This is the most common reason a Zig step fails to build.
Json is Zig's own std.json.Value, so anything the standard library does with JSON works here unchanged. The helpers are for convenience:
| To do this | Write |
|---|---|
| Read a field | c.get(input, "name") |
| Read an array item | c.at(items, 0) |
| Convert a value | c.asStr(v, ""), c.asNum(v, 0), c.asInt(v, 0), c.asBool(v, false) |
| Count entries | c.len(v) |
| Build a value | c.str("text"), c.int(42), c.num(1.5), c.boolean(true) |
| Build a formatted string | c.strf("hello {s}", .{name}) |
| Build an object | c.newObject(), then c.set(&obj, "key", value) |
| Build a list | c.newArray(), then c.push(&arr, value) |
c.getreturns an optional, which you can pass straight toc.asStrand friends — they take a fallback and never fail on a missing or wrong-typed field.- Use
c.allocatorwhen something needs one. Never free anything; memory is released when the step finishes. - Fail the step with
return c.stepError("message"). c.env("NAME")returns[]const u8, empty when unset.std.processwill not reach your variables.
When the workflow is started by a webhook
A webhook wraps the request, so the data you posted is under body rather than at the top level. This applies to every language — but it is easiest to miss in Go and Rust, where reading a missing field hands you your fallback rather than complaining.
func Run(input any) (any, error) {
m, _ := input.(map[string]any)
body, _ := m["body"].(map[string]any) // the JSON you posted
x, ok := body["x"].(float64)
if !ok {
return nil, fmt.Errorf("expected x")
}
return x * 2, nil
}
The same in Rust:
pub fn run(input: Json) -> Result<Json, String> {
let x = input
.get("body")
.and_then(|b| b.get("x"))
.and_then(|v| v.as_f64())
.ok_or_else(|| "expected x".to_string())?;
Ok(Json::from(x * 2.0))
}
Alongside body you also get method, headers, query and path.
See the real shape before you write against it. Running a Webhook step in the editor gives it an empty input, because no request has arrived — so code written against the wrong shape returns your fallback, looks like it works, and keeps doing exactly that after deployment.
Use Listen for request on the Webhook step instead. It arms the webhook to capture the next incoming request without running the workflow, and the captured payload appears in the node's output. Send a real request from wherever you plan to call it, and you can see precisely what your code will receive.
Two small differences between a captured request and a live one, worth knowing before you rely on either:
capturedAtappears only in a capture. It is not there when the workflow actually runs, so don't read it in your code.projectIdanduserIdare there on a live run but not in a capture.
Everything you normally want — body, query, headers, method, path — is identical in both.
Building
The first time you run a Go, Rust, C, C++ or Zig step — and again after you change its code — Circuitry compiles it to WebAssembly (WASM): a portable, sandboxed machine-code format that runs at close to native speed. That is where the speed of a compiled step comes from, and it is also why the built result isn't tied to the machine that built it.
Compiling needs a compiler, and a compiler is a real program that has to live on a real machine — so this is the one part that can't happen on the device in your hand:
- In the desktop app — Circuitry Studio for macOS, Windows or Linux — it just works. The first time you run a step in one of these languages, Circuitry offers to install that language's compiler on your computer and then builds with it. You don't need the language installed already, and there is nothing to configure.
- On iPhone, iPad, Android or the web, you need Circuit running on your computer and connected. Circuitry installs the compiler there and builds there; the result comes back to the device you're holding and runs on it.
Both the desktop app and Circuit come with the Personal plan and above. If a step says its build tools are missing, run it again to retry.
Once built, the result is saved with your workflow. That means:
- the step runs instantly afterwards, with no rebuilding
- it still runs on a device with nothing connected, and offline
- it runs on any device you open the workflow on afterwards — a phone included, because WebAssembly is portable
- sharing the workflow shares a step that already works — on any plan
- you are only asked to build again when you actually change the code
So the plan requirement is about changing a compiled step, not running one — build once beside your computer, then run anywhere. Python, JavaScript and TypeScript need none of this: they run on the device you are working on, on every plan.
For the whole picture across both surfaces — which language runs where, and how the same document is both a notebook and a canvas — see Languages, and Where They Run.
Running in the cloud
A workflow started by a webhook can run Go, Rust, C, C++ and Zig steps, using the build saved with it. If a step has never been built, the workflow tells you when you deploy it rather than failing later — run the step once with your own computer connected, and it is ready.
What Go, Rust, C, C++ and Zig steps cannot do
A compiled step is a self-contained transformation of its input:
- No files or network. Use the other nodes for those.
- Standard library only — third-party packages and crates are not available.
- No background work. Anything started inside the step must finish before it returns.
- No breakpoints yet. Print instead —
fmt.Printlnin Go,println!in Rust,printfin C and C++,std.debug.printin Zig — and the output appears in the step's log. JavaScript and Python steps do support breakpoints.
📝 Basic Usage
Simple Example
// Access input data from previous node
const message = input.message;
const timestamp = new Date().toISOString();
// Return processed data
return {
original: message,
processed: message.toUpperCase(),
timestamp: timestamp,
wordCount: message.split(' ').length
};Input and Output
Input: The Code node receives data from the previous node as the input variable
Output: Whatever you return becomes the output for the next node
🎯 Common Use Cases
1. Data Transformation
Transform data structures to match your needs:
// Transform array of objects
const users = input.users || [];
const transformed = users.map(user => ({
fullName: `${user.firstName} ${user.lastName}`,
email: user.email.toLowerCase(),
age: calculateAge(user.birthDate),
status: user.isActive ? 'active' : 'inactive'
}));
function calculateAge(birthDate) {
const diff = Date.now() - new Date(birthDate).getTime();
return Math.floor(diff / (1000 * 60 * 60 * 24 * 365.25));
}
return { users: transformed };2. Data Validation
Validate and clean incoming data:
// Validate email and phone
const email = input.email;
const phone = input.phone;
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const phoneRegex = /^\d{10}$/;
const errors = [];
if (!emailRegex.test(email)) {
errors.push('Invalid email format');
}
if (!phoneRegex.test(phone.replace(/\D/g, ''))) {
errors.push('Invalid phone number');
}
return {
isValid: errors.length === 0,
errors: errors,
cleaned: {
email: email.trim().toLowerCase(),
phone: phone.replace(/\D/g, '')
}
};3. Complex Calculations
Perform calculations that would be difficult with standard nodes:
// Calculate order totals with tax and discounts
const items = input.orderItems || [];
const taxRate = input.taxRate || 0.08;
const discountCode = input.discountCode;
let subtotal = 0;
let discountAmount = 0;
// Calculate subtotal
items.forEach(item => {
subtotal += item.price * item.quantity;
});
// Apply discount
if (discountCode === 'SAVE10') {
discountAmount = subtotal * 0.10;
} else if (discountCode === 'SAVE20') {
discountAmount = subtotal * 0.20;
}
const afterDiscount = subtotal - discountAmount;
const tax = afterDiscount * taxRate;
const total = afterDiscount + tax;
return {
subtotal: subtotal.toFixed(2),
discount: discountAmount.toFixed(2),
tax: tax.toFixed(2),
total: total.toFixed(2),
itemCount: items.reduce((sum, item) => sum + item.quantity, 0)
};4. Data Aggregation
Aggregate and summarize data:
// Analyze sales data
const sales = input.sales || [];
const summary = {
totalSales: 0,
averageSale: 0,
topProduct: null,
byCategory: {},
byMonth: {}
};
const productCounts = {};
sales.forEach(sale => {
// Total sales
summary.totalSales += sale.amount;
// By category
const category = sale.category;
if (!summary.byCategory[category]) {
summary.byCategory[category] = 0;
}
summary.byCategory[category] += sale.amount;
// By month
const month = new Date(sale.date).toLocaleString('default', { month: 'long' });
if (!summary.byMonth[month]) {
summary.byMonth[month] = 0;
}
summary.byMonth[month] += sale.amount;
// Product counts
const product = sale.product;
productCounts[product] = (productCounts[product] || 0) + 1;
});
// Calculate average
summary.averageSale = (summary.totalSales / sales.length).toFixed(2);
// Find top product
const topProductName = Object.keys(productCounts).reduce((a, b) =>
productCounts[a] > productCounts[b] ? a : b
);
summary.topProduct = {
name: topProductName,
count: productCounts[topProductName]
};
return summary;5. API Response Processing
Process and extract data from API responses:
// Process weather API response
const weatherData = input.apiResponse;
if (!weatherData || weatherData.error) {
return {
error: true,
message: weatherData?.error || 'No data received'
};
}
const current = weatherData.current;
const forecast = weatherData.forecast?.forecastday || [];
// Extract relevant information
const processed = {
location: `${weatherData.location.name}, ${weatherData.location.country}`,
current: {
temp: `${current.temp_f}°F`,
condition: current.condition.text,
humidity: `${current.humidity}%`,
windSpeed: `${current.wind_mph} mph`
},
forecast: forecast.slice(0, 3).map(day => ({
date: day.date,
high: `${day.day.maxtemp_f}°F`,
low: `${day.day.mintemp_f}°F`,
condition: day.day.condition.text
})),
alerts: weatherData.alerts?.length > 0
};
return processed;🔧 Advanced Features
Using Template Variables
Combine template variables with JavaScript:
// Template variables are replaced before code execution
const userName = "{{user.name}}";
const apiKey = "{{env.API_KEY}}";
const previousResult = "{{nodes.agent1.output}}";
// Use them in your logic
return {
greeting: `Hello, ${userName}!`,
authorized: apiKey !== 'undefined',
enhanced: processData(previousResult)
};
function processData(data) {
// Your processing logic
return data.toUpperCase();
}Working with Dates
Common date operations:
// Date utilities
const now = new Date();
const inputDate = new Date(input.date);
// Format dates
const formatted = {
iso: now.toISOString(),
local: now.toLocaleString(),
dateOnly: now.toLocaleDateString(),
timeOnly: now.toLocaleTimeString()
};
// Calculate differences
const daysDiff = Math.floor((now - inputDate) / (1000 * 60 * 60 * 24));
const isOverdue = inputDate < now;
// Add/subtract days
const futureDate = new Date(now);
futureDate.setDate(futureDate.getDate() + 30);
return {
current: formatted,
daysSince: daysDiff,
isOverdue: isOverdue,
dueIn30Days: futureDate.toISOString()
};Array Operations
Powerful array manipulations:
// Advanced array operations
const items = input.items || [];
// Filter, map, reduce
const processed = items
.filter(item => item.active)
.map(item => ({
...item,
value: item.price * item.quantity
}))
.sort((a, b) => b.value - a.value);
// Group by category
const grouped = items.reduce((acc, item) => {
const key = item.category;
if (!acc[key]) acc[key] = [];
acc[key].push(item);
return acc;
}, {});
// Find duplicates
const seen = new Set();
const duplicates = items.filter(item => {
const duplicate = seen.has(item.id);
seen.add(item.id);
return duplicate;
});
return {
processed: processed,
grouped: grouped,
duplicates: duplicates,
stats: {
total: items.length,
active: items.filter(i => i.active).length,
categories: Object.keys(grouped).length
}
};Error Handling
Robust error handling:
try {
// Potentially risky operation
const data = JSON.parse(input.jsonString);
// Validate required fields
const required = ['name', 'email', 'age'];
const missing = required.filter(field => !data[field]);
if (missing.length > 0) {
throw new Error(`Missing required fields: ${missing.join(', ')}`);
}
// Process data
const result = processUserData(data);
return {
success: true,
data: result
};
} catch (error) {
return {
success: false,
error: error.message,
input: input
};
}
function processUserData(data) {
// Your processing logic
return {
...data,
processed: true,
timestamp: Date.now()
};
}📚 Best Practices
1. Always Return Data
Every Code node should return something:
// Good ✅
return {
result: processedData,
status: 'complete'
};
// Bad ❌
processData(); // No return statement2. Handle Missing Input
Check for undefined or null values:
// Safe input handling
const items = input.items || [];
const config = input.config || {};
const name = input.name || 'Unknown';
// Check before accessing nested properties
const city = input.address?.city || 'N/A';3. Use Meaningful Variable Names
// Good ✅
const userEmail = input.email;
const orderTotal = calculateTotal(items);
// Bad ❌
const e = input.email;
const t = calc(i);4. Comment Complex Logic
// Calculate compound interest
// Formula: A = P(1 + r/n)^(nt)
const principal = input.principal;
const rate = input.annualRate / 100;
const time = input.years;
const n = 12; // Monthly compounding
const amount = principal * Math.pow(1 + rate/n, n * time);5. Test with Edge Cases
Consider:
- Empty arrays
- Null/undefined values
- Invalid data types
- Zero/negative numbers
- Empty strings
🔒 Security Considerations
Safe Practices
The Code node runs in a sandboxed environment with these limitations:
- No file system access
- No network requests (use Action nodes for HTTP)
- No external modules (no require/import)
- No global scope pollution
- Execution timeout (prevents infinite loops)
What You CAN Do
✅ All standard language features (JavaScript or Python) ✅ JSON operations ✅ Date/time manipulation ✅ Math calculations ✅ String/array (list) operations ✅ Regular expressions ✅ Object/dictionary manipulation
What You CANNOT Do
❌ File operations ❌ Direct HTTP requests ❌ Import external/third-party libraries ❌ Access browser APIs ❌ Modify global objects
💡 Tips and Tricks
1. Debugging
Use console.log for debugging (visible in browser console):
console.log('Input data:', input);
console.log('Processing step 1...');
const result = processData(input);
console.log('Result:', result);
return result;2. Type Checking
Validate data types:
// Type checking utilities
function isNumber(val) {
return typeof val === 'number' && !isNaN(val);
}
function isArray(val) {
return Array.isArray(val);
}
function isObject(val) {
return val !== null && typeof val === 'object' && !Array.isArray(val);
}3. Default Values
Use default parameters and nullish coalescing:
// Default values
const process = (data = {}, options = {}) => {
const limit = options.limit ?? 10;
const offset = options.offset ?? 0;
// ...
};🎓 Examples Gallery
Generate UUID
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
return {
id: generateUUID(),
timestamp: Date.now(),
...input
};Parse CSV Data
const csvText = input.csvData;
const lines = csvText.split('\n');
const headers = lines[0].split(',');
const data = lines.slice(1).map(line => {
const values = line.split(',');
return headers.reduce((obj, header, index) => {
obj[header.trim()] = values[index]?.trim() || '';
return obj;
}, {});
});
return {
headers: headers,
rows: data,
count: data.length
};Generate Statistics
const numbers = input.values || [];
const stats = {
count: numbers.length,
sum: numbers.reduce((a, b) => a + b, 0),
mean: 0,
median: 0,
min: Math.min(...numbers),
max: Math.max(...numbers)
};
stats.mean = stats.sum / stats.count;
const sorted = [...numbers].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
stats.median = sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
return stats;Next Steps
- Template Variables Guide - Learn about dynamic variable replacement
- Workflow Examples - See Code nodes in action
- Developer & API Integration - Combine Code nodes with API calls