Inspiration
Complex professional software is where accessibility fails hardest. Dispatch boards, scheduling grids, drag-and-drop conflict resolvers, all of it leans on spatial interaction: drag this, hover that, click a target that's twelve pixels wide. Someone who can't use a mouse gets locked out of exactly the tools where their judgment would matter most. We wanted to build the sharpest possible counterargument to that, a real operations console where an agent has full authority to do everything a mouse user can, with nothing held back.
What it does
Relay is a logistics-dispatch console: jobs, drivers, vehicles, time windows, and the double-bookings and coverage gaps that inevitably come with all of that. Its core claim is enforced parity. Every action in the UI has a matching WebMCP tool with identical authority, and the reverse holds too. No tool exists that the UI can't also trigger, and no button exists that only a mouse can reach.
Ask an agent to close a coverage gap, resolve a scheduling conflict, or preview a bulk reassignment before committing to it, and it does real work against real schedule state, the same reducer the UI itself writes through. simulate_change is a genuine dry run: it calls that reducer, discards the result, and hands back only the diff, so an agent can show you what a reschedule would do before anything actually happens.
How we built it
React, TypeScript, and Vite on the client, Cloudflare Workers plus a Durable Object as the backend, the same pattern as our other two entries.
src/shared/reducer.ts is the single mutation path for assign, reschedule, swap, and bulk reassign, and it throws a real SkillMismatchError when someone tries to put a driver on a job they're not qualified for. src/shared/derive.ts computes conflicts and coverage gaps against live state, not canned examples.
The part we're actually proudest of is scripts/check-tool-parity.ts. It asserts that Relay's mutating tools and the UI's mutating actions are the exact same set, not a superset in either direction, and it runs as the first step of every build. Ship a button with no matching tool, or a tool with no matching control, and the build just fails. That's not a design principle we're asking you to trust. It's a gate.
The seed schedule ships with a real conflict (two jobs double-booked onto one driver, overlapping by an hour) and a real coverage gap (a hazmat job with exactly one qualified driver, and that driver's marked unavailable), so list_conflicts and find_coverage_gaps have something to actually find the moment you open the app.
Challenges we ran into
Enforcing parity mechanically turned out to be harder than just building the tools. It's easy to write a tool and a button that do roughly the same thing and call it done. Building the CI check that fails the build the moment they drift apart took real thought about what counts as "the same action", is reschedule_assignment the same capability as dragging a job on the timeline? We decided yes, and the registry in src/shared/ui-actions.ts reflects that decision, not just a list of function names that happened to match.
We also hit a real validation gap the hard way. reschedule_assignment originally accepted any two numbers as a time window, so an agent that meant "6am" as a time-of-day offset instead of an absolute timestamp could silently corrupt a job's schedule into 1970. We added an assertValidWindow check and clearer schema descriptions after catching this from a live activity-feed log, which was a genuinely useful reminder that a tool's input schema saying "number" isn't the same as it saying what the number means.
And the same shared-board demo-state problem we hit on our other two apps showed up here too: because a Durable Object's seed loads exactly once, repeated testing consumed the same live board every time. Fixed with per-visitor isolated schedules and a visible reset button.
Accomplishments we're proud of
A parity guarantee that's mechanical, not aspirational. check-tool-parity.ts isn't a claim in a README, it's a build step that fails loudly the moment someone adds a UI action without a paired tool.
simulate_change as a real dry run, not a description of one. It runs the actual reducer and throws the result away, so the diff an agent shows you is the real diff, computed the real way.
A schedule view built as a genuine <table> with proper <th scope="col"> and labeled controls instead of a drag-and-drop grid, specifically so it's screen-reader operable from day one rather than retrofitted later.
What we learned
Accessibility and agent-operability turned out to be the same problem wearing different names. Once every capability in an app is expressed as a named, described tool with a real schema, that description is simultaneously a machine interface and the clearest possible explanation of what the app does, for a screen reader, for a voice interface, for anyone who's never seen the UI. Building for agents and building for accessibility pulled us in exactly the same direction the entire time. That surprised us going in and feels obvious in hindsight.
What's next
A full WCAG 2.2 AA audit to sit alongside the enforced-parity guarantee, since right now the table-based schedule view is built with accessibility in mind but hasn't been formally verified. And a client-side undo stack for mutations, matching what Cadence already has, since Relay currently only has the audit log for accountability, not reversibility.
Built With
- cloudflare-durable-objects
- cloudflare-workers
- css3
- html5
- model-context-protocol
- node.js
- react
- typescript
- vite
- webmcp
- websockets
Log in or sign up for Devpost to join the conversation.