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
| Piece | What it gives you |
|---|---|
| REST API resource | One FXMacroData resource with the key in the X-API-Key header, shared by every app and workflow. |
| Query library | Ready-made GET queries for the indicator catalogue, release history, release calendar, COT positioning, FX rates and commodities, with result transformers. |
| Macro dashboard | Currency and indicator pickers, latest value cards, a history chart, the next 14 days of releases and a CFTC positioning chart. |
| Morning release brief | A Retool Workflow that posts the next 24 hours of high-importance releases to Slack every weekday. |
| Retool Agents | The 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
- In Retool, open Resources, choose Create new, then REST API.
- Name it
FXMacroDataand set the base URL tohttps://api.fxmacrodata.com. - Add the header
X-API-Key:- Business and Enterprise plans: create a secret configuration variable
FXMD_API_KEYunder 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.
- Business and Enterprise plans: create a secret configuration variable
- Add
Accept: application/jsonand turn on Sanitize custom headers in logs. - Save, then create a query with the path
/v1/data_catalogue/usdto 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.
| Query | Path | URL parameters | Transformer |
|---|---|---|---|
fxmdCatalogue | /v1/data_catalogue/{{ currencySelect.value.toLowerCase() }} | none | catalogue_options.js |
fxmdHistory | /v1/announcements/{{ currencySelect.value.toLowerCase() }}/{{ indicatorSelect.value }} | fields=compact, limit=100, offset=0 | history_rows.js |
fxmdCalendar | /v1/calendar/{{ currencySelect.value.toLowerCase() }} | start_date today, end_date in 14 days | calendar_rows.js |
fxmdCot | /v1/cot/{{ currencySelect.value.toLowerCase() }} | limit=52, offset=0 | cot_rows.js |
fxmdForex | /v1/forex/{base}/{quote} | limit=100, offset=0 | Optional, needs a key |
fxmdCommodity | /v1/commodities/{commodity} | limit=100, offset=0 | Optional, 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
// 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
// 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
// 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
// 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.
- Create the app and, optionally, apply the FXMacroData dark theme in the app's Appearance settings using the values in fxmacrodata-dark.theme.json.
- Header row. Add a Select named
currencySelectwith the 22 currencies as manual options and defaultUSD, and a Select namedindicatorSelectwith mapped options from{{ fxmdCatalogue.data }}(value{{ item.value }}, label{{ item.label }}), defaultinflation, searchable. - 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. - History chart. Add the standalone transformer
historyPlotlybelow, 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() }}withdateon the x axis andvalueas the series. - Release calendar. Add a Table on
{{ fxmdCalendar.data }}showingscheduled_at(UTC),name,reference_period,importanceandschedule_url. - COT chart. Add the standalone transformer
cotPlotlybelow 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. - 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
// 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
// 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'].
- Create a workflow and add a Schedule trigger in Cron mode,
0 7 * * 1-5, in your desk's time zone. - Add a Code block named
releaseWindowwith the script below. EditCURRENCIESandHOURS_AHEADat the top. - Add a Loop block named
getCalendarsover{{ releaseWindow.data.currencies }}, run sequentially, with a GET query onFXMacroDatato/v1/calendar/{{ value.toLowerCase() }}and the URL parametersstart_date={{ releaseWindow.data.start_date }}andend_date={{ releaseWindow.data.end_date }}. - Add a Code block named
buildMessagewith the script below. SetTIMEZONE, for example'Europe/London', and add'medium'toIMPORTANCEif you want medium-importance releases too. - Add a Branch block on
{{ buildMessage.data.count > 0 }}to skip quiet days. - On the true branch, post
{{ buildMessage.data.text }}with Slackchat.postMessage, or send{{ buildMessage.data.payload }}to an incoming webhook stored as a secret configuration variable. - Run the workflow from the editor, check each block's output, then Deploy it. The schedule only runs deployed versions.
releaseWindow.js
// 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
// 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.