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.

Use iRail CDN Use illustrative sample

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.

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.