← All posts

Product

Shipping the workflow builder

Branching, transfers, and mid-call actions without writing code. What we cut to keep it usable, and why deterministic steps sit alongside the model.

Bilal Sarwar5 min read
Illustration for Shipping the workflow builder

The first version of the builder let you express almost anything, which meant nobody could get anything working. The rewrite started from the handful of shapes real call flows actually take.

That sentence took us a long time to be able to write. The first version was, by every measure we cared about at the time, the more impressive product: arbitrary nesting, custom logic in any node, per-node configuration of nearly everything. It demoed beautifully. Then we watched people use it, and almost nobody finished a flow.

This is a note about what we removed and why, because the removals are the interesting part.

Expressiveness is not the same as capability

A builder that can express anything gives you no help deciding what to express. Every node was a blank page, every connection was legal, and the result was that people built something plausible, hit a case it didn't handle, added a branch, and repeated until the canvas was unreadable. They hadn't done anything wrong. The tool had simply offered no opinion.

What we found watching real flows get built is that call flows are not arbitrary graphs. They are a small number of recurring shapes: collect some facts, look something up, decide between a handful of outcomes, act, confirm. The variation between businesses is almost entirely in the specifics — which facts, which lookup, which outcomes — and almost never in the shape.

So the rewrite made those shapes first-class and made everything else harder to do. That is a deliberate trade of ceiling for floor: the most complicated flow you can build is now simpler than before, and the number of people who get a working agent out of it went up sharply.

Deterministic where it matters

Some steps should never be left to a model's judgment: verifying a caller's identity, confirming a payment amount, deciding whether a transfer is allowed. Those run as deterministic checks the agent has to pass through.

The model handles the conversation around them. The gates stay fixed.

The distinction we settled on is roughly: language is the model's job, consequences are not. Understanding that "yeah go on then" means yes is exactly what a model is good at. Deciding whether that yes is sufficient authorisation to take a payment is not a language question, and it should not be resolved by one.

In practice that means a gate is a real branch with a real condition, evaluated against state, with a defined path for failing it. Not an instruction in a prompt asking the agent to check something first. The prompt version works most of the time, which is the problem — the failures are rare, silent and exactly the ones that matter.

A rule of thumb that has held up: if getting it wrong would require an apology, a refund or a phone call from your compliance team, it is a gate and not a sentence.

What we cut

Arbitrary nesting, custom scripting in the canvas, and per-node model overrides all went. Each one made the builder more expressive and considerably less likely to produce a working agent.

The specific case against each:

  • Arbitrary nesting. Depth was where flows went to die. Past about two levels nobody could hold the structure in their head, including the person who built it a fortnight earlier, and debugging meant tracing a path by hand.
  • Custom scripting in the canvas. It turned a flow into a codebase with no tests, no review and no way to tell what a node did without reading it. Genuine logic belongs behind an integration where it can be versioned properly.
  • Per-node model overrides. These looked like tuning and behaved like drift: an agent whose voice and judgement changed halfway through a call, for reasons buried three panels deep in a node nobody remembered configuring.
  • Free-form connections between any two nodes. Removing these lost some legitimate flows and prevented a much larger number of accidental loops.

What we kept, and what it cost us

We were not being minimalist for its own sake. Some things survived precisely because the failure they prevent is worse than the complexity they add: conditional branching on collected state, transfers with context attached, mid-call actions against external systems, and explicit failure paths on every step that can fail.

That last one is the least glamorous and the most load-bearing. Every node that talks to something outside the agent has a defined path for when that thing is slow, unavailable or returns something unexpected. Making it impossible to leave that path undefined removed a whole class of calls that used to end in silence.

The honest cost: a handful of genuinely sophisticated flows became harder to build, and a small number of people who had mastered the old canvas were annoyed. We think that was the right trade — a tool that a hundred people can use to build something solid beats one that ten people can use to build anything — but it was a real loss for those ten.

What we'd tell anyone building the same thing

Watch people build a flow from scratch before adding a single feature. Not a demo, not a guided walkthrough — someone with a real call flow in their head and an empty canvas in front of them. The features you think are missing and the ones actually blocking them overlap much less than you'd expect.

And count finished flows, not created ones. Creation is easy and tells you nothing; a canvas full of half-built agents is the signal that your tool is expressive and unhelpful at the same time.

Build with Solaris