Building Flowneer: A Tiny LLM Flow Builder in TypeScript

How I built a zero-dependency, 3kB TypeScript library for composing LLM agent workflows — fluent chains, branching, loops, parallelism, and a plugin system with no bloat.

Over the past year I’ve worked with a lot of LLM orchestration frameworks. Most of them follow the same pattern: heavy dependencies, sprawling abstractions, and a configuration-heavy API that locks you in before you’ve written a single useful line of code. I wanted something that felt like writing normal TypeScript but could still express complex agent behaviour, so I built Flowneer.

What is it?

Flowneer is a tiny (~3 kB gzipped), zero-dependency TypeScript flow builder designed for LLM agents. The core idea is a fluent chainable API that lets you describe a pipeline of steps, branches, loops, and parallel executions in a way that reads almost like prose.

const flow = flowneer()
  .startWith(fetchUserContext)
  .then(callLLM)
  .branch(
    (ctx) => ctx.needsMoreInfo,
    (yes) => yes.then(askClarifyingQuestion).loop(() => stillNeedsInfo),
    (no) => no.then(generateFinalResponse),
  )
  .parallel([saveToMemory, logToAnalytics])
  .stream(outputToUser);

await flow.run(initialContext);

Why I built it

I was building a side project that needed a ReAct-style agent loop: observe, think, act, repeat. Every framework I tried either required me to define everything through JSON config files, or pulled in half the npm registry as dependencies.

I wanted to write the orchestration logic in plain TypeScript, see exactly what was happening, and be able to unit-test each step in isolation. None of the existing tools gave me that combination.

The technical challenges

Getting the types right

The hardest part was the TypeScript type system. Flowneer passes a context object through every step, and each step can read and write to it. I wanted TypeScript to track the shape of the context as it moves through the pipeline, so that if a step adds a summary field, later steps can reference ctx.summary with full autocompletion.

Making this work with branching (where different branches might produce different shapes) without losing type safety took a lot of iteration. I ended up settling on a generic Context type that each step can intersect with, trading some inference convenience for correctness.

The plugin system

I wanted Flowneer to be extensible without imposing structure on users who don’t need it. The plugin API uses lifecycle hooks (beforeStep, afterStep, onError, onComplete), so you can intercept any point in the flow.

That made it possible to build the preset packages: @flowneer/react for ReAct-style agent loops, @flowneer/memory for attaching a memory store, and @flowneer/tools for structured tool calling. Each preset is just a plugin and needed no changes to the core.

Keeping it small

The 3 kB budget was self-imposed, but I cared about it, and every feature had to justify itself against that limit. Streaming was the closest call. I nearly left it out, but being able to .stream() straight to a UI was too useful to cut.

What I learned

The most useful thing I learned is that orchestration logic is just control flow. TypeScript already handles branches, loops, and parallelism. What a library like Flowneer adds is consistent context passing, plugin hooks, and structured error handling; everything else is sugar.

I also learned a lot about keeping a bundle tiny. Tree-shaking alone isn’t enough. You have to design the API so unused features never get imported in the first place.

Where it’s going

Flowneer is on v0.9.x and under heavy development. Upcoming work includes better tooling for debugging flow execution, a visualizer for inspecting the pipeline at runtime, and more community-contributed presets.

If you’re building LLM agents in TypeScript and want full control over the orchestration without committing to a heavyweight framework, give it a try. I’d love feedback on the API design.