100 lines of code. That's the entire framework.
Most LLM frameworks ship thousands of lines of abstraction — classes, adapters, vendor wrappers, plugin systems — and yet the underlying idea is always the same: chain some LLM calls together, pass state between them, and handle failures.
Pocketflow strips all of that away. The core engine is a single TypeScript file that models LLM workflows as a Nested Directed Graph: nodes do work, actions route between them, flows orchestrate the graph, and shared state ties it all together.
This is the original core abstraction behind the Pocketflow Platform, where non-developers create custom AI workflows in natural language.

Every LLM framework eventually converges on the same pattern:
The difference is how much ceremony surrounds that pattern. Pocketflow's answer: almost none. The entire runtime is ~100 lines. There are no vendor lock-ins, no mandatory adapters, no configuration files. You bring your own LLM client, your own database, your own whatever — and the framework just orchestrates the graph.
Key properties:

The framework models every workflow as a Nested Directed Graph:
| Concept | What it does |
|---|---|
| Node | An atomic unit of work. Reads from shared state, executes logic, writes back, and returns an action string. |
| Action | A labeled edge connecting one node to the next. The string returned by post() determines routing. |
| Flow | An orchestrator that walks the graph from a start node, following actions until no successor is found. |
| Shared State | A plain object passed to every node. This is how nodes communicate — no message passing, no events. |
Every node runs the same three-step lifecycle:

| Step | Method | Purpose |
|---|---|---|
| 1. Prep | prep(sharedState) |
Pull data from shared state. Validate inputs. Return a prep result for the next step. |
| 2. Exec | execCore(prepResult) |
Do the actual work — call an LLM, query a database, run a calculation. Pure logic, no side effects on shared state. |
| 3. Post | post(prep, exec, sharedState) |
Write results back to shared state. Return an action string to pick the next node. |
The execWrapper sits between prep and exec — it's where RetryNode adds its retry loop, and where you can add your own middleware (rate limiting, caching, logging, etc.).
git clone https://github.com/Osly-AI/Pocket-Flow-Framework.git
cd Pocket-Flow-Framework
npm install
Run the test suite:
npm test
Build:
npm run build
Every node extends BaseNode and implements three methods:
import { BaseNode, DEFAULT_ACTION } from "pocketflow";
class SummarizeNode extends BaseNode {
async prep(sharedState: any) {
// Read from shared state
return { text: sharedState.document };
}
async execCore(prepResult: any) {
// Call your LLM (bring your own client)
const response = await openai.chat.completions.create({
model: "gpt-4",
messages: [{ role: "user", content: `Summarize: ${prepResult.text}` }]
});
return { summary: response.choices[0].message.content };
}
async post(prepResult: any, execResult: any, sharedState: any) {
// Write back to shared state
sharedState.summary = execResult.summary;
return DEFAULT_ACTION;
}
_clone() { return new SummarizeNode(); }
}
Connect nodes with addSuccessor, then wrap them in a Flow:
import { Flow, DEFAULT_ACTION } from "pocketflow";
const summarize = new SummarizeNode();
const review = new ReviewNode();
const publish = new PublishNode();
// Linear chain: summarize → review → publish
summarize.addSuccessor(review, DEFAULT_ACTION);
review.addSuccessor(publish, DEFAULT_ACTION);
const pipeline = new Flow(summarize);
await pipeline.run({ document: "..." });
Nodes can return different action strings to route execution:
class QualityCheckNode extends BaseNode {
async post(prepResult: any, execResult: any, sharedState: any) {
if (execResult.score >= 0.8) return "approved";
if (sharedState.retryCount < 3) return "retry";
return "reject";
}
// ...
}
const check = new QualityCheckNode();
const publish = new PublishNode();
const revise = new ReviseNode();
const reject = new RejectNode();
check.addSuccessor(publish, "approved");
check.addSuccessor(revise, "retry");
check.addSuccessor(reject, "reject");
// Create a cycle: revise feeds back into check
revise.addSuccessor(check, DEFAULT_ACTION);
Wrap any node with automatic retry and configurable backoff:
import { RetryNode } from "pocketflow";
class ResilientLLMNode extends RetryNode {
constructor() {
super(3, 2000); // 3 attempts, 2 seconds between retries
}
async prep(sharedState: any) { /* ... */ }
async execCore(prepResult: any) {
// If this throws, RetryNode catches it and retries
return await callUnreliableAPI(prepResult);
}
async post(prep: any, exec: any, sharedState: any) { /* ... */ }
_clone() { return new ResilientLLMNode(); }
}
Flows are nodes — so you can nest them arbitrarily:
// Inner flow: extract → summarize
const extract = new ExtractNode();
const summarize = new SummarizeNode();
extract.addSuccessor(summarize, DEFAULT_ACTION);
const extractAndSummarize = new Flow(extract);
// Outer flow: extractAndSummarize → publish
const publish = new PublishNode();
extractAndSummarize.addSuccessor(publish, DEFAULT_ACTION);
const fullPipeline = new Flow(extractAndSummarize);
await fullPipeline.run(sharedState);
This is the key design insight: because Flow extends BaseNode, you can compose arbitrarily complex systems from small, individually testable flows.
Process multiple items concurrently. Override prep() to return an array — each element spawns a parallel flow execution:
import { BatchFlow } from "pocketflow";
class ProcessDocuments extends BatchFlow {
async prep(sharedState: any) {
// Return one config per document — each runs the flow in parallel
return sharedState.documents.map((doc: string) => ({ document: doc }));
}
}
// The inner flow runs once per document, concurrently
const summarize = new SummarizeNode();
const batch = new ProcessDocuments(summarize);
await batch.run({
documents: ["paper1.pdf", "paper2.pdf", "paper3.pdf", /* ...100 more */]
});
All common LLM patterns are just different graph topologies:

| Pattern | Graph Shape | Example |
|---|---|---|
| Chaining | Linear path | Summarize → Draft Reply |
| Chat | Self-loop with history | Chat node cycling with stored context |
| RAG | Write + Read through vector store | Upload docs → Answer questions |
| Chain-of-Thought | Self-loop with reasoning store | Think → Evaluate → Think again |
| Map-Reduce | Fan-out + merge | Split text → Summarize chunks → Reduce |
| Agent | Loop with branching | Summarize → Review ↔ Draft Reply |
| Multi-Agent | Multiple loops with pub/sub | Agents communicating via shared state |
| Supervisor | Nested flow with approval loop | Worker flow ↔ Supervisor approval |
You don't need different APIs for these. They're all the same Node + Flow + addSuccessor primitives, wired differently.

| Abstraction | Description |
|---|---|
| Node | Single-step processing unit |
| Flow | Multi-step orchestration (directed path through nodes) |
| Comms | Shared state store for inter-node communication |
| Batch | Repeat the same flow across multiple inputs |
| Async | Overlap I/O operations for parallel execution |
| Action | Conditional routing (branching and cycles) |
Scaffold new components quickly:
npx pocket new node MyProcessor # Creates src/nodes/MyProcessor.node.ts
npx pocket new flow MyPipeline # Creates src/flows/MyPipeline.flow.ts
Generated files include typed interfaces, lifecycle stubs, and inline documentation.
├── src/
│ └── pocket.ts # The entire framework (~100 lines)
├── tests/
│ ├── pocket.test.ts # 14 tests, ~99% coverage
│ └── testNodes.ts # Test helper nodes
├── cli/ # Code generation CLI
│ ├── src/ # CLI logic
│ └── templates/ # Node and Flow templates
├── examples/
│ └── mcp-addition-ts/ # MCP server example
├── docs/ # Full documentation site (MkDocs)
├── WHITEPAPER.md # Technical whitepaper
└── CONTRIBUTING.md # Contribution guidelines
For a deeper technical discussion of the design philosophy, architecture decisions, and comparisons with other frameworks, see WHITEPAPER.md.
We welcome contributions! See CONTRIBUTING.md for setup instructions and guidelines.
$ claude mcp add Pocket-Flow-Framework \
-- python -m otcore.mcp_server <graph>