Data Sources

JavaScript Scripts

Write custom JavaScript to fetch, combine, and transform data from any source.

JavaScript Scripts

Script data sources let you write custom JavaScript to pull data from any API, combine multiple sources, or run complex transformations — all without setting up a backend. Your code runs in a sandboxed Node.js environment with a built-in HTTP client.

Quick Start

  1. Go to the Data page and click New Data Source
  2. Select Script — this opens the full-screen script editor
  3. Accept the Terms of Service (one-time per team)
  4. Write your script in the left panel
  5. Click Run to test — console output and returned data appear in the right panel
  6. Name your source and click Create Source
  7. In the chart editor, switch to the Connect tab and select your script source

How Scripts Work

Your script is the body of an async function that receives three parameters:

// These are available as top-level variables:
// secrets — your team's decrypted secrets (object)
// fetch   — SSRF-protected HTTP client (like window.fetch)
// params  — per-chart parameters (object)

const res = await fetch("https://api.example.com/data");
const json = await res.json();
return json;

You must return an array of objects. Each object becomes a row of data for your chart.

// ✅ Good — array of objects
return [
  { month: "Jan", sales: 12000 },
  { month: "Feb", sales: 15000 }
];

// ❌ Bad — not an array
return { month: "Jan", sales: 12000 };

// ❌ Bad — array of primitives
return [12000, 15000];

The fetch Function

Scripts receive a special fetch that works like the standard Fetch API but blocks requests to private/internal IP addresses (localhost, 10.x.x.x, 172.16-31.x.x, 192.168.x.x) for security.

// GET request
const res = await fetch("https://api.example.com/data");
const data = await res.json();

// POST request with headers
const res = await fetch("https://api.example.com/graphql", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + secrets.API_TOKEN
  },
  body: JSON.stringify({
    query: `{ users { name email } }`
  })
});

Using Secrets

Store API keys and credentials as encrypted team secrets on the Data → Secrets tab. They’re passed to your script as the secrets object, keyed by name.

// If you created a secret named "GITHUB_TOKEN":
const res = await fetch("https://api.github.com/repos/owner/repo/issues", {
  headers: { "Authorization": "Bearer " + secrets.GITHUB_TOKEN }
});
const issues = await res.json();
return issues.map(i => ({
  title: i.title,
  state: i.state,
  created: i.created_at.slice(0, 10)
}));

Secret names must be valid identifiers (letters, numbers, underscores — e.g. MY_API_KEY). Values are encrypted at rest and never displayed after creation.

Using Params

The params object lets you reuse one script across multiple charts with different inputs. Set params in the chart editor’s Connect tab as a JSON object.

// Chart A sets params: { "symbol": "AAPL" }
// Chart B sets params: { "symbol": "GOOG" }
const symbol = params.symbol || "AAPL";
const res = await fetch(`https://api.example.com/stock/${symbol}/history`);
const data = await res.json();
return data.prices.map(p => ({
  date: p.date,
  close: p.close
}));

In the script editor, expand Test Parameters below the code panel to set a test params JSON object when clicking Run.

Combining Multiple APIs

One of the most powerful uses of scripts is combining data from multiple sources into a single chart:

// Fetch revenue from billing API and costs from accounting API
const [revenueRes, costsRes] = await Promise.all([
  fetch("https://billing.example.com/monthly", {
    headers: { "Authorization": "Bearer " + secrets.BILLING_KEY }
  }),
  fetch("https://accounting.example.com/expenses", {
    headers: { "Authorization": "Bearer " + secrets.ACCOUNTING_KEY }
  })
]);

const revenue = await revenueRes.json();
const costs = await costsRes.json();

// Merge into one dataset by month
return revenue.map(r => {
  const cost = costs.find(c => c.month === r.month);
  return {
    month: r.month,
    revenue: r.amount,
    costs: cost ? cost.amount : 0,
    profit: r.amount - (cost ? cost.amount : 0)
  };
});

Debugging with console.log

Use console.log, console.warn, and console.error to debug your scripts. Output appears in the Console tab of the script editor.

const res = await fetch("https://api.example.com/data");
const data = await res.json();

console.log("Fetched", data.length, "items");
console.log("First item:", JSON.stringify(data[0]));

if (data.length === 0) {
  console.warn("No data returned — check API key");
}

return data;

Console output is captured even when a script fails, so you can see what happened before the error.

Limits

Constraint Limit
Execution time 30 seconds
Memory 128 MB
Response size 1 MB
Maximum rows 10,000 objects
Executions per month (free) 50

Scripts that exceed the time or memory limit are terminated and return an error.

Common Patterns

Pagination

let allItems = [];
let page = 1;

while (true) {
  const res = await fetch(
    `https://api.example.com/items?page=${page}&per_page=100`,
    { headers: { "Authorization": "Bearer " + secrets.API_KEY } }
  );
  const data = await res.json();

  if (data.items.length === 0) break;
  allItems = allItems.concat(data.items);
  page++;
}

return allItems.map(item => ({
  name: item.name,
  value: item.metric
}));

Date Filtering

const since = new Date();
since.setDate(since.getDate() - 30);

const res = await fetch(
  `https://api.example.com/events?since=${since.toISOString()}`,
  { headers: { "Authorization": "Bearer " + secrets.TOKEN } }
);
const events = await res.json();

// Group by day
const byDay = {};
for (const e of events) {
  const day = e.timestamp.slice(0, 10);
  byDay[day] = (byDay[day] || 0) + 1;
}

return Object.entries(byDay).map(([date, count]) => ({ date, count }));

Error Handling

const res = await fetch("https://api.example.com/data", {
  headers: { "Authorization": "Bearer " + secrets.API_KEY }
});

if (!res.ok) {
  const text = await res.text();
  console.error("API error:", res.status, text);
  throw new Error(`API returned ${res.status}`);
}

return await res.json();

CSV / Text Parsing

const res = await fetch("https://data.example.com/report.csv");
const text = await res.text();

const lines = text.trim().split("\n");
const headers = lines[0].split(",");

return lines.slice(1).map(line => {
  const values = line.split(",");
  const row = {};
  headers.forEach((h, i) => {
    row[h.trim()] = isNaN(values[i]) ? values[i].trim() : Number(values[i]);
  });
  return row;
});

Troubleshooting

Issue Solution
“Script must return an array of objects” Your script returned a plain object, string, or number. Wrap the result in an array or map it to [{ ... }].
“Script timed out” Reduce the number of API calls or paginated requests. The limit is 30 seconds.
“Script exceeded memory limit” You’re loading too much data into memory. Filter or aggregate server-side, or reduce the dataset size.
“Requests to private/internal addresses are not allowed” fetch blocks requests to localhost, 10.x, 172.16-31.x, and 192.168.x IPs. Use a public URL.
“Could not resolve hostname” Check the URL for typos. The hostname must be publicly resolvable.
Script works in Run but fails on schedule Ensure the script doesn’t depend on browser APIs (no document, window, etc.). Check that secrets haven’t been rotated.
Console shows data but Data tab is empty Make sure you return the array — a missing return statement produces undefined.