Jaspers Hub

jaspers / bls

pluginOfficialReviewed★ 0↓ 3

BLS Economic Data: Fetches monthly time series from the Bureau of Labor Statistics public API, such as CPI, payrolls, and unemployment, by friendly name or BLS series id.

2.8 KB
1import { defineSource } from '@jaspers-ai/sdk'
2import { z } from 'zod'
3
4
5// The Bureau of Labor Statistics. Its public API v2 answers without a key; registering raises the
6// daily limit and is not needed to read a series. Inflation and the labour market, from the office
7// that measures them.
8
9const HOST = 'api.bls.gov'
10
11/** The series worth knowing by name, so a request can say "CPI" rather than CUUR0000SA0. */
12const KNOWN: Record<string, string> = {
13 cpi: 'CUUR0000SA0',
14 'cpi-core': 'CUUR0000SA0L1E',
15 unemployment: 'LNS14000000',
16 payrolls: 'CES0000000001',
17 'average-hourly-earnings': 'CES0500000003',
18 'labor-force-participation': 'LNS11300000',
19 ppi: 'WPUFD4',
20 'job-openings': 'JTS000000000000000JOL',
21}
22
23interface Answer {
24 status: string
25 message?: string[]
26 Results?: { series: { seriesID: string; data: { year: string; period: string; periodName: string; value: string }[] }[] }
27}
28
29export const series = defineSource({
30 description: `One BLS series over time, monthly. series is a BLS id like CUUR0000SA0, or one of these names: ${Object.keys(KNOWN).join(', ')}. Values come back oldest first, ready to chart.`,
31 hosts: [HOST],
32 input: z.object({
33 series: z.string().default('cpi'),
34 startYear: z.number().int().optional(),
35 endYear: z.number().int().optional(),
36 }),
37 async run({ series: wanted, startYear, endYear }, ctx) {
38 const id = KNOWN[wanted.trim().toLowerCase()] ?? wanted.trim().toUpperCase()
39 // BLS refuses one year without the other, so naming either fills in the rest.
40 const query = new URLSearchParams()
41 if (startYear || endYear) {
42 const thisYear = new Date().getFullYear()
43 query.set('startyear', String(startYear ?? (endYear ?? thisYear) - 10))
44 query.set('endyear', String(endYear ?? thisYear))
45 }
46 const url = `https://${HOST}/publicAPI/v2/timeseries/data/${encodeURIComponent(id)}${query.size ? `?${query.toString()}` : ''}`
47 const response = await ctx.fetch(url)
48 if (!response.ok) throw new Error(`BLS answered ${response.status}.`)
49 const body = (await response.json()) as Answer
50 if (body.status !== 'REQUEST_SUCCEEDED') throw new Error(body.message?.join(' ') || `BLS refused: ${body.status}.`)
51 const found = body.Results?.series?.[0]
52 if (!found || found.data.length === 0) throw new Error(`BLS has no data for ${id}. Check the series id.`)
53 const rows = found.data
54 // M13 is the annual average, which does not belong on a monthly line.
55 .filter((one) => one.period !== 'M13')
56 .map((one) => ({
57 series: found.seriesID,
58 date: `${one.year}-${one.period.replace('M', '')}-01`,
59 year: Number(one.year),
60 period: one.periodName,
61 value: Number(one.value),
62 }))
63 .filter((one) => Number.isFinite(one.value))
64 .sort((a, b) => a.date.localeCompare(b.date))
65 return { series: found.seriesID, rows }
66 },
67})