Find train and bus connections in Austria (ÖBB Scotty).
ÖBB's journey planner Scotty: connections between two stations or places in Austria and beyond, departing at or arriving by a time, with departure and arrival times (and live times where known), duration, changes, platforms, and each leg's train or bus line and direction. Cancelled connections are marked. Tickets and prices stay in the ÖBB ticket shop.
- Site
- ÖBB Scotty, Austria
- Version
- v2 of 2
- Runs, last 7 days
- No reports yet
- sha256
- af56b1d791c2…
Run it with your agent
after installing WieldAsk in plain words. Your agent finds this recipe and calls it:
“Find train and bus connections in Austria (ÖBB Scotty).”
Read-only: it changes nothing on the site. No login needed.
{ "tool": "run", "arguments": { "recipe_id": "oebb.at/search-connections", "params": { "date": "2027-01-14", "from": "Wien Hbf", "time": "08:00", "to": "Salzburg Hbf" } } }
Requests
sent from your browser, in order- 1POST/gateRead-only POST: ÖBB's own journey planner gateway (HAFAS, https://fahrplan.oebb.at/gate, as configured in the Scotty web app's config/webapp.config.json) with two LocMatch requests in one call, one per end. The aid and client are the public values the web app ships to every visitor.
- 2POST/gateRead-only POST: TripSearch on the same gateway (outFrwd false = arrive by). The one-item foreach formats date and time the way HAFAS wants them (YYYYMMDD, HHMMSS). Lines and stations are looked up from the response's common lists by index. A planner error (no connection found, date outside the timetable) comes back as an empty list with HAFAS's own message.
Parameters
from your agentReturns
to your agentWhere it can send
- fahrplan.oebb.at
Anything else is blocked by the extension.
Reads from your session
Nothing. Your browser attaches the site's cookies by itself.
Used only in requests to the addresses on the left. Never sent to wield.run or your agent.
Checks
a run passes when- length(output.connections) >= `3`
- starts_with(output.from, 'Wien')
- starts_with(output.to, 'Salzburg')
- length(output.connections[?departure_date == '2027-01-14' && duration_min > `100` && duration_min < `400`]) == length(output.connections)
- length(output.connections[?length(legs) > `0` && legs[0].line != `null`]) == length(output.connections)
- to_number(join('', ['1', replace(output.connections[0].departure_time, ':', '')])) >= `10730`
- output.message == `null`
Raw recipe
Served as canonical JSON from /api/v1/recipes/oebb.at/search-connections, so its sha256 matches the ETag and the index.
curl -s https://wield.run/api/v1/recipes/oebb.at/search-connections | shasum -a 256 # af56b1d791c23fe306c13ea69eddabea2798274f25056a6651de5ffebe8026f8
{ "description": "ÖBB's journey planner Scotty: connections between two stations or places in Austria and beyond, departing at or arriving by a time, with departure and arrival times (and live times where known), duration, changes, platforms, and each leg's train or bus line and direction. Cancelled connections are marked. Tickets and prices stay in the ÖBB ticket shop.", "effect": "read", "id": "oebb.at/search-connections", "keywords": [ "öbb", "oebb", "scotty", "fahrplan", "zug", "bahn", "verbindung", "railjet", "nightjet", "westbahn", "wien", "salzburg", "graz", "innsbruck", "linz", "train", "austria", "timetable", "connections", "österreich" ], "output": { "schema": { "properties": { "connections": { "items": { "properties": { "arrival_day_offset": { "description": "1 = arrives the next day", "type": "integer" }, "arrival_platform": { "type": [ "string", "null" ] }, "arrival_realtime": { "type": [ "string", "null" ] }, "arrival_time": { "type": "string" }, "cancelled": { "type": "boolean" }, "changes": { "type": "integer" }, "departure_date": { "type": "string" }, "departure_platform": { "type": [ "string", "null" ] }, "departure_realtime": { "description": "HH:MM, live forecast if any", "type": [ "string", "null" ] }, "departure_time": { "description": "HH:MM, scheduled", "type": "string" }, "duration_min": { "type": "integer" }, "from": { "type": [ "string", "null" ] }, "legs": { "items": { "properties": { "arrival_time": { "type": "string" }, "departure_time": { "type": "string" }, "direction": { "type": [ "string", "null" ] }, "from": { "type": [ "string", "null" ] }, "line": { "description": "e.g. 'RJX 860', 'REX 7'", "type": [ "string", "null" ] }, "platform": { "type": [ "string", "null" ] }, "to": { "type": [ "string", "null" ] } }, "type": "object" }, "type": "array" }, "to": { "type": [ "string", "null" ] } }, "type": "object" }, "type": "array" }, "from": { "description": "The station ÖBB matched", "type": "string" }, "message": { "description": "The planner's own message when there are no connections, e.g. 'Datum außerhalb der Fahrplanperiode (04.09.2026 - 11.12.2027)'", "type": [ "string", "null" ] }, "to": { "type": "string" } }, "required": [ "from", "to", "connections" ], "type": "object" }, "value": "{from: steps.stations.from_name, to: steps.stations.to_name, message: steps.trips[0].message || `null`, connections: map(&(let $c = @ in {departure_date: join('-', [$c.date[0:4], $c.date[4:6], $c.date[6:8]]), departure_time: (length($c.dep) == `8` && join(':', [$c.dep[2:4], $c.dep[4:6]]) || join(':', [$c.dep[0:2], $c.dep[2:4]])), departure_realtime: $c.dep_rt && (length($c.dep_rt) == `8` && join(':', [$c.dep_rt[2:4], $c.dep_rt[4:6]]) || join(':', [$c.dep_rt[0:2], $c.dep_rt[2:4]])) || `null`, arrival_time: (length($c.arr) == `8` && join(':', [$c.arr[2:4], $c.arr[4:6]]) || join(':', [$c.arr[0:2], $c.arr[2:4]])), arrival_realtime: $c.arr_rt && (length($c.arr_rt) == `8` && join(':', [$c.arr_rt[2:4], $c.arr_rt[4:6]]) || join(':', [$c.arr_rt[0:2], $c.arr_rt[2:4]])) || `null`, arrival_day_offset: (length($c.arr) == `8` && (to_number(join('', ['1', $c.arr[0:2]])) - `100`) || `0`), duration_min: (length($c.dur) == `8` && (to_number(join('', ['1', $c.dur[0:2]])) - `100`) * `1440` + (to_number(join('', ['1', $c.dur[2:4]])) - `100`) * `60` + (to_number(join('', ['1', $c.dur[4:6]])) - `100`)) || ((to_number(join('', ['1', $c.dur[0:2]])) - `100`) * `60` + (to_number(join('', ['1', $c.dur[2:4]])) - `100`)), changes: $c.changes, from: $c.from, to: $c.to, departure_platform: $c.dep_platform, arrival_platform: $c.arr_platform, cancelled: $c.cancelled, legs: map(&(let $s = @ in {line: $s.line, direction: $s.direction, from: $s.from, to: $s.to, departure_time: (length($s.dep) == `8` && join(':', [$s.dep[2:4], $s.dep[4:6]]) || join(':', [$s.dep[0:2], $s.dep[2:4]])), arrival_time: (length($s.arr) == `8` && join(':', [$s.arr[2:4], $s.arr[4:6]]) || join(':', [$s.arr[0:2], $s.arr[2:4]])), platform: $s.platform}), $c.legs)}), steps.trips[0].connections || `[]`)}" }, "params": { "additionalProperties": false, "properties": { "arrive_by": { "default": false, "description": "time is the latest arrival instead of the earliest departure", "type": "boolean" }, "date": { "description": "Travel date YYYY-MM-DD", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "type": "string" }, "from": { "description": "Station or place, e.g. 'Wien Hbf', 'Salzburg', 'Innsbruck Hbf'. The first match of ÖBB's station search is used", "maxLength": 80, "minLength": 2, "type": "string" }, "results": { "default": 5, "maximum": 10, "minimum": 1, "type": "integer" }, "time": { "default": "08:00", "description": "HH:MM, Austrian local time", "pattern": "^([01][0-9]|2[0-3]):[0-5][0-9]$", "type": "string" }, "to": { "description": "Station or place, e.g. 'Graz Hbf', 'München Hbf'", "maxLength": 80, "minLength": 2, "type": "string" } }, "required": [ "from", "to", "date" ], "type": "object" }, "provenance": { "method": "browser-inspect", "notes": "The Scotty web app (fahrplan.oebb.at/webapp/) reads config/webapp.config.json, whose urlMgate is https://fahrplan.oebb.at/gate (the older /bin/mgate.exe answers VERSION errors for ver 1.88). js/hafas_webapp_config.js has the client {id OEBB, type WEB, name webapp, l vs_webapp}, ext OEBB.14, hciVersion 1.88 and the public aid 5vHavmuWPWIfetEe that every visitor's browser sends (a public key shipped to all visitors, not a secret; the numeric client version is left out, the gateway rejects the web app's '2.20.1' string). LocMatch (field S, stations and places) and TripSearch (depLocL, arrLocL, outDate YYYYMMDD, outTime HHMMSS, outFrwd, numF) are HAFAS's standard read requests. Times are HHMMSS or DDHHMMSS with a day offset; lines and stations come from res.common.prodL/locL by index. The gateway allows CORS from any origin; robots.txt allows /gate (it disallows /bin/). Answers servers. Zero-padded numbers ('07') are read as 1xx - 100, because to_number rejects leading zeros. v2 (2026-11-07 22:38): read_only_post on 2 steps only; requests and output unchanged.", "recorded_at": "2026-11-07T21:52:00+01:00", "recorded_by": "C4" }, "requires_login": false, "schema": "https://wield.run/schema/recipe/v1.json", "site": { "country": "AT", "id": "oebb.at", "language": "de", "name": "ÖBB Scotty", "origin": "https://fahrplan.oebb.at", "origins": [ "https://fahrplan.oebb.at" ] }, "steps": [ { "description": "Read-only POST: ÖBB's own journey planner gateway (HAFAS, https://fahrplan.oebb.at/gate, as configured in the Scotty web app's config/webapp.config.json) with two LocMatch requests in one call, one per end. The aid and client are the public values the web app ships to every visitor.", "expect": { "assert": "body.err == 'OK' && length(body.svcResL[0].res.match.locL) > `0` && length(body.svcResL[1].res.match.locL) > `0`", "content_type": "json", "status": [ 200 ] }, "extract": { "json": { "from_lid": "svcResL[0].res.match.locL[0].lid", "from_name": "svcResL[0].res.match.locL[0].name", "to_lid": "svcResL[1].res.match.locL[0].lid", "to_name": "svcResL[1].res.match.locL[0].name" } }, "id": "stations", "read_only_post": true, "request": { "body": { "type": "json", "value": { "auth": { "aid": "5vHavmuWPWIfetEe", "type": "AID" }, "client": { "id": "OEBB", "l": "vs_webapp", "name": "webapp", "type": "WEB" }, "ext": "OEBB.14", "formatted": false, "id": "wield", "lang": "deu", "svcReqL": [ { "meth": "LocMatch", "req": { "input": { "field": "S", "loc": { "name": "{{params.from}}", "type": "S" }, "maxLoc": 1 } } }, { "meth": "LocMatch", "req": { "input": { "field": "S", "loc": { "name": "{{params.to}}", "type": "S" }, "maxLoc": 1 } } } ], "ver": "1.88" } }, "credentials": "omit", "headers": { "Accept": "application/json", "Content-Type": "application/json" }, "method": "POST", "url": "/gate" } }, { "description": "Read-only POST: TripSearch on the same gateway (outFrwd false = arrive by). The one-item foreach formats date and time the way HAFAS wants them (YYYYMMDD, HHMMSS). Lines and stations are looked up from the response's common lists by index. A planner error (no connection found, date outside the timetable) comes back as an empty list with HAFAS's own message.", "expect": { "assert": "body.err == 'OK'", "content_type": "json", "status": [ 200 ] }, "extract": { "json": { "connections": "svcResL[0].err == 'OK' && (let $r = svcResL[0].res in let $P = zip(`[\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"10\",\"11\",\"12\",\"13\",\"14\",\"15\",\"16\",\"17\",\"18\",\"19\",\"20\",\"21\",\"22\",\"23\",\"24\",\"25\",\"26\",\"27\",\"28\",\"29\",\"30\",\"31\",\"32\",\"33\",\"34\",\"35\",\"36\",\"37\",\"38\",\"39\",\"40\",\"41\",\"42\",\"43\",\"44\",\"45\",\"46\",\"47\",\"48\",\"49\",\"50\",\"51\",\"52\",\"53\",\"54\",\"55\",\"56\",\"57\",\"58\",\"59\",\"60\",\"61\",\"62\",\"63\",\"64\",\"65\",\"66\",\"67\",\"68\",\"69\",\"70\",\"71\",\"72\",\"73\",\"74\",\"75\",\"76\",\"77\",\"78\",\"79\",\"80\",\"81\",\"82\",\"83\",\"84\",\"85\",\"86\",\"87\",\"88\",\"89\",\"90\",\"91\",\"92\",\"93\",\"94\",\"95\",\"96\",\"97\",\"98\",\"99\",\"100\",\"101\",\"102\",\"103\",\"104\",\"105\",\"106\",\"107\",\"108\",\"109\",\"110\",\"111\",\"112\",\"113\",\"114\",\"115\",\"116\",\"117\",\"118\",\"119\",\"120\",\"121\",\"122\",\"123\",\"124\",\"125\",\"126\",\"127\",\"128\",\"129\",\"130\",\"131\",\"132\",\"133\",\"134\",\"135\",\"136\",\"137\",\"138\",\"139\",\"140\",\"141\",\"142\",\"143\",\"144\",\"145\",\"146\",\"147\",\"148\",\"149\"]`, $r.common.prodL || `[]`), $L = zip(`[\"0\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"10\",\"11\",\"12\",\"13\",\"14\",\"15\",\"16\",\"17\",\"18\",\"19\",\"20\",\"21\",\"22\",\"23\",\"24\",\"25\",\"26\",\"27\",\"28\",\"29\",\"30\",\"31\",\"32\",\"33\",\"34\",\"35\",\"36\",\"37\",\"38\",\"39\",\"40\",\"41\",\"42\",\"43\",\"44\",\"45\",\"46\",\"47\",\"48\",\"49\",\"50\",\"51\",\"52\",\"53\",\"54\",\"55\",\"56\",\"57\",\"58\",\"59\",\"60\",\"61\",\"62\",\"63\",\"64\",\"65\",\"66\",\"67\",\"68\",\"69\",\"70\",\"71\",\"72\",\"73\",\"74\",\"75\",\"76\",\"77\",\"78\",\"79\",\"80\",\"81\",\"82\",\"83\",\"84\",\"85\",\"86\",\"87\",\"88\",\"89\",\"90\",\"91\",\"92\",\"93\",\"94\",\"95\",\"96\",\"97\",\"98\",\"99\",\"100\",\"101\",\"102\",\"103\",\"104\",\"105\",\"106\",\"107\",\"108\",\"109\",\"110\",\"111\",\"112\",\"113\",\"114\",\"115\",\"116\",\"117\",\"118\",\"119\",\"120\",\"121\",\"122\",\"123\",\"124\",\"125\",\"126\",\"127\",\"128\",\"129\",\"130\",\"131\",\"132\",\"133\",\"134\",\"135\",\"136\",\"137\",\"138\",\"139\",\"140\",\"141\",\"142\",\"143\",\"144\",\"145\",\"146\",\"147\",\"148\",\"149\"]`, $r.common.locL || `[]`) in map(&(let $c = @ in {date: $c.date, dep: $c.dep.dTimeS, dep_rt: $c.dep.dTimeR || `null`, arr: $c.arr.aTimeS, arr_rt: $c.arr.aTimeR || `null`, dep_platform: $c.dep.dPltfR.txt || $c.dep.dPltfS.txt || `null`, arr_platform: $c.arr.aPltfR.txt || $c.arr.aPltfS.txt || `null`, dur: $c.dur, changes: $c.chg, cancelled: $c.dep.dCncl == `true` || $c.arr.aCncl == `true`, from: ($L[?[0] == to_string($c.dep.locX)] | [0][1].name), to: ($L[?[0] == to_string($c.arr.locX)] | [0][1].name), legs: map(&(let $s = @ in {line: ($P[?[0] == to_string($s.jny.prodX)] | [0][1].name) || `null`, direction: $s.jny.dirTxt || `null`, from: ($L[?[0] == to_string($s.dep.locX)] | [0][1].name), to: ($L[?[0] == to_string($s.arr.locX)] | [0][1].name), dep: $s.dep.dTimeS, arr: $s.arr.aTimeS, platform: $s.dep.dPltfR.txt || $s.dep.dPltfS.txt || `null`}), $c.secL[?type == 'JNY'] || `[]`)}), $r.outConL || `[]`)) || `[]`", "message": "svcResL[0].err != 'OK' && (svcResL[0].errTxtOut || svcResL[0].errTxt || svcResL[0].err) || `null`" } }, "foreach": { "as": "f", "in": "[{d: join('', split(params.date, '-')), t: join('', [replace(params.time || '08:00', ':', ''), '00']), fwd: params.arrive_by != `true`}]", "max": 1 }, "id": "trips", "read_only_post": true, "request": { "body": { "type": "json", "value": { "auth": { "aid": "5vHavmuWPWIfetEe", "type": "AID" }, "client": { "id": "OEBB", "l": "vs_webapp", "name": "webapp", "type": "WEB" }, "ext": "OEBB.14", "formatted": false, "id": "wield", "lang": "deu", "svcReqL": [ { "meth": "TripSearch", "req": { "arrLocL": [ { "lid": "{{steps.stations.to_lid}}", "type": "S" } ], "depLocL": [ { "lid": "{{steps.stations.from_lid}}", "type": "S" } ], "getPasslist": false, "getPolyline": false, "numF": "{{params.results}}", "outDate": "{{f.d}}", "outFrwd": "{{f.fwd}}", "outTime": "{{f.t}}" } } ], "ver": "1.88" } }, "credentials": "omit", "headers": { "Accept": "application/json", "Content-Type": "application/json" }, "method": "POST", "url": "/gate" } } ], "task": "Find train and bus connections in Austria (ÖBB Scotty)", "tos": { "notes": "Terms not reviewed yet for automated access. One person's search: 2 read-only requests to the journey planner's own gateway, the same ones the Scotty web app makes. No tickets: buying stays in the ÖBB shop.", "reviewed": false }, "verify": { "checks": [ "length(output.connections) >= `3`", "starts_with(output.from, 'Wien')", "starts_with(output.to, 'Salzburg')", "length(output.connections[?departure_date == '2027-01-14' && duration_min > `100` && duration_min < `400`]) == length(output.connections)", "length(output.connections[?length(legs) > `0` && legs[0].line != `null`]) == length(output.connections)", "to_number(join('', ['1', replace(output.connections[0].departure_time, ':', '')])) >= `10730`", "output.message == `null`" ], "example_params": { "date": "2027-01-14", "from": "Wien Hbf", "time": "08:00", "to": "Salzburg Hbf" }, "max_ms": 5000 }, "version": 2 }