Inspiration
Ask an LLM for a workflow today and you get a wall of YAML to copy, paste, and hope you didn't fat-finger. Give an agent write access to your actual canvas instead, and you get the opposite problem — an agent silently rewriting live infrastructure with no review step.
WebMCP made a third option possible: expose the canvas itself as typed tools, so an agent proposes changes on the same surface a human is looking at, instead of generating text a human has to translate back into trust. I wanted to build the thing that argument implies — not a chatbot next to a canvas, but a canvas an agent and a human genuinely share.
What it does
CoNode is a visual workflow builder. An AI agent and a person edit the same DAG at the same time —
the agent through thirteen typed tools registered on document.modelContext, the human through the
mouse. Three rules make that safe:
- Every agent change is a proposal, rendered as dashed ghost nodes, until a human approves it.
- Touching a field locks it. The agent's later writes to that field are refused and it's told it was overruled, instead of silently failing and retrying forever.
- Secrets never leave the browser. The agent can see that a token is set — never its value — enforced at three separate points: the write path, the read path, and the export path.
When the diagram's right, Export code compiles it into a real Cloudflare Workflow: every action
becomes a step.do with retries, every approval gate becomes a step.waitForEvent — a genuine
durable pause that survives restarts and waits up to 24 hours for a human.
How I built it
React + React Flow for the canvas, Zustand for state, @mcp-b/global for the WebMCP polyfill. The
keystone decision: every agent mutation is an Op — a plain data value — before it ever touches the
store. Because applyOps() is a pure function, the same list of ops can be applied immediately, or
held as a proposal and rendered as a preview, or rejected outright. Review mode isn't a special case
bolted on top; it's a branch that falls out of that one choice.
Both export formats — the Cloudflare Workflow and a Kestra YAML alternative — walk one shared tree builder rather than each parsing the raw graph, so they can't drift apart on how a branch resolves.
Demo domain is Shopify order triage: fetch an order, hold high-value ones for human approval, sync
inventory either way. It's a genuine human-in-the-loop workflow, which is what makes the approval
node and step.waitForEvent mean something rather than being decoration.
Challenges I ran into
Chrome's registerTool() never settles. I originally awaited every tool registration before
reporting the app as "online." On the polyfill that resolves fine — so it looked correct in every
test I ran. On native Chrome (152, behind the WebMCP flag) the returned promise simply never
resolves or rejects. My status pill would have shown "offline, zero tools" on the one browser that
actually implements the spec, which is exactly the browser a judge might use. Fixed by reporting
success optimistically and only downgrading on an actual rejection.
A branch-rejoin bug that looked correct until you drew a diamond. My graph walker computed where two branches of a condition reconverge by intersecting what's reachable from each branch's target — but excluded the target node itself from "reachable." A false-arm edge pointing directly at the join node was invisible to that check, so the true arm silently swallowed the shared node into its own branch. One-line fix, caught by a test built specifically to catch it.
TypeScript fought me on Cloudflare's Rpc.Serializable constraint. step.do() requires its
return value to satisfy Serializable<T> — which rejects both unknown (what response.json()
gives you) and a recursive JSON type alias (TS2589, "excessively deep"). I only found this because I
committed a real Worker project and type-checked the generated code against @cloudflare/workers-types
instead of just checking it parsed.
PowerShell silently corrupted my own demo commands. Windows PowerShell passes arguments to
native executables through a legacy quoting layer that doesn't escape embedded double quotes for the
target program's argument parser. My curl.exe -d '{"json":"here"}' commands worked perfectly in
every shell I'd tested — and would have failed live on the recording, because the JSON body was
getting truncated on the wire before it left the machine. Switched every demo command to
Invoke-RestMethod, PowerShell's native HTTP client, which has no such bug.
A public mock endpoint was too slow for Cloudflare's own retry logic. I pointed a demo deployment
at httpbin.org as a stand-in for a real Shopify store. It worked when I curled it directly, but was
inconsistent enough under Cloudflare's outbound calls that runs sat for minutes without erroring or
progressing. Removed the external dependency entirely — the demo Worker now returns instant, clearly
commented stand-in responses instead of calling out to anything.
Accomplishments that I'm proud of
- 62 tests covering the graph algorithms, both exporters, the collaboration rules, and the tool handlers — including a snapshot test that fails the moment a generator's output drifts from the example committed in the repo.
- The generated Cloudflare Workflow isn't just syntax-checked — it type-checks against real
@cloudflare/workers-types, and it's genuinely deployed and running. - Proved the full durable-execution loop live: start a run, watch it durably park at
waitForEvent, approve it from a separate terminal, watch it resume and complete.
What I learned
The gap between "looks correct" and "is correct" lives almost entirely in the paths you didn't personally exercise — native Chrome instead of the polyfill, a real type-checker instead of a transpile-only check, a judge's shell instead of mine. Testing against the actual target each time (a real browser flag, real Cloudflare types, a real deployed Worker) surfaced four real bugs that a "looks right" pass would have shipped straight to the judges.
What's next
Real Shopify HMAC verification (currently an explicit TODO in the generated code, not hidden), a
Chrome origin-trial token so the app reads as native WebMCP for every visitor rather than just
behind a flag, and multiplayer — right now "shared canvas" means one human and one agent; a
Durable-Object-backed canvas would let two humans and an agent share it for real.

Log in or sign up for Devpost to join the conversation.