Get started
Installation
Install the package, satisfy the ESM and Node requirements, add a license key, and decide where your environment variables live.
PicoFlow ships as a single package, @picoflow/core. It has no CLI, no code generator,
and no project scaffold. You add it to an existing TypeScript service and start writing
Flow and Step classes.
Install the package
npm install @picoflow/core
yarn add @picoflow/core
The published package version is 1.1.2. Pin it explicitly rather than floating a caret range while you are still learning the API surface.
Runtime requirements
The published package.json declares:
| Field | Value | Consequence |
|---|---|---|
type |
module |
The package is ESM. |
engines.node |
>=22.5 |
Older Node releases are unsupported. |
exports["."] |
import points at dist/picoflow.mjs; require points at dist/picoflow.cjs |
Both ESM and CommonJS applications are supported. |
The package is ESM
@picoflow/core publishes ESM and CommonJS entry conditions. In practice this means:
- Your own package must set
"type": "module", or your files must use the.mts/.mjsextensions. - Use
"module": "NodeNext"and"moduleResolution": "NodeNext"intsconfig.json. - Relative imports in your own source need explicit
.jsextensions, because Node’s ESM loader does not do extension resolution:
import { HotelFlow } from "./myflow/hotel-flow/hotel-flow.js";
- CommonJS applications can use
require("@picoflow/core")through the publishedrequireexport.
Decorators
@Tool and @Tools are legacy TypeScript method decorators. Enable them in
tsconfig.json:
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Without experimentalDecorators, TypeScript compiles the newer ECMAScript decorator
form, and the handler registration that @Tool performs will not run.
Peer expectations
PicoFlow declares its LangChain and Zod dependencies directly rather than as peers, but your application code shares those types across the boundary, so version drift shows up as confusing type errors rather than clean resolution failures.
| Package | Version PicoFlow 1.1.2 depends on | Why your code sees it |
|---|---|---|
zod |
4.4.3 | defineTool() returns ToolType[], whose schema field is typed as z.ZodObject. You author those schemas. |
@langchain/core |
1.2.3 | Memory holds LangChain messages. withMessage(...), HumanMessageEx, AiMessageEx and structOutputSchema() all sit on LangChain types. |
Install the same major versions in your own package.json:
npm install zod@4 @langchain/core@1
Zod 4 is not optional. PicoFlow’s ToolType.schema is z.ZodObject from Zod 4, and the
session document schema is built with Zod 4 constructs such as
z.record(z.string(), z.any()). A Zod 3 object will not be assignable.
Provider SDKs (@langchain/openai, @langchain/anthropic, @langchain/deepseek,
@langchain/google, @langchain/ollama, @langchain/openrouter) are bundled with
@picoflow/core. You do not install them separately to use the built-in adapters.
Azure OpenAI, Moonshot, and Z.AI use the bundled OpenAI runtime with their respective connection settings, so they do not require another provider SDK.
The license key
PicoFlow is proprietary and validates a signed license token at runtime. The token is read
from the PICOFLOW_KEY environment variable.
PICOFLOW_KEY=your-license-token
Personal evaluation keys are free and issued on request. See licensing to request one; it is normally returned the same working day.
Verification happens inside the model/tool loop, not at import time. The practical effect
is that a service with a missing or expired key starts and accepts HTTP requests normally,
and then fails on the first turn that reaches a model response. The failure surfaces as a
flow error: License token missing, Invalid license signature, Unsupported license version or License expired.
Add a smoke test that runs one real turn of a trivial flow in CI. A unit test that only constructs a Flow will pass without a valid key and will not tell you that production is about to fail.
Where environment variables go
PicoFlow reads configuration through its own ConfigManager, which is created when you
build a FlowEngine. Precedence is, from lowest to highest:
- values loaded from a dotenv file (
.envin the current working directory by default); process.env;- explicit
valuespassed to theConfigManagerconstructor.
That means a real environment variable always wins over a value in .env, which is the
behaviour you want in a container.
You can point it at a different file, or bypass dotenv entirely:
import { ConfigManager, FlowEngine } from "@picoflow/core";
const engine = await FlowEngine.create({
configManager: new ConfigManager({
envFilePath: [".env.local", ".env"],
}),
flows: [],
providers: [],
});
If a .env file is absent, ConfigManager continues silently; any other read error is
thrown.
The variables PicoFlow itself reads
| Variable | Purpose |
|---|---|
PICOFLOW_KEY |
Runtime license token. Required. |
SESSION_STORE |
Session backend: MEMORY (default), SQLITE, MONGO, COSMO/COSMOS. |
SQLITE_PATH |
SQLite file path. Defaults to ignore/session/session.sqlite. |
COSMODB_KEY, COSMODB_URL, COSMODB_ID, COSMODB_SESSION_ID |
Azure Cosmos DB connection. |
MONGODB_NAME, MONGODB_COLLECTION, MONGODB_URL |
MongoDB connection. |
SELF_URL |
The application’s own run endpoint. Only needed for concurrentSteps(...) batch mode. |
Provider API keys such as OPENAI_API_KEY are also read into CoreConfig, but the
adapters you register in FlowEngine.create({ providers }) receive their credentials
explicitly from your bootstrap code. Reading the key from configuration and passing it to
an adapter is your application’s job, not a hidden default. See
Models and providers.
Verify the install
import { Flow, FlowEngine, Step, TerminateSessionStep } from "@picoflow/core";
class PingFlow extends Flow {
protected configModel() {
return { provider: "openai", name: "gpt-4o-mini", retryAttempts: 3 } as const;
}
protected defineSteps(): Step[] {
return [new TerminateSessionStep(this)];
}
}
const engine = await FlowEngine.create({ flows: [PingFlow], providers: [] });
console.log(engine.getFlowNames()); // [ 'PingFlow' ]
FlowEngine.create(...) is asynchronous and returns a promise. This example registers no
providers, so it proves that imports and registration work but cannot run a turn. Continue
with Your first flow.