EntroπaLabs
Agentic AIAggregation · Public record

Every AI deadline, and the ones nobody publishes

Merging two volunteer feeds, destroying every date string on sight, and the discipline of leaving a column blank when the honest answer is that nobody published it.

A conference deadline looks like the simplest datum in the field: a name, a date, a link. It is not. It is quoted in a timezone that is a full day behind the one you read it in, it lives in two volunteer-maintained files that do not always agree, it moves without warning, and the things you actually want next to it — the workshops, what got in last year, what a plane ticket and a badge will cost — live in four other systems that were never designed to be joined. This post is about building the join, and about the discipline of leaving a field blank when the honest answer is that nobody published it.

AoE, or why a deadline is not a date

Almost every call for papers in machine learning quotes its deadline in AoE — anywhere on earth, UTC−12. It is a humane convention: your paper is on time as long as it is still the deadline day somewhere. It is also the single most common way a deadline board misleads people, in both directions. A "May 15 AoE" deadline is still open at 7am on May 16 in Berlin, and a board that renders the string 2026-05-15 in your local calendar has quietly moved it a day earlier. Do the same thing to a deadline quoted in UTC+8 and you have moved it later.

So the first thing the pipeline does with a deadline is destroy the string:

def to_utc(stamp, tz):
    naive = datetime.strptime(text, "%Y-%m-%d %H:%M:%S")
    hours, known = tz_offset(tz)          # "AoE" -> -12.0, "UTC+8" -> 8.0
    utc = naive.replace(tzinfo=timezone.utc) - timedelta(hours=hours)
    return utc.strftime("%Y-%m-%dT%H:%M:%SZ"), known

Everything downstream — the countdown, the sort, the calendar file, the filters — works on that instant. The original local string is kept and shown underneath, because a submitter checking against the call for papers wants to see the same characters the CFP used, but nothing computes on it.

Note the second return value. An unrecognised timezone is treated as UTC and flagged, rather than silently assumed. That distinction — between a value and a value you are sure of — is the whole design of this thing, and it comes up again in every section below.

Two feeds, and the value of the disagreement

The field's deadline data is maintained in a handful of open repositories, and this board merges the two best: huggingface/ai-deadlines, which carries a full timeline per conference (abstract, supplementary, rebuttal window, notification, camera-ready, each with its own timezone), and ccfddl/ccf-deadlines, which carries CCF and CORE ranks, DBLP keys, and a second, independent copy of the dates.

The obvious design is to pick a winner: prefer one feed, fall back to the other. It is also the wrong one. Both are volunteer-maintained and both are occasionally wrong, and a tie-break rule produces a board that is confidently incorrect some fraction of the time with no way for a reader to know which fraction. What a third party can add here is not arbitration — it is the disagreement:

gap = abs(_parse(theirs) - _parse(mine["utc"]))
if gap > timedelta(hours=6):
    return f"ccf-deadlines quotes {theirs} for the paper deadline"

Six hours, not zero, because the two feeds frequently record the same deadline against slightly different times of day and flagging that would make every row shout. Past six hours they are not rounding differently; one of them is wrong, and the row carries a feeds differ chip that says so and sends you to the CFP.

Parsing YAML without PyYAML

Both feeds are YAML, and every pipeline in this repository is standard-library-only — not as asceticism but because pip install is one more thing that can be down on the morning the board needs to refresh. So there is a parser in the module, and the interesting part is not writing it. It is proving it.

The dialect these files use is small: block mappings, block sequences, quoted and plain scalars, plain scalars folded across several indented lines, and one-line flow collections. Anchors, aliases, tags and multi-document streams do not appear — and miniyaml.py raises on all of them rather than guessing, so if a feed ever adopts one, the run fails loudly instead of dropping a conference off the board.

How do you know the dialect is what you think it is? By checking, over the whole corpus, against a real implementation:

$ python3 modules/deadlines/tools/check_yaml.py --refresh
cached 223 files
223/223 files parse identically

The first run of that check was 215/223. The eight failures were all the same construct — a long note: field wrapped across indented continuation lines, which is a plain scalar folded, not a nested structure. Twelve lines of _absorb() later, the corpus was clean. That is the entire argument for writing the check first: a hand-rolled parser that has never been diffed against a real one is a liability, and one that has been diffed over 223 files is a dependency you happen to own.

What actually got in: OpenReview, and what it will not tell you

The board wants more than dates. If you are choosing between two venues, you want to know how big each one is, how selective its oral track is, and what its last edition thought was good enough to highlight. OpenReview hosts the review process for a large share of the field and publishes it, so that is answerable — with three caveats that shape the whole feature.

First: a paper's decision is not a field, it is a sentence. Accepted notes carry a human string like NeurIPS 2025 spotlight, so the oral/spotlight/poster split comes from bucketing that string, and the code says as much where it does it.

Second: the note API sits behind a bot challenge that fires on some networks and not others. Every call degrades to None instead of raising, the page reports which source failed on its own status panel, and — the part that matters — every finished edition is cached permanently:

if vid in cache or ed["year"] > NOW.year:
    continue                     # a finished conference never changes its mind

Because of that line, the statistics on the page deepen every day the job runs, and a day when OpenReview refuses costs only what had not been collected yet rather than the whole table.

Third, and most important: there is no acceptance-rate column, because a rate needs a submission count and most venues never publish one. There is also no "top papers by citation" board, because neither feed carries citations. What the page shows instead is the programme committee's own ordering — the orals and spotlights — labelled as exactly that. A plausible-looking percentage that nobody can check is worse than a dash, because a dash tells you to go and look it up.

The cost question, and the honest answer to it

"What will it cost to go?" is the question the deadline lists never answer, and the reason is not laziness. It is that the answer barely exists in public form when you need it. The pipeline probes each conference's registration page daily and extracts fee/label pairs conservatively — a number counts only when a role word (student, industry, early, late) sits within a few text nodes of it, so a $5,000 sponsorship never becomes a badge price, and every hit keeps its surrounding text and the date it was read so you can check it.

There is one more guard, added after the scraper found something that looked perfect and was wrong. Societies redirect a guessed URL to whatever edition they are currently selling: asking for aaai-27/registration serves AAAI-26's fee table, which the extractor happily read and would have published under the wrong year — precise, sourced, and about a different conference.

if year is not None and str(year) not in html:
    return []

Most of the time this whole subsystem finds nothing, because fees go up a few months out and several conferences render them behind a login. So the cost panel is an estimator: registration is pre-filled from the observed table where one exists, and nights, hotel, flights and per-diem are yours, saved in your browser, with the arithmetic shown. Nobody can quote your flight, and a page that pretends otherwise is inventing the number you were least able to verify.

A calendar you can subscribe to

The board writes its deadlines twice: once in the browser, for whatever you have filtered down to, and once at build time to a fixed URL. Both use the same UID scheme, so importing the download and then subscribing does not duplicate every event.

The fixed file is the one that matters. A Blob the page builds cannot be subscribed to; a file at a stable address that gets rewritten every morning can, and any calendar that subscribes by URL will then track upstream changes on its own. Each event is the deadline instant in UTC — the client does the timezone conversion, which is the one job it is guaranteed to do correctly — with a one-day and a seven-day alarm. Lines are folded at 74 octets because a strict RFC 5545 reader will reject a whole calendar over one long line, which is a fun afternoon to lose.

Rendering it twice

The page renders the board server-side into the HTML and then again in the browser from the same JSON. That is not an accident of the build. A page whose content is drawn entirely by script is a page a search engine has to choose to render before it can index it, and this one exists to be found by someone typing "NeurIPS 2026 deadline" into a search box. So the HTML ships with the open deadlines as a plain list with real <time> elements and real links, plus schema.org Event and FAQPage blocks; the script removes that copy on boot and takes over with live countdowns and filters. Only editions with a real start date become structured-data events — an Event with an invented date would be a lie a machine reads.

What it does not do

It does not know about a conference the upstream feeds have not added, and the fix for that belongs in the feed where everyone benefits. It does not catch an extension in the hour it is announced — a CFP page changes, then a feed, then this. It has no acceptance rates for venues that publish no submission counts, and no workshop that does not use OpenReview. Every one of those limits is stated on the page itself, next to the thing it limits, because the alternative is a board that looks complete and quietly is not.

Open the board