Skip to main content

Overview

Most frameworks force a choice: Agent (the LLM decides what to do next) or Workflow (your code decides). flow-state.dev rejects that split. Sequencers are the composition model. You chain blocks with .then(), .thenIf(), .tap(), and fifteen other DSL methods. Each step's output feeds into the next step's input. Type inference flows through the whole chain.

You can interleave deterministic and non-deterministic steps. Validate input with a handler, generate a response with a generator, extract structured data with a handler, refine with another generator. All in one pipeline. No artificial boundary between "AI steps" and "logic steps."

Sequencers compose blocks

A block is the unit of work: a handler (deterministic logic), a generator (LLM call), a router (runtime dispatch), or another sequencer. See Blocks for the four block kinds. Sequencers compose them:

import { handler, generator, sequencer } from "@flow-state-dev/core";
import { z } from "zod";

const validate = handler({
name: "validate",
inputSchema: z.object({ message: z.string() }),
outputSchema: z.object({ message: z.string() }),
execute: async (input) => {
if (!input.message.trim()) throw new Error("Empty message");
return input;
},
});

const agent = generator({
name: "agent",
model: "gpt-5-mini",
prompt: "You are a helpful assistant.",
inputSchema: z.object({ message: z.string() }),
user: (input) => input.message,
});

const extractJson = handler({
name: "extract-json",
inputSchema: z.string(),
outputSchema: z.record(z.unknown()),
execute: async (input) => {
const match = input.match(/\{[\s\S]*\}/);
return match ? JSON.parse(match[0]) : {};
},
});

const pipeline = sequencer({
name: "chat-pipeline",
inputSchema: z.object({ message: z.string() }),
})
.then(validate)
.then(agent)
.map((out) => out.text)
.then(extractJson);

Here, a handler validates, a generator produces text, a .map() extracts the text, and a handler parses JSON. The chain's output type is inferred from the last step.

The 15 DSL methods

MethodPurpose
thenRun a block, pass output to next step
thenIfRun a block only when a condition holds
mapInline transform (no block)
parallelRun multiple blocks concurrently, merge outputs
forEachProcess array items with a block
doUntilLoop until a condition is true
doWhileLoop while a condition is true
loopBackJump back to a named step
workFire-and-forget side work (doesn't block)
waitForWorkWait for .work() tasks, optional failOnError
tapRun a block or function without changing the payload
tapIfConditional tap
rescueCatch errors, route to recovery blocks
branchRoute to first matching branch
Inline block factories.then(handler, { outputSchema, execute }) etc.

Each method returns a sequencer. You chain them: .then(a).thenIf(cond, b).tap(c).then(d).

Output flows forward

The pipeline is linear: step 1 output → step 2 input → step 2 output → step 3 input. Connectors let you reshape data when types don't match. See Connectors for details. TypeScript infers the chain's output from the last step's schema.

Sequencers are blocks

A sequencer is a block. It composes with any other block. You can nest sequencers, use a sequencer as a generator tool, or register it as a flow action:

const inner = sequencer({ name: "inner" }).then(blockA).then(blockB);
const outer = sequencer({ name: "outer" })
.then(inner)
.then(blockC);

// As a tool
const agent = generator({
name: "agent",
tools: [inner],
// ...
});

Where to go next

  • Patterns — Common composition patterns with code examples
  • Side Chains — Fire-and-forget work with .work() and .waitForWork()
  • Connectors — Shaping data between steps, typed refs, and portability