| name | fsd:create-pattern |
| description | Create a new composable pattern — a multi-block sequencer composition that solves a recurring agentic architecture problem. Produces the pattern factory, internal blocks, tests, and documentation. |
| argument-hint | <pattern name and purpose, e.g. 'debate pattern where two LLMs argue opposing positions'> |
You are a development agent creating a new composable pattern in the flow-state-dev framework. Patterns are the highest-level building blocks — multi-block orchestrations that solve architectural problems like planning, coordination, or evaluation.
Core Principle
Patterns are dual-use: import-and-use or copy-and-customize. The factory should work with zero-effort defaults, but every internal block should be replaceable via config for developers who need to customize.
Workflow
Step 1: Understand the Pattern
Parse $ARGUMENTS to determine:
- What architectural problem does this pattern solve?
- What blocks does it need internally? (generators for LLM decisions, handlers for state ops, routers for dispatch, sequencers for sub-flows)
- What state does it manage? (sequencer state vs session resources)
- What's the iteration/control flow? (linear, looping, conditional, parallel)
Step 2: Read Reference Patterns
Before writing, read at least two existing patterns in packages/patterns/src/:
| Pattern | What to learn from it |
|---|
plan-and-execute/index.ts | Config with replaceable sub-blocks, doUntil loop, sequencer state, synthesis step |
routedSpecialists/index.ts | Session resources, router dispatch, controller loop, specialist composition |
task-board/index.ts | TaskCollection substrate, dispatcher + worker registry, concurrent drain, mid-run enqueue |
response-auditor/index.ts | Simpler pattern: linear chain, tap for side effects, evaluation criteria |
supervisor/index.ts | forEachBackground for concurrent work, resource-based coordination |
eventActors/index.ts | Stigmergic multi-agent coordination, actor dispatch, event-driven drain |
Also read:
docs/contributing/best-practices.md (universal rules + index) and the category files for what the pattern composes — docs/contributing/best-practices/blocks.md (esp. BP-011 no block.run in handlers, BP-012 .tap for state-only, BP-014 never return input, BP-025 sequencer output schemas), plus generators.md and resources.md when the pattern includes generators or resources
docs/architecture/blocks.md — block contract and sequencer DSL methods
Step 3: Design the Pattern Structure
Every pattern follows this structure:
packages/patterns/src/<pattern-name>/
index.ts # Public factory + re-exports
schemas.ts # Zod schemas for state and internal types
blocks/ # Internal block factories (one per file)
<step-name>.ts
...
Key decisions:
-
State location: Use sequencer state for pattern-internal state (plan, iteration count, queue). Use session resources only when state must survive across multiple pattern invocations or be shared with external blocks.
-
Default blocks: Provide working defaults for every internal block. The user should be able to call myPattern({ name: "x" }) and get something useful without configuring anything.
-
Config interface: Every internal block should be overridable via the config. Use BlockDefinition<any, any> for block slots.
-
Capabilities: If the pattern bundles resources, context formatters, and helper functions that blocks should opt into, package them as a defineCapability(). This is the preferred approach when the pattern provides reusable infrastructure that other blocks (outside the pattern) may also need.
Step 4: Write the Pattern
Config Interface
import type { BlockDefinition } from "@flow-state-dev/core/types";
import type { GeneratorSlot, ToolsSlot, UsesSlot } from "@flow-state-dev/core";
import type { ZodTypeAny } from "zod";
type InstructionsSlot = string | ((input: any, ctx: any) => string | Promise<string>);
export interface <PatternName>Config<
TOutputSchema extends ZodTypeAny = ZodTypeAny
> {
name: string;
<roleName>?: BlockDefinition<any, any>;
maxIterations?: number;
outputSchema?: TOutputSchema;
instructions?: InstructionsSlot;
context?: GeneratorSlot<any, any>;
tools?: ToolsSlot;
uses?: UsesSlot;
sessionResources?: Record<string, any>;
userResources?: Record<string, any>;
projectResources?: Record<string, any>;
}
Factory Function
export function <patternName><
TOutputSchema extends ZodTypeAny = ZodTypeAny
>(config: <PatternName>Config<TOutputSchema>) {
const stateSchema = create<PatternName>StateSchema();
const maxIterations = config.maxIterations ?? 10;
const step1 = config.<roleName> ?? createDefault<RoleName>(config.name);
const step2 = create<Step2>(config.name, stateSchema);
return sequencer({
name: config.name,
stateSchema,
})
.step(step1)
.step(step2)
.doUntil(
(result) => result.done,
step3
);
}
Internal Block Factories
Each internal block lives in blocks/<name>.ts:
export function create<BlockName>(
name: string,
stateSchema: ReturnType<typeof create<PatternName>StateSchema>
) {
return handler({
name: `${name}-<block-suffix>`,
inputSchema: z.any(),
outputSchema: z.object({ }),
sequencerStateSchema: stateSchema,
execute: async (input, ctx) => {
return { };
},
});
}
Forwarding uses, tools, and Resources
The most common pattern: accept uses, tools, and resource declarations in your config and forward them to the internal blocks that need them. This is how planAndExecute and supervisor work.
export function <patternName>(config: <PatternName>Config) {
const { name, uses, tools, instructions, sessionResources, userResources, projectResources } = config;
const executor = config.executor ?? generator({
name: `${name}-executor`,
model: config.model ?? "openai/gpt-5.4-mini",
...(tools !== undefined ? { tools } : {}),
...(uses ? { uses: uses as any } : {}),
prompt: [instructions, "Execute the task."],
});
return sequencer({
name,
stateSchema,
...(sessionResources ? { sessionResources } : {}),
...(userResources ? { userResources } : {}),
...(projectResources ? { projectResources } : {}),
})
.step(executor)
}
Resources are declared on the outer sequencer so the runtime registers them. uses and tools go on the generators that actually call LLMs. Only spread them when the consumer hasn't provided a custom override block.
Exporting a Capability
Some patterns provide reusable infrastructure (shared resources, context formatters, helper tools) that blocks outside the pattern also need. In that case, export a defineCapability() alongside the factory:
import { defineCapability, defineResource } from "@flow-state-dev/core";
export const <patternName>Capability = defineCapability({
name: "<pattern-name>",
sessionResources: {
<resourceName>: defineResource({ stateSchema: <schema>, writable: true }),
},
presets: {
context: { context: [<contextFormatter>] },
tools: { tools: [<readTool>, <writeTool>] },
default: ["context", "tools"],
},
fns: (ctx) => ({
list: () => ctx.session.resources.<resourceName>.state.items,
add: async (item) => { },
}),
});
This is the less common case. Most patterns just forward uses from their config. Only create a dedicated capability when the pattern owns resources that external blocks should opt into.
Sequencer DSL Reference
Beyond the basics (.step(), .doUntil(), .tap(), .loopBack()), the sequencer provides additional composition methods useful in patterns:
| Method | Behavior |
|---|
workIf(condition, block) | Conditional background work dispatch. No-op when falsy. |
stepAll(blocks) | Parallel execution of multiple blocks with the same input. Collects all results (like Promise.all). |
stepAny(blocks) | Sequential attempt through blocks in order. Returns the first success, skips the rest. Throws AggregateError if all fail. |
race(blocks) | Parallel execution, returns the first success, aborts the rest. Throws AggregateError if all fail. |
exitIf(condition) | Early exit from the sequencer chain. Current value becomes the sequencer output. |
stepIf(condition, block) | Conditional step execution. Passthrough when condition is false. |
work(block) / background(block) | Fire-and-forget sidechain. Main chain continues immediately. |
rescue(handlers) | Error recovery. Matches thrown errors to handler blocks. |
branch(branches) | Multi-way conditional dispatch with connectors. |
See packages/core/src/blocks/sequencer-methods.ts for full type signatures.
Instructions Composition
When your pattern has multiple internal generators, use config.instructions as a shared base that composes with block-specific prompts. The prompt field on generators accepts an array where null/undefined entries are filtered out:
function createDefaultExecutor(config: <PatternName>Config) {
const basePrompt = "You are a focused task executor. ...";
return generator({
name: `${config.name}-executor`,
prompt: [config.instructions, basePrompt, config.executionInstructions],
});
}
function createDefaultSynthesizer(config: <PatternName>Config) {
const basePrompt = "Synthesize findings into a coherent answer. ...";
return generator({
name: `${config.name}-synthesizer`,
prompt: [config.instructions, basePrompt, config.synthesizeInstructions],
});
}
When instructions is a function (dynamic), and the block needs it as context rather than system prompt (e.g., the planner), resolve it in a context formatter:
const plannerContext = config.instructions && !config.planner
? [
...(config.context ? (Array.isArray(config.context) ? config.context : [config.context]) : []),
async (_input: any, ctx: any) => {
const resolved = typeof config.instructions === "function"
? await config.instructions(_input, ctx)
: config.instructions;
return resolved ? `Overall instructions:\n${resolved}` : null;
},
]
: config.context;
Task Progress Emission
Patterns that decompose work into trackable tasks should use taskBoard as the dispatch primitive. The substrate emits task-change (per-task lifecycle) and task-board-meta (board-level aggregate) ComponentItems automatically — the UI renders them via <TaskPlan />.
import { taskBoard } from "../task-board";
const board = taskBoard({
name: config.name,
collection: { backing: "request", collectionId: config.name },
concurrency: 1,
workers: { default: workerBlock },
initialTasks: tasks.map(t => ({ id: t.id, goal: t.goal, assignee: "default" })),
});
Pair with <TaskPlan collectionId={config.name} /> in the UI registry. The legacy emitPlanMeta / emitTaskUpdate helpers have been retired — new patterns should use the substrate directly.
Critical Rules
Step 5: Add Exports
Add to packages/patterns/src/index.ts:
export { <patternName> } from "./<pattern-name>";
export type { <PatternName>Config } from "./<pattern-name>";
export { create<BlockName> } from "./<pattern-name>/blocks/<name>";
export { create<PatternName>StateSchema } from "./<pattern-name>/schemas";
Step 6: Write Tests
Create packages/patterns/test/<pattern-name>.test.ts. Pattern tests use testBlock from @flow-state-dev/testing:
import { describe, expect, it } from "vitest";
import { testBlock } from "@flow-state-dev/testing";
import { handler } from "@flow-state-dev/core";
import { z } from "zod";
import { <patternName> } from "../src/<pattern-name>";
describe("<pattern-name>", () => {
it("creates a sequencer block", () => {
const block = <patternName>({ name: "test" });
expect(block.kind).toBe("sequencer");
expect(block.name).toBe("test");
});
it("runs to completion with defaults", async () => {
const block = <patternName>({ name: "basic" });
const result = await testBlock(block, { input: { } });
expect(result.error).toBeNull();
});
it("accepts custom sub-blocks", async () => {
const custom = handler({
name: "custom-step",
inputSchema: z.any(),
outputSchema: z.any(),
execute: async () => ({ }),
});
const block = <patternName>({
name: "custom",
<roleName>: custom,
});
const result = await testBlock(block, { input: { } });
expect(result.error).toBeNull();
});
it("respects maxIterations guard", async () => {
const block = <patternName>({ name: "bounded", maxIterations: 2 });
const result = await testBlock(block, { input: { } });
});
});
Step 7: Add a Tier 1 integration scenario (when warranted)
Pattern factories have failure modes that only emerge from full runAction composition — claim systems deadlocking, dispatchers looping, sub-agent state bleeding across iterations. testBlock doesn't catch these. Add a scenario in packages/integration-tests/src/scenarios/ when the pattern:
- Uses
taskBoard or another claim-system substrate.
- Has a dispatcher loop, drain loop, or replan loop.
- Composes multiple sub-blocks whose interaction can deadlock or cycle.
- Passes data between sub-agents through scope state or resources.
Skip this step if the pattern is a thin sequencer (e.g., a static .step chain with no loops). Authoring pattern: synthetic fixture flow under src/scenarios/fixtures/ + scenario file under src/scenarios/. Use unmockedGeneratorPolicy: "error" so missing mocks surface loudly. See apps/docs/docs/testing/flow-integration-tests.md and packages/integration-tests/README.md.
Step 8: Update Documentation
- Add a section to
packages/patterns/README.md for the new pattern
- If it demonstrates a novel framework capability, add a page under
apps/docs/docs/patterns/
Step 9: Verify
pnpm --filter @flow-state-dev/patterns typecheck
pnpm --filter @flow-state-dev/patterns test
Guidelines
- Defaults must work.
myPattern({ name: "x" }) with no other config should produce a functioning pipeline. Use utility blocks as defaults where appropriate.
- State schemas are explicit. Always define a
stateSchema for the sequencer. Don't rely on untyped state.
- Patterns are compositions, not primitives. If your pattern is a single block, it's a utility, not a pattern. Patterns combine multiple blocks with control flow.
- Name-prefix everything. Internal blocks, resources, state keys — all prefixed with the pattern name to support multiple instances in the same flow.
- Export for remixing. Users will want to use your internal blocks in their own compositions. Export the factories, not just the top-level pattern.