Custom Code Agent

The Custom Code Agent lets you write and execute custom JavaScript or TypeScript directly inside your workflow. When no existing agent fits your use case -- or you need precise data transformation or conditional logic -- the Custom Code Agent gives you full programmatic control without leaving the canvas.

Scripts run inside a secure, sandboxed environment powered by the script-executor service. Each execution is isolated, resource-limited, and logged so you can debug failures quickly.

Use Cases

  • Data transformation -- reshape, filter, or enrich structured data (CSV, JSON) before passing it downstream.
  • Custom LLM prompts -- call the built-in call_llm() function with a dynamically constructed prompt.
  • Branching logic -- use if/else or switch based on data values.
  • String manipulation -- regex extraction, formatting, encoding/decoding (Base64, UUID generation).
  • Lightweight computation -- calculations, aggregations, or rule-based classification that don't require a full agent.
  • Format conversion -- parse CSV into JSON, serialize an object to a string, encode binary data.

Limitations

  • Limited external network access -- direct HTTP requests are not supported, but outbound calls are available through configured integrations via call_ema_connector(). See Custom Integrations.
  • No file system access -- you cannot read from or write to disk.
  • No long-running processes -- execution is bounded by a strict time limit.
  • No calling other agents or workflows -- chaining to other canvas nodes from inside the script is not supported.
  • JavaScript and TypeScript -- both JavaScript and TypeScript are supported (select via the script_language input, with valid values "JavaScript" and "TypeScript").
  • No stateful side-effects -- each invocation is fully stateless; nothing persists between runs.
  • No async/await -- the sandbox runs synchronous JavaScript only.

Inputs

Required

Script: JavaScript code to execute. The script must define a main function with the following signature:

function main(inputs) {
 // your logic
 return "result";
}

Required function signature: The function name must be exactly main, it must accept exactly one argument named inputs, and it must return a value (preferably a string). Using async is not supported.

Optional: Named Inputs

Data from other agents passed into the script. Each input is converted to a script-executor type and exposed under inputs.<name>. Input names are sanitized to valid JavaScript identifiers (spaces become underscores; a leading underscore is added if the name starts with a digit). The inputs object is frozen -- your script cannot modify it.

Supported Input Types

Input TypeJavaScript ValueNotes
StringstringPassed as-is
Numbernumber (float64)Use directly; no parsing needed
BooleanbooleanCompare with === true / === false
DocumentObject with .text, .filenameAccess via inputs.doc_name.text
Search ResultObject with .text, .source, .matches.matches is an array of strings
ListArrayOrdered; use .forEach, indexing, etc.
MapObjectString-keyed; use dot or bracket notation
NullnullCheck with === null before use

Other well-known types may not be supported as Custom Code Agent inputs and can cause an error if used.

Output

The agent returns the script's result as Text with Sources. The main function must return a value that can be converted to a string; the returned value becomes the agent's output via JavaScript's .toString().

Always return a string explicitly to control format:

return "done";
return JSON.stringify({ status: "ok", count: 42 });
return Base64.encode(inputs.raw_data);

If you return an object without stringifying it, .toString() yields "[object Object]". Downstream agents receive this string; if you return JSON, the receiving agent must parse it.

Script Environment

Supported Libraries

The following libraries are available as globals (no require() needed):

LibraryGlobalUse Case
papaparsePapaCSV parsing and serialization
uuiduuidUUID generation (uuid.v4(), etc.)
lodash_Utility functions for arrays/objects
base64Base64Base64 encoding/decoding

Built-in globals: JSON, Math, crypto (e.g., crypto.getRandomValues()).

Calling the LLM

call_llm(user_prompt, system_prompt?) is available as a global function. It is synchronous -- no await needed. Time spent in call_llm() is excluded from the script's time limit. Each call is tracked in the Show Work panel (cost, latency, token counts).

ParameterTypeRequiredDescription
user_promptstringYesThe user message sent to the LLM. Must be a non-empty string.
system_promptstringNoOverrides the default system prompt. Use to control persona, output format, or constraints.
// Using the default system prompt
var summary = call_llm("Summarize:\n\n" + inputs.text);

// Using a custom system prompt
var result = call_llm(
 "Extract all dates from the following text:\n\n" + inputs.text,
 "You are a precise data extractor. Return only a JSON array of ISO 8601 date strings. No explanation."
);

Calling External Integrations

call_ema_connector(integrationId, method, path, body?, headers?) is available as a global function when external integrations are configured on the persona. It is synchronous and its execution time is excluded from the script's time limit. Returns an object with ok (boolean), status (number), body (string), headers (object), and request_log_id (string). See Custom Integrations for setup.

Work Logs (Show Work)

Use log.info(), log.warning(), and log.error() to emit work logs. These appear in the Show Work panel. For objects or arrays, pass the result of JSON.stringify() for clear output.

Type Hints for Code Generation

The script editor displays auto-generated type hints at the top of every script once you add named inputs and connect them. These hints tell the LLM exactly what inputs exist and their types. When asking an LLM to generate code, paste the full type-hint block first:

// Libraries: lodash (_), uuid, papaparse (Papa), base64-js (Base64)
// call_llm(user_prompt, system_prompt?) -> string
// log.info(msg) / log.warning(msg) / log.error(msg)
//
/** @param {{
 query: string;
 csv: { text: string, filename: string}[]; // Document inputs are arrays
}} inputs
 * @returns {string} */
function main(inputs) {}

Testing Scripts

The Custom Code Agent includes a testing mode that lets you run your script against test data in real time before saving the workflow. Test runs execute real call_llm() and call_ema_connector() calls, so they incur the same cost as a live run.

Running a Test

  1. Click the Test button above the script editor to open the test modal. The script editor appears on the left and the test panel on the right.
  2. Edit the Test inputs JSON in the right panel. A template is pre-populated based on your configured named inputs.

Tip: You can copy real inputs from a previous workflow run to use as test data. Open Show Work → Execution logs → Script Inputs on any past run and paste them into the test input panel.

  1. Click Run Test. The script executes against the provided inputs.
  2. Review results in the panel: the Output section shows the value returned by main() or the error message, and the Work Log section shows execution steps, log messages, and LLM call details (prompt, response, cost, latency).

Test Input Format

Test inputs are a JSON object keyed by named input name. Types must match what the script expects:

TypeJSON Format
String / Number / BooleanJSON primitive
Document{ "text": "...", "filename": "..." }
Search Result{ "text": "...", "source": "...", "matches": [] }
ListJSON array
MapJSON object
Nullnull

Test results are not saved after you close the modal.

Time and Memory Limits

LimitValueNotes
Time100 msWall-clock for script execution; call_llm() time is excluded
Memory24 MB initial / 128 MB maxHeap for the script
LLM calls40 per executionMaximum number of call_llm() invocations per script run
External connector calls20 per executionMaximum number of call_ema_connector() invocations per script run

Exceeding the time limit returns a TimeLimitExceeded error; exceeding memory causes the runtime to terminate execution.

Common Failure Modes

ErrorCauseFix
Memory Limit ExceededScript accumulates large structures in memory.Reduce payload size or avoid accumulating large structures.
Wrong arguments to call_llm()First argument is not a non-empty string, or an object was passed.Ensure user_prompt is a string: call_llm("Summarize: " + inputs.text).
TypeError: Cannot read properties of undefinedAccessing a field on an input that may be null or undefined.Guard optional fields: if (inputs.user) { var name = inputs.user.name; }.
Time Limit ExceededScript processing takes longer than 100 ms.Reduce data volume per run or offload heavy work upstream.
Invalid script result formatScript executor returns an unexpected shape.Check Show Work for details; ensure main returns a string.

Open the Show Work panel after a failed run for stack traces and log output. Add log.info() at key points to trace where execution stopped.

Examples

Simple greeting:

function main(inputs) {
 return "Hello, " + inputs.first_name + " " + inputs.last_name + "!";
}

Parse and filter CSV data (document input):

// Named input "csv_data" is a list of documents -- use csv_data[0].text
function main(inputs) {
 var result = Papa.parse(inputs.csv_data[0].text, { header: true, skipEmptyLines: true });
 var filtered = result.data.filter(function(row) { return row.amount >= 10; });
 return Papa.unparse(filtered);
}

LLM summarization:

function main(inputs) {
 var summary = call_llm("Summarize in 2 sentences:\n\n" + inputs.content);
 return summary.trim();
}

UUID generation and Base64 encoding:

function main(inputs) {
 var id = uuid.v4();
 return Base64.encode(JSON.stringify({ id: id, data: inputs.query }));
}
  • Custom Agent -- for LLM-powered reasoning; use when the task requires natural language understanding rather than deterministic code.
  • JSON Extractor -- for extracting fields from JSON without writing code.
  • Convert to Text -- for simple type conversion without custom logic.

Last updated: Jul 3, 2026