iRail planner
Journey planning over Linked Connections 2.0-alpha, in Node.js and your browser.
Now on npm:
npm install @irail/planner. A 0.x release on a draft specification: the API can
still change between minor versions.
By default the playground loads the iRail CDN timetable: NMBS/SNCB open data converted to Linked Connections 2.0-alpha, with the NMBS/SNCB GTFS Realtime feed for delays and cancellations. The format is a draft. Enter your own LC alpha manifest URL to try another publisher, or load the sample, which uses invented services on 8 November 2026 and is not a real timetable.
Explore accessibility with isochrones: click the map to see which stops you can reach within a chosen travel time across the selected networks. Timetable pages load as reachable stops activate their regions.
Advanced
Use the library
@irail/planner is an ESM npm library for Node.js 22.12+
and modern browsers. It scans Arrow connection pages locally and
returns alternatives with departure time, arrival time and vehicle
changes.
npm install @irail/planner
This Node.js example reads the live iRail CDN timetable:
import { createPlanner } from '@irail/planner';
const planner = createPlanner({
dataUrl: 'https://irail.github.io/iRail-cdn/manifest.jsonld',
});
const [source] = await planner.init();
const stop = (iri) => source.stops.id.indexOf(iri);
const result = await planner.plan({
// Gent-Sint-Pieters to Liège-Guillemins
origin: { sourceId: source.id, stopIdx: stop('http://irail.be/stations/NMBS/008892007') },
destination: { sourceId: source.id, stopIdx: stop('http://irail.be/stations/NMBS/008841004') },
departureAfter: Math.floor(Date.now() / 1000),
maxWalkMeters: 1000,
});
console.log(result.options);
Stop indices refer to the loaded source.
source.stops.id contains the publisher's stop IRIs; find
an index with source.stops.id.indexOf(stopIri), or use
{ lon, lat } as either endpoint. To work with station
names instead, use the iRail calls.
To work on the library itself, clone the
repository, run
npm ci, and start this playground locally with
npm run dev.
Keep the browser responsive
With a worker-aware bundler such as Vite, import the browser entry. The playground uses this same API:
import { PlannerClient, createPlannerWorker } from '@irail/planner/browser';
const client = new PlannerClient(createPlannerWorker());
const sources = await client.request('init', { dataUrl: manifestUrl });
const result = await client.request('plan', query, onProgress);
client.close();
API reference
| Method | Behavior |
|---|---|
createPlanner(options) |
Returns a planner. Options: dataUrl, optional
fetch, and cacheBytes (64 MiB
default).
|
await planner.init(url?) |
Loads reference tables and returns source metadata, stop IRIs, names and coordinates. Call again to load a new release. |
await planner.plan(query, onProgress?) |
Returns found, options,
totalOptions, window,
scanned and diagnostics. Each option has departure,
arrival, transfers and ride/walk legs.
|
await planner.isochrone(query, onProgress?) |
Uses a coordinate origin and
maxDurationSeconds (60–10800). Returns reachable
stop points and travel durations.
|
Journey queries require origin, destination,
and departureAfter (leave at or after) or
arrivalBefore (arrive at or before), as UTC epoch
seconds. Optional
sourceIds limits active feeds.
maxWalkMeters is 0–5000 (Node API default: 1000), for
each continuous walk at 5 km/h.
windowMode: 'adaptive' is the default: find the earliest
arrival in 24 hours, then search a window twice that elapsed time,
capped at 24 hours. Use 'full' to search the whole day.
A search returns up to six nondominated alternatives; later or slower
departures outside the adaptive window may be excluded. Pass
minOptions to keep searching later departures until that
many are found. With arrivalBefore the scan runs
backward from the deadline and returns the journeys closest to it, so
no window is needed (three hours back first, up to two days).
Ride legs expose the publisher's tripIri,
routeIri, connectionIri (boarding
connection), boardStopIri, and
alightStopIri. Times are always seconds, not JavaScript
milliseconds. Calls on a planner instance run sequentially and reuse
its connection cache.
iRail API calls
The planner also offers the features of the
iRail API, answered from the
timetable and its real-time feed, as ordinary JavaScript: times are
Dates, numbers are numbers, lists are arrays and what is
unknown is null. They are methods of
Planner and PlannerClient.
await planner.init();
const board = await planner.liveboard({
station: 'Gent-Sint-Pieters', time: new Date(), language: 'nl',
});
const { vehicle, stops } = await planner.vehicle({ id: 'BE.NMBS.IC508' });
const connections = await planner.connections({
from: 'Gent-Sint-Pieters', to: 'Liège-Guillemins',
});
| Call | Behavior |
|---|---|
stations({ language }) |
Every station, labelled in nl, fr, en or de. |
liveboard({ station, time, type }) |
The hour from time; departures name the
destination, arrivals the origin.
|
connections({ from, to, time, limit }) |
Six connections by default (at most 12), searching past the window until found; one leg per vehicle, with the stops in between. |
vehicle({ id, date }) |
All stops of a train, for example BE.NMBS.IC508.
|
composition(), disturbances(),
occupancyFeedback()
|
Not supported yet. They exist, but reject with
a NotSupportedError (status 501): their data comes
from sources the planner does not read.
|
A station is an id, a name in any language, or a station returned by
an earlier call. A time is the scheduled time and
delay (seconds) how late the real-time feed says it is;
canceled and platform come from the feed too
(null without one). Journeys that need a canceled train
are returned flagged canceled: true (pass
includeCanceled: false to leave them out).
connections with arriveBy: true arrives by
time; vehicles other than
all is not supported yet. Errors are IrailApiErrors with the
HTTP status the iRail API would use (400 bad parameter, 404 unknown
station or train).
Bring your timetable
Use an absolute HTTP(S) manifest URL (any filename), or a directory
URL ending in / containing manifest.jsonld.
For multiple feeds, a directory can contain sources.json:
{
"sources": [
{ "id": "nmbs", "name": "NMBS / SNCB", "manifest": "nmbs/manifest.jsonld" }
]
}
The loader supports LC schemaVersion: '2-alpha', Arrow
IPC files, IRI identifiers, regional page lists, scheduled pagination
and redirect pagination. Relative links resolve against response URLs.
Tables and pages must share the manifest's release ID. Cross-origin
publishers must enable CORS.
Real-time delays, cancellations, skipped stops and platforms from a
GTFS Realtime feed named in the manifest are applied while
connections are read;
planner.watchRealtime() polls it when a new copy is due (about every 30 seconds) and
emits update when it changes. Trains that split or join
(in-seat transfers, type 4) are followed without a transfer; shapes
are not implemented and transfer overrides between
different stations are ignored. Walking uses
straight-line distance and does not model streets, accessibility or
barriers.
Read the LC 2.0-alpha draft for the manifest and table schemas, and the README for development, test provenance and GitHub Pages setup.