MCP HeartbeatTHE MCP OBSERVATORY
Public workspace

Explore

Evidence for your next connection.

Independent measurements of remote MCP servers. Every observation has a timestamp.

Read-only probes

MCP server · Data access / search

tlly

Hosted dashboards and scheduled data jobs for AI agents, with thresholds, alerts and share links.

Checks passed
15 / 22

0 failed · 7 not measured

Median response
824 ms

Median of successful probes over 7 days

30-day uptime
Not measured

Share of countable probes over 30 days, including daily rollups. Auth challenges are excluded.

Discovered tools
30

Tool definitions returned by this server

Independent monitoring

Health & checks

Status history

Responded

The server completed its last measured handshake. This is an observation, not a guarantee.

Last checked
Check summary by test group
Test areaPassedFailedUnmeasured
ConnectionCan a client connect and discover tools?
500
Reliability & speedDoes it respond consistently and quickly?
103
Access & authenticationWhat is required to use this server?
102
Tool qualityAre the tool definitions usable by an agent?
400
Publisher identityCan the publisher be traced to its source?
402

Unmeasured means there is not enough evidence to decide. How these checks work

Inspect individual checks 22 recorded

Core

  • Pass
    Reachable

    Latest probe returned HTTP 200 in 731 ms.

  • Pass
    Valid handshake

    protocolVersion 2025-06-18, serverInfo.name tlly.

  • Pass
    Lists tools

    tools/list returned 30 tools.

  • Pass
    Current protocol

    Negotiated 2025-06-18. The bar is 2025-06-18 or newer.

  • Pass
    HTTPS

    The endpoint URL is https.

Performance

  • Not measured
    Fast (p50)

    Median latency 824 ms over 2 samples in 7 days. The bar is 1000 ms. Need at least 5 samples.

  • Not measured
    Consistent (p95)

    95th percentile 908 ms over 2 samples in 7 days. The bar is 3000 ms. Need at least 20 samples.

  • Not measured
    Reliable (uptime)

    2 of 2 attempts returned a tool list in 30 days (100%). The bar is 99%. Need at least 30 samples.

  • Pass
    Stable tool list

    No tool was removed and no required parameter was added in the last 30 days.

Access

  • Pass
    Open access

    tools/list succeeded with no credential, and the record declares none.

  • Not measured
    Standard auth

    Auth is not required, so the OAuth metadata check does not apply.

  • Not measured
    Free tier cited

    No publisher quote has been checked.

Tools

  • Pass
    Tools described

    Every tool description is at least 20 characters.

  • Pass
    Valid input schemas

    Every inputSchema is a JSON Schema object.

  • Pass
    Parameters documented

    39 of 43 parameters have a description (91%). The bar is 80%.

  • Pass
    Safety hints

    Every tool sets readOnlyHint or destructiveHint.

Publisher

  • Pass
    Verified namespace

    Listed in the Official MCP Registry.

  • Pass
    Website matches publisher

    The website host matches the reverse-domain namespace.

  • Pass
    Source repo linked

    A repository URL is on file.

  • Not measured
    Repo owner matches

    This namespace is not an io.github login, so the owner comparison does not apply.

  • Pass
    Recently updated

    Updated 2026-10-30T21:44:41.899191Z, within 180 days.

  • Not measured
    Open-source licence

    The GitHub licence lookup has not returned an OSI result yet.

Start using tlly

Connect to this server

Choose your client, copy the configuration, then enable the server in your client’s MCP settings.

Run this command in your terminal, then restart Claude Code.

Claude Code

claude mcp add --transport http tlly https://tlly.dev/mcp

Capabilities

Tools 30

add_annotations

Attach short dated notes to a series (e.g. 'Policy rate raised to 4.5%'); shown as markers on charts. Single-line plain text, max 280 chars; one note per date (re-adding replaces it); each counts as one data point.

Discovered
create_dashboard

Create a hosted dashboard; returns its public URL. Dashboard spec: {"title": str, "description"?: str, "range"?: "1m"|"3m"|"6m"|"ytd"|"1y"|"2y"|"5y"|"10y"|"max", "theme"?: "auto"|"light"|"dark" (default mode; auto follows the viewer's system and viewers can switch), "layout"?: "auto"|"overview"|"detail" (only the default for panels without "display": overview makes them tiles, detail makes them charts, auto makes them tiles above 8 panels), "visibility"?: "public"|"private" (default public: anyone with the link; private: only members of this account signed in to tlly.dev, so embeds and shared links stop working for others), "panels": [{"title"?: str, "series": "name" | ["a","b"], "type"?: "line"|"area"|"bar"|"stat"|"gauge"|"bargauge"|"pie"|"heatmap"|"table", "unit"?: str, "note"?: str, "stale_after_days"?: num, "thresholds"?: {"red_above"?, "yellow_above"?, "yellow_below"?, "red_below"?}, "range"?: (as above, overrides the dashboard's), "section"?: str (starts a new titled section at this panel), "views"?: bool (show the Level / Y/Y % / Index buttons; default: shown unless the unit is already a rate like %, y/y or pp), "display"?: "tile"|"chart" (tile: a compact number with a sparkline, click for the full chart; chart: a card with the full chart; mix them freely, e.g. tiles for the many supporting numbers and charts for the few that need their history), "headline"?: bool (show as a big number at the top of the dashboard, under a status strip; use for the 3-6 most important), "width"?: "half"|"full", "scale"?: "linear"|"log", "id"?: str (stable panel id, e.g. pn_abc123XYZ012; keep existing ids when editing so alerts stay attached), "right_axis"?: [series names] (line, area and bar charts: draw these series against a second y axis on the right, for series of different sizes or units; at least one stays on the left), "right_unit"?: str (unit of the right axis; "unit" is the left's), "left_label"?/"right_label"?: str (≤40 chars, a title written vertically along the left / right y axis, e.g. "Policy rate"; right_label needs right_axis), "view"?: "level"|"yoy"|"index" (initial view; viewers can switch), "source"?: str (shown as "Source: …"; defaults to the job's host), "labels"?: [display name per series], "status_series"?: name (a red/yellow/green text series that sets the panel colour), "compare"?: "prev"|"3m"|"1y" (what the change is measured against), "period"?: "D"|"M"|"Q" (show dates as 2026-06 or 2026Q2), "value_names"?: {"green": "Hold", …} (display names for text values)}], "badge"?: "worst" (default: the header badge shows the worst panel) | "off" | panel index (0-based; the badge follows that panel), "shade"?: [{"from": date, "to": date, "label"?: str}] (up to 20 shaded periods on every chart)}. "type": "table" shows one row per series (up to 20): label, latest value, date and a status dot; its "status_series" is a list with one entry per row ("" for none). "area" is a line chart filled underneath; "gauge" is a dial of the latest value with the threshold zones, and "bargauge" a horizontal bar per series (up to 20, coloured by thresholds), both scaled by "min"?/"max"?: num (default 0 to a bit above the largest value or threshold); "pie" is a donut of the series' latest values (shares of a whole, up to 20); "heatmap" is a calendar of the first series' daily values (good for activity per day). Status colour is computed from the latest value of the panel's first series; a series of red/yellow/green labels (e.g. a derived status) colours itself. Notes may use {latest}, {date}, {period} and {change}, filled in from the panel's latest value. Annotations on a panel's series appear as markers. Viewers can pick a time range (1M to Max, year to date, or a custom from-to period), zoom, switch views and download CSV; ?embed=1 hides the page chrome and &panel=N shows one panel. Text limits: title ≤120, description ≤500, panel title ≤120, unit ≤20, note ≤280, section and source ≤80 characters, single line, plain text.

Discovered
create_job

Schedule a fetch job. It runs once immediately and the result is returned. Job spec: {"series": name, "cron": "m h dom mon dow" (UTC, at most every 30 min), "request": {"url", "method"?: "GET"|"POST", "headers"?: {}, "body"?: obj|str, "pages"?: {"param": "page", "start"?: 0, "max"?: 1-10} (json with items: bump a query parameter until an empty items array) | {"cursor": "paging.next.after", "into": "after", "max"?: 1-10} (copy that JSON path from each response into the JSON body; stop when it is missing. A cursor that remains after max pages is an error, so a sum is never a partial total. HubSpot CRM search uses this. A json aggregate with no request.pages fails when the body still has another page, paging.next.after, has_more true, links.next, pageInfo.hasNextPage or another next cursor, instead of saving a partial total. Set pages. A response with no next marker is complete.)}, "extract": {"format": "json", "items"?: "path.to.array", "date"?: "field", "value": "field", "value_type"?: "number"|"text", "aggregate"?: "count"|"sum"|"mean"|"min"|"max" (records per date, e.g. hourly prices to a daily mean; needs items; all but count need value), "period"?: "D"|"M"|"Q", "where"?: [{"field": "path", "equals"|"not_equals": value}] (up to 5, with aggregate). Omit "date" with aggregate to snapshot every matching record as one point on the fetch's half hour. String numbers (HubSpot properties.amount) are summed.} | {"format": "json_columns", "dates": "path.to.array", "values": "path.to.array", "value_type"?} (two parallel arrays, e.g. Yahoo chart: timestamp + close) | {"format": "csv", "date"?: "COLUMN", "value": "COLUMN", "delimiter"?, "value_type"?} (omit date to stamp every row with the fetch's half hour; one value per half hour, so the last row wins. That does not sum the rows the way a JSON aggregate that omits date does. A history needs a Date column. Make a copy https://docs.google.com/spreadsheets/d/1Ty5X3LkY2unxTNGbkq36qDzuuYxQMsuywaTNyzL_pac/copy. Public CSV https://docs.google.com/spreadsheets/d/1Ty5X3LkY2unxTNGbkq36qDzuuYxQMsuywaTNyzL_pac/export?format=csv&gid=1746689811. Downloads /templates/google-sheets.csv and /templates/google-sheets.xlsx.) | {"format": "jsonstat2"} (SSB/Eurostat; filter the query to a single series), "transform"?: [{"op": "pct_change"|"diff"|"rolling_mean"|"rolling_sum"|"rolling_min"|"rolling_max"|"shift", "n": int} | {"op": "scale", "factor": num}], "keep"?: "all"|"last"}. "value_type": "text" stores labels (no transforms; see Data policy). Dates like 2026-09-30, 2026M08, 2026Q2/2026K2, 2026 or unix time are normalised to ISO period-end dates; date-times (2026-10-05T14:00+02:00) count on the day as written. A single JSON value with no timestamp (omit "date", or a date path the body doesn't have) is dated with the fetch's half hour: that is valid for a snapshot such as a price. A date that is present but not a date is an error. Do not invent a timestamp field. URLs and string bodies may use {today}, {yesterday}, {tomorrow}, {year}, {month}, {day} (UTC, filled in on every run) for sources that put the date in the URL. Private APIs: the URL, header values and string body may use {{secret:NAME}} for a secret the workspace owner stored (list_secrets shows the names and the one host each may be sent to); the value is filled in at fetch time and never shown, e.g. "headers": {"Authorization": "Bearer {{secret:STRIPE_KEY}}"}. A stored secret may be sent on GET, on a POST whose body is a GraphQL query, or on POST https://api.hubapi.com/crm/v3/objects/{deals,contacts,companies}/search only (HubSpot's read-only CRM search, JSON body; https://developers.hubspot.com/docs/api-reference/legacy/crm/search-the-crm). No other POST may carry a secret, and the secret is still sent only to the host it was saved for. If a needed secret is missing, ask the user to add it at /account under Data sources.

Discovered
define_derived

Create or replace a derived series, computed from other series every time it is read (always fresh, stores no points, counts toward the series limit). definition: {"op":"add"|"subtract"|"multiply"|"divide","a":series,"b":series} (matched by date) | {"op":"pct_change"|"diff"|"moving_average"|"rolling_min"|"rolling_max"|"shift","a":series,"n"?:1-1000} (shift: the value n points earlier) | {"op":"scale","a":series,"factor":number} | {"op":"resample","a":series,"to":"M"|"Q","how"?:"last"|"mean"|"sum"|"ffill"} (to month/quarter ends; ffill also fills gaps up to today) | {"op":"mean"|"sum","inputs":[2-20 series]} (dates all inputs have) | {"op":"status","a":series,"red"?:[rule],"yellow"?:[rule]} → text labels red/yellow/green (red if any red rule holds, else yellow if any yellow rule, else green); rule: {"series"?:name (default a; read as of each date of a),"above"|"below":number,"for"?:1-24 (holds for the last k points)} or {"all":[2-10 rules]}; at most 10 rules | {"op":"composite","inputs":[status series],"min_inputs"?:n} → 0-100 score (red 2, yellow 1, green 0) | {"op":"count","inputs":[status series],"value":"red"|"yellow"|"green","share"?:true (percent)}. Inputs may themselves be derived (up to 10 levels; one read may touch at most 200 stored series). Example: a yield-curve spread {"op":"subtract","a":"govt_10y","b":"tbill_3m"}.

Discovered
delete_annotations

Delete annotations of a series on the given dates, or all of them if dates is omitted.

Discovered
delete_dashboard

Delete a dashboard (series data is kept).

Discovered
delete_job

Delete a job (its series data is kept).

Discovered
delete_series

Delete all points of a series, or remove a derived series' definition.

Discovered
get_integration_skill

Read one integration playbook by id: what to track, the minimum credential and scopes, the exact tlly job, and gotchas. google-sheets and json-api also return a template field (Make a copy, public CSV, downloads). Make a copy https://docs.google.com/spreadsheets/d/1Ty5X3LkY2unxTNGbkq36qDzuuYxQMsuywaTNyzL_pac/copy. Public CSV https://docs.google.com/spreadsheets/d/1Ty5X3LkY2unxTNGbkq36qDzuuYxQMsuywaTNyzL_pac/export?format=csv&gid=1746689811. Downloads /templates/google-sheets.csv and /templates/google-sheets.xlsx. Public: no API key required. Call this before inventing an API URL or scope.

Discovered
get_series

Read the latest points of a series.

Discovered
get_status

Health check: failing jobs, stale or empty panels, dashboards, storage usage. Call this to verify everything works.

Discovered
get_usage

Storage usage against quotas (data points = values + annotations, series, dashboards, jobs) and the largest series. Check before large writes.

Discovered
import_once

Fetch a job spec once and save the result, without scheduling it or using a job slot ("cron" is optional and ignored). Use for one-off dashboards whose data doesn't need to update; points count toward the data quota. Job spec: {"series": name, "cron": "m h dom mon dow" (UTC, at most every 30 min), "request": {"url", "method"?: "GET"|"POST", "headers"?: {}, "body"?: obj|str, "pages"?: {"param": "page", "start"?: 0, "max"?: 1-10} (json with items: bump a query parameter until an empty items array) | {"cursor": "paging.next.after", "into": "after", "max"?: 1-10} (copy that JSON path from each response into the JSON body; stop when it is missing. A cursor that remains after max pages is an error, so a sum is never a partial total. HubSpot CRM search uses this. A json aggregate with no request.pages fails when the body still has another page, paging.next.after, has_more true, links.next, pageInfo.hasNextPage or another next cursor, instead of saving a partial total. Set pages. A response with no next marker is complete.)}, "extract": {"format": "json", "items"?: "path.to.array", "date"?: "field", "value": "field", "value_type"?: "number"|"text", "aggregate"?: "count"|"sum"|"mean"|"min"|"max" (records per date, e.g. hourly prices to a daily mean; needs items; all but count need value), "period"?: "D"|"M"|"Q", "where"?: [{"field": "path", "equals"|"not_equals": value}] (up to 5, with aggregate). Omit "date" with aggregate to snapshot every matching record as one point on the fetch's half hour. String numbers (HubSpot properties.amount) are summed.} | {"format": "json_columns", "dates": "path.to.array", "values": "path.to.array", "value_type"?} (two parallel arrays, e.g. Yahoo chart: timestamp + close) | {"format": "csv", "date"?: "COLUMN", "value": "COLUMN", "delimiter"?, "value_type"?} (omit date to stamp every row with the fetch's half hour; one value per half hour, so the last row wins. That does not sum the rows the way a JSON aggregate that omits date does. A history needs a Date column. Make a copy https://docs.google.com/spreadsheets/d/1Ty5X3LkY2unxTNGbkq36qDzuuYxQMsuywaTNyzL_pac/copy. Public CSV https://docs.google.com/spreadsheets/d/1Ty5X3LkY2unxTNGbkq36qDzuuYxQMsuywaTNyzL_pac/export?format=csv&gid=1746689811. Downloads /templates/google-sheets.csv and /templates/google-sheets.xlsx.) | {"format": "jsonstat2"} (SSB/Eurostat; filter the query to a single series), "transform"?: [{"op": "pct_change"|"diff"|"rolling_mean"|"rolling_sum"|"rolling_min"|"rolling_max"|"shift", "n": int} | {"op": "scale", "factor": num}], "keep"?: "all"|"last"}. "value_type": "text" stores labels (no transforms; see Data policy). Dates like 2026-09-30, 2026M08, 2026Q2/2026K2, 2026 or unix time are normalised to ISO period-end dates; date-times (2026-10-05T14:00+02:00) count on the day as written. A single JSON value with no timestamp (omit "date", or a date path the body doesn't have) is dated with the fetch's half hour: that is valid for a snapshot such as a price. A date that is present but not a date is an error. Do not invent a timestamp field. URLs and string bodies may use {today}, {yesterday}, {tomorrow}, {year}, {month}, {day} (UTC, filled in on every run) for sources that put the date in the URL. Private APIs: the URL, header values and string body may use {{secret:NAME}} for a secret the workspace owner stored (list_secrets shows the names and the one host each may be sent to); the value is filled in at fetch time and never shown, e.g. "headers": {"Authorization": "Bearer {{secret:STRIPE_KEY}}"}. A stored secret may be sent on GET, on a POST whose body is a GraphQL query, or on POST https://api.hubapi.com/crm/v3/objects/{deals,contacts,companies}/search only (HubSpot's read-only CRM search, JSON body; https://developers.hubspot.com/docs/api-reference/legacy/crm/search-the-crm). No other POST may carry a secret, and the secret is still sent only to the host it was saved for. If a needed secret is missing, ask the user to add it at /account under Data sources.

Discovered
list_annotations

Read the latest annotations of a series.

Discovered
list_dashboard_versions

List a dashboard's saved versions (newest first) with which one is live and a members-only preview URL for each.

Discovered
list_dashboards

List dashboards with their URLs.

Discovered
list_integration_skills

List integration setup playbooks (Stripe, Shopify, Sheets, and the other sources tlly can read). A spreadsheet starts from the KPI template. Make a copy https://docs.google.com/spreadsheets/d/1Ty5X3LkY2unxTNGbkq36qDzuuYxQMsuywaTNyzL_pac/copy. Public CSV https://docs.google.com/spreadsheets/d/1Ty5X3LkY2unxTNGbkq36qDzuuYxQMsuywaTNyzL_pac/export?format=csv&gid=1746689811. Downloads /templates/google-sheets.csv and /templates/google-sheets.xlsx. Public: no API key required.

Discovered
list_jobs

List scheduled fetch jobs with their last run status and any error message.

Discovered
list_secrets

List the names of the workspace's stored secrets (API keys for private sources) and the host each may be sent to. Use one in a job's url, headers or body as {{secret:NAME}}.

Discovered
list_series

List all series with their type (number or text), point counts and date ranges; text series also list their values.

Discovered
publish_dashboard_version

Make a saved version of a dashboard the live one (also to roll back). The dashboard keeps its URL and visibility.

Discovered
push_points

Write dated values to a series (upsert by date). A date is YYYY-MM-DD (midnight UTC), or a time such as 2026-09-30T14:30Z, floored to the half hour. Use when you compute data yourself. Values are numbers, or short text labels for a text series (e.g. a rating or stance; see Data policy). A series' first write fixes its type.

Discovered
rotate_dashboard_url

Give a dashboard a new public URL and disable the old one at once (e.g. if a link leaked). Spec and data are unchanged.

Discovered
run_job_now

Run a scheduled fetch job immediately, store any new points, and return the run result.

Discovered
set_alert_webhook

Set or clear the workspace webhook URL for job-failure and dashboard status alerts (pass an empty url to clear). When events fire, tlly POSTs JSON to that URL (Slack and Discord incoming webhooks get a short text payload instead). Events include job_failed, status_changed (keep panel "id" fields in specs so alerts stay attached), and threshold_crossed. Email alerts work without a webhook. Caps: 12 emails and 60 webhooks per account per hour; the same panel is not alerted twice within an hour.

Discovered
set_panel_alert

Set when one dashboard panel sends alert email and/or a webhook (colour change, threshold cross, or off). trigger: change (default), red, notgreen, above, below, or off; above/below need limit. notify: both, email, or webhook. panel is the 0-based index in the live spec. Saving can send immediately if a limit was just crossed (unless this panel alerted within the last hour). Later sends happen after scheduled fetches. Default change+both removes a custom rule. Does not change colour lines or save a dashboard version.

Discovered
set_stale_alert

Configure when tlly sends stale-data alert email and webhooks because a series stopped updating. after: auto (from the job schedule and point spacing, at least two days), off, hours, or a window like 48h or 7d. Use dashboard_id for every series on a dashboard, series for one series, or both for one series on one dashboard. One email and one webhook when it goes quiet; another when a newer point arrives. Counts toward the hourly caps.

Discovered
test_job

Dry-run a job spec: fetch, extract and transform, return a preview. Saves nothing. Job spec: {"series": name, "cron": "m h dom mon dow" (UTC, at most every 30 min), "request": {"url", "method"?: "GET"|"POST", "headers"?: {}, "body"?: obj|str, "pages"?: {"param": "page", "start"?: 0, "max"?: 1-10} (json with items: bump a query parameter until an empty items array) | {"cursor": "paging.next.after", "into": "after", "max"?: 1-10} (copy that JSON path from each response into the JSON body; stop when it is missing. A cursor that remains after max pages is an error, so a sum is never a partial total. HubSpot CRM search uses this. A json aggregate with no request.pages fails when the body still has another page, paging.next.after, has_more true, links.next, pageInfo.hasNextPage or another next cursor, instead of saving a partial total. Set pages. A response with no next marker is complete.)}, "extract": {"format": "json", "items"?: "path.to.array", "date"?: "field", "value": "field", "value_type"?: "number"|"text", "aggregate"?: "count"|"sum"|"mean"|"min"|"max" (records per date, e.g. hourly prices to a daily mean; needs items; all but count need value), "period"?: "D"|"M"|"Q", "where"?: [{"field": "path", "equals"|"not_equals": value}] (up to 5, with aggregate). Omit "date" with aggregate to snapshot every matching record as one point on the fetch's half hour. String numbers (HubSpot properties.amount) are summed.} | {"format": "json_columns", "dates": "path.to.array", "values": "path.to.array", "value_type"?} (two parallel arrays, e.g. Yahoo chart: timestamp + close) | {"format": "csv", "date"?: "COLUMN", "value": "COLUMN", "delimiter"?, "value_type"?} (omit date to stamp every row with the fetch's half hour; one value per half hour, so the last row wins. That does not sum the rows the way a JSON aggregate that omits date does. A history needs a Date column. Make a copy https://docs.google.com/spreadsheets/d/1Ty5X3LkY2unxTNGbkq36qDzuuYxQMsuywaTNyzL_pac/copy. Public CSV https://docs.google.com/spreadsheets/d/1Ty5X3LkY2unxTNGbkq36qDzuuYxQMsuywaTNyzL_pac/export?format=csv&gid=1746689811. Downloads /templates/google-sheets.csv and /templates/google-sheets.xlsx.) | {"format": "jsonstat2"} (SSB/Eurostat; filter the query to a single series), "transform"?: [{"op": "pct_change"|"diff"|"rolling_mean"|"rolling_sum"|"rolling_min"|"rolling_max"|"shift", "n": int} | {"op": "scale", "factor": num}], "keep"?: "all"|"last"}. "value_type": "text" stores labels (no transforms; see Data policy). Dates like 2026-09-30, 2026M08, 2026Q2/2026K2, 2026 or unix time are normalised to ISO period-end dates; date-times (2026-10-05T14:00+02:00) count on the day as written. A single JSON value with no timestamp (omit "date", or a date path the body doesn't have) is dated with the fetch's half hour: that is valid for a snapshot such as a price. A date that is present but not a date is an error. Do not invent a timestamp field. URLs and string bodies may use {today}, {yesterday}, {tomorrow}, {year}, {month}, {day} (UTC, filled in on every run) for sources that put the date in the URL. Private APIs: the URL, header values and string body may use {{secret:NAME}} for a secret the workspace owner stored (list_secrets shows the names and the one host each may be sent to); the value is filled in at fetch time and never shown, e.g. "headers": {"Authorization": "Bearer {{secret:STRIPE_KEY}}"}. A stored secret may be sent on GET, on a POST whose body is a GraphQL query, or on POST https://api.hubapi.com/crm/v3/objects/{deals,contacts,companies}/search only (HubSpot's read-only CRM search, JSON body; https://developers.hubspot.com/docs/api-reference/legacy/crm/search-the-crm). No other POST may carry a secret, and the secret is still sent only to the host it was saved for. If a needed secret is missing, ask the user to add it at /account under Data sources.

Discovered
update_dashboard

Replace a dashboard's spec (URL stays the same). Every update is saved as a numbered version. With "draft": true it is saved without going live: viewers keep seeing the live version, members can preview the draft at the returned preview_url, and publish_dashboard_version makes it live. Use drafts for larger changes and publish when the user confirms. Dashboard spec: {"title": str, "description"?: str, "range"?: "1m"|"3m"|"6m"|"ytd"|"1y"|"2y"|"5y"|"10y"|"max", "theme"?: "auto"|"light"|"dark" (default mode; auto follows the viewer's system and viewers can switch), "layout"?: "auto"|"overview"|"detail" (only the default for panels without "display": overview makes them tiles, detail makes them charts, auto makes them tiles above 8 panels), "visibility"?: "public"|"private" (default public: anyone with the link; private: only members of this account signed in to tlly.dev, so embeds and shared links stop working for others), "panels": [{"title"?: str, "series": "name" | ["a","b"], "type"?: "line"|"area"|"bar"|"stat"|"gauge"|"bargauge"|"pie"|"heatmap"|"table", "unit"?: str, "note"?: str, "stale_after_days"?: num, "thresholds"?: {"red_above"?, "yellow_above"?, "yellow_below"?, "red_below"?}, "range"?: (as above, overrides the dashboard's), "section"?: str (starts a new titled section at this panel), "views"?: bool (show the Level / Y/Y % / Index buttons; default: shown unless the unit is already a rate like %, y/y or pp), "display"?: "tile"|"chart" (tile: a compact number with a sparkline, click for the full chart; chart: a card with the full chart; mix them freely, e.g. tiles for the many supporting numbers and charts for the few that need their history), "headline"?: bool (show as a big number at the top of the dashboard, under a status strip; use for the 3-6 most important), "width"?: "half"|"full", "scale"?: "linear"|"log", "id"?: str (stable panel id, e.g. pn_abc123XYZ012; keep existing ids when editing so alerts stay attached), "right_axis"?: [series names] (line, area and bar charts: draw these series against a second y axis on the right, for series of different sizes or units; at least one stays on the left), "right_unit"?: str (unit of the right axis; "unit" is the left's), "left_label"?/"right_label"?: str (≤40 chars, a title written vertically along the left / right y axis, e.g. "Policy rate"; right_label needs right_axis), "view"?: "level"|"yoy"|"index" (initial view; viewers can switch), "source"?: str (shown as "Source: …"; defaults to the job's host), "labels"?: [display name per series], "status_series"?: name (a red/yellow/green text series that sets the panel colour), "compare"?: "prev"|"3m"|"1y" (what the change is measured against), "period"?: "D"|"M"|"Q" (show dates as 2026-06 or 2026Q2), "value_names"?: {"green": "Hold", …} (display names for text values)}], "badge"?: "worst" (default: the header badge shows the worst panel) | "off" | panel index (0-based; the badge follows that panel), "shade"?: [{"from": date, "to": date, "label"?: str}] (up to 20 shaded periods on every chart)}. "type": "table" shows one row per series (up to 20): label, latest value, date and a status dot; its "status_series" is a list with one entry per row ("" for none). "area" is a line chart filled underneath; "gauge" is a dial of the latest value with the threshold zones, and "bargauge" a horizontal bar per series (up to 20, coloured by thresholds), both scaled by "min"?/"max"?: num (default 0 to a bit above the largest value or threshold); "pie" is a donut of the series' latest values (shares of a whole, up to 20); "heatmap" is a calendar of the first series' daily values (good for activity per day). Status colour is computed from the latest value of the panel's first series; a series of red/yellow/green labels (e.g. a derived status) colours itself. Notes may use {latest}, {date}, {period} and {change}, filled in from the panel's latest value. Annotations on a panel's series appear as markers. Viewers can pick a time range (1M to Max, year to date, or a custom from-to period), zoom, switch views and download CSV; ?embed=1 hides the page chrome and &panel=N shows one panel. Text limits: title ≤120, description ≤500, panel title ≤120, unit ≤20, note ≤280, section and source ≤80 characters, single line, plain text.

Discovered
update_job

Replace a job's spec, or pause it with enabled=false. Job spec: {"series": name, "cron": "m h dom mon dow" (UTC, at most every 30 min), "request": {"url", "method"?: "GET"|"POST", "headers"?: {}, "body"?: obj|str, "pages"?: {"param": "page", "start"?: 0, "max"?: 1-10} (json with items: bump a query parameter until an empty items array) | {"cursor": "paging.next.after", "into": "after", "max"?: 1-10} (copy that JSON path from each response into the JSON body; stop when it is missing. A cursor that remains after max pages is an error, so a sum is never a partial total. HubSpot CRM search uses this. A json aggregate with no request.pages fails when the body still has another page, paging.next.after, has_more true, links.next, pageInfo.hasNextPage or another next cursor, instead of saving a partial total. Set pages. A response with no next marker is complete.)}, "extract": {"format": "json", "items"?: "path.to.array", "date"?: "field", "value": "field", "value_type"?: "number"|"text", "aggregate"?: "count"|"sum"|"mean"|"min"|"max" (records per date, e.g. hourly prices to a daily mean; needs items; all but count need value), "period"?: "D"|"M"|"Q", "where"?: [{"field": "path", "equals"|"not_equals": value}] (up to 5, with aggregate). Omit "date" with aggregate to snapshot every matching record as one point on the fetch's half hour. String numbers (HubSpot properties.amount) are summed.} | {"format": "json_columns", "dates": "path.to.array", "values": "path.to.array", "value_type"?} (two parallel arrays, e.g. Yahoo chart: timestamp + close) | {"format": "csv", "date"?: "COLUMN", "value": "COLUMN", "delimiter"?, "value_type"?} (omit date to stamp every row with the fetch's half hour; one value per half hour, so the last row wins. That does not sum the rows the way a JSON aggregate that omits date does. A history needs a Date column. Make a copy https://docs.google.com/spreadsheets/d/1Ty5X3LkY2unxTNGbkq36qDzuuYxQMsuywaTNyzL_pac/copy. Public CSV https://docs.google.com/spreadsheets/d/1Ty5X3LkY2unxTNGbkq36qDzuuYxQMsuywaTNyzL_pac/export?format=csv&gid=1746689811. Downloads /templates/google-sheets.csv and /templates/google-sheets.xlsx.) | {"format": "jsonstat2"} (SSB/Eurostat; filter the query to a single series), "transform"?: [{"op": "pct_change"|"diff"|"rolling_mean"|"rolling_sum"|"rolling_min"|"rolling_max"|"shift", "n": int} | {"op": "scale", "factor": num}], "keep"?: "all"|"last"}. "value_type": "text" stores labels (no transforms; see Data policy). Dates like 2026-09-30, 2026M08, 2026Q2/2026K2, 2026 or unix time are normalised to ISO period-end dates; date-times (2026-10-05T14:00+02:00) count on the day as written. A single JSON value with no timestamp (omit "date", or a date path the body doesn't have) is dated with the fetch's half hour: that is valid for a snapshot such as a price. A date that is present but not a date is an error. Do not invent a timestamp field. URLs and string bodies may use {today}, {yesterday}, {tomorrow}, {year}, {month}, {day} (UTC, filled in on every run) for sources that put the date in the URL. Private APIs: the URL, header values and string body may use {{secret:NAME}} for a secret the workspace owner stored (list_secrets shows the names and the one host each may be sent to); the value is filled in at fetch time and never shown, e.g. "headers": {"Authorization": "Bearer {{secret:STRIPE_KEY}}"}. A stored secret may be sent on GET, on a POST whose body is a GraphQL query, or on POST https://api.hubapi.com/crm/v3/objects/{deals,contacts,companies}/search only (HubSpot's read-only CRM search, JSON body; https://developers.hubspot.com/docs/api-reference/legacy/crm/search-the-crm). No other POST may carry a secret, and the secret is still sent only to the host it was saved for. If a needed secret is missing, ask the user to add it at /account under Data sources.

Discovered

Recent measurements

Probe history

View status
Latest stored probes. Raw probe rows are kept for 14 days.
CheckedResultResponseTools
Connected731 ms30
Connected917 ms30