Code Steps
A Code Step runs a small piece of JavaScript inside your Action.
Use a Code Step when you need deterministic logic that should behave the same way every time, without depending on a language model. It is ideal for the "glue" work between AI and tool steps in an accounts-payable Action Flow: reshaping data, validating totals, normalizing vendor identifiers, and deciding how an invoice should be routed.
Common uses include:
- validating that invoice line amounts add up to the header total,
- normalizing or cleaning extracted invoice fields,
- mapping an AI or OCR result into the exact shape a tool expects,
- computing due dates, discounts, or tax splits,
- choosing a routing decision (auto-post, hold, or review) from clear business rules.
For general step concepts, see Step Types. For AI reasoning steps, see Agent Steps.
When to Use a Code Step
Prefer a Code Step over an Agent Step whenever the logic can be expressed as explicit rules.
- Use a Code Step for math, comparisons, formatting, and data transformation. The result is exact and repeatable.
- Use an Agent Step when the task needs judgement over unstructured content, such as reading a document or classifying an ambiguous request.
A typical accounts-payable Action mixes both: an Agent or OCR step extracts the invoice fields, a Code Step validates and normalizes them, and a Tool step posts the result to your finance system.
Adding a Code Step
When you add a step, choose Code Step in the step type picker. It is described as a "JavaScript code step for custom data transformations and logic".
After the step is created, you write your JavaScript in the Step Configuration editor.
Writing Code
The editor under Parameters is a JavaScript code editor. Your code runs as the body of a function, so you produce the step output with a return statement.
A minimal Code Step looks like this:
return { result: "Hello from Code Step" };
The object you return becomes the step output. Later steps can read its fields by name.
Always
returnan object. If your code does not return anything, the step output is empty and later steps will not see any values from it.
Available Variables and Functions
Inside a Code Step you have access to a few built-in variables and helper functions:
steps— the outputs of previous steps, keyed by step name, for examplesteps["Extract invoice"].vendorName.inputs(also available asinput) — the Action's input parameters and any assigned step input data. Loop Over batches are available asinput.loopData.context— run metadata:runId,actionId,actionName,startedUtc, andcurrentStepIndex.executeTool(toolName, parameters)— calls a tool that is assigned to this step (see Calling a Tool from Code).decodeBase64Utf8(value)— decodes a base64 string into UTF-8 text, useful for base64-encoded file or message payloads.variables.get(name)— reads a tenant Variable value, for examplevariables.get("BASE_URL"). Returnsnullif the Variable is not set.variables.set(name, value)— creates or updates a tenant Variable. The value persists after the run and is shared across Actions, so it is handy for counters or state that must survive between runs. Names must start with a letter and contain only letters, numbers, and underscores; values are capped at 8192 characters, and a tenant can hold at most 1000 Variables.
Standard JavaScript is available too, including JSON, Math, Date, String, Number, Array, and Object.
The editor shows this same reference, with short examples, in the Available Variables & Functions panel directly below the code.
Referencing previous steps and inputs
The steps and inputs objects mirror the {{Step Name.field}} references used in prompts, but in code you use normal JavaScript property access:
// Prompt style: {{Extract invoice.vendorName}}
// Code style:
const vendorName = steps["Extract invoice"].vendorName;
// Prompt style: {{input.invoiceId}}
// Code style:
const invoiceId = inputs.invoiceId;
When an earlier Agent Step uses structured output, read its parsed object through structured:
const decision = steps["Review invoice"].structured;
return { postingDecision: decision.postingDecision, vendorId: decision.vendorId };
You can drag a value from the step output sidebar into the editor, and Studio inserts the matching steps... or inputs... accessor for you.
Example: Validate Invoice Totals
A common first check in accounts payable is confirming that the invoice line amounts add up to the header total, and that the amount is within an automatic-posting limit. This is pure arithmetic, so it belongs in a Code Step.
// 'inputs.invoice' carries the invoice payload provided to the Action.
const invoice = inputs.invoice;
const lines = invoice.lines || [];
const lineTotal = lines.reduce((sum, line) => sum + line.amount, 0);
const headerTotal = invoice.totalAmount;
// Round to 2 decimals to avoid floating-point noise.
const round2 = (n) => Math.round(n * 100) / 100;
const difference = round2(headerTotal - lineTotal);
const AUTO_POST_LIMIT = 5000;
const totalsMatch = Math.abs(difference) < 0.01;
const withinLimit = headerTotal <= AUTO_POST_LIMIT;
let routingDecision = "review";
if (totalsMatch && withinLimit) {
routingDecision = "autoPost";
}
return {
invoiceNumber: invoice.invoiceNumber,
vendorId: invoice.vendorId,
headerTotal: round2(headerTotal),
lineTotal: round2(lineTotal),
difference: difference,
totalsMatch: totalsMatch,
withinAutoPostLimit: withinLimit,
routingDecision: routingDecision
};
A following Switch Step can branch on routingDecision to either post the invoice automatically or send it to a reviewer.
Number types: a whole-number result such as
42comes back as an integer, but arithmetic likeprice * 1.24produces a decimal. Round monetary values explicitly, as shown above, so downstream comparisons stay predictable.
Example: Normalize Extracted Invoice Fields
OCR and AI extraction often return values that are almost right but not clean: extra whitespace, inconsistent casing, currency symbols, or thousands separators. A Code Step turns that raw output into a tidy, schema-safe object.
const raw = steps["Extract invoice"].structured;
// Vendor IDs should be uppercase with no surrounding spaces.
const vendorId = (raw.vendorId || "").trim().toUpperCase();
// Strip currency symbols and thousands separators, then parse.
const parseAmount = (value) => {
const cleaned = String(value).replace(/[^0-9.-]/g, "");
return Math.round(parseFloat(cleaned) * 100) / 100;
};
const amount = parseAmount(raw.totalAmount);
// Normalize the currency code to ISO style.
const currency = (raw.currency || "EUR").trim().toUpperCase();
const missingFields = [];
if (!vendorId) missingFields.push("vendorId");
if (!raw.invoiceNumber) missingFields.push("invoiceNumber");
if (isNaN(amount)) missingFields.push("totalAmount");
return {
vendorId: vendorId,
invoiceNumber: (raw.invoiceNumber || "").trim(),
totalAmount: amount,
currency: currency,
isComplete: missingFields.length === 0,
missingFields: missingFields
};
Because the output shape is fixed, later steps and tools can rely on totalAmount always being a number and vendorId always being uppercase.
Calling a Tool from Code
A Code Step can call a tool that is assigned to it, using executeTool(toolName, parameters). This is useful when you need trusted system data, such as confirming a vendor exists before posting an invoice.
Assign the tool to the step first (in the Tools panel), then call it by name:
// Look up the vendor to confirm it is active before approving the invoice.
const lookup = executeTool("Get_Vendor", {
vendor_id: inputs.invoice.vendorId
});
if (!lookup.success) {
return { approved: false, reason: "Vendor lookup failed", routingDecision: "review" };
}
const vendor = lookup.data;
const approved = vendor.status === "Active" && !vendor.onHold;
return {
approved: approved,
vendorName: vendor.name,
routingDecision: approved ? "autoPost" : "review"
};
A few things to know about tool calls in code:
- The tool must be assigned to the step. Calling an unassigned tool returns an error object instead of throwing.
- A tool result is shaped as
{ success, data, statusCode }on success, or{ success: false, error, statusCode }on failure. - A failed tool call does not fail the step. Always check
result.successand decide what your step should return. - Tool calls run one at a time, in order. Keep the number of calls small for speed and cost.
Output and Settings
The Settings tab covers how the step output is used and how errors are handled.
- Step output is whatever object your code returns. Its fields are dynamic, so other steps reference them as
{{Step Name.fieldName}}. After you run the Action once, the editor can autocomplete these field paths. - Step Error Action lets a Code Step use its own error Action instead of the Action-level one. Leave it as the default to fall back to normal Action error handling. See Error Handling.
Limits and Sandbox
Code Steps run in a restricted JavaScript sandbox so they stay fast and safe:
- No network or file access. The only way to reach an external system is
executeToolagainst an assigned tool. There is nofetch, and there is noconsole. - Execution timeout: roughly 30 seconds.
- Statement limit: up to about 10,000 statements, so avoid unbounded loops.
- Memory limit: a few megabytes; keep payloads focused rather than copying huge arrays.
If a step exceeds a limit or throws an error, it fails with a clear message and normal step or Action error handling applies.
Debugging Code Steps
Use Runs to inspect Code Step behavior.
In run details you can review:
- the code that ran,
- the input data and previous step outputs the code received,
- any tool calls the code made and their results,
- the returned output object,
- the error message if the step failed.
When the output is not what you expected, first confirm the code received the values you intended. Most issues come from a missing input, an unexpected field name, or assuming a tool succeeded without checking result.success.
Best Practices
- Use Code Steps for deterministic logic; leave judgement over unstructured content to Agent Steps.
- Always
returnan object, and keep its shape stable so later steps can depend on it. - Round monetary values explicitly and compare with a small tolerance.
- Validate inputs and handle missing fields instead of assuming they are present.
- Check
result.successafter everyexecuteToolcall. - Keep the code small and focused; one clear responsibility per step makes runs easy to read.