Skip to content

Getting Started

Installation

bash
bun add flowneer
# or
npm install flowneer
# or
pnpm add flowneer

Your First Flow

Every Flowneer flow starts with a FlowBuilder. You define a shared state type, chain steps, and call .run().

typescript
import { FlowBuilder } from "flowneer";

interface State {
  input: string;
  result: string;
}

const flow = new FlowBuilder<State>()
  .startWith(async (s) => {
    s.result = s.input.toUpperCase();
  })
  .then(async (s) => {
    console.log(s.result); // "HELLO WORLD"
  });

await flow.run({ input: "hello world", result: "" });

The Shared State Model

All steps operate on the same objects in every step is the same reference. Mutate it directly; never replace it with a spread (s = { ...s }), as that would break the reference shared between steps.

typescript
// ✅ Correct — mutate in place
async (s) => {
  s.count += 1;
};

// ❌ Incorrect — replaces the reference, upstream steps see the old object
async (s) => {
  s = { ...s, count: s.count + 1 };
};

Registering Plugins

Plugins extend FlowBuilder with new methods. Use FlowBuilder.extend([...plugins]) to create a subclass that has those methods available.

typescript
import { FlowBuilder } from "flowneer";
import { withTiming } from "flowneer/plugins/observability";
import { withCostTracker } from "flowneer/plugins/llm";

const AppFlow = FlowBuilder.extend([withTiming, withCostTracker]);

// Now all AppFlow instances have .withTiming() and .withCostTracker()
const flow = new AppFlow<State>()
  .withTiming()
  .withCostTracker()
  .startWith(myStep);

See Writing Plugins for how to create your own.

Step Options

Every step (.startWith, .then, .parallel) accepts an optional NodeOptions object:

OptionTypeDefaultDescription
retriesnumber | (s, p) => number1How many total attempts (1 = no retry)
delaySecnumber | (s, p) => number0Seconds between retry attempts
timeoutMsnumber | (s, p) => number0Per-step wall-clock timeout (0 = disabled)
typescript
const flow = new FlowBuilder<State>().startWith(fetchData, {
  retries: 3,
  delaySec: 1,
  timeoutMs: 5000,
});

retries and delaySec can be functions for dynamic per-step behaviour:

typescript
.then(myStep, {
  retries: (s) => (s.isImportant ? 5 : 1),
})

Aborting a Flow

Pass an AbortSignal to .run() to cancel mid-flow:

typescript
const controller = new AbortController();
setTimeout(() => controller.abort(), 3000);

await flow.run(shared, {}, { signal: controller.signal });

TypeScript Generics

FlowBuilder<S, P> has two type parameters:

  • S — the shared state type (required)
  • P — the optional params type (defaults to Record<string, unknown>)

params are read-only contextual values injected at .run() time — useful for request-scoped data like user IDs or request metadata.

typescript
interface Params {
  userId: string;
  requestId: string;
}

const flow = new FlowBuilder<State, Params>().startWith(async (s, params) => {
  s.userId = params.userId;
});

await flow.run(initialState, { userId: "u123", requestId: "r456" });