← All posts

FloMorphic v0.4.5: Coding Agents on the Canvas, and the Work to Make Them Reliable

There are two coding-agent plugins in the catalog now — Claude Agent and Codex Agent — and the interesting thing about them isn't that an agent can write code. It's that the plugin runs on a host you chose, so the agent reaches exactly what that host reaches, and a flow holds the sequence. That's a factory floor. One of these already ships as a Venapce operation, and v0.4.5 is mostly the work of making the line not stop.

In Venapce we used the Claude Agent node to build something close to a CI/CD security gate: a flow where a GitHub node and a Claude Agent node read what changed in a repository and raise one security finding per row, for a person to sign off before anything ships. The agent runs as a node on the canvas — the same Claude Code you'd run in a terminal — on a host you choose, and the flow, not the agent, decides what happens to what it finds.

The flow itself is modest. What it points at is not: FloMorphic can take a real part in the stages of a software factory, standing inside a delivery pipeline as one collaborator among the tools and people already there, rather than a demo bolted on beside them.

Two plugins in the catalog make the agent half of that possible: Claude Agent and Codex Agent. Neither ships inside the FloMorphic image — a plugin is a process you run, which turns out to be exactly the point. And v0.4.5, released on 12 October, is almost entirely the work of making a flow built out of pieces like these not stop in the middle.


A coding agent as a node

A model node sends a prompt and gets a completion back. These don't do that. CLAUDE AGENT drives the Claude Code installed on the plugin's host through @anthropic-ai/claude-agent-sdk; CODEX AGENT drives the Codex app-server over JSON-RPC through openai-codex. Both work in a real tree on real disk, run commands, and decide their own steps. Reach for one when the task is do this in this repo, not answer this.

Two things about the node shape matter more than the model behind it.

Each run is one turn of a conversation that survives. The session id is the inflow job id, so the engine's own memory of the node's last run — _registry.jobId — is the handle on that conversation. No database, no in-memory map, nothing lost when the plugin restarts. Codex works the other way round and mints its own thread id, which the node then adopts as the job id; same result from the opposite end.

It can stop and ask. Both actions have four ports, and the branches aren't editable in the drawer because their shape belongs to the plugin:

PortTagFires when
AnswerdoneThe turn finished. text, plus output when you set a schema.
Needs a humanaskThe agent asked. Parked mid-question — a successful job, not a failure.
Limit reachedlimitOut of turns, tokens or budget with work left. Run the node again to continue.
Failed_exceptionThe agent couldn't run, or ended in error.

Wire ask to a Human in the Loop node in park mode and bring the reply back into the same node, and the session resumes with the person's words delivered as the answer to the question the agent was mid-way through asking. Nothing is held open between those two runs. No process, no connection, no node sitting at 40% — the transcript waits on disk and the flow is the thing that remembers. On Codex, the same port carries the app-server's own approval requests, which the node refuses rather than answering, so nothing proceeds on a decision nobody made; approve and decline come back as the reply, and anything else is passed through as instructions.

Turn on Return structured data, give the node a JSON Schema, and the turn's answer conforms to it — committed as output, so downstream nodes read {{$.claudeAgent.output.verdict}} instead of parsing prose. That's the difference between an agent whose result a human reads and an agent whose result a flow can route on.

The two aren't equally proven, and I'd rather say so here than in a footnote. Claude Agent is verified end to end, including the human loop. Codex Agent is beta: the runtime boot, the zero-token probe, thread minting, config overrides, the notification stream and error mapping are all verified against Codex 0.162.1 on a real host, and 47 unit tests pass, but I have no funded OpenAI credential, so a full turn, the approval park and the resume have not been exercised against a live model. What Codex does have that Claude doesn't is a fence the model can't talk its way around: read-only and workspace-write are enforced by Seatbelt on macOS and bubblewrap on Linux. Claude Agent's workspace root is checked before Claude starts, which stops a node being pointed outside it, but it's organisation rather than a sandbox — Claude Code resolves its project from the git root above cwd, so its Read and Grep tools can reach files outside the workspace, including the plugin's own .env.inflow. Run that plugin as a user who can't read what the agent mustn't see.


Placement is the feature

Here's the part I think is underrated, and it's not about models at all.

A plugin is a process on a host. That host has a filesystem, a network position, credentials, and a set of things it can reach — and historically that was a limitation you worked around. With an agent inside the plugin it becomes the design surface. An agent on the host that holds your API gateway's source can edit that source and run its tests. An agent on the host that serves your documentation can read a changelog and rewrite a page. Neither can reach the other's tree, and nothing in the flow had to arrange that: it's a consequence of where you started the process.

The Venapce operation leans on this directly, and in a way that surprised me the first time it failed. The GitHub plugin clones the repository to its own disk; the Claude node's workspace is {{$.clone.path}}, whatever directory the clone actually produced. Both plugins have to run on the same host, or share that directory, because the clone is a path on a disk and not a payload passed between nodes. If the Claude profile's workspace root doesn't contain the GitHub plugin's clone root, the agent refuses the workspace before it starts and every run fails. That's a hard constraint, it's written down in the operation's README, and it's also the mechanism: the agent reads the real tree, at the real commit, rather than a diff someone serialised into a prompt.

So the unit of trust stops being "the agent" and becomes "this agent, on this host, with this profile, at this step." That is a much easier thing to reason about, and it's the thing you can actually show a security review.


The factory

The term going round for this is a software factory, and the discipline underneath it got named harness engineering: you stop shipping changes by hand and build the loops that let agents ship them. Drew Knox of Tessl put the payoff in the way I find most convincing — not that you ship faster, but that the backlog of things you never had time for stops being a backlog, so the capacity goes into test quality and the refactors nobody could justify.

The version of that aimed at one coding agent in one repo is well covered. What I keep running into is the other shape, and it's the reason I wanted these plugins: a system with several components, where a change in one of them creates obligations in the others.

Say you've built the thing with Inflow and FloMorphic, and the stages of your delivery process are flows. A migration lands on the backend — a table gains a column, or a feature changes a response. Right now, who notices? Usually a person, usually late, usually because something downstream broke or a document was wrong for three weeks.

Drawn as a flow, with agents placed rather than invoked, it looks like this:

Stage one — the gateway. A GitHub node reads what landed. A contract node extracts the shape of the change. A CLAUDE AGENT node on the gateway host, with its workspace root set to the gateway's tree and a schema on its output, is asked to make the gateway match the new backend and run its tests. Three outcomes, three ports, and all three are drawn: done carries a structured verdict the next node reads; limit means the work isn't finished and the node gets run again, resuming the same session; ask parks on a person, because "this migration drops a field two endpoints still return — which one wins?" is not a question an agent should answer by itself at 3am.

Stage two — the tests. Not the agent's claim that tests pass, a separate step that runs them. The agent says what it did; the flow verifies it. A failure is a backward edge to the same agent node, with the output of the test run as the next turn's input, and a counter on the context that gives up after n attempts and parks instead of looping forever. The loop is edges on a canvas, not a while-loop inside a prompt, which means you can see how many times it's allowed to go round.

Stage three — the other side. Another CLAUDE AGENT, different host, different workspace root, whose job is to read what actually changed and bring the front-side documents into line. It cannot reach the gateway's tree. It doesn't need to: what crossed the boundary was the structured output of the previous stage, which is a description of the change rather than access to it.

And at the end, a row somewhere that says what happened: which commit, which agent, which model, what it cost, how many turns, what a human was asked and what they said.

I want to be straightforward that this particular three-stage flow is a design, not something I've shipped. The pieces are all real and the one that's hardest — a coding agent as a node with a human loop and a durable session — is the one that's verified. But I haven't run the full backend-to-docs chain in anger, and I'd expect the first attempt to teach me two or three things the design doesn't know yet.

What is shipped, and running, is the smaller sibling of exactly this shape.


The one that already exists

github-commit-security-review is a Venapce operation at version 1.1.0: twelve nodes, thirteen edges, two plugins and one builtin.

          ┌─ Clone the repository ─────┐
Start ──▶ │  (git, on the plugin host) │──▶ Wait ──▶ Review the commits ──▶ findings? ──▶ normalize ──▶ map ──▶ Stage
 seed     └─ List the recent commits ──┘   for all     with Claude          │                                  (1 row each)
 params      (GitHub's own record)                     (schema-bound)       └─ clean ──▶ record the review, write nothing

The clone and the commit list don't depend on each other, so they run in parallel and join at a Wait for All. Two actions from the GitHub (OpenConnector) plugin — github.repo.clone and github.activity.list — one from Claude Agent, one db.stages.upsert on the Venapce plugin, and a Cast doing the mapping onto Stage's columns.

The agent answers against a schema, so the rest of the flow reads fields. It's also required to return reviewedCommits: every commit it read, with the paths each one touched. That list is the evidence the review happened, and it's what makes a clean verdict worth anything at all — without it, "no findings" and "didn't look" are the same string.

Nothing is promoted automatically. Every row lands at disposition: pending, carrying the evidence (file, line, snippet, remediation, confidence, category), the provenance (the agent's model and session id, the repo, the commit, GitHub's own record of it, the workspace) and the enrichment (falsePositiveRisk, severity rank, the floor applied, the review's cost and turn count). A person decides in the panel whether it becomes a Finding, an Issue, or nothing.

There's a node in that flow called "Note the unfinished review", wired to limit. It exists because a review that ran out of turns halfway through eight commits is a different fact from a review that found nothing, and if the two look the same in your panel you will eventually trust a gap.

Two things it doesn't do, both of which I'd fix before calling it finished. It doesn't deduplicate across runs: db.stages.upsert updates in place only when given an id, and the Venapce plugin exposes no read action a flow could use to look a fingerprint up first, so running it twice over the same commits stages the same finding twice. And the fingerprint itself — github:<owner>/<repo>:<file>:<line>:<slug of title> — is only as stable as the model's own wording. Two runs over the same commits reported the same issue at line 47 and then line 48, with slightly different titles. Treat it as a strong hint for recognising a repeat, not a key.


Why draw it instead of writing one big prompt

The obvious objection: a sufficiently good coding agent could do all three of those stages itself, with a long prompt and access to every repo. Why spend an afternoon on a canvas?

Three answers, and the first is the one that would make me choose this even if nothing else held.

The authority is per step. Each agent node has one workspace, one profile, one set of ports, and a sandbox mode you chose for that step. The blast radius of stage three is the docs tree, by construction, not by instruction. A prompt that says "don't touch the gateway" is a comment; a workspace root the plugin refuses to start outside of is a boundary. (Within the limits I named above — Claude's root fences the node and organises the agent; Codex's is enforced by the kernel.)

The sequence outlives the session. A parked question can wait a week. A six-hour build can run while the flow isn't watching and be picked up by a later run of the same node — which is the detached-work pattern, now written down in the SDK, and which both agent plugins are instances of. There's no async node and no poll node in the runtime, because a plugin decides its own temporal shape.

The run is the record. When somebody asks why the gateway changed, the answer isn't an agent's recollection. It's which node fired, which port it left through, what the schema carried, what the human was asked, and what it cost.


The half of v0.4.5 that this post depends on

Everything above is a long-running process built out of nodes that can refuse, park, run out of budget, or sit on a host that's down. So the release that matters most to it is the boring one. v0.4.5 has one subject approached from four sides: an exception has to be able to reach the branch drawn for it.

It couldn't. A plugin node that raised an exception and a plugin node that failed were the same thing to the engine, which made the _exception port a decoration. Both builtin plugins paired CmdNextFilter([_exception]) with DoneWithErrorData, and that error payload is the core's verdict that the node didn't deliver — which fractal answers by pruning every outgoing edge of the node, the exception one included, after the routing has already selected it. So a node reported its exception and the run stopped dead at it, on the branch an author had drawn for precisely that case.

The fix is a distinction the plugins now make explicitly. Some exceptions are the node delivering: a refusal (the Decisions API declining one question while answering the rest), a confidence below the floor the designer set, a model answering with prose when a tool call was offered. Those end with Done, carry the reason in the committed payload, and route. Others are the node failing: a provider error past its retry budget, a missing answer, an answer naming an option the question never declared, a call to a function nobody bound. Those still mark the node red, still count in the run's errors, and still stop a run that asked to stop at errors. On ai-decision that splits five codes three to two; on llm, three codes one to two. The whole decision is one pure function, nodeDelivered, and an unrecognised code reports a failure, so a future case over-reports rather than passing a defect off as a decision.

There's a trade-off in there I took deliberately and don't love: stop_on_error no longer stops a run at a delivered exception. In fractal the error record and the branch cut are the same flag — Task.didNotDeliver drives both — so a plugin can't report the error without also triggering the cut. Honouring both at once means teaching the engine that this tag is a declared error route and sparing it in cutIfStopOnError. That's the alternative design, not a follow-up task.

And the exception that fired most often wasn't one. A vLLM-backed OpenAI-compatible endpoint opens a streamed tool call the way OpenAI does, then sends one more opening chunk with the arguments still empty. langchaingo v0.1.14 accumulates deltas by heuristic rather than by the delta's index — its ToolCall type has no index field to key on — so it files that chunk as a second, nameless call and welds every later fragment onto it. The node was handed a named call with no arguments beside an anonymous call holding the entire argument object, and a perfectly correct single-function answer left through _exception as unbound_function. Of 38 deltas in one measured turn, one carried type, one was that empty-argument chunk, and 36 were fragments. Both the llm node and the mcp node rejoin the halves now, and two tests pin the repair as the identity on well-formed output: an OpenAI-shaped stream must come out byte-identical, and a plain-text turn must not acquire a tool call from nowhere.

The mcp case failed more quietly and cost more. The named half arrived with no arguments and the MCP server rejected it on its schema; the unnamed half was refused as an unbound tool; both answers went back to the model as tool content, so one correct answer cost a turn twice over and the loop could run out to maxTurns and report "reached max tool turns without a final answer" with nothing saying the model's arguments had been thrown away. If you've been running an agent loop against a self-hosted endpoint and wondering why it burns turns, that's a candidate.

The canvas side is the same subject from the reader's end. The AI Decision and LLM drawers now say which of a node's exception codes will end a run and which are decisions — which mattered most on min_confidence, where stopping would cut the very branch the floor was drawn for. A row with no name no longer gets an output port it could never fire, since the name is the route tag. And "plugin not running" is no longer reported as "branch stopped", because the reader's next move is different: a stop-on-error cut is the run's setting acting on a node that didn't deliver and the flow author fixes it, while a plugin that was never up is nobody's flow bug, happens whatever stop_on_error says, and is fixed by starting the plugin. For a factory whose stages are agents on four different hosts, that one badge is the difference between debugging a flow and restarting a process. It needs @inflowenger/flow-trace 0.2.4.

Beside that, @inflowenger/node-plugin-sdk reached 0.2.2 and parity with the Go SDK: the middleware pipeline, the jobstop registry and the cancelable job context that v0.4.4 introduced in Go are now the same shape in TypeScript. Both agent plugins carry jobstop on their run action, which is why a stopped flow aborts the agent subprocess instead of leaving one writing to somebody's disk — while the conversation itself survives, because the transcript is on disk and the next run resumes it. Middleware runs before the SDK replies the jobId to the runtime, which is what makes per-job registration race-free: nothing can happen to a job before what was set up under its jobId is in place.


And the performance, which is the part you don't see

The factory shape puts small scripts everywhere. Between the GitHub node and the agent there's a contract node shaping the input; between the agent and the Stage write there's a normalize step and a mapping. That glue is JS, and in v0.4.4 fractal-core moved JS execution off quickjs onto pooled V8 isolates.

Measured as aggregate scripts per second across all 24 cores, against quickjs:

ScriptquickjsV8 pooled
Tiny script — what most nodes actually carry1,402/s14,685/s10.5×
20k string build61/s3,968/s65×
2M-iteration loop499/s1,174/s2.4×

A second, independent harness — 64 goroutines, timed end to end — agrees at 8.7× overall, and Go allocations per pass fall from 82 / 13.4 KB to 46 / 1.8 KB.

The finding behind those numbers is the one worth carrying away: quickjs anti-scales. A tiny script takes 153 µs sequentially, which is about 6,500/s on one core, yet 24 cores deliver only ~1,400/s — roughly 4.7× worse under concurrency, with string work degrading furthest (3.25 ms sequential becomes 16.4 ms under load). V8 scales positively instead. So the sequential benchmark I'd been running was the wrong instrument, and the "V8 pooled is 13% slower on tiny scripts" result it produced is true in isolation and irrelevant in production. I had that backwards for a while.

Two caveats. For a compute-heavy loop a per-pass isolate beats the pool (619 µs vs 852 µs), because a reused isolate carries accumulated heap and collects more — GetV8Vm stays exported for a node that wants that trade. And memory is cheaper than it reads: 24 live isolates add about 10 MiB RSS, because the 256 MiB heap figure is a cap, not a reservation.


Where that leaves v0.4.5

The three-stage factory above is still a drawing. But the thing I was missing for it wasn't a model and it wasn't a canvas feature. It was an agent that lives somewhere specific, keeps its conversation between runs, can stop and ask a person, and dies when the flow does — and then a release that makes sure the branch I drew for the awkward case is a branch the run can actually take.


FloMorphic is open source, Apache-2.0, and v0.4.5 is tagged latest. Install with the script from the getting-started repo — bash on Linux and macOS, PowerShell on Windows:

curl -fsSL https://raw.githubusercontent.com/FloMorphic/getting-started/main/install.sh | bash
irm https://raw.githubusercontent.com/FloMorphic/getting-started/main/install.ps1 | iex

The agent plugins are separate processes you run yourself — Claude Agent on the Node SDK, Codex Agent on the Python SDK. Both are listed in the plugin catalog, and the full v0.4.5 notes are in the changelog.