Skip to content
Retool integration

Use FXMacroData in Retool

Build internal tools on official macro, FX and central-bank data. FXMacroData works as a standard Retool REST API resource, and this guide adds ready-made queries, a macro dashboard and a Retool Workflow that posts the day's important releases to Slack.

What you can build

PieceWhat it gives you
REST API resourceOne FXMacroData resource with the key in the X-API-Key header, shared by every app and workflow.
Query libraryReady-made GET queries for the indicator catalogue, release history, release calendar, COT positioning, FX rates and commodities, with result transformers.
Macro dashboardCurrency and indicator pickers, latest value cards, a history chart, the next 14 days of releases and a CFTC positioning chart.
Morning release briefA Retool Workflow that posts the next 24 hours of high-importance releases to Slack every weekday.
Retool AgentsThe FXMacroData MCP server as a tool source for an agent.

Free USD data

Without an API key, USD indicators, the USD release calendar and USD COT positioning are available for the last 90 days, with a 15-minute delay after each release. The indicator catalogue is free for every currency.

Subscription data

Other currencies, FX rates, commodities, full history and release webhooks need an API key from a subscription.

Key handling

Retool runs resource queries on its own servers, so the key travels only in the X-API-Key header set on the resource. App users never see it, and it is never placed in a URL.

Step 1

Create the REST API resource

  1. In Retool, open Resources, choose Create new, then REST API.
  2. Name it FXMacroData and set the base URL to https://api.fxmacrodata.com.
  3. Add the header X-API-Key:
    • Business and Enterprise plans: create a secret configuration variable FXMD_API_KEY under Settings > Configuration variables and set the header value to {{ environment.variables.FXMD_API_KEY }}. You can give staging and production different keys.
    • Free and Team plans: paste the key itself as the header value. Retool stores resource settings on its servers and does not show them to app users.
    • No key yet: leave the header out. USD data works without one.
  4. Add Accept: application/json and turn on Sanitize custom headers in logs.
  5. Save, then create a query with the path /v1/data_catalogue/usd to check the resource.

For an endpoint picker in the query editor, choose Spec URL instead of a base URL and enter https://api.fxmacrodata.com/api/openapi.json. Keep the same header.

Every setting above is also in fxmacrodata-rest-resource.json for reference. Retool does not import resources from a file, so enter the values in the resource form.

Step 2

Add the queries

In the app's Code panel, add each query as a resource query on FXMacroData with action type GET. Enable Transform results and paste the matching transformer. Set the run behaviour to run automatically when inputs change.

QueryPathURL parametersTransformer
fxmdCatalogue/v1/data_catalogue/{{ currencySelect.value.toLowerCase() }}nonecatalogue_options.js
fxmdHistory/v1/announcements/{{ currencySelect.value.toLowerCase() }}/{{ indicatorSelect.value }}fields=compact, limit=100, offset=0history_rows.js
fxmdCalendar/v1/calendar/{{ currencySelect.value.toLowerCase() }}start_date today, end_date in 14 dayscalendar_rows.js
fxmdCot/v1/cot/{{ currencySelect.value.toLowerCase() }}limit=52, offset=0cot_rows.js
fxmdForex/v1/forex/{base}/{quote}limit=100, offset=0Optional, needs a key
fxmdCommodity/v1/commodities/{commodity}limit=100, offset=0Optional, needs a key

For fxmdCalendar, use {{ new Date().toISOString().slice(0, 10) }} for the start date and {{ new Date(Date.now() + 14 * 86400000).toISOString().slice(0, 10) }} for the end date. Paged endpoints return at most 100 rows per request; page with offset or narrow with start_date and end_date for more. Commodity slugs: gold, silver, platinum, oil_wti, oil_brent, natural_gas, natural_gas_storage and crude_oil_inventories.

catalogue_options.js

Download
// Transformer for the fxmdCatalogue query (GET /v1/data_catalogue/{currency}).
// Paste into the query's "Transform results" box. Retool provides `data`.
//
// The response is an object keyed by indicator slug, for example
// {"inflation": {"name": "Inflation (CPI)", "unit": "%YoY", ...}}.
// Returns Select options: [{value, label, unit, frequency}], sorted by label.
const body = data && typeof data === 'object' && !Array.isArray(data) ? data : {};
const entries = body.data && typeof body.data === 'object' && !Array.isArray(body.data) ? body.data : body;
return Object.keys(entries)
  .filter(slug => entries[slug] && typeof entries[slug] === 'object' && typeof entries[slug].name === 'string')
  .map(slug => ({
    value: slug,
    label: entries[slug].name,
    unit: typeof entries[slug].unit === 'string' ? entries[slug].unit : '',
    frequency: typeof entries[slug].frequency === 'string' ? entries[slug].frequency : '',
  }))
  .sort((a, b) => a.label.localeCompare(b.label));

history_rows.js

Download
// Transformer for the fxmdHistory query
// (GET /v1/announcements/{currency}/{indicator}?fields=compact).
// Paste into the query's "Transform results" box. Retool provides `data`.
//
// Returns one row per release, newest first:
// {date, value, previous, change, direction, released_at}
// `change` is the API's change_from_previous; `direction` compares value
// with the previous value (Up / Down / Unchanged, or null when unknown).
const rows = data && Array.isArray(data.data) ? data.data : [];
const num = v => (typeof v === 'number' && Number.isFinite(v) ? v : null);
const iso = s => (typeof s === 'number' && Number.isFinite(s)
  ? new Date(s * 1000).toISOString().replace('.000Z', 'Z')
  : null);
const direction = (cur, prev) => {
  if (cur === null || prev === null) return null;
  if (Math.abs(cur - prev) < 1e-9) return 'Unchanged';
  return cur > prev ? 'Up' : 'Down';
};
return rows
  .filter(r => r && /^\d{4}-\d{2}-\d{2}$/.test(String(r.date)))
  .map(r => {
    const value = num(r.val);
    const previous = num(r.previous_value);
    return {
      date: String(r.date),
      value,
      previous,
      change: num(r.change_from_previous),
      direction: direction(value, previous),
      released_at: iso(r.announcement_datetime),
    };
  })
  .sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));

calendar_rows.js

Download
// Transformer for the fxmdCalendar query (GET /v1/calendar/{currency}).
// Paste into the query's "Transform results" box. Retool provides `data`.
//
// Returns scheduled releases in time order:
// {scheduled_at, scheduled_epoch, release, name, reference_period,
//  importance, schedule_url, calendar_event_id}
const rows = data && Array.isArray(data.data) ? data.data : [];
const text = v => (v === null || v === undefined ? '' : String(v).trim());
const title = slug => text(slug)
  .split('_')
  .filter(Boolean)
  .map(w => w.charAt(0).toUpperCase() + w.slice(1))
  .join(' ');
const importance = v => {
  const out = text(v).toLowerCase();
  return ['high', 'medium', 'low'].includes(out) ? out : '';
};
return rows
  .filter(r => r && typeof r.announcement_datetime === 'number' && Number.isFinite(r.announcement_datetime))
  .map(r => ({
    scheduled_at: new Date(r.announcement_datetime * 1000).toISOString().replace('.000Z', 'Z'),
    scheduled_epoch: r.announcement_datetime,
    release: text(r.release),
    name: text(r.name) || title(r.release),
    reference_period: text(r.reference_period),
    importance: importance(r.event_importance),
    schedule_url: text(r.schedule_url),
    calendar_event_id: text(r.calendar_event_id),
  }))
  .sort((a, b) => a.scheduled_epoch - b.scheduled_epoch);

cot_rows.js

Download
// Transformer for the fxmdCot query (GET /v1/cot/{currency}).
// Paste into the query's "Transform results" box. Retool provides `data`.
//
// Returns weekly CFTC positioning rows, oldest first (ready to chart):
// {date, open_interest, noncommercial_net, commercial_net,
//  leveraged_funds_net, asset_manager_net, dealer_net, released_at}
const rows = data && Array.isArray(data.data) ? data.data : [];
const num = v => (typeof v === 'number' && Number.isFinite(v) ? v : null);
return rows
  .filter(r => r && /^\d{4}-\d{2}-\d{2}$/.test(String(r.date)))
  .map(r => ({
    date: String(r.date),
    open_interest: num(r.open_interest),
    noncommercial_net: num(r.noncommercial_net),
    commercial_net: num(r.commercial_net),
    leveraged_funds_net: num(r.leveraged_funds_net),
    asset_manager_net: num(r.asset_manager_net),
    dealer_net: num(r.dealer_net),
    released_at: typeof r.announcement_datetime === 'number' && Number.isFinite(r.announcement_datetime)
      ? new Date(r.announcement_datetime * 1000).toISOString().replace('.000Z', 'Z')
      : null,
  }))
  .sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));

Step 3

Build the macro dashboard

About 20 minutes. Component names matter, because the bindings refer to each other by name.

  1. Create the app and, optionally, apply the FXMacroData dark theme in the app's Appearance settings using the values in fxmacrodata-dark.theme.json.
  2. Header row. Add a Select named currencySelect with the 22 currencies as manual options and default USD, and a Select named indicatorSelect with mapped options from {{ fxmdCatalogue.data }} (value {{ item.value }}, label {{ item.label }}), default inflation, searchable.
  3. Latest value cards. Add Statistic components for the latest value ({{ ((fxmdHistory.data || [])[0] || {}).value }}), the previous value, the change and the release time. Leave trend colouring off on the change card: whether a rise is good depends on the indicator.
  4. History chart. Add the standalone transformer historyPlotly below, then a Plotly JSON Chart with data {{ historyPlotly.value.data }} and layout {{ historyPlotly.value.layout }}. Without a Plotly JSON Chart, use a Line Chart on {{ (fxmdHistory.data || []).slice().reverse() }} with date on the x axis and value as the series.
  5. Release calendar. Add a Table on {{ fxmdCalendar.data }} showing scheduled_at (UTC), name, reference_period, importance and schedule_url.
  6. COT chart. Add the standalone transformer cotPlotly below and a second Plotly JSON Chart bound to it. Net long weeks are drawn in mint, net short in magenta, with open interest on the right axis.
  7. Check it. Preview with USD selected: the indicator list fills, the cards show the latest CPI release and the chart and calendar load. Switch to another currency: without a key the queries return a subscription message, with a key they load.

Once built, use App actions > Export to JSON to keep an importable copy. Importing it into another Retool organisation needs a resource named FXMacroData set up the same way.

historyPlotly.js

Download
// Standalone transformer `historyPlotly` for the Plotly JSON Chart that draws
// the selected indicator's history. Create it in the Code panel
// (+ > Transformer), name it historyPlotly, and paste this in.
// Bind the chart's Data to {{ historyPlotly.value.data }} and its Layout to
// {{ historyPlotly.value.layout }}.
const rows = ({{ fxmdHistory.data }} || []).slice().reverse();
const options = {{ fxmdCatalogue.data }} || [];
const selected = options.find(o => o.value === {{ indicatorSelect.value }}) || {};
const unit = selected.unit || '';
const P = {
  bg: '#0d1117', text: '#cdd9e5', tick: '#e6edf3', muted: '#94a3b8',
  grid: 'rgba(80,90,102,0.45)', border: '#30363d', line: '#00e5ff',
};
return {
  data: [{
    type: 'scatter',
    mode: 'lines',
    name: selected.label || {{ indicatorSelect.value }} || 'Value',
    x: rows.map(r => r.date),
    y: rows.map(r => r.value),
    line: { color: P.line, width: 2 },
    hovertemplate: '%{x}<br>%{y}' + (unit ? ' ' + unit : '') + '<extra></extra>',
  }],
  layout: {
    paper_bgcolor: P.bg,
    plot_bgcolor: P.bg,
    font: { family: 'IBM Plex Mono, monospace', color: P.text, size: 12 },
    margin: { l: 56, r: 16, t: 16, b: 40 },
    showlegend: false,
    xaxis: { gridcolor: P.grid, linecolor: P.border, tickfont: { color: P.tick } },
    yaxis: { gridcolor: P.grid, linecolor: P.border, tickfont: { color: P.tick }, title: { text: unit, font: { color: P.muted } } },
    hoverlabel: { bgcolor: '#161b22', bordercolor: P.line, font: { color: '#f0f6fc' } },
  },
};

cotPlotly.js

Download
// Standalone transformer `cotPlotly` for the Plotly JSON Chart that draws
// CFTC non-commercial net positioning. Create it in the Code panel
// (+ > Transformer), name it cotPlotly, and paste this in.
// Bind the chart's Data to {{ cotPlotly.value.data }} and its Layout to
// {{ cotPlotly.value.layout }}. Net long weeks are mint, net short magenta.
const rows = {{ fxmdCot.data }} || [];
const P = {
  bg: '#0d1117', text: '#cdd9e5', tick: '#e6edf3', muted: '#94a3b8',
  grid: 'rgba(80,90,102,0.45)', border: '#30363d',
  bull: '#00ff9c', bear: '#ff4d9d', line: '#ffd60a',
};
const net = rows.map(r => r.noncommercial_net);
return {
  data: [
    {
      type: 'bar',
      name: 'Non-commercial net',
      x: rows.map(r => r.date),
      y: net,
      marker: { color: net.map(v => (typeof v === 'number' && v < 0 ? P.bear : P.bull)) },
      hovertemplate: '%{x}<br>Net %{y:,}<extra></extra>',
    },
    {
      type: 'scatter',
      mode: 'lines',
      name: 'Open interest',
      x: rows.map(r => r.date),
      y: rows.map(r => r.open_interest),
      yaxis: 'y2',
      line: { color: P.line, width: 1.5, dash: 'dot' },
      hovertemplate: '%{x}<br>Open interest %{y:,}<extra></extra>',
    },
  ],
  layout: {
    paper_bgcolor: P.bg,
    plot_bgcolor: P.bg,
    font: { family: 'IBM Plex Mono, monospace', color: P.text, size: 12 },
    margin: { l: 64, r: 64, t: 16, b: 40 },
    bargap: 0.25,
    legend: { orientation: 'h', y: -0.2, font: { color: P.muted } },
    xaxis: { gridcolor: P.grid, linecolor: P.border, tickfont: { color: P.tick } },
    yaxis: { gridcolor: P.grid, linecolor: P.border, zerolinecolor: P.border, tickfont: { color: P.tick }, title: { text: 'Contracts (net)', font: { color: P.muted } } },
    yaxis2: { overlaying: 'y', side: 'right', showgrid: false, tickfont: { color: P.muted }, title: { text: 'Open interest', font: { color: P.muted } } },
    hoverlabel: { bgcolor: '#161b22', bordercolor: '#00e5ff', font: { color: '#f0f6fc' } },
  },
};

Retool Workflow

Morning release brief to Slack

Every weekday morning, post the next 24 hours of high-importance releases for the currencies you follow to a Slack channel. You need the FXMacroData resource and a Slack resource with the chat:write scope, or a Slack incoming webhook URL. Without an API key only the USD calendar is available, so set CURRENCIES to ['USD'].

  1. Create a workflow and add a Schedule trigger in Cron mode, 0 7 * * 1-5, in your desk's time zone.
  2. Add a Code block named releaseWindow with the script below. Edit CURRENCIES and HOURS_AHEAD at the top.
  3. Add a Loop block named getCalendars over {{ releaseWindow.data.currencies }}, run sequentially, with a GET query on FXMacroData to /v1/calendar/{{ value.toLowerCase() }} and the URL parameters start_date = {{ releaseWindow.data.start_date }} and end_date = {{ releaseWindow.data.end_date }}.
  4. Add a Code block named buildMessage with the script below. Set TIMEZONE, for example 'Europe/London', and add 'medium' to IMPORTANCE if you want medium-importance releases too.
  5. Add a Branch block on {{ buildMessage.data.count > 0 }} to skip quiet days.
  6. On the true branch, post {{ buildMessage.data.text }} with Slack chat.postMessage, or send {{ buildMessage.data.payload }} to an incoming webhook stored as a secret configuration variable.
  7. Run the workflow from the editor, check each block's output, then Deploy it. The schedule only runs deployed versions.

releaseWindow.js

Download
// Code block `releaseWindow` (first block after the schedule trigger).
// Edit CURRENCIES and HOURS_AHEAD to taste. Without an FXMacroData API key
// only the USD calendar is available, so keep CURRENCIES to ['USD'] on the
// free tier.
const CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY'];
const HOURS_AHEAD = 24;

const now = new Date();
const end = new Date(now.getTime() + HOURS_AHEAD * 3600 * 1000);
const day = d => d.toISOString().slice(0, 10);
return {
  currencies: CURRENCIES.map(c => String(c).trim().toUpperCase()).filter(Boolean),
  start_date: day(now),
  end_date: day(end),
  from_epoch: Math.floor(now.getTime() / 1000),
  to_epoch: Math.floor(end.getTime() / 1000),
  hours_ahead: HOURS_AHEAD,
};

buildMessage.js

Download
// Code block `buildMessage` (after the `getCalendars` loop block).
// Keeps releases inside the window from `releaseWindow` whose importance is in
// IMPORTANCE, and builds a Slack message. TIMEZONE is any IANA zone name,
// for example 'Europe/London' or 'America/New_York'.
const TIMEZONE = 'UTC';
const IMPORTANCE = ['high'];
const LINK = 'https://fxmacrodata.com/dashboard/release-calendar?utm_source=retool&utm_medium=referral&utm_campaign=retool-workflow&utm_content=slack-brief';

const win = releaseWindow.data;
const results = Array.isArray(getCalendars.data) ? getCalendars.data : [];
const text = v => (v === null || v === undefined ? '' : String(v).trim());
const title = slug => text(slug).split('_').filter(Boolean)
  .map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
const clock = new Intl.DateTimeFormat('en-GB', {
  timeZone: TIMEZONE, weekday: 'short', hour: '2-digit', minute: '2-digit', hour12: false,
});

const seen = new Set();
const events = [];
const skipped = [];
results.forEach((result, i) => {
  const fallback = win.currencies[i] || '';
  const rows = Array.isArray(result) ? result
    : result && Array.isArray(result.data) ? result.data : null;
  if (!rows) {
    skipped.push(fallback);
    return;
  }
  const currency = text(result && result.currency) || fallback;
  rows.forEach(r => {
    const at = r && r.announcement_datetime;
    if (typeof at !== 'number' || !Number.isFinite(at)) return;
    if (at < win.from_epoch || at > win.to_epoch) return;
    const importance = text(r.event_importance).toLowerCase();
    if (!IMPORTANCE.includes(importance)) return;
    const id = text(r.calendar_event_id) || `${currency}_${text(r.release)}_${at}`;
    if (seen.has(id)) return;
    seen.add(id);
    events.push({
      id,
      currency: currency.toUpperCase(),
      at,
      when: clock.format(new Date(at * 1000)),
      name: text(r.name) || title(r.release),
      period: text(r.reference_period),
      url: text(r.schedule_url),
    });
  });
});
events.sort((a, b) => a.at - b.at || a.currency.localeCompare(b.currency));

const zone = TIMEZONE === 'UTC' ? 'UTC' : TIMEZONE;
const heading = `*High-importance releases, next ${win.hours_ahead} hours* (${zone})`;
const lines = events.map(e => {
  const period = e.period ? ` (${e.period})` : '';
  const source = e.url ? ` <${e.url}|schedule>` : '';
  return `• ${e.when}  *${e.currency}* ${e.name}${period}${source}`;
});
const body = lines.length
  ? lines.join('\n')
  : `No high-importance releases scheduled for ${win.currencies.join(', ')}.`;
const footer = `Calendar: <${LINK}|FXMacroData>`
  + (skipped.length ? `\nCalendar unavailable (check the API key): ${skipped.join(', ')}` : '');
const message = `${heading}\n${body}\n${footer}`;

return {
  count: events.length,
  events,
  skipped,
  text: message,
  payload: { text: message, unfurl_links: false, unfurl_media: false },
};

For a Slack message the moment a release is published rather than a morning digest, create a release webhook with the slack destination, or point a generic webhook at a Retool Workflow webhook trigger. See webhooks.

Retool Agents

Retool Agents can connect to remote MCP servers. Add the FXMacroData MCP server as a tool source:

https://mcp.fxmacrodata.com/mcp

USD data works without a key. Setup details are in the MCP server guide.

Related docs

Questions about a Retool setup? Contact info@fxmacrodata.com.

AI Answer-Ready

Key Facts

Page
Retool
Section
Documentation
Canonical URL
https://fxmacrodata.com/documentation/retool
Source
FXMacroData editorial and official publisher references
Last Updated
See page metadata

Provenance And Trust

Cite the canonical URL and source field above. Where available, this page maps to official publisher releases and timestamped updates.

Quick Q&A

What is this page about? This page explains Retool with directly usable context for trading, research, and API workflows.

What source should be cited? Use the canonical URL and the listed source field; cite official publisher references when available.

How fresh is this content? The last updated value above reflects the page metadata or latest available data timestamp.

Can this be used in AI assistants? Yes. This section is intentionally structured for retrieval and citation in chat assistants.

Prompt Packs

Use these in ChatGPT, Claude, Gemini, Mistral, Perplexity, or Grok for consistent source-aware outputs.

Share page X LinkedIn Email