一键导入
fsd-add-flow
Create a new flow definition with actions, scopes, resources, and capabilities. Produces the flow file, action blocks, and server registration.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Create a new flow definition with actions, scopes, resources, and capabilities. Produces the flow file, action blocks, and server registration.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | fsd:add-flow |
| description | Create a new flow definition with actions, scopes, resources, and capabilities. Produces the flow file, action blocks, and server registration. |
| argument-hint | <flow kind and purpose, e.g. 'research assistant flow with chat and summarize actions'> |
You are a development agent creating a new flow in the flow-state-dev framework. Flows are the top-level orchestration unit — they bind actions, scopes, state, resources, and capabilities into a registerable runtime.
Flows are thin orchestration glue. The interesting logic lives in blocks and patterns. A flow's job is to wire actions to block pipelines, declare state schemas, and configure scopes. Keep the flow file focused on structure, not business logic.
Parse $ARGUMENTS to determine:
kind (URL-safe identifier)?Before writing, read at least one existing flow:
| Flow | What to learn from it |
|---|---|
examples/hello-chat/src/flows/hello-chat/flow.ts | Minimal single-action flow, session state, userMessage, history |
apps/kitchen-sink/flows/chat-agent/ | Multi-action flow, resources, clientData, capabilities, middleware |
Also read:
docs/architecture/flows-and-actions.md — Flow definition contractdocs/architecture/state-and-scopes.md — Scope hierarchy and state operationsdocs/architecture/resources-and-client-data.md — Resources and clientData patternsdocs/contributing/best-practices.md — universal rules + situational index; block rules (BP-011–BP-014) live in docs/contributing/best-practices/blocks.md, and resource/state rules (BP-015/019/023/027) — relevant when a flow declares scopes, resources, or client projections — in docs/contributing/best-practices/resources.mdsrc/flows/<flow-kind>/
flow.ts # defineFlow() + export default instance
blocks/ # Action pipeline blocks (one per file)
<action-name>.ts
...
schemas.ts # Shared Zod schemas (input, state, resource)
capabilities.ts # Optional: flow-specific capabilities
Key decisions:
Actions: Each action is an entry point with inputSchema, block, and optional userMessage. Chat actions typically have userMessage; system/background actions don't.
Scope state: Use session.stateSchema for conversational state (message count, mode, preferences). Use user.stateSchema for cross-session user state. Use project.stateSchema for shared team state.
Resources vs state: Resources are named, typed data containers with their own schemas. Use resources for structured entities (plans, artifacts, documents). Use scope state for scalar/simple values (counters, modes, flags).
Client data: The sole gateway for exposing data to the frontend. Each clientData entry computes a view from state + resources.
/**
* Schemas for the <flow-kind> flow.
*/
import { z } from "zod";
// Action input schemas
export const chatInputSchema = z.object({
message: z.string().min(1),
});
// Session state schema
export const sessionStateSchema = z.object({
messageCount: z.number().default(0),
mode: z.enum(["chat", "plan"]).default("chat"),
});
Each action's block pipeline lives in blocks/:
/**
* Chat action pipeline — processes user messages through LLM and updates state.
*/
import { generator, handler, sequencer } from "@flow-state-dev/core";
import { z } from "zod";
import { chatInputSchema } from "../schemas";
const chatGenerator = generator({
name: "chat-generator",
model: "openai/gpt-5-mini",
prompt: "You are a helpful assistant.",
inputSchema: chatInputSchema,
history: (_input, ctx) => ctx.session.items.history(),
user: (input) => input.message,
itemVisibility: { client: true, history: true },
});
// Generator `prompt` accepts arrays for composition (PromptSlot):
// prompt: ["Base instructions.", config.systemPrompt, "Output format instructions."],
const incrementCount = handler({
name: "increment-count",
inputSchema: z.string(),
outputSchema: z.string(),
sessionStateSchema: z.object({ messageCount: z.number().default(0) }),
execute: async (input, ctx) => {
const count = ctx.session.state.messageCount ?? 0;
await ctx.session.patchState({ messageCount: count + 1 });
return input;
},
});
export const chatPipeline = sequencer({ name: "chat-pipeline", inputSchema: chatInputSchema })
.step(chatGenerator)
.step(incrementCount);
/**
* <Flow Kind> Flow
*
* <What this flow does in 2-3 sentences.>
*/
import { defineFlow } from "@flow-state-dev/core";
import { chatInputSchema, sessionStateSchema } from "./schemas";
import { chatPipeline } from "./blocks/chat";
const <flowKind>Flow = defineFlow({
kind: "<flow-kind>",
requireUser: true,
actions: {
chat: {
inputSchema: chatInputSchema,
block: chatPipeline,
userMessage: (input) => input.message,
},
},
session: {
stateSchema: sessionStateSchema,
},
});
export default <flowKind>Flow({ id: "default" });
Declare typed data containers on scopes:
import { defineFlow, defineResource } from "@flow-state-dev/core";
import { z } from "zod";
const planResource = defineResource({
stateSchema: z.object({
steps: z.array(z.object({
id: z.string(),
title: z.string(),
status: z.enum(["pending", "active", "done"]).default("pending"),
})).default([]),
}),
writable: true,
// Expose resource data to clients via ResourceClientConfig:
client: {
content: { read: true },
data: (state) => ({ stepCount: state.steps.length }),
},
});
defineFlow({
kind: "planner",
requireUser: true,
actions: { /* ... */ },
session: {
stateSchema: sessionStateSchema,
resources: {
plan: planResource,
},
},
});
Derived views for the frontend — computed from scope state and resources:
session: {
stateSchema: sessionStateSchema,
resources: { plan: planResource },
clientData: {
activePlan: (ctx) => ctx.resources.plan?.state.steps ?? [],
mode: (ctx) => ctx.state.mode,
stats: (ctx) => ({
messageCount: ctx.state.messageCount,
planStepCount: ctx.resources.plan?.state.steps.length ?? 0,
}),
},
},
Use capabilities to bundle resources, helpers, and presets that blocks can opt into:
import { defineCapability, defineResource } from "@flow-state-dev/core";
const memoryCapability = defineCapability({
name: "memory",
sessionResources: {
memory: defineResource({
stateSchema: z.object({ facts: z.array(z.string()).default([]) }),
writable: true,
}),
},
fns: (ctx) => ({
recall: () => ctx.session.resources.memory.state.facts,
store: async (fact: string) => {
const facts = ctx.session.resources.memory.state.facts;
await ctx.session.resources.memory.patchState({ facts: [...facts, fact] });
},
}),
presets: {
context: { context: [memoryContextFormatter] },
tools: { tools: [recallTool, storeTool] },
default: ["context", "tools"],
},
});
// Generators opt into the capability
const agent = generator({
name: "agent",
uses: [memoryCapability],
model: "openai/gpt-5-mini",
prompt: "You are an assistant with memory.",
});
Request-level hooks fire at request boundaries:
request: {
onStarted: logRequestStart, // Fire when request begins
onCompleted: logRequestComplete, // Terminal success only
onErrored: handleRequestError, // Terminal failure only
onStepErrored: logStepError, // Non-terminal step/work failure (observational)
onFinished: cleanupRequest, // Always (success or failure)
heartbeatIntervalMs: 10000, // Active request registry heartbeat (default 10s, 0 to disable). Relevant for serverless.
},
Action-level hooks fire per-action:
actions: {
chat: {
inputSchema: chatInputSchema,
block: chatPipeline,
userMessage: (input) => input.message,
onCompleted: notifySuccess, // This action succeeded
onErrored: notifyFailure, // This action failed
},
},
Intercept all block execution for logging, timing, or transformation:
middleware: [
{
name: "timing",
filter: (block) => block.kind === "generator", // Optional: target specific block kinds
execute: async (ctx, next) => {
const start = Date.now();
const output = await next();
console.log(`${ctx.block.name} took ${Date.now() - start}ms`);
return output;
},
},
],
Flow-level tool defaults and observability:
tools: {
defaults: {
timeoutMs: 30000,
concurrency: "parallel",
},
onToolStarted: logToolStart,
onToolCompleted: logToolComplete,
onToolErrored: handleToolError,
},
Per-action token controls:
actions: {
research: {
inputSchema: researchInputSchema,
block: researchPipeline,
tokenBudget: {
maxTotalTokens: 100000,
warnAt: 80000,
onExceeded: "stop",
},
},
},
Flows are discovered by convention at src/flows/**/flow.ts. Each file exports a default FlowInstance:
// src/flows/<flow-kind>/flow.ts
export default myFlow({ id: "default" });
For server registration, flows are passed to createServer() or registerFlows():
import { createServer } from "@flow-state-dev/engine";
import myFlow from "./flows/my-flow/flow";
const server = createServer({
flows: [myFlow],
stores: createSqliteStores({ path: "./data/store.db" }),
});
pnpm typecheck
pnpm test
If the flow is in an example app, start the dev server and test via the client or CLI:
pnpm --filter <app> dev
# In another terminal:
fsdev run <flow-kind> chat --input '{"message": "hello"}'
requireUser: true is mandatory in Phase 1. The framework throws if set to false.onStarted, onCompleted, onErrored, onFinished — never present tense.sessionResources etc. get merged into the flow's scope configs automatically. Flow-level declarations win on conflict.stateSchema is the full picture.block.run() inside handlers. Compose with sequencers.return input. Use .tap() for state-only mutations."aborted". Blocks can check ctx.signal (AbortSignal) for client-initiated cancellation. Long-running handlers should respect the signal to enable graceful abort.stateSchema. Structured entities go in resources.userMessage for conversational actions. It emits a user-role MessageItem in the stream. Omit for background/system actions.Create a new persistence store adapter package implementing all StoreRegistry interfaces. Produces a complete package with factory, individual stores, schema initialization, and tests.
Add a new page to the flow-state-dev documentation site (Docusaurus). Handles frontmatter, sidebar placement, content structure, and cross-linking.
Create a new block (handler, generator, utility, or router) following project conventions. Produces the block file, exports, and a matching test file.
Create a Linear issue for work already done (or in progress), check out a fix/ branch, commit the changes, and open a PR. For quick logging and shipping of on-the-fly work.
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.
Pull a Linear issue, deeply research implementation approaches using web sources and codebase patterns, validate with multiple agents, then publish the spec as a versioned doc at docs/specs/<ISSUE-ID>.md opened as a spec PR for automated review and mirrored to the Linear issue (repo and Linear kept in sync).