OhEarningsCal: earnings dates as a calendar subscription
Subscribe to US stock earnings dates from your calendar app
At a glance
- What is it?
- OhEarningsCal generates iCalendar files of US earnings dates and publishes them as static files, so any calendar app can subscribe to them. The repository is a build pipeline: two scheduled GitHub Actions workflows fetch earnings data and index constituents, a generator writes the .ics files, and GitHub Pages serves the result.
- Who is it for?
- OhEarningsCal suits a self-directed investor who wants earnings dates to appear in the calendar they already look at, because subscribing to a static .ics URL is a better workflow than opening a brokerage app, and the rolling window means the feed stays relevant. It does not suit you if you need historical data or forecasts, since the window is plus or minus 30 days around today and the README says anything further out is not accurate enough to matter.
- Can I use it commercially?
- Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A static feed rather than an application
OhEarningsCal subscribes you to US stock earnings dates from your calendar app, and the origin story is worth repeating because it explains the design. It started as a tool the author built for himself, on the grounds that he would rather glance at his calendar than open a brokerage app, and turned out useful enough to open-source.
The delivery mechanism is a plain URL, and the usage is three steps. Visit the site, pick a calendar, copy the link, then add it as a calendar subscription in your app of choice.
What that buys is that the calendar app does the refreshing. A subscription URL is fetched by the client, so the subscriber gets new events pushed to them without a page reload, and the whole thing works on a phone. The project is not a web application that renders a list of dates, it is a generator that produces files which a calendar client polls.
The scope is deliberately narrow in two ways. The calendars cover the US market only, and they cover a rolling window of plus or minus 30 days around today, with the reason given plainly: anything further out is not accurate enough to matter. That last point is the most important design decision in the project, because earnings dates far in the future are provisional, and a calendar that fills up with dates that later move is worse than one that shows only the near term.
The homepage is earnings.beavern.com, and the project also appears on Trendshift.
Six calendars and what each one contains
The calendar list is small enough to enumerate, and the distinction between them is what a subscriber actually chooses between.
all.ics holds every company that reports earnings in the window, which is the whole market rather than any index. nasdaq100.ics covers Nasdaq-100 constituents, sp500.ics covers S&P 500 constituents, and dow30.ics covers the Dow Jones Industrial Average. customstock.ics is a custom watchlist driven by the CUSTOM_STOCKS environment variable. And selected.ics is the union of the four above, deduplicated.
The selected file is the one most people will want, since the union with deduplication means a company in two indices appears once. The deduplication is worth noting because it is not free: a name that appears in both the Nasdaq-100 and the S&P 500 has to be recognised as the same company, and the process directory in the layout is described as the filter and dedupe step, so the matching logic lives there.
The index membership question is also where the update frequency matters. Constituents change quarterly, and the indices workflow runs on Mondays at 06:17 UTC to refresh the index constituents from Wikipedia, committing them into data/indices/. The earnings workflow runs twice daily, at 04:34 and 16:34 UTC, fetching earnings, building the ics files and deploying to GitHub Pages.
That split is the right one. Earnings dates move daily and the index membership does not, so refreshing constituents weekly and earnings twice a day keeps each source as fresh as it needs to be without hammering either.
The index data is committed into git while the earnings data is not, and the README states the rule plainly: earnings data is cached in GitHub Actions cache, never in git, and main only carries source code and the index constituents. That keeps the repository history small and readable while the volatile data lives in the Actions cache.
Why a ±30 day window, and what the data source costs you
The narrow time window is the design decision that makes this project work rather than merely exist, and it deserves a second look because it cuts against the usual instinct to publish more.
Earnings dates are announced, then revised, and then sometimes moved entirely when a company changes its reporting calendar. A calendar subscription that populated six months of provisional dates would look authoritative and be wrong often, and a user who trusted it would be worse off than one that said nothing. The stated reasoning is that anything further out is not accurate enough to matter, so the feed deliberately refuses to be complete.
The consequence for the reader is that the subscription is a near-term planning tool rather than a calendar of the year. That is a limitation, but it is a limitation you can reason about, and it is different from a feed that is confidently wrong.
The data source side is where the scraping lives, and the dependency list says how it is done. cheerio is the HTML parser, which means the earnings rows come from parsing a web page rather than from a documented API. The fetch directory lists the data sources as Nasdaq earnings and Wikipedia constituents, so one source is a financial data page and the other is a wiki. The Wikipedia choice for index membership is a pragmatic one: the constituent lists are published there and are easy to parse, and a weekly refresh is enough for data that changes quarterly.
The process step is where the raw rows become usable. The process directory is described as filtering and deduping earnings rows, and the filtering has to reconcile what the source returns against the index constituent lists, because a calendar for the Nasdaq-100 has to contain exactly the companies in the Nasdaq-100 and not the ones merely nearby. The deduplication matters for selected.ics, since a company in two indices must appear once in the union.
The environment file names the outcome variables explicitly, SHOULD_GEN_SELECTED and SHOULD_GEN_ALL, with the comments describing them as whether to generate selected.ics, the union of all indices plus custom, and whether to generate all.ics, every ticker the API returned. So there is one upstream fetch and two different cuts of it, which is a cheaper design than running two separate fetches.
The whole thing runs on Node 20 or newer, with pnpm 11.10.0 as the package manager and the package itself marked private, because it is never published to a registry. It is a site generator with a schedule, not a library anybody installs.
Self-hosting it with GitHub Pages, and the two flags that control output
The self-hosting path assumes you are comfortable with GitHub Actions, because the whole pipeline is a scheduled workflow.
You fork the repository, then in Settings, Pages, Source you switch to GitHub Actions. In Settings, Secrets and variables, Actions, Variables you add three variables. SHOULD_GEN_SELECTED is true or false and controls whether selected.ics is generated. SHOULD_GEN_ALL is true or false and controls whether all.ics is generated. CUSTOM_STOCKS is a comma-separated list of tickers driving customstock.ics, with the example given as PDD,BABA,TCEHY, and leaving it empty skips that file.
After that you wait for the next scheduled run, or kick off the Update earnings calendar workflow manually from the Actions tab.
The two generation flags are the interesting design choice, and the environment file explains why. Whether to generate selected.ics, the union of all indices plus custom, and whether to generate all.ics, every ticker the API returned. They are separate switches because the two files have very different sizes, and a subscriber who only wants their watchlist should not have their calendar client download a file with every company in the market twice a day.
The remaining variable is the local dev server port, PORT=18302, which applies to the server.js entry point only, and the file is explicitly a copy-to-.env template that maps onto the Actions variables, with the comment noting that the same names are set under the repository settings in Actions.
That last point is the detail that makes the design coherent. The same three variable names drive the local development and the scheduled build, so there is no second configuration format to keep in sync, and a person debugging locally is configuring exactly what the workflow configures.
The local development loop and the code layout
The local workflow is a set of pnpm scripts, and the three-stage split mirrors the pipeline exactly.
pnpm install
cp .env.example .env # edit env vars
pnpm fetch:indices # first run / when refreshing constituents
pnpm fetch # pull yesterday + next 30 days into data/earnings/
pnpm gen # write docs/ics/*.ics
pnpm dev # http://localhost:18302The three stages are fetch-indices for the constituents, fetch for the earnings rows themselves, pulling yesterday and the next 30 days into data/earnings/, and gen for writing the .ics files into docs/ics/. The dev server then serves the result on port 18302.
The dev server also has convenience endpoints that trigger the same stages on demand, which is the part that speeds up development considerably. GET /dev/fetch triggers one fetch, GET /dev/fetch-indices triggers one fetch:indices, and GET /dev/gen triggers one gen. Without them you would have to run a separate pnpm command and then reload, and the endpoints collapse that into a single fetch from the browser.
The source layout is organised by stage. src/lib/ holds the shared helpers for date, paths, http and fs. src/config/ is env parsing and index metadata. src/fetch/ is the data sources, with the Nasdaq earnings feed and the Wikipedia constituents. src/process/ filters and dedupes the earnings rows. src/generate/ builds the .ics files. src/cli/ holds the entry points for fetch, fetch-indices and gen, and server.js is the local dev server.
The data and output directories follow the same logic. data/earnings/ is the daily JSON cache, gitignored and carried between runs in the Actions cache, and data/indices/ holds the index constituents, refreshed weekly and kept in git. docs/ holds the static landing page, the CNAME and the ics/ directory where the generated files land, which is also gitignored and written by the build.
The dependencies are minimal and tell you what it does. cheerio is the HTML parser for the earnings source, dotenv reads the local env file, express 5 is the dev server, and ics is the calendar generator. The tests run with the Node built-in test runner, node --test over the src test files, and nodemon is the only dev dependency, for the watch mode on the dev server.
Editorial conclusion
OhEarningsCal suits a self-directed investor who wants earnings dates to appear in the calendar they already look at, because subscribing to a static .ics URL is a better workflow than opening a brokerage app, and the rolling window means the feed stays relevant. It does not suit you if you need historical data or forecasts, since the window is plus or minus 30 days around today and the README says anything further out is not accurate enough to matter. Before you self-host it, read the environment file, because the two generation flags decide which calendars are published at all, and note the licence is recorded as MIT in the package metadata while the repository itself is recorded as unclassified.
Frequently asked questions
How do I subscribe to earnings dates with OhEarningsCal?
Visit earnings.beavern.com, pick a calendar, copy the link, then add it as a calendar subscription in your app of choice. The calendar client then refreshes the feed for you, so new earnings dates appear without opening a brokerage app.
Which calendars does OhEarningsCal publish?
all.ics for every company reporting in the window, nasdaq100.ics, sp500.ics and dow30.ics for the three index sets, customstock.ics for a watchlist driven by the CUSTOM_STOCKS variable, and selected.ics as the deduplicated union of the four.
How far ahead does the OhEarningsCal calendar go?
The calendars cover the US market in a rolling window of plus or minus 30 days around today, because the README says anything further out is not accurate enough to matter.
How do I run OhEarningsCal locally?
Run pnpm install, copy .env.example to .env and edit it, then pnpm fetch:indices on the first run, pnpm fetch to pull yesterday and the next 30 days of earnings into data/earnings/, pnpm gen to write the .ics files, and pnpm dev to serve on port 18302.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/jason5ng32-ohearningscal)Was this useful?