# Routing24 route optimizer > Plan and optimize vehicle delivery routes: turn a list of stop addresses and vehicles into an efficient multi-stop route plan with stop assignments, sequence, distance and ETAs, plus a shareable plan link. Use whenever the user wants to plan routes, optimize delivery or pickup stops, build a delivery run or dispatch schedule, solve a vehicle routing problem (Rich VRP), sequence stops for one or more drivers, vans or trucks from a depot, do route planning or last-mile delivery optimization, or asks for Routing24 routing24.com. Runs entirely in the user's own browser tab via WebMCP tools (routing24_geocode, routing24_optimize, routing24_status, routing24_save, ...) registered on document.modelContext. Turn a natural-language routing request ("optimize these 8 addresses with 2 vans from this depot") into an optimized route plan on **Routing24**, shown on the map, with a link the user can open. Routing24 has **no server Optimization API** — everything runs in the browser. The page registers **WebMCP tools** under the `routing24_*` prefix on `document.modelContext` (the W3C WebMCP draft surface; the app bundles a polyfill so the tools exist even without native browser support). Discover them with `getTools()`, invoke them with `executeTool(tool, JSON.stringify(args))` — which resolves to a **JSON string** you must parse (and **rejects** on validation or handler errors). The optimizer runs client-side in WASM, so `routing24_optimize` is asynchronous: you start it, then **poll `routing24_status`**. This document is self-contained (runtime requirements + procedure + API contract + snippets). It is generated from Routing24's own types, so it always matches the deployed API; the installable skill version of the same content is at `https://routing24.com/routing24.skill`. ## Runtime requirements - This skill drives Routing24 **in the user's own browser**. It needs either a **WebMCP-capable browser agent host**, or a JS-eval browser integration — **Claude in Chrome / Cowork** (Chrome or Edge) with the `navigate` and `javascript_tool` tools, which calls the same tools via `document.modelContext`. If neither is available, tell the user this automation requires a browser integration and stop. - Everything runs **client-side** via the `routing24_*` WebMCP tools; there is no server API and no API key. **No sign-in is required** — the full flow (geocode, optimize, render, save, share) works anonymously. Do not ask the user to log in. Signing in only changes **where** the plan is stored (see the plan-link note under *Notes & pitfalls*). ## Procedure 1. **Open the app.** `navigate` to `https://routing24.com/app/plan/new/optimize`. 2. **Verify the tools are present.** Via `javascript_tool`: ```js const mc = document.modelContext ?? navigator.modelContext; (await mc?.getTools())?.map(t => t.name) ?? null; ``` Expect the `routing24_*` names (`routing24_optimize` etc.). If `null` or missing, the page may still be loading — wait 2s and retry, then reload once; if still missing, tell the user the Routing24 agent tools aren't available on this page and stop. 3. **Set up a call helper** (used by every later step; `executeTool` takes the tool OBJECT from `getTools()` and a JSON-string argument, and resolves to a JSON string): ```js const mc = document.modelContext ?? navigator.modelContext; const tools = await mc.getTools(); window.__r24call = async (name, args = {}) => { const tool = tools.find(t => t.name === name); if (!tool) throw new Error('tool not found: ' + name); const raw = await mc.executeTool(tool, JSON.stringify(args)); return raw == null ? null : JSON.parse(raw); }; ``` 4. **(Optional) Note who's signed in.** `await __r24call('routing24_get_auth_user')` returns `{ user }` — the user's email, or `"anonymous"`. **Do not require sign-in** — the whole flow works anonymously. This only affects *where* the saved plan lives and what you tell the user about the plan link (see step 10). 5. **Parse the request** into the `routing24_optimize` input shape (see the **API contract** for the full definition). Times are seconds-since-midnight; all fields except the addresses are optional. Use **≥2 stops** and **≥1 vehicle**. Bad input makes the call reject with a message naming the offending fields — relay it to the user. 6. **Geocode + confirm.** Run `await __r24call('routing24_geocode', { addresses: [] })`. - Show the user any row with `ok:false` (not found) and ask them to fix the address. Also surface `matched` values that look wrong and confirm. - Once confirmed, use the returned `lat`/`lng` for each entity (pass them in the `routing24_optimize` input so you don't re-geocode). `routing24_optimize` will geocode any address you leave without coordinates, but doing it here lets the user confirm ambiguous matches first. 7. **Start optimization.** `await __r24call('routing24_optimize', { depot, stops, vehicles })`. It returns quickly with `{ started: true, planUuid }`. 8. **Poll progress.** Every ~3 seconds run `await __r24call('routing24_status')` until `phase === 'done'` or `phase === 'error'`. - Relay `progress` (0–1) and, once available, `routes` / `distance` / `feasible`. - Small jobs finish in seconds; large ones can take minutes (keep the tab active). If `phase === 'error'`, report `error` and stop. - To abort on the user's request: `await __r24call('routing24_cancel')`. 9. **Show + save.** Run `await __r24call('routing24_render')` (brings the routes onto the map), then `await __r24call('routing24_save')` to persist and get `{ saved, planUrl }`. - To *show the user the map*, take a screenshot of the tab after render — the tools return JSON, not images. 10. **Report** to the user: - Number of routes, total distance (with unit), total duration, and any `unservedCount` (stops that couldn't be served — mention them). - Whether the solution is `feasible`. - The **plan link** (`planUrl`) they can open, and note the map on screen shows the routes. If the user is **anonymous** (from step 4), add that this link opens the plan **only on this computer** and may be deleted later — it is not a durable share link. ## JavaScript snippets Ready-to-eval expressions for `javascript_tool` — each resolves to a value that comes back to you. Replace the ADDRESS/STOP/VEHICLE placeholders. ```js // 0) Discover the tools (undefined/missing names => page still loading; retry). const mc = document.modelContext ?? navigator.modelContext; (await mc?.getTools())?.map(t => t.name) ?? null; // 1) One-time call helper: executeTool takes the tool OBJECT + a JSON-string // argument and resolves to a JSON string (rejects on errors). const mc2 = document.modelContext ?? navigator.modelContext; const tools = await mc2.getTools(); window.__r24call = async (name, args = {}) => { const tool = tools.find(t => t.name === name); if (!tool) throw new Error('tool not found: ' + name); const raw = await mc2.executeTool(tool, JSON.stringify(args)); return raw == null ? null : JSON.parse(raw); }; // 2) (Optional) who's signed in — { user: email | "anonymous" }. Never gate on // this; the whole flow works anonymously. It only affects where the plan is // stored. await __r24call('routing24_get_auth_user'); // 3) Geocode depot + all stop addresses. Inspect results for ok:false and // sanity-check the `matched` strings with the user before optimizing. await __r24call('routing24_geocode', { addresses: ["DEPOT ADDRESS", "STOP 1 ADDRESS", "STOP 2 ADDRESS"], }); // 4) Start optimization. Prefer passing lat/lng from step 3 so nothing is // re-geocoded (addresses alone also work; they'll be geocoded). await __r24call('routing24_optimize', { depot: { address: "DEPOT ADDRESS" /*, lat, lng */ }, stops: [ { id: "S1", address: "STOP 1 ADDRESS", delivery: 1, service_duration_s: 300 }, { id: "S2", address: "STOP 2 ADDRESS", delivery: 1, service_duration_s: 300 }, ], vehicles: [ { available_count: 2, capacity: 20, tw_early_s: 8 * 3600, tw_late_s: 18 * 3600 }, ], // options: { time_limit_s: 30 }, }); // 5) Poll (~every 3s) until phase is "done" or "error". Relay progress meanwhile. await __r24call('routing24_status'); // 6) Show routes on the map, then persist and get the plan link. await __r24call('routing24_render'); await __r24call('routing24_save'); // -> { saved, planUrl } // 7) Current plan URL (also returned by save). await __r24call('routing24_plan_url'); // Optional: cancel a long-running solve (keeps the best solution so far). await __r24call('routing24_cancel'); // Optional single blocking wait (prefer separate status calls to stream progress): await (async () => { for (let i = 0; i < 400; i++) { const s = await __r24call('routing24_status'); if (s.phase === "done" || s.phase === "error") return s; await new Promise((r) => setTimeout(r, 3000)); } return __r24call('routing24_status'); })(); ``` ## API reference WebMCP tools registered on `document.modelContext` on every `https://routing24.com/app/*` page (`navigator.modelContext` is a deprecated alias). Discover them with `getTools()`; invoke with `executeTool(tool, JSON.stringify(args))` — it resolves to a **JSON string of the result object** (parse it) and **rejects** on validation/handler errors. `routing24_optimize` is **fire-and-poll**: it returns immediately after starting the background WASM solve; observe it with `routing24_status`. Shapes reuse the solver's own `Site`/`VehicleType`/`Location` fields and are validated at runtime — the JSON Schema is authoritative. Zero-argument tools take `'{}'`. ### `routing24_get_auth_user` → `{ user: string }` No input. Returns the signed-in user's email, or `"anonymous"` when nobody is logged in. **Informational only** — every tool works anonymously, so never gate the flow on this or ask the user to sign in. It only tells you *where* a saved plan will live (see `routing24_save`). ### `routing24_geocode` — `GeocodeInput` → `{ results: GeocodeResult[] }` Batch-geocodes address strings (order preserved; every input gets a row back). `ok:false` (`quality:"failed"`) = not found → ask the user to correct it. ```ts // Input for the `routing24_geocode` WebMCP tool. type GeocodeInput = { addresses: string[]; // Address strings to geocode (free-form, as a user would type them). }; ``` ```ts type GeocodeResult = { input: string; // The address string as passed in. ok: boolean; matched?: string; // Canonical/expanded address the geocoder matched. lat?: number; lng?: number; quality: "rooftop" | "failed"; }; ``` ### `routing24_optimize` — `OptimizeInput` → `{ started: true; planUuid: string }` Creates a fresh plan, fetches the O/D matrix, and starts the solve. Returns at once. ```ts type OptimizeInput = { depot: Place; // Single depot, used as both start and end for every vehicle. stops: OptimizeStop[]; // min 1 vehicles: OptimizeVehicle[]; // min 1 options?: { time_limit_s?: integer }; }; ``` ```ts // A geographic point the caller supplies either as coordinates or as an address // string to geocode. `lat`/`lng` are the solver's own location fields. type Place = { lat?: number; lng?: number; address?: string; }; ``` ```ts // A delivery/pickup stop: a place plus solver site constraints. type OptimizeStop = { lat?: number; lng?: number; address?: string; pickup?: number; delivery?: number; service_duration_s?: number; tw_early_s?: number; tw_late_s?: number; prize?: number; required?: boolean; id?: string; }; ``` ```ts // A vehicle (type): solver vehicle constraints; `available_count` clones this type. type OptimizeVehicle = { tw_early_s?: number; tw_late_s?: number; capacity?: number; available_count?: number; cost?: { fixed?: number; distance?: number; duration?: number; stop?: number }; id?: string; }; ``` - Each depot/stop needs **either** `lat`+`lng` **or** an `address` (geocoded automatically; if any fail, the call rejects listing them). - Times are **seconds since midnight**. `delivery`/`pickup` are single-dimension loads. `available_count` = identical vehicles of that type. The single depot is both start and end. ### `routing24_status` → `OptimizeStatus` No input. Snapshot of the current optimization — poll (~every 3s) while solving. `phase` walks `idle → geocoding → matrix → solving → done` (or `error`). ```ts type OptimizeStatus = { phase: OptimizePhase; running: boolean; progress?: number; // 0..1 while solving, when available. feasible?: boolean; routes?: number; stops?: number; unservedCount?: number; distance?: number; distanceUnit?: "km" | "mi"; durationHours?: number; error?: string; planUuid?: string; }; ``` ### `routing24_render` → `{ ok: true }` No input. Navigates to the plan's optimize page so the routes draw on the map (screenshot the tab afterwards to show the user). ### `routing24_save` → `{ saved: boolean; planUrl: string }` No input. Persists the plan. When **anonymous**, the plan is stored in this browser on this computer only — the link opens just here and may be deleted later, so it is not a durable share link. Tell the user this when you hand over the link. (If the user is signed in, the plan persists to their account and the link opens on their other devices too.) ### `routing24_plan_url` → `{ planUrl: string }` No input. Absolute URL of the current plan's optimize page. ### `routing24_optimization_map` → `OptimizationMap` No input. Renders the current optimization's routes to a PNG map and returns a fetchable https link (`image/png`) plus its dimensions and plan metadata — view `url` directly to see the routes drawn on the map. ```ts // A rendered PNG snapshot of the current optimization's routes drawn on the map. // `url` is a stable, publicly-fetchable https link (`image/png`) an agent can // view directly; the uuid-named file mirrors what a server-side renderer emits. type OptimizationMap = { url: string; // Absolute https URL of the rendered image (fetchable, `image/png`). format: "png"; // Image container format — always `'png'`. width: number; // Rendered image width in pixels. height: number; // Rendered image height in pixels. planUuid?: string; // The plan this map was rendered for. routes?: number; // Number of routes drawn on the map. generatedAt?: number; // Epoch milliseconds the image was produced. }; ``` ### `routing24_cancel` → `{ ok: true }` No input. Aborts an in-flight solve (mirrors the UI's cancel button). ### Machine-readable JSON Schema Generated by typia from the app types (OpenAPI 3.1 / JSON Schema 2020-12): ```json { "version": "3.1", "components": { "schemas": { "OptimizeInput": { "type": "object", "properties": { "depot": { "$ref": "#/components/schemas/Place", "description": "Single depot, used as both start and end for every vehicle." }, "stops": { "type": "array", "items": { "$ref": "#/components/schemas/OptimizeStop" }, "minItems": 1 }, "vehicles": { "type": "array", "items": { "$ref": "#/components/schemas/OptimizeVehicle" }, "minItems": 1 }, "options": { "type": "object", "properties": { "time_limit_s": { "type": "integer", "minimum": 1, "description": "Solve budget in seconds; omit to auto-scale with problem size." } }, "required": [] } }, "required": [ "depot", "stops", "vehicles" ] }, "Place": { "type": "object", "properties": { "lat": { "type": "number" }, "lng": { "type": "number" }, "address": { "type": "string" } }, "required": [], "description": "A geographic point the caller supplies either as coordinates or as an address\nstring to geocode. `lat`/`lng` are the solver's own location fields." }, "OptimizeStop": { "type": "object", "properties": { "lat": { "type": "number" }, "lng": { "type": "number" }, "address": { "type": "string" }, "pickup": { "type": "number" }, "delivery": { "type": "number" }, "service_duration_s": { "type": "number" }, "tw_early_s": { "type": "number" }, "tw_late_s": { "type": "number" }, "prize": { "type": "number" }, "required": { "type": "boolean" }, "id": { "type": "string" } }, "required": [], "description": "A delivery/pickup stop: a place plus solver site constraints." }, "OptimizeVehicle": { "type": "object", "properties": { "tw_early_s": { "type": "number" }, "tw_late_s": { "type": "number" }, "capacity": { "type": "number" }, "available_count": { "type": "number" }, "cost": { "$ref": "#/components/schemas/solver.VehicleCost" }, "id": { "type": "string" } }, "required": [], "description": "A vehicle (type): solver vehicle constraints; `available_count` clones this type." }, "solver.VehicleCost": { "type": "object", "properties": { "fixed": { "type": "number" }, "distance": { "type": "number" }, "duration": { "type": "number" }, "stop": { "type": "number" } }, "required": [] }, "GeocodeInput": { "type": "object", "properties": { "addresses": { "type": "array", "items": { "type": "string" }, "description": "Address strings to geocode (free-form, as a user would type them)." } }, "required": [ "addresses" ], "description": "Input for the `routing24_geocode` WebMCP tool." }, "GeocodeResult": { "type": "object", "properties": { "input": { "type": "string", "description": "The address string as passed in." }, "ok": { "type": "boolean" }, "matched": { "type": "string", "description": "Canonical/expanded address the geocoder matched." }, "lat": { "type": "number" }, "lng": { "type": "number" }, "quality": { "oneOf": [ { "const": "rooftop" }, { "const": "failed" } ] } }, "required": [ "input", "ok", "quality" ] }, "OptimizeStatus": { "type": "object", "properties": { "phase": { "$ref": "#/components/schemas/OptimizePhase" }, "running": { "type": "boolean" }, "progress": { "type": "number", "description": "0..1 while solving, when available." }, "feasible": { "type": "boolean" }, "routes": { "type": "number" }, "stops": { "type": "number" }, "unservedCount": { "type": "number" }, "distance": { "type": "number" }, "distanceUnit": { "oneOf": [ { "const": "km" }, { "const": "mi" } ] }, "durationHours": { "type": "number" }, "error": { "type": "string" }, "planUuid": { "type": "string" } }, "required": [ "phase", "running" ] }, "OptimizePhase": { "oneOf": [ { "const": "idle" }, { "const": "geocoding" }, { "const": "matrix" }, { "const": "solving" }, { "const": "saving" }, { "const": "done" }, { "const": "error" } ] }, "OptimizationMap": { "type": "object", "properties": { "url": { "type": "string", "description": "Absolute https URL of the rendered image (fetchable, `image/png`)." }, "format": { "const": "png", "description": "Image container format — always `'png'`." }, "width": { "type": "number", "description": "Rendered image width in pixels." }, "height": { "type": "number", "description": "Rendered image height in pixels." }, "planUuid": { "type": "string", "description": "The plan this map was rendered for." }, "routes": { "type": "number", "description": "Number of routes drawn on the map." }, "generatedAt": { "type": "number", "description": "Epoch milliseconds the image was produced." } }, "required": [ "url", "format", "width", "height" ], "description": "A rendered PNG snapshot of the current optimization's routes drawn on the map.\n`url` is a stable, publicly-fetchable https link (`image/png`) an agent can\nview directly; the uuid-named file mirrors what a server-side renderer emits." } } }, "schemas": [ { "$ref": "#/components/schemas/OptimizeInput" }, { "$ref": "#/components/schemas/GeocodeInput" }, { "$ref": "#/components/schemas/GeocodeResult" }, { "$ref": "#/components/schemas/OptimizeStatus" }, { "$ref": "#/components/schemas/OptimizationMap" } ] } ``` ## Version & keeping current - This skill is **version 0.1.0~beta**. Its bundled reference (`references/api.md` + `references/schema.json`) is generated from Routing24's own types and is correct as of this version. - The **always-current** copy of the full contract is served at `https://routing24.com/llms.txt` (regenerated from the deployed API on every release). If a call rejects with a validation error that looks like a field this reference doesn't describe, fetch that URL and use its schema — then consider re-downloading the latest skill from `https://routing24.com/routing24.skill`. - To update the skill itself, re-download `https://routing24.com/routing24.skill` and re-install it; that is the update mechanism. ## Notes & pitfalls - Everything happens in the user's own tab; you are not calling any server API directly. - `executeTool` resolves to a **JSON string** (parse it; `null` means no value) and **rejects** on validation or handler errors — wrap calls in try/catch and relay the message. You cannot receive an image from the tools — to show the user the map, call `routing24_render` then screenshot the tab. - Prefer `document.modelContext`; `navigator.modelContext` is a deprecated alias kept for older hosts. - `routing24_optimize` starts a **new plan** each time. When the user is **anonymous**, the plan is stored **only in this browser on this computer** and may be deleted later, so the plan link opens only here — it is not a durable share link. Say this when you hand over the link. (Signing in before saving persists the plan to the user's account so the link also opens on their other devices — but sign-in is never required to plan, save, or share.) - If `routing24_status` never leaves `matrix`/`solving`, the network (matrix service) or the solve may be slow — keep polling; only treat it as failed on `phase:'error'`.