AGENTIC WORKFLOW ORCHESTRATOR

Changelog

What has actually shipped

Read straight from the product's own changelog, not retyped for the website. Each entry says what changed and why it needed changing — most of them are defects found by using AWO rather than features planned in advance.

Showing the 40 most recent entries.

Autopilot plans are short by default, and how long they may be is now a setting
The planner stage was told to sharpen each phase's intent and acceptance with nothing bounding how much sharpening was, so a phase arrived with a multi-paragraph intent citing line numbers and eighteen paragraph-long criteria — roughly 14,000 characters on a real run. That is paid for twice, and the second time is the one that hurts: the plan is rendered into the task of every stage of that phase on every attempt, so a five-stage composition that retries once reads all of it ten times, and the planner then sharpens its own output again next phase. Setup now carries a **plan detail** choice (short, medium, long), governing both what the planner is told to write and what is carried downstream; long reproduces exactly what was hardcoded before. New installs get short.
A read-only Autopilot stage is no longer cut off mid-answer
Nothing set a token ceiling on that path, so the Anthropic connector's 1024-output-token default applied — and a planner asked to return JSON containing whole phase intents and acceptance lists does not fit in 1024 tokens. The reply was truncated mid-object, `JSON.parse` threw, and because the planner does not gate its phase the run carried on with an unrevised plan, logging a stage failure every phase: "6 stages failed" without any phase stopping. Read-only stages now get room to finish unless the operator sets their own limit, and a reply that did run out of room says so rather than reporting itself as malformed JSON.
Two fields with the same name no longer lose one of them in silence
"What to send" and "Headers" let you add rows freely, but two rows sharing a name collapse to one entry — so a webhook body typed as `amount: 100` and `amount: 250` showed both rows on screen while storing only the last, and the first vanished the next time the step was opened. The row that loses its name is now marked where it sits, and the field says which name is doubled and that only the last is sent. Only the offending row is marked: the field-wide error edge reddened every input in a row editor, pointing at none of them.
Stopping an Autopilot run right after starting it is no longer ignored
A run read the steer signal one step into its own startup, so an `abort` sent while the project file was still loading was mistaken for a leftover from an earlier run and cleared — the drain then cloned the repo, ran a write-mode agent and opened a PR that had been cancelled. The signal is now read before that load, and signal writes are serialised so a cancel landing mid-clear cannot be overwritten either.
A failed run no longer says it finished every step
The run bar read `Failed · 3/3 steps` over a workflow whose middle step errored and whose last was skipped, claiming it got all the way through in the same breath as saying it had failed. Every finished node counted as progress regardless of what it reported, and each loop iteration counted again — which is why the readout needed a clamp to keep it from printing `4/3`. It counts the steps that actually did their work now, once per step: `Failed · 1/3 steps`.
The run button keeps one name
`Test run` became `Retry` once a run had failed or been cancelled, so the control an operator had just learned changed names at the moment they most needed it. `Retry` also named an action Relay does not offer: the move after a failure is to fix the step and press again, which runs the edited workflow rather than the one that failed. The status beside the button already reads `Failed`.
A workflow keeps the name you typed
`Deal watcher` was refused by the schema and silently rewritten to `Deal-watcher` by a tab rename, because the name was ruled "filesystem-safe" — a rule nothing needed: run history, snapshots and watchers each derive their own slug, and no route carries the name. A name is now a label, spaces and apostrophes and all, refused only at the ends, where a trailing space is invisible and would make two names look identical. It is judged trimmed while it is being typed, so `Deal watcher v2` no longer turns the field red as it passes through `Deal watcher `, and `toYaml` trims what it sends. Suffixed saves read `alpha 2` rather than `alpha-2`, which is what a file manager shows. Webhook addresses stay slugified: a name is a label, but `/hooks/deal-watcher` is a URL.
Two workflows whose names slug alike get a watcher each
A watcher's id is a slug of the workflow name, and `create` treats an existing id as the same workflow — so starting a watcher on `Deal watcher` stopped the one already running for `deal-watcher` and overwrote its definition on disk, while the rest of Relay still counted them as two workflows. The id now falls through to `-2` when a different workflow holds the slug, and the name it was created for is persisted so the two can be told apart.
A new step is named in plain words, not after the schema
Adding a block minted `reason1` and put it on the boldest line of the card — a word Simple mode shows nowhere else, having replaced it with "Ask the AI" everywhere it could reach. The shipped recipes already name their steps `judge`, `worthIt`, `alert`; the palette was the one place that did not. A step now arrives as `ai`, `data`, `check` or `alert`, numbered only once that name is taken, so a first workflow reads `data → ai` rather than `extract1 → reason1`. Advanced still shows the exact schema kind on the badge. The inspector placeholders that taught the reference syntax through invented step names (`{{ summarise.output }}`, `{{ impl.output.pullRequestUrl }}`) now use names the palette actually mints.
A long step summary keeps its plain English
The card truncated to 46 characters and only then rewrote `{{ … }}` into words, so any summary long enough to clip had its reference cut in half and the rewrite silently did nothing — leaving raw template syntax on the one card a newcomer reads first. The rewrite now runs on the whole string and the single truncation follows it.
A project row says which project it is
A real list held 37 projects under 25 distinct readings — nine rows reading `my-workflow · edited 5 h ago`, with nothing on screen to pick between them. A row now says what is in the workflow (`2 steps`), which is what an operator recognises their own work by and what separates most same-named saves; rows that would still read alike fall back to the exact time, and only those. The Autopilot picker had the same defect and no timestamp to fall back on, so its rows carry the goal the project was given.
One dropdown in the document strip at a time, and both close
Opening the Autopilot picker left the All projects menu standing behind it, two overlapping panels drawing their text through each other. Neither closed on a click outside or on Escape, so a menu sat over the canvas until its own button was found again. React Flow's pane stops mousedown from bubbling, which is why the dismiss listeners are in the capture phase.
The UI test suite typechecks itself
CI's builder-UI job runs `tsconfig.test.json`, which the shipped build deliberately excludes and no command in the local gate reached — so three errors accumulated in it and `main` was red for five consecutive pushes. `npm --prefix ui test` now runs the typecheck first, mirroring the root's own `pretest`, and cannot pass while CI's typecheck step would fail.
A repeated control in a list says which row it acts on
The project menu's buttons were named `Open` and `Delete` and nothing else, so a screen reader read "Open, Delete, Open, Delete" down the whole list with no way to tell which project each pair belonged to. The same held for a run's `Cancel`, a connector's `Remove`, a repo's `Use this repo` and a phase's `Up`/`Down`/`Delete` — the last of these in a row where every other control was already named for its phase. Each now carries the name of the row it sits in, including the edited time in the project menu, which is what tells two same-named projects apart there. The visible text is unchanged and still begins every name, so voice control keeps working.
A failed run says what went wrong in the operator's words, not the engine's
The run panel's step line, its start-failure banner and the group-run row each printed the engine's own sentence — `TemplateError: template path "trigger.payload" is not defined in the run context`, or `FetchError: failed to fetch https://…: TypeError: fetch failed (Error: getaddrinfo ENOTFOUND …)` — to a reader who has never seen a stack trace. One mapper now turns a run-time failure into a sentence that names what to do next, and names the reference in the same words the canvas uses for it ("the incoming event"). An alert that could not be delivered blames the delivery rather than the page. Advanced keeps the exact string, the same split the rest of the Studio makes, and a failure this build has not heard of keeps every word of its substance and loses only the class name. The group row also routes its outcome through `run-status` at last, so a run the operator stopped is no longer reported there as one that failed.
A tab rename no longer stores a name Relay will refuse to run
Renaming a tab wrote straight into the schema-validated workflow name, and the only warning lives in the inspector — which is showing a step's fields at the moment you rename a tab. So "Deal watcher" was accepted in silence and failed at the run, in the loader's words, after the work. The tab now commits the name Relay can run, changing only the characters the schema refuses and leaving case alone; the inspector still warns rather than rewriting, because it is judging a half-typed name on every keystroke. Two e2e tests had renamed tabs to spaced names and asserted the Studio stored them.
A Validate verdict stops speaking once the canvas it judged has been edited
The verdict was one value with no tie to what it had checked, so fixing the exact problem it named left the error standing, and — the dangerous half — editing a checked workflow into one that cannot run kept `✓ Looks good` up over it, still naming the old name. A verdict now carries the canvas it judged and is shown only while that is still what is on screen.
A group run names the project the operator checked
The row read the workflow name out of the run, and a workflow that fails before it starts reports none — so any failure produced `✗ workflow 1` about a project picked by name from a list. The names checked in the picker are carried through to the result rows. The persona test had asserted only that a row appeared, which is what let this stand.
An edit made in the second before you leave is no longer lost
Three paths, all showing `Saving…` at the moment the work was discarded. Leaving the draft cancelled its debounce and cached the canvas in memory only, so the edit survived a switch back and vanished on the next reload — a project's `stash` has always flushed to the server, and the draft has had a home in the browser since the reload fix. A reload or a closed tab inside the same 1.2 seconds lost the edit for either kind of document; the pending edit is now flushed on `pagehide` (not `beforeunload`, which the back/forward cache skips), synchronously for the draft and with a `keepalive` PUT for a project, which is what lets the request outlive the page. And `create` emptied the draft's storage after flushing it, leaving the fresh starter claiming to be saved.
Deleting a project no longer saves an edit back to it
Closing a tab flushes it, which is right when another document is taking the canvas and wrong on the way to the bin: the PUT landed on a project the server had just removed, so the operator was told `Couldn't save "…"` about the thing they had chosen to throw away. `close` now carries whether to flush, `remove` is the only caller that declines, and the cached canvas, the saved-text baseline and any pending unload flush go with the project.
A suffixed project name reaches the canvas, and is a name Relay accepts
Saving a second canvas called `alpha` produced `alpha 2` — a name the Studio generated itself and the workflow schema then refused, because it requires a filesystem-safe name. The suffix also never reached the canvas, so the tab read `alpha-2` while the inspector read `alpha`, and the next autosave carried `alpha` back to the server: the tab renamed itself under the operator and two tabs ended up called `alpha`, which is the confusion suffixing exists to prevent. The separator is a dash, `create` puts the name on the canvas in the same breath as posting the source, and the webhook address follows a suffix rename by the same rule it follows a typed one.
A page check that changed nothing no longer reports "Succeeded"
A poll-diff test run whose page had not changed executed no step, and the engine's phase is still `completed` — so the readout said `Succeeded · 0/1 steps` in the success colour, and `proven` handed the primary emphasis to `Start watching`, inviting the operator to schedule a workflow no step of which had ever run. It reads `Checked` on the neutral dot now, `Test run` keeps the emphasis, and the drawer says which of the three things happened in words: `poll outcome: did not fire (no-change)` became "The page has not changed, so the workflow did not run." Advanced keeps the exact reason.
The run result says what a step produced, not what the transport replied
An action node returns its delivery envelope, so every alert that worked ended the result list with `{"ok":true,"status":204}` — and a list of found items arrived as one unwrapped line of JSON. A delivery Relay can vouch for now reads `Sent`, found items are listed one per line, an empty result says `Nothing found`, and anything else keeps its value. Only the four sending actions are spoken for; a `db-write` envelope is not called `Sent`. Advanced keeps the exact JSON throughout. The history view lost its own third copy of the same formatter, and every branch is now length-capped — a page-sized string output used to go into the panel whole.
The getting-started card counted nodes instead of asking whether the canvas was untouched
`nodes.length <= 2` meant a two-step workflow someone imported and ran kept "Start with a recipe" standing over it, and deleting a step from a three-step workflow made the card appear mid-edit — while a canvas emptied to the trigger alone lost the invitation it most needed. It now shows only while every node on the canvas is one the starter put there.
The tab strip stopped answering "is my work saved?" about the wrong document
(issue #786). With an Autopilot project on the canvas, the strip still showed the deferring workflow draft's save mark and its `+ Save as a project` button. So a project stored on the server reported `Draft saved in this browser`, with a tooltip telling the reader to save it — and the button offered to save a canvas that was not on screen. Both speak for the workflow document and are now hidden while a project holds the canvas; Autopilot keeps its own `New project` in the picker, and both return the moment a workflow tab is selected. A second test pins that return, so the guard cannot be satisfied by silencing the honest line too.
Autopilot's roll-up spells a waiting phase the way the cards beside it do
`autopilot-status.ts` exists so one status cannot read as two things, and it deliberately labels a phase's `blocked` as **waiting** — it is an unmet dependency, not a fault. The project rail spelled the same count by hand, so a freshly drafted plan announced `0/3 done · 1 ready · 2 blocked` inches from two cards reading `WAITING`, and "blocked" reads as an error on a plan that has not started. The rail draws every phase word from the shared map now, so a local spelling cannot come back. Every existing rail test passed `blocked: 0`, which is how it survived.
Wiring a workflow no longer requires a mouse
Every edge in the product was made by dragging between two 8px React Flow handles, and those handles carry `tabIndex="-1"` — so the one interaction Relay exists for could not be performed from the keyboard at all, and the empty canvas, the palette footer and the validation error all instructed the user to drag. The inspector now has **Runs after** and **Runs next**: the steps leading into the selected one and the steps it leads to, each with a picker that offers only edges the schema can hold (never into the trigger, never to itself, never a duplicate) and a `Disconnect` naming the step it removes. Both directions matter — adding a step strands it, and what it needs is an INCOMING edge. Four tests through the real panel plus a browser test that reaches the picker by Tab alone and wires a step with arrow keys and Enter; every guard mutation-checked. Dragging still works.
A focused canvas card shows that it is focused
React Flow ships `.react-flow__node.selectable:focus-visible { outline: none }` so a theme can supply its own ring, and Relay never did — while the app's global focus rule is written with `:where()`, which has zero specificity and therefore lost. Every card on the canvas was keyboard-reachable and gave no sign of it. The ring is drawn on the inner card so it hugs the shape the user sees, which also means it is not a specificity fight: it targets a different element.
The trigger and dev-block forms name their fields in words, and a guard keeps them that way
Phase 3 replaced schema keys with plain labels in the node forms but never reached `TriggerForm.tsx` or `dev-node-forms.tsx`, which had no `schema=` pairing at all — so Simple mode still asked for `url`, `intervalSeconds`, `maxBackoffSeconds`, `operation` and `path`. Every field in both now carries a plain label plus the exact YAML key for Advanced. The gap survived because only node KINDS had a friendly-name check; a sweep over every rendered `.field-label` outside an Advanced disclosure now fails on any bare lowercase token, and it found the four dev blocks on its first run. A second assertion pins the Advanced half, which the Simple sweep cannot see. Both mutation-checked.
Autopilot's create dialog could not be finished on a laptop screen
(issue #786). Measured at 1440×900: `Create and draft plan` sat at y=888 in a 900px viewport, below the fold and outside the modal's own box, because the form rendered its actions inside the scrolling body — the exact failure `Modal` documents its pinned `footer` slot to prevent. The primary action of the flagship flow was reachable only by scrolling. `AutopilotCreate` now owns its dialog and puts both decisions in that footer; the action row lands at 785. Preset placeholder labels lost their Title Case (`Bug Description` → `Bug description`) so they match the hand-written fields beside them.
Autopilot's run controls say which one applies
Start, Pause, Resume and Stop were all live on a project that had never started, and all four again once it completed — four equal buttons and no signal, where the workflow run bar has always offered only what the engine can act on. The three steer controls now gate on there being a run to steer, staying live through `capped`, `timed-out` and `blocked`, which are resumable, and through the open drain request they were deliberately never gated on. `Abort` became `Stop`, matching the `stopped: …` wording the status badge already used, and a finished run reports what happened instead of echoing its own badge (`Run finished: complete.` → `Finished — every phase is done.`).
One definition per design element in the stylesheet
The small uppercase label was written out 20 times across six modules with its tracking drifted between two values; it is now `.eyebrow`, defined once. The kind-to-colour mapping collapsed from 49 lines to 11 via a `--kind` custom property each surface reads with its own fallback, which retired the last 13 legacy token aliases. Consolidating also deleted the overrides that existed only to escape a rule inherited from being the wrong tag in the right container. 4,135 → 3,979 lines.
Plain English on the surface, the exact YAML key in Advanced
The inspector still labelled fields with schema keys — `left`, `op`, `input`, `prompt (template)`, `id` — so the first thing a newcomer met after "Only continue if…" was a form asking for `left`. Simple mode now names each field for what it does; Advanced keeps the exact key, because that mode is where the panel maps 1:1 onto the document. The swap lives in `Field` itself rather than at each call site, so the rule cannot drift field by field. Condition operators read as words too, on the card as well as in the dropdown — `is truthy` was the last raw schema value on a Simple-mode surface. Dev blocks keep their schema names throughout, since they only appear in Advanced.
The action block is a form, not a JSON textarea
"Send an alert" — the block the product is sold on — asked the least technical person using Relay to hand-write `{"webhookUrl": "…", "content": "…"}`. Discord now has a webhook URL field carrying the click-path to find it in Discord, a message field, and card/sender settings under Advanced; a webhook has a URL and a row editor for its body and headers, as does a websocket subscribe frame. Both raw editors survive per block behind a collapsed disclosure, and the round-trip is real in both directions — which needed a fix, since the raw editor re-seeded only when a different node was selected and showed stale text after a typed edit. Nothing is discarded: entries a row editor cannot show are carried through untouched and named under the rows, and switching action kind keeps the other kind's config. Empty required fields say what is missing and how to fix it before Run rather than after. A new alert block defaults to Discord, which needs one pasted URL where a webhook needs a URL plus a body.
The unsaved draft is a document with a tab, and it stops losing work
(issue #786). The document strip is meant to show what is on the canvas, and the canvas starts out holding an unsaved draft that had no tab — so a newcomer's first workflow belonged to no tab at all, while the top bar echoed its name beside the app's. Giving it a tab uncovered the real defect underneath: `stash()` skipped the draft, so opening any saved project threw the draft's canvas away with no warning, and closing the last tab left the closed project's nodes on a canvas that now called itself the draft. The draft caches like every other tab, is never closable, and resets to a fresh starter once saved rather than lingering as an unsaved twin. Three tests, each mutation-checked. The strip's `+` says `Save as a project`, which is what it does — `New project tab` named the opposite.
Undo and redo on the canvas
(`feat/the-canvas-remembers`, issue #800). Deleting a node was final: the Delete key removed it, autosave wrote the smaller document to the server a second later, and nothing anywhere brought it back — on a canvas whose whole interaction model is direct manipulation, the one affordance an operator reaches for without thinking. What counts as a step is decided by the workflow DOCUMENT rather than by React Flow's change events, so dragging a node and clicking around never fill the history while every change to what the workflow IS becomes exactly one step; the snapshot restored is the whole canvas, positions included, because positions are not in the YAML. Replacing the document — an import, a template, another project's tab — forgets the history instead of recording it, since undoing across that boundary would drag one document's nodes into another. Ctrl/Cmd-Z and Ctrl/Cmd-Shift-Z (or Ctrl-Y), plus buttons in the palette, because a shortcut nobody knows about is not a way back. 15 tests, plus a browser test that presses the actual Delete key — the path that never goes through the App's own delete handler, and the one an operator hits by accident.
Stopping something stops it everywhere, not just in the Studio
(`feat/real-execution-controls`, issue #800). "Stop watching" aborted the poll SCHEDULE and left the run already part-way through to finish and deliver its alert — after the operator had asked it to stop — and the request blocked until it did. Stopping a watcher, shutting the studio down, and ctrl-c on `relay watch` / `relay serve` now cancel the run in flight as well. Cancellation is cooperative and held to exactly what it promises: the step already executing finishes (bounded by its own 30s timeout), and the graph stops there — no step after the cancel runs. A run also outlives the page that started it, so reloading mid-run no longer strands one: the Studio asks the server what is executing and offers Cancel for anything it is not already driving. Mutation-verified — removing the cancel makes the alert go out again — plus a browser test that reloads mid-run and stops the orphan.
Projects and their tabs move into their own hook
(`refactor/projects-know-themselves`, issue #800). The last of `App.tsx`'s five concerns to come out, after watchers and the run: 228 lines and eight actions whose only tie to the rest of the Studio is "what is on the canvas" and "put this on it", now a four-operation port so the two can change independently. `App.tsx` is 1 024 → 796 lines. Projects had been covered only by a Playwright run of the whole product; 16 tests now state the rules where a reader looking for projects will find them — the tab cache winning over stored YAML (node positions are not in the YAML), closing the active tab, a refused save opening no tab, and a group run using the active tab as EDITED rather than as stored.
Play, pause, resume and cancel a run — and they are real
(`feat/real-execution-controls`, issue #800). The Studio could start a workflow and nothing else: no way to stop one, and a "test run" that calls the model and posts to Discord for real was labelled `simulated`. The engine now carries a `RunControl` it checkpoints against before every node, so Cancel stops the graph AND aborts the request in flight, and Pause parks it between nodes. Phases come from the engine, never from the UI: a pause reads `Pausing…` until the node in flight finishes, and a cancelled run is shown as cancelled rather than failed — in the run panel, the history and the CLI alike. `relay run` handles ctrl-c the same way and exits 130 with the partial run printed. A cancelled poll run leaves its snapshot uncommitted, so the change re-fires instead of being dropped. 45 tests, including a real browser pressing the buttons; that browser found a race where the cancel reply outran the finished run and left the UI stuck on `Cancelling…`.
An Autopilot project opens on the main canvas, like any other workflow
(`feat/autopilot-on-the-canvas`, issue #786). Autopilot lived in a modal — a stack of cards over the canvas, with the phase plan as a list and its dependencies as an array printed as text. A phase plan is a DAG: `validatePhases` proves it acyclic and `topoOrder` drains it in dependency order. So the one part of Relay that is literally a dependency graph was the part that never got drawn, in a product whose premise is a drag-and-drop canvas.

Every one of those is in the current build

Free tier, no account, nothing sent anywhere.