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/v2/notices?line=dart"
Follow every change without missing any: see Polling for changes.
Client libraries
Python: pyrailworks (PyPI) has sync and async clients, Pydantic models, RFC 9457 errors as exceptions, and cursor handling for the change log:
pip install pyrailworks
from pyrailworks import RailworksClient
with RailworksClient() as client:
for day in client.get_station_days("WBROK").days:
print(day.date, day.effect.value) # 2026-10-03 no_service
Every endpoint below has a Python example. Anything else can use the OpenAPI document with a client generator.
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; an extension is one that adds days and keeps every day still to come. |
| 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 or extended), 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 /v2/notices/{id} for its current state. Treat the
cursor as opaque. /v2/changes is the same log with the same cursors.
Compatibility and versioning
/v1 only changes additively: new endpoints, new optional query parameters, new response
fields, and new enum values. Within /v1, railworks never removes, renames or retypes a
field, makes an optional parameter required, or changes what a value means. So that it keeps
working:
- Ignore fields you don't know.
- Treat every enum as open. A new
effect, change-eventtypeorstatusmay appear; handle values you don't recognise (pyrailworks 0.2+ does this for you). - Send a
User-Agentnaming your client and version (myapp/1.2), so changes can take clients still in use into account.
A change that can't be additive becomes /v2, served alongside /v1 for an overlap period.
Anything being retired is marked with Deprecation (RFC 9745)
and Sunset (RFC 8594) response headers first.
Every change to this document is checked for breaking changes, and pyrailworks' test suite runs
against every change to the service.
/v2 and the /v1 Sunset. /v2 was added on 8 November 2026, because railworks no
longer republishes Iarnród Éireann's text (ADR 0003). The /v1 routes that serve it are
deprecated from that date and gone on 1 December 2026 (00:00 in Dublin, which is
2026-12-01T00:00:00Z):
Retired /v1 route |
Use instead |
|---|---|
/v1/notices |
/v2/notices |
/v1/notices/{id} |
/v2/notices/{id} |
/v1/notices/{id}/raw |
/v2/notices/{id} (revision history, without the HTML) |
/v1/health |
/v2/health |
Until the Sunset they answer as before, with Deprecation: @1794096000 (8 November 2026),
Sunset: Tue, 01 Dec 2026 00:00:00 GMT, and a Link to the same request under /v2
(rel="successor-version") and to these notes (rel="deprecation"). From the Sunset on they
answer 410 Gone with a problem document naming the successor, and the same headers.
Everything else in /v1 stays, unchanged and with no Deprecation header:
/v1/changes, /v1/stations, /v1/stations/{code}, /v1/stations/{code}/days,
/v1/openapi.yaml and, for good, /v1/feeds/notices.ics, since calendar subscriptions
can't be moved (their text has been railworks' own since before /v2). Each also has a /v2
twin that answers the same.
Migrating to /v2
Swap the /v1/ prefix for /v2/ everywhere; ids, cursors, query parameters, caching and
errors are the same. Then, for notices and health:
heading→title. railworks' own one-line name, generated from the facts ("DART: no trains Grand Canal Dock–Bray · Sat 24–Mon 26 Oct"). Usedays,starts_onandends_onfor dates: never parsetitle, which may be reworded.detail→summary. A list of lines (days, Works Segments, Effects, Service Alterations counted by change type), also generated, and in lists too.day_sections[]keep theirdaysand loseheading.quoteis gone from Works Segments, Effects and Service Alterations; their structured fields and codes stay. Works Segments loseclause; theirfromandto(and an Effect's) are now names from railworks' station list, absent when the place isn't known. A Service Alteration'soriginanddestinationare named from its codes, and its as-writtenstationslist is gone (usestation_codes)./v1/notices/{id}/rawhas no successor:/v2/notices/{id}has the revision history without the HTML. For the published text, see irishrail.ie.- Health:
held_notices,low_confidenceandneeds_reviewname notices bytitleinstead ofheading. - The container healthcheck (
railworks healthcheck) asks/v2/health, so it keeps working after the Sunset.
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
Deprecated: gone on 1 December 2026 (Sunset 2026-12-01T00:00:00Z). It serves Iarnród Éireann's published text, which railworks no longer republishes (ADR 0003). Use /v2/notices instead (see Migrating to /v2). Until then its responses carry Deprecation, Sunset and a successor-version link; from the Sunset it answers 410 Gone.
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
Deprecated: gone on 1 December 2026 (Sunset 2026-12-01T00:00:00Z). It serves Iarnród Éireann's published text, which railworks no longer republishes (ADR 0003). Use /v2/notices/{id} instead (see Migrating to /v2). Until then its responses carry Deprecation, Sunset and a successor-version link; from the Sunset it answers 410 Gone.
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
Deprecated: gone on 1 December 2026 (Sunset 2026-12-01T00:00:00Z). It serves Iarnród Éireann's published text, which railworks no longer republishes (ADR 0003). Use /v2/notices/{id} instead (see Migrating to /v2). Until then its responses carry Deprecation, Sunset and a successor-version link; from the Sunset it answers 410 Gone. /v2 has no equivalent of the HTML itself:
/v2/notices/{id} has the revision history without it.
The verbatim upstream HTML of every revision, oldest first, so you can check railworks' reading against the source.
GET /v2/changes Follow the change log (v2)
The same change log as /v1/changes (its events carry no upstream text), with the same
cursors: a cursor from either works on both. Each event names a notice_id: fetch
/v2/notices/{id} for its current state.
GET /v2/notices List Works Notices (v2)
Works Notices with any day in the window, as /v1/notices lists them but without Iarnród
Éireann's text: each notice has railworks' own title and summary instead of its
published heading, and no quotes. Unmerged across Service Groups (use Station Days for one
answer per date). Lists leave out day_sections, alterations and revisions: fetch a
single notice for those.
GET /v2/notices/{id} Get one Works Notice (v2)
The notice in full: its title and summary, Day Sections (days only), Works Segments,
Effects, per-train Service Alterations and revision history, with every place named from
railworks' station list. Works for any status. It replaces both /v1/notices/{id} and
/v1/notices/{id}/raw: the upstream HTML isn't served.
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.
GET /v2/stations List stations (v2)
The same as /v1/stations.
GET /v2/stations/{code} Get one station (v2)
The same as /v1/stations/{code}. Alias codes redirect to /v2/stations/{canonical code}.
GET /v2/stations/{code}/days Is this station affected, and when? (v2)
The same Station Days as /v1/stations/{code}/days; alias codes redirect within /v2.
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: no trains
Grand Canal Dock–Bray, Howth Junction–Howth"), UID
<notice-id>-<run-start>@railworks.paddez.com.
Text. Titles and descriptions are railworks' own wording, generated deterministically from the structured facts: the Service Group, the days, the Works Segments' ends, the Effects, times, and the Service Alterations counted by change type with a few examples ("Cancelled (2): 16:05 Dublin Connolly–Sligo; 17:05 Sligo–Dublin Connolly"). Station names come from railworks' station list. No heading, detail or quote from Iarnród Éireann's text is copied. A notice event's description lists those facts; a station event's lists each notice behind the day by its title and link. Every description ends with "Source: Iarnród Éireann (https://www.irishrail.ie/)", a disclaimer, and the link to the railworks notice or station resource. Event text isn't part of the JSON contract and may be reworded.
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").
GET /v2/feeds/notices.ics Calendar feed (v2)
The same calendar as /v1/feeds/notices.ics, byte for byte: calendar text was already
railworks' own wording. Both stay; /v1/feeds/notices.ics isn't retired, because
subscriptions can't be moved.
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
Deprecated: gone on 1 December 2026 (Sunset 2026-12-01T00:00:00Z). It serves Iarnród Éireann's published text, which railworks no longer republishes (ADR 0003). Use /v2/health instead (see Migrating to /v2). Until then its responses carry Deprecation, Sunset and a successor-version link; from the Sunset it answers 410 Gone. (Its notices' heading is the published heading;
/v2/health names them by title.)
Freshness, the last failure, the extraction backlog, and anything railworks couldn't resolve by itself.
GET /v1/openapi.yaml This document
GET /v2/health Health and things needing a human (v2)
The same as /v1/health, except that each listed notice is named by its generated title,
not its published heading. The container healthcheck (railworks healthcheck) uses this
route.
GET /v2/openapi.yaml This document (v2)
The same document as /v1/openapi.yaml, which describes both versions.
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.