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_languageinput, 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 Type | JavaScript Value | Notes |
|---|---|---|
| String | string | Passed as-is |
| Number | number (float64) | Use directly; no parsing needed |
| Boolean | boolean | Compare with === true / === false |
| Document | Object with .text, .filename | Access via inputs.doc_name.text |
| Search Result | Object with .text, .source, .matches | .matches is an array of strings |
| List | Array | Ordered; use .forEach, indexing, etc. |
| Map | Object | String-keyed; use dot or bracket notation |
| Null | null | Check 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):
| Library | Global | Use Case |
|---|---|---|
| papaparse | Papa | CSV parsing and serialization |
| uuid | uuid | UUID generation (uuid.v4(), etc.) |
| lodash | _ | Utility functions for arrays/objects |
| base64 | Base64 | Base64 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).
| Parameter | Type | Required | Description |
|---|---|---|---|
user_prompt | string | Yes | The user message sent to the LLM. Must be a non-empty string. |
system_prompt | string | No | Overrides 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
- 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.
- 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.
- Click Run Test. The script executes against the provided inputs.
- 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:
| Type | JSON Format |
|---|---|
| String / Number / Boolean | JSON primitive |
| Document | { "text": "...", "filename": "..." } |
| Search Result | { "text": "...", "source": "...", "matches": [] } |
| List | JSON array |
| Map | JSON object |
| Null | null |
Test results are not saved after you close the modal.
Time and Memory Limits
| Limit | Value | Notes |
|---|---|---|
| Time | 100 ms | Wall-clock for script execution; call_llm() time is excluded |
| Memory | 24 MB initial / 128 MB max | Heap for the script |
| LLM calls | 40 per execution | Maximum number of call_llm() invocations per script run |
| External connector calls | 20 per execution | Maximum 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
| Error | Cause | Fix |
|---|---|---|
| Memory Limit Exceeded | Script 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 undefined | Accessing a field on an input that may be null or undefined. | Guard optional fields: if (inputs.user) { var name = inputs.user.name; }. |
| Time Limit Exceeded | Script processing takes longer than 100 ms. | Reduce data volume per run or offload heavy work upstream. |
| Invalid script result format | Script 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 }));
}
Related Agents
- 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.