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:

  1. A libraries comment listing what's available.
  2. An interface Inputs { ... } block describing the inputs your 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-inSignaturePurpose
call_llmcall_llm(user_prompt: string, system_prompt?: string): stringRun an LLM completion and get back the text. Synchronous from your code's perspective.
log.info / log.warning / log.errorlog.info(...args): voidWrite to the run's execution log, visible in the step trace.
_lodashUtility functions.
uuiduuid.v4()Generate UUIDs.
PapapapaparseParse and unparse CSV.
base64base64-jsBase64 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_integration built-in becomes available: call_integration(integration_id, method, path, body?, headers?) returns { ok, status, headers, body }.
  • An integrations namespace 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

ConcernDetail
Entry pointfunction main(inputs: Inputs) — return value is the node output.
LanguageTypeScript.
InputsTyped from the agent's input mapping / input schema.
LLMcall_llm(user_prompt, system_prompt?) → string, metered and capped per run.
Integrationscall_integration(...) and the integrations namespace, available only when integrations are attached.
Bundled librarieslodash (_), uuid, papaparse (Papa), base64-js (base64).
Logginglog.info / log.warning / log.error, surfaced in the run's step trace.
IsolationSandboxed runtime; no direct network or filesystem access.

What's next

Last updated: Jul 3, 2026