Skip to main content

Code Component

The Code component runs your own JavaScript (Node.js 22) as a step in a workflow. Use it when the built-in components can't express what you need, for example:

  • multi-step API calls where one request depends on another (paging, look-ups, fan-out)
  • calling a system through a Connection, with authentication handled for you
  • data reshaping that is too complex for a Transform
  • working with files: CSV, Excel, ZIP, SFTP
  • querying a Microsoft SQL Server database

This page assumes you're comfortable with JavaScript, including async/await and Promises.


Quick start​

const authorizedAxios = require('authorizedAxios');
const getContext = require('getContext');

module.exports = async function (prevStep, allSteps, trigger) {
// 1. Load the Connection by name (or by its CONNECTION-... id)
const connection = await getContext('globals.connection', 'M3 Test');

// 2. Create an HTTP client that is already authenticated with that Connection
const axios = await authorizedAxios(connection);

// 3. Call the API
const result = await axios.post(
'https://mingle-ionapi.eu1.inforcloudsuite.com/VINCE_DEM/CustomerApi/m3iptest/execute/m3api-rest/v2/execute',
{
program: 'MNS150MI',
transactions: [{ transaction: 'GetUserInfo', record: {} }]
}
);

// 4. Whatever you return becomes this step's output
return result.data;
};

How the script runs​

Your script must export a single function with module.exports. The platform calls that function once per execution and passes it three arguments:

module.exports = async function (prevStep, allSteps, trigger) {
// ...
return output;
};
ArgumentContents
prevStepOutput of the step directly before this one.
allStepsArray of every earlier step's output, in execution order. allSteps[0] is the first step after the trigger, and the last item is the same as prevStep.
triggerThe data the workflow was started with, from the Trigger component.

The names are up to you. The arguments are passed by position, so function (prev, steps, input) works just as well.

Output​

  • The value you return becomes the step's output, and the next step receives it as prevStep. It must be JSON-serialisable: plain objects, arrays, strings, numbers, booleans or null.
  • If you return an array, its length is reported as the step's number of processed records. Anything else counts as 1.
  • If you forget to return, the step outputs nothing.

Errors​

If your code throws, or a Promise you await rejects, the step fails and the workflow reports the error. You can fail a step on purpose:

if (!prevStep?.orderNumber) {
throw new Error('Missing orderNumber from previous step');
}

To handle an error without failing the step, use try/catch:

try {
const res = await axios.get(url);
return res.data;
} catch (err) {
// err.response is set when the server answered with a non-2xx status
return { ok: false, status: err.response?.status, body: err.response?.data };
}

Limits​

  • A step times out after 3 minutes (180 seconds). Split long jobs over several steps, or process data in batches.
  • Code runs in an isolated sandbox. require can only load the modules listed under Available modules. Anything else fails at runtime.
  • There is no file system. Keep data in memory or send it somewhere else, for example SFTP or an API.
  • console.log output is not shown in the workflow execution view. To inspect values while you build, return them from the step, for example return { debug: someValue }.
  • Obfuscated code, or code that uses known sandbox-escape patterns such as Symbol.species or util.inspect.custom, is rejected before it runs.

async / await: read this first​

Every platform helper returns a Promise. That covers getContext, authorizedAxios, and every HTTP call you make with the client. If you don't await them, your code keeps a pending Promise rather than the value, and fails in confusing ways further down.

Follow these rules:

  1. Declare the exported function as async. await only works inside an async function. Without it you get a syntax error.

    module.exports = async function (prevStep, allSteps, trigger) { ... }
  2. await every helper call.

    // ❌ Wrong: `connection` is a Promise, so authorizedAxios receives no real connection
    const connection = getContext('globals.connection', 'M3 Test');
    const axios = await authorizedAxios(connection);

    // ✅ Right
    const connection = await getContext('globals.connection', 'M3 Test');
    const axios = await authorizedAxios(connection);
  3. await the client itself, not only the request. authorizedAxios(...) returns a Promise of a client, not a client:

    // ❌ Wrong: TypeError: axios.post is not a function
    const axios = authorizedAxios(connection);
    await axios.post(url, body);

    // ✅ Right
    const axios = await authorizedAxios(connection);
    await axios.post(url, body);
  4. Don't use forEach with async callbacks. forEach doesn't wait, so the function returns before the requests finish, and any errors are lost.

    // ❌ Wrong: returns before any request has finished
    items.forEach(async (item) => { await axios.post(url, item); });

    // ✅ Sequential: one request at a time, in order
    const results = [];
    for (const item of items) {
    const res = await axios.post(url, item);
    results.push(res.data);
    }

    // ✅ Parallel: all at once, wait for all of them
    const results = await Promise.all(
    items.map(async (item) => (await axios.post(url, item)).data)
    );

    Promise.all is faster, but it sends every request at the same time. For large lists, or APIs with rate limits, go sequential or send in small batches.

  5. Create the client once and reuse it. Call getContext and authorizedAxios once at the top of your function, not inside a loop.


getContext: read platform data​

const getContext = require('getContext');

const value = await getContext(path, name);

getContext gives your script read access to platform data: Connections, Variables, the tenant, the current user, and details of the running workflow. It always returns a Promise, so always await it.

pathnameReturns
'globals.connection'Connection name, or id (CONNECTION-…)The full Connection object, including credentials. Pass it to authorizedAxios.
'globals.variable'Variable name, or id (VARIABLE-…)The Variable object. Read the value from .value. Secret variables come back decrypted.
'env'(optional) 'apiUrl' or 'apiDomain'Platform API address for the current environment, e.g. { apiDomain: 'api.vince.live', apiUrl: 'https://api.vince.live' }. Pass a key to get just that string.
'tenant'(none)The tenant the workflow runs in, e.g. { tenantId: 'TENANT-…', companyName, companyEmail, active, … }. Use tenantId to tag data or to tell tenants apart in shared scripts.
'user'(none)The user who started the execution. For system runs, such as schedules and background jobs, you only get { userId }.
'workflowExecution'(none){ workflowId, executionId, stepId, stepConfig, workflowConfig } for the current run.

If a Connection or Variable with the given name doesn't exist, you get undefined back rather than an error. Check for it yourself so the step fails with a clear message:

const connection = await getContext('globals.connection', 'M3 Test');
if (!connection) {
throw new Error('Connection "M3 Test" not found');
}

Example: read a Variable​

const pageSize = await getContext('globals.variable', 'M3_PAGE_SIZE');
const limit = Number(pageSize?.value ?? 100);

Variable values are always strings. Convert them to the type you need (Number(...), JSON.parse(...), === 'true').

Example: sign a payload with a secret Variable​

const secret = (await getContext('globals.variable', 'PARTNER_SIGNING_SECRET')).value;

const signature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');

Don't use a Variable for credentials that log in to another system, such as an API key, bearer token, password or client secret. Put them on a Connection and call the system with authorizedAxios, which authenticates every request for you.

Example: tag outgoing data with the execution id​

const { workflowId, executionId } = await getContext('workflowExecution');
return { ...prevStep, source: { workflowId, executionId } };
caution

Connections and secret Variables come back with their credentials in plain text. Never return them from the step or put them in request bodies or logs. Anything you return is stored as step output and is visible to anyone who can view the execution.


authorizedAxios: authenticated HTTP through a Connection​

const authorizedAxios = require('authorizedAxios');

const client = await authorizedAxios(connection);

authorizedAxios takes a Connection object from getContext('globals.connection', …) and returns an axios instance already set up for that Connection. Every request you send with it is authenticated for you. You never handle tokens or passwords in your code.

warning

Pass the Connection object, not the Connection's name. authorizedAxios('M3 Test') doesn't work. Load it with getContext first.

What it handles for you​

Connection settingWhat authorizedAxios does
OAuth 2.0 (client credentials, password grant)Fetches the access token, caches it, refreshes it when needed, and sends it as Authorization: Bearer ….
BasicSends the Authorization: Basic … header.
API KeySends the key in the Authorization header, or in a custom header if Auth header name is set on the Connection.
JWT (self-signed)Signs a JWT with the Connection's private key and sends it.
NoneNo auth header. Headers and static IP settings still apply.
Custom headers marked use for requestsAdded to every request.
Use Static IPRoutes requests through Vince's static IP, so you can reach systems that allow-list by source IP.
Ignore certificate errorsAccepts self-signed or invalid TLS certificates.
AWS Signature v4 signingSigns every request with the Connection's AWS keys.

Using the client​

The client is a standard axios instance, so everything in the axios docs applies: get, post, put, patch, delete, request, params, custom headers, responseType, and so on. The response body is in res.data.

The client doesn't set a base URL, so always pass a full URL. To avoid hard-coding the host, build it from the Connection's base URI:

const connection = await getContext('globals.connection', 'M3 Test');
const axios = await authorizedAxios(connection);
const baseUri = connection.attributes.baseUri;

const res = await axios.get(`${baseUri}/M3/m3api-rest/v2/execute/CRS610MI/LstByNumber`, {
params: { maxrecs: 50 }
});
return res.data;

Example: page through an API​

const authorizedAxios = require('authorizedAxios');
const getContext = require('getContext');

module.exports = async function (prevStep, allSteps, trigger) {
const connection = await getContext('globals.connection', 'Webshop API');
const axios = await authorizedAxios(connection);

const all = [];
let page = 1;
while (true) {
const res = await axios.get('https://api.webshop.example.com/v1/orders', {
params: { page, pageSize: 200, status: 'new' }
});
all.push(...res.data.items);
if (!res.data.hasMore) break;
page++;
}
return all;
};

Example: combine two systems​

const authorizedAxios = require('authorizedAxios');
const getContext = require('getContext');

module.exports = async function (prevStep, allSteps, trigger) {
// Two Connections, two clients. Load both in parallel.
const [m3Conn, crmConn] = await Promise.all([
getContext('globals.connection', 'M3 Prod'),
getContext('globals.connection', 'CRM')
]);
const [m3, crm] = await Promise.all([
authorizedAxios(m3Conn),
authorizedAxios(crmConn)
]);

const customer = (await crm.get(`https://crm.example.com/api/customers/${trigger.customerId}`)).data;

const res = await m3.post(`${m3Conn.attributes.baseUri}/M3/m3api-rest/v2/execute`, {
program: 'CRS610MI',
transactions: [{
transaction: 'ChgBasicData',
record: { CUNO: customer.erpNumber, CUNM: customer.name }
}]
});
return res.data;
};

authorizedSocks5: reach a firewalled system through a static IP​

const authorizedSocks5 = require('authorizedSocks5');

const connector = await authorizedSocks5(connection, { host, port });

Some systems — most often an on-premise or firewalled SQL Server — only accept connections from an allow-listed IP address. authorizedSocks5 routes the TCP connection through the Vince Platform's static outbound IP, so the target system always sees the same, known source address, regardless of which platform instance actually runs the step.

authorizedSocks5 takes a Connection object (loaded with getContext('globals.connection', …), same as authorizedAxios) plus the host and port you're connecting to, and returns a Promise for a connector. Pass that connector into a TCP client's connection options — for example the connector option on an mssql ConnectionPool — instead of connecting directly. authorizedSocks5 only arranges how the socket is opened; the client itself still does the actual protocol handshake.

See Static IP for the concept behind this — why cloud-to-on-prem calls need a fixed source address, which addresses Vince uses, and the Dedicated IP option.

warning

The Connection you pass to authorizedSocks5 must have Use Static IP enabled. Without it, authorizedSocks5 has no proxy to route through: it resolves to undefined instead of a connector, and the target system's firewall or IP allow-list will reject the connection. Enable it on the Connection before using authorizedSocks5 with it.

Example: query SQL Server through a static IP​

const authorizedSocks5 = require('authorizedSocks5');
const sql = require('mssql');
const getContext = require('getContext');

module.exports = async function (prevStep, allSteps, trigger) {
const conn = await getContext('globals.connection', 'Warehouse DB');

// Routes the connection through the Vince Platform's static IP. The
// Connection must have "Use Static IP" enabled, or this resolves to
// `undefined`.
const connector = await authorizedSocks5(conn, {
host: conn.attributes.baseUri,
port: Number(conn.attributes.port ?? 1433)
});

const pool = await new sql.ConnectionPool({
server: conn.attributes.baseUri,
port: Number(conn.attributes.port ?? 1433),
database: 'WMS',
user: conn.secureAttributes.username,
password: conn.secureAttributes.password,
options: {
encrypt: true,
...(connector ? { connector } : {})
}
}).connect();

try {
const result = await pool.request().query('SELECT sku, qty FROM stock');
return result.recordset;
} finally {
await pool.close();
}
};

The ...(connector ? { connector } : {}) spread keeps this step from breaking if it's ever pointed at a Connection without Use Static IP enabled — it just falls back to a direct connection. Enable the setting on the Connection rather than relying on that fallback.


Available modules​

Load modules with require('<name>'). Only these names are available:

require(...)What it isTypical use
getContextPlatform helper, see above.Load Connections, Variables, tenant, user, execution info.
authorizedAxiosPlatform helper, see above.Authenticated HTTP through a Connection.
authorizedSocks5Platform helper, see above.Proxy a TCP connection (e.g. SQL Server) through the Connection's static IP.
axiosInstanceA plain axios instance with no authentication.Calls to public APIs, or APIs where you set the auth header yourself.
axiosSame as axiosInstance. Deprecated. Use axiosInstance in new code.
form-dataform-dataBuild multipart/form-data bodies for file uploads.
fflatefflateZip, unzip, gzip, gunzip.
ssh2-sftp-clientssh2-sftp-clientRead, write and list files on SFTP servers.
mssqlmssqlQuery Microsoft SQL Server.
xlsxSheetJS v0.18.5Read and write Excel and CSV files.
VinceLiveMorphVince JSONata engine, the same one the Transform component uses.Run a JSONata expression from code: VinceLiveMorph.parse(expression, data).
cryptoNode.js cryptoHashing, HMAC signatures, UUIDs (crypto.randomUUID()).
streamNode.js streamStream-based APIs, e.g. for SFTP.

Standard JavaScript globals such as Buffer, URL, URLSearchParams, JSON, Date, Math, setTimeout and Promise are also available.

Example: plain HTTP call (no Connection)​

const axiosInstance = require('axiosInstance');

module.exports = async function (prevStep) {
const res = await axiosInstance.get('https://api.frankfurter.app/latest', {
params: { from: 'EUR', to: 'NOK' }
});
return res.data;
};

Example: sign a webhook with HMAC​

const crypto = require('crypto');
const axiosInstance = require('axiosInstance');
const getContext = require('getContext');

module.exports = async function (prevStep) {
const secret = (await getContext('globals.variable', 'WEBHOOK_SECRET')).value;
const body = JSON.stringify(prevStep);
const signature = crypto.createHmac('sha256', secret).update(body).digest('hex');

const res = await axiosInstance.post('https://partner.example.com/webhook', body, {
headers: { 'content-type': 'application/json', 'x-signature': signature }
});
return { status: res.status };
};

Example: parse an Excel file downloaded from an API​

const XLSX = require('xlsx');
const authorizedAxios = require('authorizedAxios');
const getContext = require('getContext');

module.exports = async function () {
const axios = await authorizedAxios(await getContext('globals.connection', 'SharePoint'));
const res = await axios.get('https://example.sharepoint.com/.../prices.xlsx', {
responseType: 'arraybuffer'
});

const workbook = XLSX.read(res.data, { type: 'buffer' });
const sheet = workbook.Sheets[workbook.SheetNames[0]];
return XLSX.utils.sheet_to_json(sheet); // array of row objects
};

Example: upload a CSV to SFTP​

const SftpClient = require('ssh2-sftp-client');
const getContext = require('getContext');

module.exports = async function (prevStep) {
const conn = await getContext('globals.connection', 'Partner SFTP');
const csv = ['sku;qty', ...prevStep.map((r) => `${r.sku};${r.qty}`)].join('\n');

const sftp = new SftpClient();
try {
await sftp.connect({
host: conn.attributes.baseUri,
port: conn.attributes.port ?? 22,
username: conn.secureAttributes.username,
password: conn.secureAttributes.password
});
await sftp.put(Buffer.from(csv, 'utf8'), `/inbound/stock_${Date.now()}.csv`);
} finally {
await sftp.end(); // always close the connection, even on error
}
return { uploaded: prevStep.length };
};

Example: query SQL Server​

const sql = require('mssql');
const getContext = require('getContext');

module.exports = async function (prevStep, allSteps, trigger) {
const conn = await getContext('globals.connection', 'Warehouse DB');

const pool = await sql.connect({
server: conn.attributes.baseUri,
port: Number(conn.attributes.port ?? 1433),
user: conn.secureAttributes.username,
password: conn.secureAttributes.password,
database: 'WMS',
options: { encrypt: true, trustServerCertificate: false }
});
try {
const result = await pool.request()
.input('warehouse', sql.VarChar, trigger.warehouse)
.query('SELECT sku, qty FROM stock WHERE warehouse = @warehouse');
return result.recordset;
} finally {
await pool.close();
}
};

Always use .input(...) parameters rather than building SQL strings from input data. Parameters protect against SQL injection.

Example: run a JSONata expression​

const VinceLiveMorph = require('VinceLiveMorph');

module.exports = async function (prevStep) {
return VinceLiveMorph.parse('$sum(lines.(qty * price))', prevStep);
};

Troubleshooting​

SymptomLikely cause
SyntaxError: await is only valid in async functionsThe exported function isn't declared async.
TypeError: axios.post is not a functionMissing await before authorizedAxios(...).
Requests go out without authentication, or Cannot read properties of undefinedMissing await before getContext(...), or a Connection name that doesn't exist. Check for undefined.
Step finishes but output is emptyYou didn't return anything, or you used forEach with async callbacks.
Cannot find module '…'The module isn't in the available modules list.
401 / 403 from the target systemCheck the Connection's credentials and scopes under Connections. The Connection may also be inactive.
Step times outThe step ran longer than 3 minutes. Reduce page sizes, send requests in parallel batches, or split the work across steps.
Code contains exploits!The code uses a blocked sandbox-escape pattern. See Limits.
Target system refuses the connection, or authorizedSocks5 resolves to undefinedThe Connection doesn't have Use Static IP enabled.

Further reading​

  • Transform: no-code alternative for reshaping JSON with JSONata.
  • REST API: use this for a single HTTP request that needs no custom logic.
  • Connections: set up the authentication that authorizedAxios uses, and the Use Static IP option that authorizedSocks5 requires.
  • Static IP: how Vince's static IP works, the shared IP addresses, and the Dedicated IP option.