CommunityHire an ExpertSign Up
ActionsRun Custom Code
Actions

Run Custom Code

Run JavaScript or Python inside a workflow: read the data of earlier steps, return a result, call HTTP APIs with fetch, let your connected integrations authenticate those calls, and use the built-in cryptography modules.

Run Custom Code

Applies to: Run Custom Code 1.0.0, 1.1.0, 1.2.0

The Run Custom Code action runs JavaScript or Python inside your workflow. Use it for logic that formulas cannot express: parsing complex JSON, multi-step transformations, or calls to an API that has no dedicated action yet. The code can call HTTP APIs, and from version 1.2.0 the integrations you attach to the step authenticate those calls for you.

How the step works

  1. Data to use in the code: name the values your code needs and map them to outputs of earlier steps. The code reads them from inputData.
  2. Integrations the code can call (1.2.0 and later): attach the connected integrations the code may talk to. Each one gets an alias, a short name the code can use.
  3. Language and Code: write the body of a function. Whatever you return becomes the output of the step, available to later steps as body.

Run Custom Code action with JavaScript

Run Custom Code action with JavaScript extracting a domain from an email

Reading input data

Every entry of Data to use in the code is a key of inputData. A value that is exactly one variable keeps its type: a number stays a number, a record stays an object, a list stays an array.

javascript
const email = inputData.email;
const items = inputData.items;
return { domain: email.split('@')[1], count: items.length };
Python
email = inputData['email']
items = inputData['items']
return {'domain': email.split('@')[1], 'count': len(items)}

Returning a result

Return any JSON-compatible value: an object, a list, a string, a number or a boolean. Later steps reference it through the variable menu as body, or a field of it. A thrown error or a raised exception fails the step, and the message appears in the run details and in Test your step.

Calling APIs with fetch

Applies to: 1.2.0 and later

Both languages offer fetch(url, options). It sends an HTTP request and returns a response object. In JavaScript you await it; in Python it is a plain call.

javascript
const res = await fetch('https://api.example.com/v1/contacts', {
  method: 'POST',
  headers: { 'X-Request-Source': 'softr' },
  body: { email: inputData.email, name: inputData.name },
});
if (!res.ok) {
  throw new Error(`Contact API answered ${res.status}: ${await res.text()}`);
}
return await res.json();
Python
res = fetch('https://api.example.com/v1/contacts', {
    'method': 'POST',
    'headers': {'X-Request-Source': 'softr'},
    'body': {'email': inputData['email'], 'name': inputData['name']},
})
if not res.ok:
    raise RuntimeError(f'Contact API answered {res.status}: {res.text()}')
return res.json()

Options

OptionMeaning
methodGET by default; POST, PUT, PATCH, DELETE and the others work as expected.
headersAn object of header names and values.
bodyA string is sent as it is, as text/plain unless you set Content-Type. An object or a list is sent as JSON with Content-Type: application/json. In JavaScript, URLSearchParams is sent form-encoded.
integrationThe alias of an attached integration that should authenticate this call. Needed only when two attached integrations share a host.

Response

MemberMeaning
status, okThe HTTP status code, and whether it is in the 200 range.
headersThe response headers with lower-case names. JavaScript: res.headers.get('content-type'). Python: res.headers['content-type'].
text(), json()The body as text or parsed JSON. In JavaScript both are awaited.
arrayBuffer() (JavaScript), bytes() (Python)The raw body, for binary answers.

Letting an integration authenticate the call

Attach the integrations your code talks to under Integrations the code can call. When the code calls a URL on the API host of an attached integration, the request carries that integration’s credentials automatically. The code never sees a token, and nothing you paste into the code is needed.

javascript
// "salesforce" is attached to the step; the call to its API host is authenticated for you.
const res = await fetch(`${inputData.instanceUrl}/services/data/v60.0/sobjects/Account`, {
  method: 'POST',
  body: { Name: inputData.companyName },
});
return { created: res.ok, account: await res.json() };
Python
# Two attached integrations share the host api.hubapi.com; the alias picks the one to use.
res = fetch('https://api.hubapi.com/crm/v3/objects/contacts', {
    'method': 'POST',
    'body': {'properties': {'email': inputData['email']}},
    'integration': 'sales_hubspot',
})
return res.json()
  • The credentials are only added when the URL points at the provider’s own API host. A call to any other host goes out without them, whatever alias you pass.
  • You can attach every integration the Call API action can authenticate with. Database connections, Softr Databases, Softr Apps, Telegram and Trello cannot be attached, because their credentials have no HTTP header form.
  • An alias is a short name of letters, digits and underscores, unique within the step. It defaults to the integration type, such as salesforce or rest_api.
  • A REST API integration has no provider host, so it never authenticates a call by host alone. Name its alias in the call, and its headers go to whatever host that call names.

Calls without an integration

fetch also reaches any public host without an attached integration. The request is sent exactly as you wrote it, including any headers you set. Private networks, localhost and cloud metadata addresses are blocked.

Calling HTTP APIs with http and https

Applies to: 1.0.0, 1.1.0

Removed in: 1.2.0. Use fetch instead.

JavaScript can call HTTP APIs with the Node.js http and https modules. Return the promise of the result; the step waits for it.

javascript
return new Promise((resolve, reject) => {
  const url = 'https://api.example.com/v1/contacts?email=' + encodeURIComponent(inputData.email);
  const req = https.request(url, { headers: { Authorization: 'Bearer ' + inputData.apiToken } }, (res) => {
    let data = '';
    res.on('data', (chunk) => (data += chunk));
    res.on('end', () => resolve(JSON.parse(data)));
  });
  req.on('error', reject);
  req.end();
});

Python code has no network access in these versions. In JavaScript, localhost and the cloud metadata addresses are blocked.

Cryptography

Both languages come with a cryptography module. The code can hash values, compute and check HMAC signatures, and decrypt or verify data that a partner encrypted or signed with RSA.

JavaScript: crypto and Buffer

Applies to: 1.0.0, 1.1.0, 1.2.0

crypto is the Node.js crypto module: createHash, createHmac, privateDecrypt, publicEncrypt, sign, verify, randomUUID and the rest of it. Buffer converts between text, base64, hex and raw bytes.

javascript
const decrypted = crypto.privateDecrypt(
  { key: inputData.privateKeyPem, padding: crypto.constants.RSA_PKCS1_PADDING },
  Buffer.from(inputData.encryptedToken, 'base64'),
);
const signature = crypto.createHmac('sha256', inputData.signingSecret).update(inputData.payload).digest('hex');
return { token: decrypted.toString('utf8'), signature };

Python: rsa

Applies to: 1.0.0, 1.1.0, 1.2.0

import rsa gives you the API of the rsa package: rsa.PrivateKey.load_pkcs1 and rsa.PublicKey.load_pkcs1 read a PEM or DER key in PKCS#1 or PKCS#8 form, rsa.decrypt and rsa.verify accept the ciphertext or signature as base64 text or as bytes, and rsa.encrypt, rsa.sign, rsa.DecryptionError and rsa.VerificationError work as in the package. Padding is PKCS#1 v1.5.

Python
import rsa
key = rsa.PrivateKey.load_pkcs1(inputData['privateKeyPem'])
token = rsa.decrypt(inputData['encryptedToken'], key).decode()
return {'token': token}

Runtimes and limits

JavaScriptPython
RuntimeNode.js (1.0.0, 1.1.0); Node.js 24 (1.2.0 and later)Python 3.11 (1.0.0, 1.1.0); Python 3.12 (1.2.0 and later)
Availablecrypto (the Node.js crypto module: hashes, HMAC, RSA, AES, signatures, random values), Buffer, timers, promises, JSON, Math, Date, regular expressions. http and https (1.0.0, 1.1.0). fetch, URL, URLSearchParams, TextEncoder, TextDecoder, structuredClone and top-level await (1.2.0 and later)rsa (RSA with PKCS#1 v1.5 padding, PKCS#1 or PKCS#8 keys), the standard modules json, re, math, statistics, decimal, fractions, datetime, random, and the common built-in functions such as len, range, enumerate, zip, sorted, min, max, sum, str, int, float, dict, list, set, tuple. fetch, class definitions, isinstance, map, filter, bytes, and the further modules zoneinfo, time, calendar, secrets, collections, itertools, functools, operator, string, textwrap, unicodedata, difflib, html, base64, hashlib, hmac, uuid, struct, copy, heapq, bisect, typing, enum, dataclasses, csv, io (StringIO, BytesIO), urllib.parse (1.2.0 and later)
Not availablerequire, import, console output, the file system, environment variables. In 1.0.0 and 1.1.0 also fetch and top-level await: return a promise insteadopen, print output, os, sys, subprocess, requests, urllib.request, other third-party packages, attribute names that start with __. In 1.0.0 and 1.1.0 also class definitions, isinstance, map, filter, bytes and every module not listed above
Limit1.0.0, 1.1.01.2.0 and later
Run time of one stepJavaScript: a returned promise must settle within five seconds, or the step fails with Async script timed out after 5000ms. Python: three seconds; the step then fails with Execution timed out.About two minutes; the step then fails with Execution timed out.
Synchronous JavaScriptTwo seconds of code without a promise in between.Two seconds of code without an await in between.
Python CPU timeTwo of the three seconds.Two seconds; waiting for fetch does not count.
Python memory256 MB.256 MB.
fetch calls–Up to 20 per second per run, and up to 10 open at the same time.
One fetch–The target must answer within 30 seconds, or the call fails.
Request body–Up to 5 MB per fetch; a bigger body fails before it is sent.
Response size–Up to about 6 MB per fetch response.

When something fails

  • A script error fails the step with the message of the error or exception. Test the step to see it while you write the code.
  • A fetch error (1.2.0 and later) is raised as an exception in your code, so you can catch it. Its message names the reason: a blocked host, an unknown alias, too many calls, or a target that did not answer.
  • Execution timed out, Async script timed out and Memory limit exceeded name the limit the run hit. Split the work between steps, or narrow the data the step receives.

Versions

Steps created with an earlier version of Run Custom Code keep working. Opening such a step in the workflow editor upgrades it to the current version; code that used http or https must switch to fetch then. The Applies to line at the top of this page names the versions the page covers. An Applies to line under a heading names the versions that section applies to; without one, a section applies to every version of the page. A Removed in line names the version that dropped that part, and a Version history block lists what changed inside it.

VersionWhat changed
1.2.0fetch in both languages, and the integrations attached to the step authenticate the calls. Node.js 24 and Python 3.12. Class definitions and many more standard modules in Python, top-level await in JavaScript, about two minutes per run. http and https removed.
1.1.0The same inputs and runtime as 1.0.0.
1.0.0The first version.

Use these docs with your AI tools

An AI agent can read this documentation directly. You do not need an account or an API key. Everything here is public and read-only.

Query these docs via MCP

Recommended

Add this server to Claude, Claude Code, Cursor, Mistral, or any tool that supports MCP. Your agent can then search Softr Documentation documentation and read it in full, instead of answering from memory.

https://docs.softr.io/mcp
  • searchFind the passages that answer a question.
  • fetchRead one page in full, as Markdown.
  • list_pagesSee every page in this documentation.

Query these docs over HTTP

The same tools also work as plain web requests. Use this for scripts, or for any tool that does not support MCP. There is one endpoint per tool. Arguments go in the query string, and the answer comes back as JSON.

https://docs.softr.io/api/docs/search?query=custom+domain

Read the OpenAPI description. It is built from the same definitions as the tools, so it always matches what the endpoints do.

Read these docs as Markdown

Add .md to any page URL to get its Markdown source. You can also send the headerAccept: text/markdown to the page URL itself.

To read the whole documentation in one file, open llms-full.txt. For a short index of every page, open llms.txt.