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),
"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 day of the fetch. 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 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&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 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.
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.