Building Custom Code Agents
A code agent is a workflow node that runs your own TypeScript instead of an LLM prompt. Use it when a step needs deterministic logic, data shaping, or precise control that's awkward to express through Instructions — parsing a payload, computing a score, calling an LLM in a loop, or stitching together integration calls. The code runs in an isolated sandbox, so it can't reach the host or the network except through the built-ins the platform provides.
A code agent is the code agent entry in the agent library (base type code_execution). It does not call the LLM by default — it runs your function and returns whatever your function returns.
When to use a code agent
Reach for a code agent when the step is fundamentally about logic, not language:
- Transform or validate structured data with rules that are easier to write than to describe.
- Call the LLM yourself — for example, once per item in a list — and assemble the results.
- Combine several integration calls and shape the combined result.
- Implement a deterministic algorithm (scoring, deduplication, formatting) that must behave identically every run.
For purely deterministic field reshaping with no code, consider a Transform node instead — it's a rule list, not a script.
The editor
Add a code agent from the agent library and open its panel. The body is a TypeScript editor with a read-only prefix at the top that you can't edit:
- A libraries comment listing what's available.
- An
interface Inputs { ... }block describing theinputsyour function receives, generated from the agent's input mapping or input schema.
Below the prefix you write your function. The default scaffold is:
function main(inputs: Inputs) {
return "";
}
Your entry point is main(inputs). Whatever you return becomes the agent node's output, flowing downstream like any other agent's output.
The editor toolbar includes Test (run the script against sample inputs), plus copy, format, theme, and expand actions.
The read-only prefix is saved with your code so the sandbox's error line numbers line up with what you see in the editor. Don't try to remove it — it's regenerated on load and keeps diagnostics accurate.
Inputs
The inputs parameter is typed from the agent's input mapping. Map a Start-node field or an upstream agent's output into the code agent the same way as any agent, and the editor generates a matching Inputs interface so you get autocompletion and type checking. For example, mapping a customer object and a threshold number produces:
interface Inputs {
customer: { name: string; tier: string };
threshold: number;
}
If no mapping or schema is available, Inputs falls back to an open shape ({ [key: string]: any }).
Built-ins available in the sandbox
Your code runs in a sandboxed TypeScript runtime with a fixed set of globals — no fetch, no filesystem, no arbitrary network. The built-ins are:
| Built-in | Signature | Purpose |
|---|---|---|
call_llm | call_llm(user_prompt: string, system_prompt?: string): string | Run an LLM completion and get back the text. Synchronous from your code's perspective. |
log.info / log.warning / log.error | log.info(...args): void | Write to the run's execution log, visible in the step trace. |
_ | lodash | Utility functions. |
uuid | uuid.v4() | Generate UUIDs. |
Papa | papaparse | Parse and unparse CSV. |
base64 | base64-js | Base64 encode/decode. |
A simple agent that classifies and logs:
function main(inputs: Inputs) {
const verdict = call_llm(
`Classify this message as URGENT or NORMAL: ${inputs.message}`,
"You are a triage classifier. Reply with one word.",
);
log.info(`Classified as ${verdict}`);
return { urgency: verdict.trim().toLowerCase() };
}
call_llm is metered and capped per execution. Code agents are for orchestration, not unbounded fan-out — if you call the LLM in a loop, keep the iteration count bounded. The platform enforces a maximum number of LLM calls per run.
Calling your integrations
A code agent can call your connected integrations, but only the ones you explicitly attach to the agent. When you attach integrations:
- A
call_integrationbuilt-in becomes available:call_integration(integration_id, method, path, body?, headers?)returns{ ok, status, headers, body }. - An
integrationsnamespace is populated with the attached integrations by name, so you reference them without hardcoding UUIDs in your source. Use bracket access for names that aren't valid identifiers.
function main(inputs: Inputs) {
const res = call_integration(
integrations["hubspot_crm"],
"GET",
`/contacts/${inputs.contactId}`,
);
if (!res.ok) {
log.error(`HubSpot lookup failed: ${res.status}`);
return { found: false };
}
return { found: true, contact: JSON.parse(res.body) };
}
If you don't attach any integrations, call_integration and integrations are not in scope — referencing them is a clear error in the editor and at run time, rather than a silent no-op.
Testing
Use Test in the editor toolbar to run your script against sample inputs without publishing the workflow. The test runner assembles inputs from the agent's mapping and schema, runs the script in the same sandbox the workflow uses, and shows you the output, the execution log, the time taken, and any tokens used. Iterate here until the function behaves, then save and publish the workflow.
How it runs in production
At run time, the workflow engine passes your script and the resolved inputs to the sandbox runtime, which:
- Validates the TypeScript (the same validation runs in the editor as you type).
- Executes
main(inputs)in an isolated environment with only the built-ins above in scope. - Captures
log.*output, LLM-call traces, and the return value. - Returns the output to the workflow, where it flows to downstream nodes.
The output is capped in size, and a runaway script can't exhaust the calling service. Because LLM and integration calls have side effects, code-agent runs are not automatically retried — a failed run fails the step.
Reference
| Concern | Detail |
|---|---|
| Entry point | function main(inputs: Inputs) — return value is the node output. |
| Language | TypeScript. |
| Inputs | Typed from the agent's input mapping / input schema. |
| LLM | call_llm(user_prompt, system_prompt?) → string, metered and capped per run. |
| Integrations | call_integration(...) and the integrations namespace, available only when integrations are attached. |
| Bundled libraries | lodash (_), uuid, papaparse (Papa), base64-js (base64). |
| Logging | log.info / log.warning / log.error, surfaced in the run's step trace. |
| Isolation | Sandboxed runtime; no direct network or filesystem access. |
What's next
- Build your first workflow — place a code agent in a workflow and wire its inputs.
- Writing effective Instructions — for the LLM-driven agents around your code agent.
- Core concepts — how agents fit into a workflow.