Core API
@flow-state-dev/core — Isomorphic builders, type contracts, and item taxonomy.
Block Builders
handler(config)
Create a synchronous logic block.
import { handler } from "@flow-state-dev/core";
const myHandler = handler({
name: "my-handler",
inputSchema: z.object({ value: z.string() }),
outputSchema: z.object({ result: z.string() }),
sessionStateSchema: z.object({ count: z.number().default(0) }),
targetStateSchemas: {
research: z.object({ progress: z.number() }),
},
execute: async (input, ctx) => {
await ctx.session.incState({ count: 1 });
// ctx.targets.research is StateRef<{ progress: number }> | undefined
await ctx.targets.research?.patchState({ progress: 50 });
return { result: input.value.toUpperCase() };
},
});
generator(config)
Create an LLM-calling block with tool loop support.
import { generator } from "@flow-state-dev/core";
const myGenerator = generator({
name: "my-gen",
model: "preset/fast",
prompt: "You are a helpful assistant.",
inputSchema: z.object({ message: z.string() }),
outputSchema: z.object({ response: z.string() }),
targetStateSchemas: {
research: z.object({ progress: z.number() }),
},
user: (input, ctx) => {
const progress = ctx.targets.research?.state.progress ?? 0;
return `progress:${progress} — ${input.message}`;
},
tools: [myTool],
search: true,
context: [myContextFn],
history: true,
repair: { mode: "auto", maxAttempts: 3 },
});
Identity config:
itemVisibility?: { client: boolean; history: boolean }— Declares the generator's visibility. Governs auto-emission of conversational items (messages, reasoning, tool outputs).{ client: true, history: true }= user-facing (client + history).{ client: true, history: false }= task-executor (client, not history).{ client: false, history: false }= observability-only (neither). When unset, the generator performs no auto-emission — only its typedblock_traceflows via graph edges. No position-inferred default; every generator declares.agentName?: string— Stable name stamped on every emitted item. Defaults to the block'snamewhenitemVisibilityis set. Generators that share anagentNamerepresent the same logical agent; distinct names stay isolated. Used by the client for per-agent rendering and byitems.selectForContext({ agentName })for scoped context assembly.
Callbacks:
onCompleted?: (output, ctx, meta: GeneratorCompletedMeta) => void | Promise<void>— Fires after a successful execute.meta.modelis aModelIdentitywith the resolved model that produced the output. Existing two-argument callbacks ((output, ctx)) continue to work. See lifecycle hooks and the worked example.onErrored?: (error, ctx) => void | Promise<void>— Fires when execute fails.
Search config:
search?: boolean | GeneratorSearchConfig— Enable provider-native web search.trueuses defaults; pass a config object for fine-grained control (maxUses,allowedDomains,blockedDomains,userLocation,searchDepth).
Provider tools:
providerTools?: ProviderTool[]— Raw provider-defined tool objects passed directly to the AI SDK, bypassing the block lifecycle.
providerTool(name, tool)
Create a provider tool wrapper for use in generator({ providerTools }).
import { providerTool } from "@flow-state-dev/core";
import { anthropic } from "@ai-sdk/anthropic";
const codeExec = providerTool("code_execution", anthropic.tools.codeExecution());
Returns { __providerTool: true, name: string, tool: unknown }.
sequencer(config)
Create a pipeline composition block.
import { sequencer } from "@flow-state-dev/core";
const pipeline = sequencer({
name: "my-pipeline",
inputSchema: z.object({ message: z.string() }),
container: { component: "pipeline-view", label: "Processing" },
});
Methods: step, stepIf, map, parallel, forEach, forEachBackground, doUntil, doWhile, loopBack, work, workIf, waitForWork, tap, tapIf, rescue, branch, stepAll, stepAny, race, exitIf
router(config)
Create a runtime block-selection block.
import { router } from "@flow-state-dev/core";
const myRouter = router({
name: "mode-router",
inputSchema: z.object({ mode: z.string() }),
targetStateSchemas: {
coordinator: z.object({ step: z.number() }),
},
routes: [chatBlock, agentBlock],
execute: async (input, ctx) => {
const step = ctx.targets.coordinator?.state.step ?? 0;
return step > 0 ? agentBlock : chatBlock;
},
});
Flow
defineFlow(definition)
Create a flow type.
import { defineFlow } from "@flow-state-dev/core";
const myFlow = defineFlow({
kind: "my-app",
requireUser: true,
actions: { /* ... */ },
session: { stateSchema, resources, client },
user: { stateSchema, resources, client },
request: { onStarted, onCompleted, onErrored, onFinished, onStepErrored },
});
export default myFlow({ id: "default" });
Resources
defineResource(config)
Create a portable resource definition. Can be used in flow scope configs and in block-level resource declarations (sessionResources, userResources, orgResources):
import { defineResource } from "@flow-state-dev/core";
const planResource = defineResource({
stateSchema: z.object({ steps: z.array(z.string()).default([]) }),
writable: true,
});
// Use in flow scope config
session: { resources: { plan: planResource } }
// Or declare on blocks — collected and merged into the flow automatically
const myHandler = handler({
name: "plan-manager",
sessionResources: { plan: planResource },
execute: async (input, ctx) => { /* ... */ },
});
Resource content options:
content?: string— inline definition-time bodycontentFile?: string— load initial body from a file path (mutually exclusive withcontent)render?: (content, state) => string | Promise<string>— optional renderer forreadContent()llmReadable?: boolean— allows read access whenreadResourceContentTool()is installedllmWritable?: boolean— allows write access whenwriteResourceContentTool()is installed
Runtime resource content methods:
await ctx.session.resources.plan.readContent()→ rendered content ornullawait ctx.session.resources.plan.readContentRaw()→ raw stored content ornullawait ctx.session.resources.plan.writeContent("...")→ overwrite stored content
For explicit LLM access, add tools manually to generators:
import {
generator,
readResourceContentTool,
writeResourceContentTool,
} from "@flow-state-dev/core";
const agent = generator({
name: "agent",
model: "preset/fast",
prompt: "You can inspect and edit approved resource files.",
tools: [readResourceContentTool(), writeResourceContentTool()],
});
defineResourceCollection(config)
Create a resource collection — a typed set of resources created and destroyed at runtime:
import { defineResourceCollection } from "@flow-state-dev/core";
const filesCollection = defineResourceCollection({
pattern: "files/**",
stateSchema: z.object({ language: z.string().default("text") }),
maxInstances: 200,
eviction: "lru",
});
Config options:
pattern: string— glob pattern:files/*(single-level),files/**(deep),[topic]/observations(parameterized)stateSchema: ZodTypeAny— schema for each instance's statemaxInstances?: number— cap on simultaneous instances (must be >= 1)eviction?: "none" | "lru" | "oldest"— what to do when cap is reached (default:"none"= throw)onInstanceCreated?: (key, state, ctx) => void— lifecycle hookonInstanceUpdated?: (key, state, prevState, ctx) => void— lifecycle hookonInstanceDeleted?: (key, ctx) => void— lifecycle hook
Runtime ResourceCollectionRef methods:
create(key, initial?)— create a new instance (throws if exists or at cap with no eviction)get(key)— get existing instance (throws if not found)getOptional(key)— get existing instance orundefinedgetOrCreate(key, initial?)— returns existing if present, creates if notlist(prefix?)— list all instances, optionally filtered by prefixdelete(key)— delete an instance (no-op if not found)count()— current instance count
Use in blocks the same way as defineResource:
const fileManager = handler({
name: "file-manager",
sessionResources: { files: filesNamespace },
execute: async (input, ctx) => {
const ref = await ctx.session.resources.files.create("readme.md");
return ref.state;
},
});
Context Functions
contextFn(schemas, fn)
Create a typed context function for generators. Provides typed access to scope state via schema inference:
import { contextFn } from "@flow-state-dev/core";
import { section, list } from "@flow-state-dev/core/prompt";
const researchContext = contextFn(
{ session: sessionStateSchema },
({ session }) => {
if (session.coveredTopics.length === 0) return "";
return section("Research Progress", list(session.coveredTopics));
}
);
// Use in any generator
const agent = generator({
context: [researchContext],
// ...
});
Three overloads: (session), (session, user), (session, user, org).
Prompt Formatters
@flow-state-dev/core/prompt — Composable text formatters for building clean LLM context.
| Formatter | Signature | Description |
|---|---|---|
section(title, ...content) | (string | { title, level? }, ...string[]) => string | Titled section; default ##, or pass { title, level } (1–6) to nest |
list(items, options?) | (string[], { ordered?, prefix? }) => string | Bullet or numbered list |
keyValues(data) | (Record<string, unknown>) => string | Key-value pairs |
table(rows, options?) | (Record<string, unknown>[], { columns? }) => string | Markdown table; columns default to the key union |
entries(record, formatter) | (Record, fn) => string | Mapped record entries |
codeBlock(code, language?) | (string, string?) => string | Fenced code block |
join(...parts) | (...(string | falsy)[]) => string | Join with newlines, filtering falsy |
when(condition, content) | (boolean, string) => string | "" | Conditional inclusion |
The same keyValues / list / table shapes are available inside .md prompt templates as the auto-registered fsd_keyValues / fsd_list / fsd_table / fsd_json filters — see Prompts as Markdown.
Concurrency
mapLimit(values, maxConcurrency, mapper)
(readonly T[], number | undefined, (value: T, index: number) => Promise<R>) => Promise<R[]>
Runs mapper over values with at most maxConcurrency calls in flight at once, preserving input order. undefined (or any value ≥ length) runs everything concurrently; empty input resolves to []. Use it for bounded async fan-out inside a handler — .parallel fans out blocks, this fans out plain async work.
import { mapLimit } from "@flow-state-dev/core";
// At most 5 quote fetches in flight, results in ticker order.
const quotes = await mapLimit(tickers, 5, (ticker) => fetchQuote(ticker));
Client Data
client on scope configs
Declare what slice of state crosses to the browser. Each scope (session, user, org) has a client block with two halves: expose (verbatim passthrough by field name) and derived (computed projections).
defineFlow({
session: {
stateSchema,
client: {
expose: ["progress"],
derived: {
topicList: (ctx) => ctx.state.coveredTopics,
},
},
},
});
derived compute functions receive { state, resources } from their scope. Values must be JSON-serializable. State without a client block is private to the server.
clientData is the previous name for client.derived and is deprecated. Setting both client and clientData on the same scope throws at definition time; setting only clientData emits a one-time deprecation warning.
Voice Types
SpeechModel
Provider-agnostic interface for text-to-speech synthesis.
import type { SpeechModel } from "@flow-state-dev/core";
const model: SpeechModel = {
modelId: "gpt-4o-mini-tts",
generate: async (options) => ({ audio: uint8Array, mediaType: "audio/mp3" }),
};
TranscriptionModel
Provider-agnostic interface for speech-to-text transcription.
import type { TranscriptionModel } from "@flow-state-dev/core";
const model: TranscriptionModel = {
modelId: "gpt-4o-mini-transcribe",
transcribe: async (options) => ({ text: "Hello" }),
};
VoiceConfig
Flow-level voice configuration. Set on defineFlow({ voice }).
type VoiceConfig = {
tts?: {
model: string | SpeechModel;
voice?: string;
speed?: number;
};
};
OutputAudioContent
Content part for synthesized audio.
type OutputAudioContent = {
type: "output_audio";
audio: string; // base64
mediaType: string; // "audio/mp3", "audio/wav", etc.
transcript?: string;
duration?: number;
};
Errors
FlowError
A small Error subclass author code can throw to attach a machine-readable code and structured details that survive the trip to the trace.
import { FlowError } from "@flow-state-dev/core";
throw new FlowError("Command rejected", {
code: "PATH_OUTSIDE_WORKSPACE",
details: { cwd: "/foo" }
});
new FlowError(message, options) where options is { code?: string; retryable?: boolean; details?: Record<string, unknown>; cause?: unknown }. retryable defaults to false. FlowError.isInstance(value) matches FlowError (and subclasses) by instanceof or by name-tag, which is the dual-realm-safe check.
OutputValidationError
Runtime-emitted subclass of FlowError. Thrown by the generator runtime when the model's output fails the declared outputSchema. Carries typed details:
type OutputValidationDetails = {
rawOutput: string; // raw text or JSON the model returned
issues: ZodIssue[]; // Zod issues from the failing parse
phase: "stream" | "final";
};
code is "output_validation_error". retryable is false. See Error handling for usage patterns.
StrictSchemaError
Thrown at generator() construction when an outputSchema is not compatible with OpenAI's strict structured-output mode. Strict mode requires a JSON schema with no open-keyed maps and no conflicting required sets across union variants, so a reachable z.record() or a z.union() of differently-shaped variants is rejected. Subclass of FlowError with code "strict_schema_error" and retryable false. Carries the located violations:
interface StrictViolation {
path: string; // e.g. "$.metrics", "$.items[].scores"
typeName: string; // e.g. "ZodRecord", "ZodUnion"
reason: string;
}
// error.violations: StrictViolation[]
Schema validation
assertStrictCompatible(schema, label?)
Throws a StrictSchemaError if schema — after the strict transform strips its optional / default / nullable wrappers — still contains a construct OpenAI strict mode rejects. A no-op on a compatible schema. Generators call it automatically at definition, so you only need it to check a bare schema constant in a test.
import { assertStrictCompatible } from "@flow-state-dev/core";
import { z } from "zod";
// Throws: dynamic-keyed map → additionalProperties=true
assertStrictCompatible(z.object({ scores: z.record(z.string(), z.number()) }));
// Passes: array-of-pairs carries dynamic keys without an open map
assertStrictCompatible(
z.object({ scores: z.array(z.object({ key: z.string(), value: z.number() })) }),
);
makeSchemaStrict(schema, options?)
Returns a copy of schema with optional / default / nullable wrappers unwrapped so every property lands in the provider's required set. The framework calls it internally before serializing a schema to the AI SDK. Pass { validate: true } to also throw StrictSchemaError when an incompatible construct survives (this is what assertStrictCompatible does). The transform does not rewrite z.record() / z.union() — fix those in the source schema.
Type Helpers
import { StateOf, ContextOf, ResourceContext, BlockInput, BlockOutput } from "@flow-state-dev/core";
type PlanState = StateOf<typeof planResource>;
type SessionCtx = ContextOf<typeof sessionSchema, "session">;
type Input = BlockInput<typeof myBlock>;
type Output = BlockOutput<typeof myBlock>;
Subpath Exports
@flow-state-dev/core/types— Block, flow, resource, scope, streaming, and model type definitions@flow-state-dev/core/items— Item unions, content types, and stream event helpers@flow-state-dev/core/prompt— Composable prompt formatters (section,list,keyValues,table,entries,codeBlock,join,when)