Skip to main content

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 typed block_trace flows via graph edges. No position-inferred default; every generator declares.
  • agentName?: string — Stable name stamped on every emitted item. Defaults to the block's name when itemVisibility is set. Generators that share an agentName represent the same logical agent; distinct names stay isolated. Used by the client for per-agent rendering and by items.selectForContext({ agentName }) for scoped context assembly.

Callbacks:

  • onCompleted?: (output, ctx, meta: GeneratorCompletedMeta) => void | Promise<void> — Fires after a successful execute. meta.model is a ModelIdentity with 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. true uses 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 body
  • contentFile?: string — load initial body from a file path (mutually exclusive with content)
  • render?: (content, state) => string | Promise<string> — optional renderer for readContent()
  • llmReadable?: boolean — allows read access when readResourceContentTool() is installed
  • llmWritable?: boolean — allows write access when writeResourceContentTool() is installed

Runtime resource content methods:

  • await ctx.session.resources.plan.readContent() → rendered content or null
  • await ctx.session.resources.plan.readContentRaw() → raw stored content or null
  • await 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 state
  • maxInstances?: 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 hook
  • onInstanceUpdated?: (key, state, prevState, ctx) => void — lifecycle hook
  • onInstanceDeleted?: (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 or undefined
  • getOrCreate(key, initial?) — returns existing if present, creates if not
  • list(prefix?) — list all instances, optionally filtered by prefix
  • delete(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.

FormatterSignatureDescription
section(title, ...content)(string | { title, level? }, ...string[]) => stringTitled section; default ##, or pass { title, level } (1–6) to nest
list(items, options?)(string[], { ordered?, prefix? }) => stringBullet or numbered list
keyValues(data)(Record<string, unknown>) => stringKey-value pairs
table(rows, options?)(Record<string, unknown>[], { columns? }) => stringMarkdown table; columns default to the key union
entries(record, formatter)(Record, fn) => stringMapped record entries
codeBlock(code, language?)(string, string?) => stringFenced code block
join(...parts)(...(string | falsy)[]) => stringJoin 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)