Data collection last run: 2026-04-12
⚠️ This is not an official USAJobs project
| Year | Jobs Opened | Jobs Closed | Coverage Notes |
|---|---|---|---|
| 2013 | 5 | 0 | Very limited |
| 2014 | 24 | 19 | Very limited |
| 2015 | 140 | 131 | Very limited |
| 2016 | 3,879 | 1,633 | Very limited |
| 2017 | 237,146 | 226,249 | ✅ Complete year |
| 2018 | 329,356 | 316,938 | ✅ Complete year |
| 2019 | 349,256 | 336,608 | ✅ Complete year |
| 2020 | 328,440 | 316,052 | ✅ Complete year |
| 2021 | 369,151 | 352,375 | ✅ Complete year |
| 2022 | 441,604 | 419,295 | ✅ Complete year |
| 2023 | 454,652 | 434,527 | ✅ Complete year |
| 2024 | 367,776 | 352,305 | ✅ Complete year |
| 2025 | 239,170 | 228,618 | Current through April 12, 2026 |
| 2026 | 96,021 | 95,300 | Closing dates only |
Note: This table shows fields from historical jobs data. Current jobs API contains additional fields that are preserved in the original nested structure but not included in this rationalized view.
| Field | Type | Examples | Completeness |
|---|---|---|---|
HiringPaths |
JSON Array | [{"hiringPath": "Career transition (CTAP, ICTAP, RPL)"}, {"hiringPath": "The public"}], [{"hiringPath": "Internal to an agency"}] (75962 unique combinations) | 100% |
JobCategories |
JSON Array | [{"series": "0560"}], [{"series": "0401"}, {"series": "0482"}, {"series": "0408"}], [{"series": "2216"}], [{"series": "0801"}, {"series": "0810"}, {"series": "0850"}, {"series": "0830"}, {"series": "0808"}] (4424 unique) | 100% |
PositionLocations |
JSON Array | [{"positionLocationCity": "Highlands", "positionLocationState": "New Jersey", "positionLocationCountry": "United States"}, {"positionLocationCity": "Brooklyn", "positionLocationState": "New York", "positionLocationCountry": "United States"}], [{"positionLocationCity": "Pentagon, Arlington", "positionLocationState": "Virginia", "positionLocationCountry": "United States"}] (29707 unique combinations) | 100% |
agencyLevel |
Integer | 2, 1 | 100% |
agencyLevelSort |
String | Department of Veterans Affairs\Office of the Secretary, Department of Homeland Security\U.S. Secret Service, Department of Defense\Defense Legal Services Agency, Department of Labor\Office of Federal Contract Compliance Programs (542 unique) | 100% |
announcementClosingTypeCode |
String | 01, 03, 02 | 100% |
announcementClosingTypeDescription |
String | Closing Date, Applicant Cut-Off, Open Continuous | 100% |
announcementNumber |
String | OPP-24-12365792, DS-2025-0045, ST-12632280-25-GH, 801932-MWJ-12542501-CORE (363816 unique) | 100% |
appointmentType |
String | Multiple, Intermittent, Term, Seasonal (16 unique) | 100% |
backfilled |
String | True | 0% |
disableApplyOnline |
String | N, Y | 100% |
drugTestRequired |
String | N | 100% |
hiringAgencyCode |
String | AR3A, DNNN, HUDD, DNSW (528 unique) | 100% |
hiringAgencyName |
String | Army Installation Management Command, Office of Workers' Compensation Programs, Merit Systems Protection Board, National Cemetery Administration (502 unique) | 99% |
hiringDepartmentCode |
String | FM, FJ, FI, UJ (122 unique) | 100% |
hiringDepartmentName |
String | Legislative Branch, Department of Transportation, Executive Office of the President, Department of Defense (26 unique) | 100% |
hiringSubelementName |
String | NIST Director's Office, Insights and Data Solutions Division/ Participant Management & Analysis, National Security Division, Counterterrorism Section, Ralph H. Johnson VA Healthcare System (59972 unique) | 62% |
hiringpaths |
JSON Array | [{"hiringPath": "The public"}], [{"hiringPath": "Federal employees - Excepted service"}, {"hiringPath": "The public"}] | 0% |
inserted_at |
String | 2025-07-05T13:59:26.153892, 2025-09-20T11:49:34.623503, 2025-09-20T11:30:21.480933, 2025-07-05T13:50:17.205599 (367192 unique) | 100% |
jobcategories |
JSON Array | [{"series": "2102"}], [{"series": "0610"}] | 0% |
last_seen |
String | 2025-09-20T12:32:20.429116, 2025-09-20T11:39:03.914170, 2025-09-20T12:21:27.943237, 2025-07-05T14:01:38.985450 (362993 unique) | 100% |
maximumGrade |
String | 72, KJ, 41, JJ (127 unique) | 100% |
maximumSalary |
Number | $0, $105,612, $470,281 (range: $0-$470,281) (15681 unique) | 100% |
minimumGrade |
String | 43, 08, 9, 71 (125 unique) | 100% |
minimumSalary |
Number | $0, $72,553, $400,000 (range: $0-$400,000) (15418 unique) | 100% |
payScale |
String | AC, XF, NF, NV (191 unique) | 100% |
positionCloseDate |
String | 2025-03-24, 2025-09-10, 2025-04-07, 2025-08-31 (671 unique) | 100% |
positionExpireDate |
String | 2025-01-22, 2023-03-06, 2024-02-17, 2025-02-12 (596 unique) | 9% |
positionOpenDate |
String | 2024-04-27, 2024-05-28, 2024-01-31, 2024-10-25 (365 unique) | 100% |
positionOpeningStatus |
String | Candidate selected, Job canceled, Applications under review, Job closed (5 unique) | 100% |
positionTitle |
String | RESEARCH ECOLOGIST, Diagnostic Radiologic Technologist (PACS Technologist), NURSE (CLINICAL/CASE MGMT), Social Worker (Geriatrics and Extended Care Program Coordinator) (77992 unique) | 100% |
positionlocations |
JSON Array | [{"positionLocationCity": "Richmond", "positionLocationState": "Virginia", "positionLocationCountry": "United States"}] | 0% |
promotionPotential |
String | 35, SN, PB5, 26 (115 unique) | 100% |
relocationExpensesReimbursed |
String | N | 100% |
salaryType |
String | Per Hour, Per Year, Without Compensation, Per Day (9 unique) | 100% |
securityClearance |
String | Not Required, Secret, Other, Confidential (8 unique) | 100% |
securityClearanceRequired |
String | N, Y | 100% |
serviceType |
String | Competitive, Excepted, Senior Executive | 100% |
supervisoryStatus |
String | N, Y | 100% |
teleworkEligible |
String | N | 100% |
totalOpenings |
String | 275, 89, 122, 175 (157 unique) | 90% |
travelRequirement |
String | Not required, Occasional travel, 25% or less, 50% or less (6 unique) | 100% |
usajobsControlNumber |
Integer | 767342200, 768473700, 769219600 (367192 unique) | 100% |
usajobs_control_number |
String | 767342200, 768473700, 769219600 (367190 unique) | 100% |
vendor |
String | USASTAFFING, Monster - Hiring Management, FAA - SWIFT, HQMC MCCS (PeopleSoft) (8 unique) | 100% |
whoMayApply |
String | Status Candidates (Merit Promotion and VEOA Eligibles) | 0% |
workSchedule |
String | Full-time, Intermittent, Part-time, Multiple Schedules (6 unique) | 100% |
This dataset combines data from two USAJobs APIs with field rationalization for consistent querying:
/api/historicjoa): Past job announcements by date range /api/Search): Currently active job postings Note: There is overlap between the APIs (2024-2025 jobs appear in both), but we collect from both APIs for completeness.
Important: The current_jobs_*.parquet files contain cumulative data - they include all jobs that have appeared in the Current API since we started collecting, not just jobs that are currently active. Once a job is added to these files, it remains there even after the position closes or is removed from the Current API. This provides a historical record of all jobs that were once "current."
| Historical Field Name | Historical API Source | Current API Source | Notes |
|---|---|---|---|
usajobsControlNumber |
usajobsControlNumber |
Extracted from PositionURI |
Numeric job identifier |
announcementNumber |
announcementNumber |
PositionID |
Public announcement ID |
hiringAgencyName |
hiringAgencyName |
DepartmentName |
Agency name |
hiringAgencyCode |
hiringAgencyCode |
OrganizationCodes (first part) |
Agency code |
positionTitle |
positionTitle |
PositionTitle |
Job title |
minimumGrade |
minimumGrade |
JobGrade[0].Code |
Minimum grade level |
maximumGrade |
maximumGrade |
JobGrade[-1].Code |
Maximum grade level |
minimumSalary |
minimumSalary |
PositionRemuneration[0].MinimumRange |
Minimum salary |
maximumSalary |
maximumSalary |
PositionRemuneration[0].MaximumRange |
Maximum salary |
positionOpenDate |
positionOpenDate |
PositionStartDate |
Position open date |
positionCloseDate |
positionCloseDate |
PositionEndDate |
Position close date |
usajobsControlNumber to identify records appearing in both APIs when neededHiringPaths, JobCategories, PositionLocations)MatchedObjectDescriptor, etc.)hiringAgencyName, positionTitle) while retaining access to original nested structuresThe examples.py script demonstrates both local file access and direct GitHub downloads. It runs complete analysis on 2.97M job postings and automatically cleans up downloaded files.
import pandas as pd
# Load a single year
df_2024 = pd.read_parquet('data/historical_jobs_2024.parquet')
print(f"✓ Successfully loaded {len(df_2024):,} job postings from 2024")
Data files are stored in Cloudflare R2. You can also browse the data interactively at usajobs-historical.vercel.app.
import pandas as pd
# Download directly from R2 (or use local files if you have them)
df = pd.read_parquet('data/historical_jobs_2024.parquet')
print(f"✓ Loaded {len(df):,} job postings")
# 1. TOP 15 HIRING AGENCIES (2024 DATA)
top_agencies = df_2024['hiringAgencyName'].value_counts().head(15)
for i, (agency, count) in enumerate(top_agencies.items(), 1):
percentage = count / len(df_2024) * 100
print(f"{i:2d}. {agency}: {count:,} jobs ({percentage:.1f}%)")
# Salary statistics
records_with_salary = df_2024[df_2024['maximumSalary'].notna()]
print(f"Records with salary data: {len(records_with_salary):,} ({len(records_with_salary)/len(df_2024)*100:.1f}%)")
# Salary ranges
min_range = f"${records_with_salary['minimumSalary'].min():,.0f} - ${records_with_salary['minimumSalary'].max():,.0f}"
max_range = f"${records_with_salary['maximumSalary'].min():,.0f} - ${records_with_salary['maximumSalary'].max():,.0f}"
median_range = f"${records_with_salary['minimumSalary'].median():,.0f} - ${records_with_salary['maximumSalary'].median():,.0f}"
print(f"Minimum salary range: {min_range}")
print(f"Maximum salary range: {max_range}")
print(f"Median salary range: {median_range}")
import duckdb
# Create DuckDB connection and register multiple Parquet files
conn = duckdb.connect('download/usajobs.duckdb')
# Register Parquet files as external tables
years = ['2022', '2023', '2024']
for year in years:
file_path = f'data/historical_jobs_{year}.parquet'
conn.execute(f"CREATE OR REPLACE VIEW jobs_{year} AS SELECT * FROM '{file_path}'")
# Create unified view
conn.execute("""
CREATE OR REPLACE VIEW all_jobs AS
SELECT * FROM jobs_2024
UNION ALL SELECT * FROM jobs_2023
UNION ALL SELECT * FROM jobs_2022
""")
# Query salary trends by year
salary_trends = conn.execute("""
SELECT
EXTRACT(year FROM positionOpenDate) as year,
ROUND(AVG(minimumSalary)) as avg_min,
ROUND(AVG(maximumSalary)) as avg_max,
ROUND(percentile_cont(0.5) WITHIN GROUP (ORDER BY minimumSalary)) as median_min,
ROUND(percentile_cont(0.5) WITHIN GROUP (ORDER BY maximumSalary)) as median_max,
COUNT(*) as jobs
FROM all_jobs
WHERE minimumSalary IS NOT NULL
GROUP BY EXTRACT(year FROM positionOpenDate)
ORDER BY year DESC
""").fetchdf()
print(salary_trends)
# Monthly posting patterns
monthly_posts = df_2024.groupby(df_2024['positionOpenDate'].dt.month).size()
print("Monthly posting patterns:")
month_names = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
for month, count in monthly_posts.items():
percentage = count / len(df_2024) * 100
print(f" {month_names[month-1]}: {count:,} jobs ({percentage:.1f}%)")
# Most common position titles
common_titles = df_2024['positionTitle'].value_counts().head(10)
print("Most common position titles:")
for i, (title, count) in enumerate(common_titles.items(), 1):
percentage = count / len(df_2024) * 100
print(f" {i:2d}. {title}: {count:,} ({percentage:.1f}%)")