Concept
A RailCommand route is a settable path across your layout. The key mental model (ADR-0230) is that the operable route set is DERIVED from the track graph — it is never hand-authored and never imported as source of truth. RouteDerivationService walks the graph between route terminals — every track Block, plus every leg-modeled (ported) Turntable or TransferTable, which is itself a terminal and never an interior node (the device node is one occupancy identity, ADR-0202) — honoring each turnout's port model, and records the turnout positions (Closed or Thrown, and for a three-way turnout the leg the path takes, Left or Right) (and, for a ported movable-bridge endpoint, the required bridge position) that path requires. Because the routes come from the graph, they stay correct: any layout edit re-derives them, exactly like signal masts.
When you import a TrainController .yrrg file, RailCommand decodes its route records but does not treat them as the route set. Instead the imported TrainController routes become a per-layout verification corpus — an independent ground truth used to prove the imported connectivity matches what TrainController believed. This is why the product reports things like "113/150 TC routes verified by graph derivation": the derived routes are checked against the imported corpus. The decode reads both route populations a file can carry: the routes drawn on switchboard pages, and the embedded dispatcher routes some file-classes persist inside the dispatcher graph — each of those carries TrainController's own from/to block record, so on such a file the corpus arrives fully provenanced (e.g. 255 corpus routes on a file whose pages draw only 18).
A CTC board is a view of the layout, not more track. The graph is derived from the layout's track plan panels only; a generated or designed CTC board draws the blocks, switches and signals the track plan already has, and its cells keep the graph nodes they were bound to. Before 2026-09-27 each generated board was derived as another copy of the railroad, so a layout with boards held several copies of every block.
Panels connect through off-page connectors: two connector cells sharing an authored branch label are one continuous rail, and derivation joins them with a real edge (a connector with no partner is reported by label, never silently dropped). Since 2026-08-31 the derivation also reports how joined-up the railroad derived — connector pairs stitched, unpaired labels, and the graph's connected-component count — so a layout split across panels shows exactly how many pieces it still forms and which authored jumps are missing their mate.
A TrainController route that ends at an off-page connector is followed across the connector pair to the block beyond it, the same way the derivation joins the pair. TrainController stores a run that a connector pair splits as two one-element routes, each naming only the block on its own side; the import walks each side of that one cell and across the pair, so both halves name the same two blocks and verify against the derived route between them. A route whose two ends still resolve to one block begins and ends inside that block: it stays on the layout as an imported route with the turnout positions it sets, and its verdict says it is a single-block route, because block-to-block derivation has no second block to pair it with.
Separately, Interlocking Routes (the /routes page) are the signal-to-signal reservations a dispatcher sets and locks in a live session — with turnout settings, conflict detection, and a lock state. A three-way turnout in a route is set to Straight, Left or Right; a route saved before three-way turnouts had Left and Right, which said only Thrown, shows that turnout as unknown until you pick its leg, because Thrown does not say which leg was meant.
Interlocking routes can be built from your schedules. Every step a schedule takes from one of its blocks to the next becomes a route from the first block's own exit signal at the end the train leaves by to the next block's own exit signal at its far end, where a train entering it stops, with the turnouts of the one way between them set. The way is the one TrainController's schedule named, or the only one between the two blocks. Routes are shared: one route serves every schedule and step that uses the same signals and turnouts. They belong to a control point named Routes from schedules, and the Routes page marks each with From schedules. Each schedule leg names the route its step became, unless you chose another route for that leg. Building again brings those routes up to date with the schedules and retires the ones no schedule uses any more; a route you edited is marked Edited and left as you left it, and one you deleted stays deleted. A TrainController import builds them as it imports the schedules. A step that cannot become a route keeps the way RailCommand on the desktop finds as the train runs, and the result says why: the way crosses a turntable or sets a three-way turnout, several ways join the two blocks and the schedule names none, there is no way between them on the drawing, or no exit signal of the first block leads through the second to its far end.
Schedules run on blocks and legs, and a leg runs on a route. A schedule saved in the Schedule Builder with a start block and a destination block reaches RailCommand on the desktop as blocks and legs, the same form an imported TrainController schedule has: its blocks are the start block, the block of each Move To or Wait At step in order, and the destination block, and a Wait At is the time the train waits in that block. Each leg from one block to the next runs along the one interlocking route whose path goes from the first block to the second. With no such route it runs directly when the two blocks are joined by track with no turnout between them; across turnouts, RailCommand on the desktop finds the way itself when the train departs, as TrainController does: over the track and turnouts between the two blocks, never through another block, setting and holding every turnout on the way as a Run to does. A leg whose only way passes through another block does not run; that block belongs in the schedule as a stop. RailCommand on the desktop does not yet run a schedule that mixes legs along an interlocking route with legs it finds the way for, a Shuttle or Cycle with such legs, or a way that needs one turnout set both ways, such as a reversing loop. Set Speed and Stop steps are not sent to the desktop. The builder's What the desktop runs panel lists the blocks and, for each leg, how the desktop runs it, as last saved. A schedule with no start or no destination block is saved but not sent to the desktop. An imported TrainController schedule opens in the builder with its start, destination, blocks and legs, which stay as imported; each leg names the route built from the schedule for its step where one could be built, and the rest run directly or across turnouts along the way the desktop finds.
One schedule can start the next. A schedule's Successors are the schedules that may start when it finishes in its destination, as in TrainController. Exactly one of them starts: the first in the list that can start, or one chosen at random among those that can, as the schedule says for the whole list. Each successor either keeps the same train, which stands in the successor's start block, or starts with another train; it can wait a number of seconds after the finish, and it can start only once per session. A successor reaches RailCommand on the desktop only when it can run there: its schedule is sent to the desktop too, any train filter it carries is one the package can describe, and any condition it carries is one the desktop reads. Otherwise it is left out, and the others keep their order. An imported TrainController schedule's successors come from the file and are shown read-only, because a re-import writes them again; change them in TrainController and import again.
How To
- Import and read the summary. After a TrainController import at
/app/railcommand/layouts/import/traincontroller, the result summary reports "N derived routes (M TC verification routes)" and, below it, the conformance line: e.g.113/150 TC routes verified by graph derivation (37 underivable, 27 derived-but-not-in-TC). The line is green when nothing is underivable, amber when some routes are. - Read the verdicts. Each corpus route carries a verdict: Matched (a derived route confirms that connectivity), Underivable (no derived equivalent — the detail names the exact endpoint blocks and turnout cells), and the informational derived-but-not-in-TC count (routes your graph supports that TrainController's set simply did not list — not a defect).
- Manage interlocking routes. Open Interlocking Routes at
/app/railcommand/layouts/{LayoutId:guid}/routes. The cards show Total Routes, Active, and Conflicts Detected; filter by All / Active / Inactive. Each row lists Name, Entrance / Exit Signal, Turnouts, Speed, Status, and Lock. Use + New Route to open the builder at.../routes/builder. - Add your control points. A control point is an interlocking plant: the signals and turnouts a set of interlocking routes belongs to, and every interlocking route belongs to one. Under Control points on the Routes page, type a name (for example CP West) and its Milepost, then Add control point. Edit renames a control point or changes its milepost; Delete removes one that has no routes, and one that still has routes says how many to delete first. Two control points on one layout cannot share a name. The milepost follows the same rule as a block's or a signal's (
123.4,B123.4orMP 123.4, stored asB123.40), and the Mileposts page sets every control point's milepost at once. The Route Builder offers every control point, including one that has no route yet. A control point's milepost reaches RailCommand on each of its routes the next time the layout is downloaded. - Build routes from your schedules. On the Routes page press Build routes from schedules. The result says how many routes are new, renamed, unchanged and retired, how many schedule legs now run on a route, and how many routes you changed or removed are kept that way. Under it, the steps that keep the way RailCommand finds as the train runs are listed with the reason for each. Build again after changing a schedule or the track.
- Save yard routes for the desktop's CTC board. Under Yard routes on the Routes page, name a yard route (for example Lead to track 3), choose where it starts and where it ends (each a block or a signal), then Add yard route. Edit renames it or changes its ends, and Delete removes it. Two yard routes on one layout cannot share a name. On RailCommand's desktop CTC board the Dispatcher sets a yard route with one click: RailCommand clears the way between its two ends by your interlocking routes, or by the track and turnouts when no chain of routes joins them. Yard routes reach the desktop the next time the layout is downloaded.
- Set and lock a route in operate. In a live session from the CTC panel (
.../ctc-panel), press Set Route to request-and-lock an interlocking route (a conflict-checked turnout and block lock) and Release to unlock it. The Routes page Lock column reflects the current lock state. The CTC panel also shows every block end's own exit signal; the ones with no hardware (virtual signals) are drawn in their own small hollow symbol and follow your Show virtual signals choice from the Layout Editor. - Work a U.S. CTC board's machine. A CTC board in the U.S. CTC style shows a machine under the schematic, one column per control point, lined up under it as you pan and zoom. Each column has the control point's name plate, a lever for each of its turnouts (numbered 1, 3, 5 from the left), a signal knob, a Code button and All Red. A lever's lamps show where the railroad reports the turnout, never where the lever is set. With Code on (the default), a lever or knob move waits on its column until you press Code, which throws the levers first and then asks for the route; a refusal stops the rest of that column and says which lever and why. Turn the knob left or right to ask for the route the levers are set for, and the interlocking picks the aspect. Turn it to center to cancel the column's routes and hold its signals at stop. A column's All Red does the same for its control point, and All Red (whole board) holds every signal on the board in one click. Click a name plate to rename a control point, and clear the Code button box to make levers and knobs act at once. Names and the Code setting are saved with the board and reach RailCommand on the desktop, which shows the same machine and takes those two settings from the web.
- Chain schedules. Open a schedule in the Schedule Builder at
.../schedules/builderand, under Successors, press Add Successor and choose the schedule that starts next. Order the list with the arrows, choose In list order or At random under Choose the next schedule, and for each successor set Keep the same train, Delay (sec) and Only once per session. Save the schedule; the chain reaches the desktop the next time the layout is downloaded.
Troubleshooting
- A schedule step did not become a route. Open the list under the result of Build routes from schedules; each step says why. A way across a turntable or through a three-way turnout cannot be a route. Where several ways join the two blocks, a TrainController schedule that names its route decides, and one that names none leaves the step to the way RailCommand finds. A block whose exit signals the drawing does not orient cannot be either end of a route.
- Building again did not change a route I edited. That is on purpose: a route you changed is marked Edited and building from schedules leaves it, and a route you deleted stays deleted. Delete an edited route to have the next build make it afresh.
- Conflicts Detected is red / a route has a red dot. Two routes demand incompatible turnout or block states; they cannot be locked together. Edit one route or release the conflicting one.
- A TrainController route shows Underivable. The graph cannot confirm that path. Check the localized endpoints and turnout cells in the detail — common causes are a severed track ladder at a panel edge, a connector with no mate sharing its label (the walk cannot cross it), or a turntable/transfer-table with no authored track-pairing. A single-block corpus route is one whose two ends resolve to the same block even after following connector pairs; it is kept with the turnout positions it sets and is informational. A leg-modeled (ported) turntable or transfer table is now a route terminal, so its bridge-to-track routes derive with the required bridge position carried onto the route; an unconfigured import whose stalls are drawn (the device decodes a stall count, but its position ring is not decoded yet) derives its bridge-to-track routes from the drawn legs as the position-agnostic union — the routes exist without a required bridge position, a warning names the device, and actuation stays configuration-gated: setting a route across the device is still refused in an operating session until its track plan is authored. This is a temporary stopgap, not a limit of the file format: TrainController does store the position table, and once RailCommand decodes it these devices will get exact position-tagged routes. If a ported device's bridge route is missing, derivation rejected it fail-closed — an endpoint port that did not resolve, an exit with no
requiredBridgePositionstag, or duplicate tags on one port — and a warning names the device. - The drawn-stall union is scoped to unconfigured IMPORTS, and only those. Three other movable-device situations look similar and behave differently, by design. A device you placed by hand and never configured derives no ports and no routes — nothing was imported, so there is no drawing to trust as an author's intent. A device whose track plan is authored but does not validate likewise derives no ports and no routes, and a warning names the validation errors: you configured that device, so the error is the answer rather than a substituted union. And a configured device whose bridge-position tags are missing or duplicated for a port stays fail-closed as before. Only the imported-and-not-yet-configured device falls back to its drawing.
- A turntable's stall routes are still missing after re-deriving an older layout. Re-derivation cannot decode anything — the stall count comes from the file at import time. A layout imported before this shipped picks up only the stall tracks that happen to touch the device's own cell; TrainController draws the glyph as one grid cell but renders it as a circle several cells wide, so the tracks that stop at the rim are exactly the ones missing. Derivation now says so in a warning naming the remedy: re-import the TrainController file, or author the device's track plan. On a layout that does carry a decoded stall count, a shortfall against that count is reported as its own warning rather than quietly filled.
- Fewer derived routes than expected. Path enumeration is bounded; if a cap was hit, a loud warning names the specific block pair. Re-derive after fixing connectivity, or raise the derivation limits.
- The Route Builder says the layout has no control points. Add one under Control points on the Routes page, then come back to build its routes.
- A schedule does not run on the desktop. Open it in the Schedule Builder and read What the desktop runs. It needs a start block and a destination block, saved. A leg marked in amber crosses turnouts with no interlocking route between its two blocks, and RailCommand on the desktop finds its way when the train departs. The note under the legs says when the schedule is one the desktop does not run yet: it mixes interlocking routes with amber legs, or it is a Shuttle or Cycle with amber legs. For a schedule you made in the builder, an interlocking route whose path runs from the first block to the second for each amber leg, followed by saving the schedule again, makes every leg run along a route. A leg whose only way passes through another block does not run: add that block to the schedule.
- A successor did not start on the desktop. The desktop starts only the successors the package carries. A successor is left out when its schedule is not sent to the desktop (it is inactive, or it has no start or no destination block), when its train filter is one the package cannot describe yet (length, weight or speed bounds, pattern lines, conditions, or traction types), or when it has a condition and the desktop for this environment does not read conditions yet. Fix the successor's schedule or filter, save, and download the layout again.
- The Successors panel of an imported schedule cannot be changed. That is on purpose: a TrainController re-import writes an imported schedule's successors again from the file, so a change made here would be lost. Change them in TrainController and import again.
- The CTC machine is not shown under the board. Only a board in the U.S. CTC style has one. On the web, the dispatch log says when the layout's routes could not be traced on its current track plan; on the desktop, the machine appears once an operating session is running.
- The knob says there is no route that way for these levers. No route of that control point in that direction sets its turnouts the way the levers are. Set the levers for a route, then turn the knob again. When two routes fit the levers equally, the knob cannot choose between them; set the route from the board, or remove one of them.
- A yard route cannot be saved. Each end must be a block or a signal of this layout, the two ends must differ, and the name must be new on the layout. If the layout's track has not been drawn and saved yet there is nothing to choose; draw it first.
- Set Route fails in the CTC panel. The requested route conflicts with an already-locked route, or the session is not ready (CTC readiness blockers must be cleared first).
Safety Notes
The web app is where you author and verify — derive routes, read conformance, and define interlocking routes. It does not actuate hardware. The Avalonia desktop presents live state and forwards operator intent; it does not drive trains. The local/UE5 runtime owns all safety-critical execution — interlocking conflict enforcement, movement authority, and physical turnout and signal actuation happen there, never in the browser or desktop. Setting or locking a route grants an authority window; it never itself moves a train. Treat a "verified" conformance result as an import-connectivity check, not an operational safety certification: a green verdict does not replace testing routes against your physical hardware before an operating session.