Irish Rail engineering works API
railworks reads Iarnród Éireann's planned engineering works page every 6 hours and turns it into structured, queryable data: which days, which stations, what happens there, and how that changed over time. It's read-only, unauthenticated and cacheable.
railworks is not affiliated with or endorsed by Iarnród Éireann. The data is machine-read from their page and may be wrong: check the source before travelling.
Quick start
Subscribe a calendar to one station. One all-day event per affected day, e.g. "No trains at Woodbrook":
webcal://railworks.paddez.com/v1/feeds/notices.ics?station=WBROK
- Apple Calendar / Outlook: open the
webcal://link, or add it as a subscribed calendar. - Google Calendar: Other calendars → From URL, using
https://instead ofwebcal://. Google refreshes subscriptions only every several hours. - Narrow it with
&line=dart(stations on the DART) or&group=DART%20Services(only notices published under DART).
Ask whether a station is affected, and when:
curl https://railworks.paddez.com/v1/stations/WBROK/days
List what's planned (today to 90 days ahead by default):
curl "https://railworks.paddez.com/v1/notices?line=dart"
Follow every change without missing any: see Polling for changes.
Concepts
| Term | Meaning |
|---|---|
| Works Notice | One dated block of engineering works, as published under one Service Group: a set of days, the published text, and what it does to the service. Identified across page updates by a stable id. |
| Service Group | A heading on the works page ("DART Services", "Rosslare Services") under which notices are published. |
| Line | The stations one Service Group's trains serve, in order along its branches, named after the group (dart, northern-commuters, …). |
| Build | One published version of the works page, identified by when upstream built it (built_at). |
| Revision | A change to a notice's published text between builds, identity unchanged. A trim is a revision that drops days that have passed. |
| Works Segment | A track section where works take place ("between Dromod and Sligo"), expanded to every station in between. |
| Effect | A station- or section-level consequence: no_service, station_closed, bus_replacement, reduced_service, other. |
| Service Alteration | A change to one train (cancelled, bus-replaced, terminating short, …). |
| Station Day | One date a station is affected, with the strongest effect there. The same closure published under DART and Rosslare is one Station Day. |
| Expired / Withdrawn | A notice leaves the page when its days pass (expired, the normal end of life) or while it still had days to come (withdrawn). Nothing is ever deleted. |
How notices are read
- Dates come from the heading only. The year is inferred from weekday names (e.g.
"Saturday 24 October" is 2026), and weekday typos are repaired when exactly one date fits.
date_inferenceshows how. - Where comes from the notice's "engineering works between X and Y" sentence and from an
LLM (Gemini) that structures the prose. Every LLM item is checked before it's used: its
quote must be verbatim in the notice, every station it names must appear in the text and be
a known station, and every time must appear in the text. Anything else is dropped and
lowers
confidence. - Extraction is asynchronous. A new notice is served at once with
extraction.status: pendingand fills in within minutes; anextractedchange event says so. confidence(0–1): the heading parsed to days +0.5, a validated works segment +0.3, every named station resolved +0.2; −0.1 for each weekday repair, unresolved date conflict, prose that disagrees with the heading, or LLM item that failed validation. Nothing is hidden by default (min_confidence=0).
Polling for changes
/v1/changes is an append-only log of created, revised (with trim), expired,
withdrawn and extracted events behind an opaque cursor. Start with ?since= once, then
always pass the previous response's next:
cursor=$(curl -s "https://railworks.paddez.com/v1/changes?since=2026-09-01T00:00:00Z" | jq -r .next)
# later, and forever after:
curl -s "https://railworks.paddez.com/v1/changes?after=$cursor" | jq '.events[], .next'
Each event names a notice_id: fetch /v1/notices/{id} for its current state. Treat the
cursor as opaque.
Freshness, caching and errors
- Every JSON response carries
source: when upstream built the page (built_at), when railworks first fetched that build (fetched_at), its last successful check (checked_at), andstale(no successful check for two poll intervals). "Upstream hasn't changed" and "railworks can't reach upstream" look different. - Every response has a weak
ETagandCache-Control: public, max-age=900; sendIf-None-Matchto get304 Not Modified. Please poll no more than every 15 minutes: the page itself changes a few times a week. - Errors are RFC 9457 problem documents. Unknown
query parameters are a 400, never ignored, so a typo like
?staton=fails loudly. - Dates are local calendar dates in Ireland (
YYYY-MM-DD); times of day are localHH:MM, as published; timestamps are RFC 3339 UTC.
Reference
Notices
Works Notices, their history, and the change log.
GET /v1/changes Follow the change log
Events after an opaque cursor, oldest first. Pass after (a previous next), or since
for a first call only. Each event is returned exactly once when you follow next; an
empty page means you're up to date (keep the same next).
GET /v1/notices List Works Notices
Works Notices with any day in the window, unmerged across Service Groups (the same
closure published under DART and Rosslare is two notices; use Station Days for one answer
per date). Lists leave out day_sections, detail, alterations and revisions: fetch
a single notice for those.
GET /v1/notices/{id} Get one Works Notice
The notice in full, with its Day Sections, text, per-train Service Alterations and revision history. Works for any status.
GET /v1/notices/{id}/raw Get a notice's upstream HTML
The verbatim upstream HTML of every revision, oldest first, so you can check railworks' reading against the source.
Stations
Stations, their Lines, and Station Days.
GET /v1/stations List stations
Every Irish Rail station served by timetabled trains, with its Lines and position on each. Refreshed daily from the TFI GTFS feed and Irish Rail's station list.
GET /v1/stations/{code} Get one station
Alias codes (other Irish Rail codes for the same station, e.g. ADAMF) redirect to the canonical code.
GET /v1/stations/{code}/days Is this station affected, and when?
One Station Day per affected date in the window, deduplicated across Service Groups,
with the strongest effect at the station (no_service > station_closed >
bus_replacement > reduced_service > other), any stated time scope, and the notices
behind it. A station reached only by a works segment gets other. Withdrawn notices
don't count.
Feeds
Calendar (ICS) subscriptions.
GET /v1/feeds/notices.ics Calendar feed
An iCalendar (RFC 5545) feed to subscribe to. All events are all-day; a stated time of day goes in the title.
- With
station: one event per Station Day at that station, titled with the effect ("No trains at Woodbrook", "Bus replacement at Boyle after 18:00"), UID<station>-<date>@railworks.paddez.com. The same closure under DART and Rosslare is one event. - Without: one event per Works Notice per run of consecutive days ("DART: works Dun
Laoghaire–Wicklow"), UID
<notice-id>-<run-start>@railworks.paddez.com.
UIDs are stable across rewording and trims (days that have passed stay as history), and
SEQUENCE rises with each change. Withdrawn events become STATUS:CANCELLED with a
"Cancelled: " title and leave the feed a week after their dates. Expired events stay as
ordinary past events. The feed covers 90 days back (from overrides) and asks clients to
refresh every 6 hours. The calendar is named after the filters ("Rail works · Woodbrook ·
DART").
Service
Health and this documentation.
GET / Home page
Subscribe to a station's calendar, see its upcoming affected days, and what's planned in the next four weeks. Built on this API.
GET /docs This documentation, rendered
GET /v1/health Health and things needing a human
Freshness, the last failure, the extraction backlog, and anything railworks couldn't resolve by itself.
GET /v1/openapi.yaml This document
Site
Files for browsers, crawlers and tools.
GET /favicon.svg Favicon
The site icon.
GET /llms.txt LLM summary
A plain-Markdown summary of the API for AI tools (llmstxt.org).
GET /og.png Social card
The 1200×630 image shown when the site is shared.
GET /robots.txt Robots rules
Lets crawlers index the pages and keeps them off the API.
GET /sitemap.xml Sitemap
The pages, with the home page's last change.