Skip to Content

Code Plugin

Execute custom JavaScript in a sandboxed environment. No credentials required — reference upstream node data directly in your code using template variables.

Actions

ActionDescription
Run CodeExecute JavaScript with access to workflow data via templates

Run Code

Execute user-written JavaScript in a node:vm sandbox. The code runs in an async context, so return and await are available at the top level.

Template variables like {{NodeName.field}} are resolved by the workflow engine before execution — the code receives the actual values inline. Type @ in the editor to autocomplete available upstream node outputs.

Inputs

FieldTypeRequiredDefaultDescription
codestringyesJavaScript code to execute
timeoutnumberno60Execution timeout in seconds (1-120)

Outputs

FieldTypeDescription
successbooleanWhether the code executed successfully
resultanyThe return value of the executed code
logsarrayCaptured console.log, console.warn, console.error calls
errorstringError message if execution failed
linenumberLine number where the error occurred (if available)

How Template Variables Work in Code

Template variables are resolved before execution. The engine uses processCodeTemplates which JSON-stringifies values so they are valid JavaScript when inlined:

  • Strings become quoted: {{Manual.name}} -> "Alice"
  • Numbers stay as-is: {{Manual.count}} -> 42
  • Objects/arrays become JSON: {{Query.rows}} -> [{"id":1},{"id":2}]
  • null/undefined become: null

This means you write code as if the values are already there:

// If Query Events returns { events: [...] }, this becomes valid JS: const events = {{QueryEvents.events}}; // Resolves to: const events = [{"from":"0x...","value":100}, ...];

Available Globals

  • I/O: console, fetch
  • Core types: BigInt, JSON, Math, Date, Array, Object, String, Number, Boolean, RegExp, Symbol, Map, Set, WeakMap, WeakSet, Promise
  • Error types: Error, TypeError, RangeError, SyntaxError, ReferenceError, URIError
  • Numeric/parsing: parseInt, parseFloat, isNaN, isFinite, Infinity, NaN
  • URI encoding: encodeURIComponent, decodeURIComponent, encodeURI, decodeURI
  • Base64: atob, btoa
  • Text encoding: TextEncoder, TextDecoder
  • Binary/typed arrays: ArrayBuffer, DataView, Uint8Array, Uint16Array, Uint32Array, Int8Array, Int16Array, Int32Array, Float32Array, Float64Array, BigInt64Array, BigUint64Array
  • Fetch API: URL, URLSearchParams, Headers, Request, Response, AbortController, AbortSignal
  • Utilities: structuredClone, Intl, crypto.randomUUID

Not available: require, import, process, fs, eval, Function constructor, setTimeout, setInterval, or any Node.js built-in modules.

Security

The sandbox uses node:vm which prevents accidental access to Node.js internals but is not a security boundary against determined attackers. This is appropriate for a self-hosted platform where users are authenticated team members. maxRetries is set to 0 (fail-safe).

fetch is wrapped with an AbortController deadline matching the configured timeout, so network requests cannot hang indefinitely. A wall-clock Promise.race timeout also guards the entire execution, covering any async operation (not just fetch). Only crypto.randomUUID is exposed (crypto.subtle and other methods are not available).

Example Workflows

Filter and Aggregate Transfer Events

Filter large transfers from an event query and compute totals for an alert.

Trigger (Event: Transfer) -> Query Events: get recent transfers -> Run Code (Analyze Transfers): const events = {{QueryEvents.events}}; const threshold = BigInt('1000000000000000000'); // 1 ETH in wei const large = events.filter(e => BigInt(e.value) > threshold); const total = large.reduce((sum, e) => sum + BigInt(e.value), 0n); return { count: large.length, total: total.toString(), addresses: large.map(e => e.from) }; -> Condition: {{@code:Run Code.result.count}} > 0 -> Discord: "{{@code:Run Code.result.count}} large transfers totaling {{@code:Run Code.result.total}} wei"

Fetch External Price Data

Call an external API from within the code node and format the result.

Trigger (Schedule, every 5m) -> Read Contract (Oracle): latestAnswer on price feed -> Run Code (Enrich Price): const onChainPrice = {{ReadContract.result}}; const res = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd'); const data = await res.json(); const marketPrice = data.ethereum.usd; const oraclePrice = Number(onChainPrice) / 1e8; const deviation = Math.abs(oraclePrice - marketPrice) / marketPrice * 100; return { oraclePrice: oraclePrice.toFixed(2), marketPrice: marketPrice.toFixed(2), deviationPct: deviation.toFixed(2) }; -> Condition: {{@code:Run Code.result.deviationPct}} > 5 -> Discord: "Oracle deviation: {{@code:Run Code.result.deviationPct}}% (oracle: ${{@code:Run Code.result.oraclePrice}}, market: ${{@code:Run Code.result.marketPrice}})"

Format Complex Alert Payload

Build a structured notification payload from multiple upstream nodes.

Trigger (Webhook) -> Database Query: get user preferences -> Read Contract: get current token balance -> Run Code (Build Alert): const user = {{DatabaseQuery.rows}}[0]; const balance = {{ReadContract.result}}; const balanceEth = (Number(balance) / 1e18).toFixed(4); const timestamp = new Date().toISOString(); const msg = [ `Wallet: ${user.wallet_address}`, `Balance: ${balanceEth} ETH`, `Threshold: ${user.alert_threshold} ETH`, `Time: ${timestamp}` ].join('\n'); return { message: msg, shouldAlert: Number(balanceEth) < user.alert_threshold }; -> Condition: {{@code:Run Code.result.shouldAlert}} == true -> SendGrid: send email with {{@code:Run Code.result.message}}

Deduplicate and Rank Events

Process a batch of events to remove duplicates and rank by value.

Trigger (Schedule, hourly) -> Query Events: get last hour of Swap events -> Run Code (Process Swaps): const swaps = {{QueryEvents.events}}; // Deduplicate by transaction hash const seen = new Set(); const unique = swaps.filter(s => { if (seen.has(s.transactionHash)) return false; seen.add(s.transactionHash); return true; }); // Sort by value descending unique.sort((a, b) => Number(BigInt(b.value) - BigInt(a.value))); // Take top 10 const top = unique.slice(0, 10).map((s, i) => ({ rank: i + 1, tx: s.transactionHash, value: (Number(s.value) / 1e18).toFixed(4) })); return { total: unique.length, duplicatesRemoved: swaps.length - unique.length, top }; -> Discord: "{{@code:Run Code.result.total}} unique swaps ({{@code:Run Code.result.duplicatesRemoved}} dupes removed)"

Webhook Payload Transformation

Reshape incoming webhook data into the format expected by a downstream API.

Trigger (Webhook): receives { "alerts": [...], "source": "grafana" } -> Run Code (Transform): const payload = {{Webhook.input}}; const alerts = payload.alerts || []; const critical = alerts.filter(a => a.severity === 'critical'); const summary = critical.map(a => ({ title: a.labels.alertname, description: a.annotations.description, startsAt: a.startsAt })); // Base64 encode for downstream API that expects it const encoded = btoa(JSON.stringify(summary)); return { count: critical.length, summary, encoded }; -> Condition: {{@code:Run Code.result.count}} > 0 -> HTTP Request: POST to incident API with body {{@code:Run Code.result.encoded}}

Multi-Source Data Join

Combine data from a database query and a contract read into a unified view.

Trigger (Schedule, daily) -> Database Query: get monitored addresses with labels -> For Each: iterate addresses -> Read Contract (Balance): balanceOf for each address -> Run Code (Join Results): const addresses = {{DatabaseQuery.rows}}; const balances = {{ForEach.results}}; const report = addresses.map((addr, i) => { const bal = balances[i]?.result ?? '0'; const balFormatted = (Number(bal) / 1e18).toFixed(4); return { label: addr.label, address: addr.address, balance: balFormatted }; }); const totalBal = report.reduce((s, r) => s + parseFloat(r.balance), 0); report.sort((a, b) => parseFloat(b.balance) - parseFloat(a.balance)); return { report, totalBalance: totalBal.toFixed(4), walletCount: report.length }; -> Discord: "Daily report: {{@code:Run Code.result.walletCount}} wallets, total {{@code:Run Code.result.totalBalance}} ETH"

Anomaly Detection with Rolling Window

Compare the current value against a historical average to detect anomalies.

Trigger (Event: Transfer) -> Database Query: get last 24h transfer values -> Run Code (Detect Anomaly): const history = {{DatabaseQuery.rows}}; const currentValue = {{Trigger.value}}; if (history.length < 5) { return { isAnomaly: false, reason: 'insufficient data' }; } const values = history.map(r => Number(r.value)); const mean = values.reduce((s, v) => s + v, 0) / values.length; const variance = values.reduce((s, v) => s + (v - mean) ** 2, 0) / values.length; const stdDev = Math.sqrt(variance); const zScore = Math.abs((Number(currentValue) - mean) / stdDev); return { isAnomaly: zScore > 3, zScore: zScore.toFixed(2), mean: mean.toFixed(2), stdDev: stdDev.toFixed(2), currentValue: Number(currentValue).toFixed(2) }; -> Condition: {{@code:Run Code.result.isAnomaly}} == true -> Discord: "Anomaly detected: value {{@code:Run Code.result.currentValue}} (z-score: {{@code:Run Code.result.zScore}}, mean: {{@code:Run Code.result.mean}})"