{"authentication":{"authorizationServer":"https://tlly.dev/.well-known/oauth-authorization-server","protectedResource":"https://tlly.dev/.well-known/oauth-protected-resource","registrationEndpoint":"https://tlly.dev/oauth/register","required":true,"schemes":["oauth2"]},"prompts":[{"arguments":[{"description":"Skill id, such as stripe, hubspot or google-sheets","name":"integration","required":true}],"description":"Setup playbook for one tlly integration (credential, scopes, job spec, gotchas).","name":"setup_integration"}],"resources":[{"description":"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\u0026gid=1746689811. Downloads /templates/google-sheets.csv and /templates/google-sheets.xlsx.","mimeType":"text/markdown","name":"Google Sheets KPI template","uri":"tlly://templates/google-sheets"},{"description":"GET records with data.records:read. Offset paging is a cursor tlly does not follow.","mimeType":"text/markdown","name":"Airtable","uri":"tlly://skills/airtable"},{"description":"The Data API runReport call is a POST, so a stored secret cannot be sent. Use a sheet or push.","mimeType":"text/markdown","name":"GA4","uri":"tlly://skills/ga4"},{"description":"Weekly commit counts from the statistics API, or an open-issue snapshot if search returns total_count.","mimeType":"text/markdown","name":"GitHub","uri":"tlly://skills/github"},{"description":"Send Ads numbers to the Sheets template with a script. The Ads API is not a tlly job.","mimeType":"text/markdown","name":"Google Ads","uri":"tlly://skills/google-ads"},{"description":"Chart a published CSV. Start 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\u0026gid=1746689811; downloads /templates/google-sheets.csv and /templates/google-sheets.xlsx).","mimeType":"text/markdown","name":"Google Sheets","uri":"tlly://skills/google-sheets"},{"description":"Open pipeline value and deal count from HubSpot CRM search. The search POST is allowed.","mimeType":"text/markdown","name":"HubSpot","uri":"tlly://skills/hubspot"},{"description":"A read token from the Developer Hub. Conversation JSON paths were not re-fetched.","mimeType":"text/markdown","name":"Intercom","uri":"tlly://skills/intercom"},{"description":"Any public or keyed HTTPS JSON or CSV URL. A spreadsheet starts from the Google Sheets KPI template (Make a copy https://docs.google.com/spreadsheets/d/1Ty5X3LkY2unxTNGbkq36qDzuuYxQMsuywaTNyzL_pac/copy).","mimeType":"text/markdown","name":"JSON API","uri":"tlly://skills/json-api"},{"description":"GET orders with a Bearer key. Totals in the sample are cents. One page unless page[number] walks.","mimeType":"text/markdown","name":"Lemon Squeezy","uri":"tlly://skills/lemon-squeezy"},{"description":"Read issues with a GraphQL query. Personal keys omit the Bearer prefix.","mimeType":"text/markdown","name":"Linear","uri":"tlly://skills/linear"},{"description":"GET Ads Insights with ads_read. Spend is a decimal string on a date range.","mimeType":"text/markdown","name":"Meta Ads","uri":"tlly://skills/meta-ads"},{"description":"Database query is a POST, so a stored secret cannot be sent. Use a sheet or push.","mimeType":"text/markdown","name":"Notion","uri":"tlly://skills/notion"},{"description":"GET transactions with transaction.read. Amounts are minor-unit strings. Paging is a cursor.","mimeType":"text/markdown","name":"Paddle","uri":"tlly://skills/paddle"},{"description":"Read deals with the company API token. The token cannot be limited to read-only.","mimeType":"text/markdown","name":"Pipedrive","uri":"tlly://skills/pipedrive"},{"description":"The current Stats API is POST /api/v2/query, which tlly will not call with a stored secret.","mimeType":"text/markdown","name":"Plausible","uri":"tlly://skills/plausible"},{"description":"Queries are POST with scope query:read. Use a sheet or push, not a stored-secret job.","mimeType":"text/markdown","name":"PostHog","uri":"tlly://skills/posthog"},{"description":"JSON, CSV and JSON-stat from a public URL. No key, except sources that issue their own free key.","mimeType":"text/markdown","name":"Public data","uri":"tlly://skills/public-data"},{"description":"Your own systems POST numbers to a write-only address. tlly does not receive arbitrary vendor webhooks as series.","mimeType":"text/markdown","name":"Push and webhooks","uri":"tlly://skills/push"},{"description":"No Salesforce connector. Use a sheet or the push address.","mimeType":"text/markdown","name":"Salesforce","uri":"tlly://skills/salesforce"},{"description":"Read recent orders with the Admin GraphQL API and a read-only app token.","mimeType":"text/markdown","name":"Shopify","uri":"tlly://skills/shopify"},{"description":"Snapshot the Stripe balance with a restricted key. There is no MRR field.","mimeType":"text/markdown","name":"Stripe","uri":"tlly://skills/stripe"},{"description":"Token auth is Basic, as the user. Ticket JSON paths were not re-fetched; preview before charting.","mimeType":"text/markdown","name":"Zendesk","uri":"tlly://skills/zendesk"}],"serverInfo":{"name":"tlly","version":"1.0.0"},"tools":[{"annotations":{"destructiveHint":false,"idempotentHint":false,"openWorldHint":false,"readOnlyHint":false},"description":"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,\n\"series\": \"name\" | [\"a\",\"b\"], \"type\"?: \"line\"|\"area\"|\"bar\"|\"stat\"|\"gauge\"|\"bargauge\"|\"pie\"|\"heatmap\"|\"table\", \"unit\"?: str, \"note\"?: str, \"stale_after_days\"?: num,\n\"thresholds\"?: {\"red_above\"?, \"yellow_above\"?, \"yellow_below\"?, \"red_below\"?}, \"range\"?: (as above, overrides the dashboard's),\n\"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\",\n\"id\"?: str (stable panel id, e.g. pn_abc123XYZ012; keep existing ids when editing so alerts stay attached),\n\"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),\n\"view\"?: \"level\"|\"yoy\"|\"index\" (initial view; viewers can switch), \"source\"?: str (shown as \"Source: …\"; defaults to the job's host),\n\"labels\"?: [display name per series], \"status_series\"?: name (a red/yellow/green text series that sets the panel colour),\n\"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)}],\n\"badge\"?: \"worst\" (default: the header badge shows the worst panel) | \"off\" | panel index (0-based; the badge follows that panel),\n\"shade\"?: [{\"from\": date, \"to\": date, \"label\"?: str}] (up to 20 shaded periods on every chart)}.\n\"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).\n\"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).\nStatus 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.\nNotes may use {latest}, {date}, {period} and {change}, filled in from the panel's latest value. Annotations on a panel's series appear as markers.\nViewers 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 \u0026panel=N shows one panel.\nText limits: title ≤120, description ≤500, panel title ≤120, unit ≤20, note ≤280, section and source ≤80 characters, single line, plain text.","inputSchema":{"properties":{"spec":{"description":"dashboard spec","type":"object"}},"required":["spec"],"type":"object"},"name":"create_dashboard","title":"Create dashboard"},{"annotations":{"destructiveHint":false,"idempotentHint":false,"openWorldHint":false,"readOnlyHint":false},"description":"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,\n\"series\": \"name\" | [\"a\",\"b\"], \"type\"?: \"line\"|\"area\"|\"bar\"|\"stat\"|\"gauge\"|\"bargauge\"|\"pie\"|\"heatmap\"|\"table\", \"unit\"?: str, \"note\"?: str, \"stale_after_days\"?: num,\n\"thresholds\"?: {\"red_above\"?, \"yellow_above\"?, \"yellow_below\"?, \"red_below\"?}, \"range\"?: (as above, overrides the dashboard's),\n\"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\",\n\"id\"?: str (stable panel id, e.g. pn_abc123XYZ012; keep existing ids when editing so alerts stay attached),\n\"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),\n\"view\"?: \"level\"|\"yoy\"|\"index\" (initial view; viewers can switch), \"source\"?: str (shown as \"Source: …\"; defaults to the job's host),\n\"labels\"?: [display name per series], \"status_series\"?: name (a red/yellow/green text series that sets the panel colour),\n\"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)}],\n\"badge\"?: \"worst\" (default: the header badge shows the worst panel) | \"off\" | panel index (0-based; the badge follows that panel),\n\"shade\"?: [{\"from\": date, \"to\": date, \"label\"?: str}] (up to 20 shaded periods on every chart)}.\n\"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).\n\"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).\nStatus 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.\nNotes may use {latest}, {date}, {period} and {change}, filled in from the panel's latest value. Annotations on a panel's series appear as markers.\nViewers 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 \u0026panel=N shows one panel.\nText limits: title ≤120, description ≤500, panel title ≤120, unit ≤20, note ≤280, section and source ≤80 characters, single line, plain text.","inputSchema":{"properties":{"dashboard_id":{"description":"id","type":"string"},"draft":{"description":"save as a draft version without changing the live dashboard. Chat always drafts, even if this is false.","type":"boolean"},"note":{"description":"optional short description of this version (≤200 characters)","type":"string"},"replace_thresholds":{"description":"set true only after the user agrees to drop or change an existing colour line","type":"boolean"},"spec":{"description":"dashboard spec","type":"object"}},"required":["dashboard_id","spec"],"type":"object"},"name":"update_dashboard","title":"Update dashboard"},{"annotations":{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true},"description":"List a dashboard's saved versions (newest first) with which one is live and a members-only preview URL for each.","inputSchema":{"properties":{"dashboard_id":{"description":"id","type":"string"}},"required":["dashboard_id"],"type":"object"},"name":"list_dashboard_versions","title":"List dashboard versions"},{"annotations":{"destructiveHint":false,"idempotentHint":true,"openWorldHint":false,"readOnlyHint":false},"description":"Make a saved version of a dashboard the live one (also to roll back). The dashboard keeps its URL and visibility.","inputSchema":{"properties":{"dashboard_id":{"description":"id","type":"string"},"version":{"description":"version number from list_dashboard_versions or update_dashboard","type":"integer"}},"required":["dashboard_id","version"],"type":"object"},"name":"publish_dashboard_version","title":"Publish dashboard version"},{"annotations":{"destructiveHint":true,"idempotentHint":false,"openWorldHint":false,"readOnlyHint":false},"description":"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.","inputSchema":{"properties":{"dashboard_id":{"description":"id","type":"string"}},"required":["dashboard_id"],"type":"object"},"name":"rotate_dashboard_url","title":"Rotate dashboard URL"},{"annotations":{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true},"description":"List dashboards with their URLs.","inputSchema":{"properties":{},"required":[],"type":"object"},"name":"list_dashboards","title":"List dashboards"},{"annotations":{"destructiveHint":true,"idempotentHint":true,"openWorldHint":false,"readOnlyHint":false},"description":"Delete a dashboard (series data is kept).","inputSchema":{"properties":{"dashboard_id":{"description":"id","type":"string"}},"required":["dashboard_id"],"type":"object"},"name":"delete_dashboard","title":"Delete dashboard"},{"annotations":{"destructiveHint":true,"idempotentHint":true,"openWorldHint":false,"readOnlyHint":false},"description":"Write dated values to a series (upsert by date). 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.","inputSchema":{"properties":{"points":{"description":"[{\"date\":\"2026-09-30\",\"value\":4.6}] or [{\"date\":\"2026-09-30\",\"value\":\"AA+\"}]","items":{"type":"object"},"type":"array"},"series":{"description":"series name, e.g. household_credit_yoy","type":"string"}},"required":["series","points"],"type":"object"},"name":"push_points","title":"Write series points"},{"annotations":{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true},"description":"Read the latest points of a series.","inputSchema":{"properties":{"limit":{"type":"integer"},"series":{"description":"name","type":"string"}},"required":["series"],"type":"object"},"name":"get_series","title":"Read series points"},{"annotations":{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true},"description":"List all series with their type (number or text), point counts and date ranges; text series also list their values.","inputSchema":{"properties":{},"required":[],"type":"object"},"name":"list_series","title":"List series"},{"annotations":{"destructiveHint":true,"idempotentHint":true,"openWorldHint":false,"readOnlyHint":false},"description":"Delete all points of a series, or remove a derived series' definition.","inputSchema":{"properties":{"series":{"description":"name","type":"string"}},"required":["series"],"type":"object"},"name":"delete_series","title":"Delete series data"},{"annotations":{"destructiveHint":true,"idempotentHint":true,"openWorldHint":false,"readOnlyHint":false},"description":"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\"}.","inputSchema":{"properties":{"definition":{"description":"definition","type":"object"},"series":{"description":"name for the derived series","type":"string"}},"required":["series","definition"],"type":"object"},"name":"define_derived","title":"Define derived series"},{"annotations":{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true},"description":"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}}.","inputSchema":{"properties":{},"required":[],"type":"object"},"name":"list_secrets","title":"List secret names"},{"annotations":{"destructiveHint":false,"openWorldHint":true,"readOnlyHint":true},"description":"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 5 min),\n\"request\": {\"url\", \"method\"?: \"GET\"|\"POST\", \"headers\"?: {}, \"body\"?: obj|str,\n            \"pages\"?: {\"param\": \"page\", \"start\"?: 0, \"max\"?: 1-10} (json with items: bump a query parameter until an empty items array)\n                     | {\"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.)},\n\"extract\": {\"format\": \"json\", \"items\"?: \"path.to.array\", \"date\"?: \"field\", \"value\": \"field\", \"value_type\"?: \"number\"|\"text\",\n            \"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\",\n            \"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 day of the fetch. String numbers (HubSpot properties.amount) are summed.}\n         | {\"format\": \"json_columns\", \"dates\": \"path.to.array\", \"values\": \"path.to.array\", \"value_type\"?} (two parallel arrays, e.g. Yahoo chart: timestamp + close)\n         | {\"format\": \"csv\", \"date\"?: \"COLUMN\", \"value\": \"COLUMN\", \"delimiter\"?, \"value_type\"?} (omit date to stamp every row with the fetch day; one value per day, 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\u0026gid=1746689811. Downloads /templates/google-sheets.csv and /templates/google-sheets.xlsx.)\n         | {\"format\": \"jsonstat2\"}  (SSB/Eurostat; filter the query to a single series),\n\"transform\"?: [{\"op\": \"pct_change\"|\"diff\"|\"rolling_mean\"|\"rolling_sum\"|\"rolling_min\"|\"rolling_max\"|\"shift\", \"n\": int} | {\"op\": \"scale\", \"factor\": num}],\n\"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 day of the fetch: 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.\nURLs 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.\nPrivate 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.","inputSchema":{"properties":{"spec":{"description":"job spec","type":"object"}},"required":["spec"],"type":"object"},"name":"test_job","title":"Preview a fetch job"},{"annotations":{"destructiveHint":true,"idempotentHint":false,"openWorldHint":true,"readOnlyHint":false},"description":"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 5 min),\n\"request\": {\"url\", \"method\"?: \"GET\"|\"POST\", \"headers\"?: {}, \"body\"?: obj|str,\n            \"pages\"?: {\"param\": \"page\", \"start\"?: 0, \"max\"?: 1-10} (json with items: bump a query parameter until an empty items array)\n                     | {\"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.)},\n\"extract\": {\"format\": \"json\", \"items\"?: \"path.to.array\", \"date\"?: \"field\", \"value\": \"field\", \"value_type\"?: \"number\"|\"text\",\n            \"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\",\n            \"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 day of the fetch. String numbers (HubSpot properties.amount) are summed.}\n         | {\"format\": \"json_columns\", \"dates\": \"path.to.array\", \"values\": \"path.to.array\", \"value_type\"?} (two parallel arrays, e.g. Yahoo chart: timestamp + close)\n         | {\"format\": \"csv\", \"date\"?: \"COLUMN\", \"value\": \"COLUMN\", \"delimiter\"?, \"value_type\"?} (omit date to stamp every row with the fetch day; one value per day, 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\u0026gid=1746689811. Downloads /templates/google-sheets.csv and /templates/google-sheets.xlsx.)\n         | {\"format\": \"jsonstat2\"}  (SSB/Eurostat; filter the query to a single series),\n\"transform\"?: [{\"op\": \"pct_change\"|\"diff\"|\"rolling_mean\"|\"rolling_sum\"|\"rolling_min\"|\"rolling_max\"|\"shift\", \"n\": int} | {\"op\": \"scale\", \"factor\": num}],\n\"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 day of the fetch: 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.\nURLs 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.\nPrivate 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.","inputSchema":{"properties":{"name":{"description":"optional label","type":"string"},"spec":{"description":"job spec","type":"object"}},"required":["spec"],"type":"object"},"name":"create_job","title":"Create scheduled job"},{"annotations":{"destructiveHint":true,"idempotentHint":false,"openWorldHint":true,"readOnlyHint":false},"description":"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 5 min),\n\"request\": {\"url\", \"method\"?: \"GET\"|\"POST\", \"headers\"?: {}, \"body\"?: obj|str,\n            \"pages\"?: {\"param\": \"page\", \"start\"?: 0, \"max\"?: 1-10} (json with items: bump a query parameter until an empty items array)\n                     | {\"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.)},\n\"extract\": {\"format\": \"json\", \"items\"?: \"path.to.array\", \"date\"?: \"field\", \"value\": \"field\", \"value_type\"?: \"number\"|\"text\",\n            \"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\",\n            \"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 day of the fetch. String numbers (HubSpot properties.amount) are summed.}\n         | {\"format\": \"json_columns\", \"dates\": \"path.to.array\", \"values\": \"path.to.array\", \"value_type\"?} (two parallel arrays, e.g. Yahoo chart: timestamp + close)\n         | {\"format\": \"csv\", \"date\"?: \"COLUMN\", \"value\": \"COLUMN\", \"delimiter\"?, \"value_type\"?} (omit date to stamp every row with the fetch day; one value per day, 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\u0026gid=1746689811. Downloads /templates/google-sheets.csv and /templates/google-sheets.xlsx.)\n         | {\"format\": \"jsonstat2\"}  (SSB/Eurostat; filter the query to a single series),\n\"transform\"?: [{\"op\": \"pct_change\"|\"diff\"|\"rolling_mean\"|\"rolling_sum\"|\"rolling_min\"|\"rolling_max\"|\"shift\", \"n\": int} | {\"op\": \"scale\", \"factor\": num}],\n\"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 day of the fetch: 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.\nURLs 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.\nPrivate 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.","inputSchema":{"properties":{"spec":{"description":"job spec","type":"object"}},"required":["spec"],"type":"object"},"name":"import_once","title":"Import data once"},{"annotations":{"destructiveHint":true,"idempotentHint":false,"openWorldHint":true,"readOnlyHint":false},"description":"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 5 min),\n\"request\": {\"url\", \"method\"?: \"GET\"|\"POST\", \"headers\"?: {}, \"body\"?: obj|str,\n            \"pages\"?: {\"param\": \"page\", \"start\"?: 0, \"max\"?: 1-10} (json with items: bump a query parameter until an empty items array)\n                     | {\"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.)},\n\"extract\": {\"format\": \"json\", \"items\"?: \"path.to.array\", \"date\"?: \"field\", \"value\": \"field\", \"value_type\"?: \"number\"|\"text\",\n            \"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\",\n            \"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 day of the fetch. String numbers (HubSpot properties.amount) are summed.}\n         | {\"format\": \"json_columns\", \"dates\": \"path.to.array\", \"values\": \"path.to.array\", \"value_type\"?} (two parallel arrays, e.g. Yahoo chart: timestamp + close)\n         | {\"format\": \"csv\", \"date\"?: \"COLUMN\", \"value\": \"COLUMN\", \"delimiter\"?, \"value_type\"?} (omit date to stamp every row with the fetch day; one value per day, 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\u0026gid=1746689811. Downloads /templates/google-sheets.csv and /templates/google-sheets.xlsx.)\n         | {\"format\": \"jsonstat2\"}  (SSB/Eurostat; filter the query to a single series),\n\"transform\"?: [{\"op\": \"pct_change\"|\"diff\"|\"rolling_mean\"|\"rolling_sum\"|\"rolling_min\"|\"rolling_max\"|\"shift\", \"n\": int} | {\"op\": \"scale\", \"factor\": num}],\n\"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 day of the fetch: 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.\nURLs 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.\nPrivate 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.","inputSchema":{"properties":{"enabled":{"type":"boolean"},"job_id":{"description":"id","type":"string"},"spec":{"description":"job spec","type":"object"}},"required":["job_id","spec"],"type":"object"},"name":"update_job","title":"Update scheduled job"},{"annotations":{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true},"description":"List scheduled fetch jobs with their last run status and any error message.","inputSchema":{"properties":{},"required":[],"type":"object"},"name":"list_jobs","title":"List jobs"},{"annotations":{"destructiveHint":true,"idempotentHint":false,"openWorldHint":true,"readOnlyHint":false},"description":"Run a scheduled fetch job immediately, store any new points, and return the run result.","inputSchema":{"properties":{"job_id":{"description":"id","type":"string"}},"required":["job_id"],"type":"object"},"name":"run_job_now","title":"Run job now"},{"annotations":{"destructiveHint":true,"idempotentHint":true,"openWorldHint":false,"readOnlyHint":false},"description":"Delete a job (its series data is kept).","inputSchema":{"properties":{"job_id":{"description":"id","type":"string"}},"required":["job_id"],"type":"object"},"name":"delete_job","title":"Delete job"},{"annotations":{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true},"description":"Health check: failing jobs, stale or empty panels, dashboards, storage usage. Call this to verify everything works.","inputSchema":{"properties":{},"required":[],"type":"object"},"name":"get_status","title":"Workspace health check"},{"annotations":{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true},"description":"Storage usage against quotas (data points = values + annotations, series, dashboards, jobs) and the largest series. Check before large writes.","inputSchema":{"properties":{},"required":[],"type":"object"},"name":"get_usage","title":"Storage usage"},{"annotations":{"destructiveHint":true,"idempotentHint":true,"openWorldHint":false,"readOnlyHint":false},"description":"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.","inputSchema":{"properties":{"annotations":{"description":"[{\"date\":\"2026-09-25\",\"text\":\"Policy rate raised to 4.5%\"}]","items":{"type":"object"},"type":"array"},"series":{"description":"series name","type":"string"}},"required":["series","annotations"],"type":"object"},"name":"add_annotations","title":"Add chart annotations"},{"annotations":{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true},"description":"Read the latest annotations of a series.","inputSchema":{"properties":{"limit":{"type":"integer"},"series":{"description":"name","type":"string"}},"required":["series"],"type":"object"},"name":"list_annotations","title":"List chart annotations"},{"annotations":{"destructiveHint":true,"idempotentHint":true,"openWorldHint":false,"readOnlyHint":false},"description":"Delete annotations of a series on the given dates, or all of them if dates is omitted.","inputSchema":{"properties":{"dates":{"items":{"type":"string"},"type":"array"},"series":{"description":"name","type":"string"}},"required":["series"],"type":"object"},"name":"delete_annotations","title":"Delete chart annotations"},{"annotations":{"destructiveHint":true,"idempotentHint":true,"openWorldHint":true,"readOnlyHint":false},"description":"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.","inputSchema":{"properties":{"url":{"description":"https URL, or empty to clear","type":"string"}},"required":[],"type":"object"},"name":"set_alert_webhook","title":"Set alert webhook URL"},{"annotations":{"destructiveHint":true,"idempotentHint":false,"openWorldHint":true,"readOnlyHint":false},"description":"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.","inputSchema":{"properties":{"dashboard_id":{"description":"id","type":"string"},"limit":{"description":"required for above and below","type":"number"},"notify":{"description":"both, email or webhook","type":"string"},"panel":{"description":"0-based panel index","type":"integer"},"trigger":{"description":"change, red, notgreen, above, below or off","type":"string"}},"required":["dashboard_id","panel"],"type":"object"},"name":"set_panel_alert","title":"Configure panel alerts"},{"annotations":{"destructiveHint":true,"idempotentHint":true,"openWorldHint":true,"readOnlyHint":false},"description":"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.","inputSchema":{"properties":{"after":{"description":"auto, off, 48h, 7d, or a number of hours","type":"string"},"dashboard_id":{"description":"optional dashboard id","type":"string"},"series":{"description":"optional series name","type":"string"}},"required":[],"type":"object"},"name":"set_stale_alert","title":"Configure stale-data alerts"},{"annotations":{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true},"description":"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\u0026gid=1746689811. Downloads /templates/google-sheets.csv and /templates/google-sheets.xlsx. Public: no API key required.","inputSchema":{"properties":{},"required":[],"type":"object"},"name":"list_integration_skills","title":"List integration playbooks"},{"annotations":{"destructiveHint":false,"openWorldHint":false,"readOnlyHint":true},"description":"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\u0026gid=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.","inputSchema":{"properties":{"id":{"description":"skill id from list_integration_skills, e.g. stripe or google-sheets","type":"string"}},"required":["id"],"type":"object"},"name":"get_integration_skill","title":"Read integration playbook"}],"transport":{"type":"streamable-http","url":"https://tlly.dev/mcp"}}
