Picoflow architecture analysis

Picoflow And LangGraph Architecture Contrast

A focused comparison of Picoflow's turn-based, session-document-centered agentic framework with LangGraph's graph runtime architecture.

Picoflow And LangGraph Architecture Contrast

Executive Summary

Picoflow is a turn-based agentic flow framework built around Flow, Step, persistent session documents, and a recursive LLM/tool execution loop. It uses LangChain message, model, and tool abstractions, but Picoflow itself owns orchestration. The active step is stored in session state, each step owns prompt/tool/state behavior, and transitions happen through TypeScript code that calls or returns the next step.

LangGraph is a graph runtime. Its core model is a compiled StateGraph made of explicit state channels, nodes, and edges. Nodes perform work and return state updates; edges, conditional edges, Send, or Command determine what executes next. The graph runtime checkpoints state at super-step boundaries and can expose state history, pending tasks, interrupts, and time travel.

The most important difference is this:

Picoflow is therefore closer to a persistent, step-oriented, tool-calling state machine agent. LangGraph is closer to a general-purpose durable graph runtime for stateful agents and workflows.

Picoflow Architecture

Runtime Entry Point

The NestJS ChatController registers flows such as BasicFlow, HotelFlow, InvoiceFlow, and TravelFlow, then exposes POST /ai/run. Requests contain:

FlowEngine.runFlow(...) creates or restores the selected flow through FlowCreator.create(...), invokes flow.run(...), then saves the updated session document.

Flow

Flow is the owner of a flow instance. It holds:

Each concrete flow overrides defineSteps() and returns instantiated steps. A step constructor argument marks the initially active step. On bootstrap, Picoflow restores the persisted session, rehydrates step states and memories, and records the active step sequence.

There is no declarative transition graph. Flow.activate(...) and Flow.activateByName(...) simply deactivate the prior active step, activate the target step, and append that step name to flowDoc.sequence.

Step

Step is the local execution unit. A step can define:

When Step.run(userMessage) executes, it creates a LangChain human message and delegates to LlmRunner.send(...). The active step's memory is used as the model history, with the step prompt placed at index 0 as a system message.

Model Configuration

Picoflow supports model configuration at multiple levels:

LlmRunner resolves the effective model at execution time. If a step does not specify a model, bootstrap assigns the flow default. When the active step runs, Picoflow merges flow-level parameters with step-level parameters when the step is using the flow model; if the step selects a different model, only the step's own overrides are applied. This gives a flow a global default while still allowing a specific state to use a stronger, cheaper, multimodal, or differently parameterized model.

BasicFlow demonstrates this layering with a flow default of gpt-4o, while InContextStep overrides to gpt-5.1 with reasoning parameters. InvoiceFlow uses a Gemini default, gives ExtractInvoice3Step a specific Gemini model and temperature override, and gives EndStep another model.

Step State

Each step has a JSON-like state object. State is saved into the session document and restored on later turns. A step can read its own state through getState(...) or getStateAs(...), and other steps can access it through flow helpers such as getStepState(...) and getStepStateAs(...). This makes step state both local to the state that owns it and available to other states when the flow needs cross-step data.

Tool handlers and logic steps can also inject state into a target step by returning { step, state }. That is how routing and data passing often travel together in Picoflow.

Picoflow also supports transient state through saveState(json, true). Transient state is available during recursive calls inside the same turn, so it can pass data from one state to another during a nested LLM/tool/logic execution. It is deliberately removed before persistence in Step.writeDoc(...), so it does not survive into the saved session document.

LLM And Tool Loop

LlmRunner implements the agent loop:

  1. Resolve active step and effective model settings.
  2. Handle cross-step message rebasing via onCrossing(...).
  3. If the active step is a logic step, delegate to LogicRunner.
  4. Build the model instance.
  5. Bind the active step's tools.
  6. Optionally enable structured output.
  7. Invoke the model with the step's message history.
  8. Retry on empty non-tool responses.
  9. Process either plain model output or tool calls.

If the model returns tool calls, Picoflow executes matching step methods. A tool handler can return several control shapes:

That return value can activate a new step, inject state, replace a prompt, change HTTP content type, add a tool result, or add a direct AI/human message to memory. After tool execution, Picoflow either returns directly or recursively re-enters LlmRunner.send(...).

This is the core agentic loop: model chooses tools, TypeScript executes tools, tool code can mutate state and route to another step, and the model continues from the new active step.

Result Capture And Direct Responses

Picoflow supports two primary ways for a step to capture model results:

Tool-call capture is useful when the model should submit a domain event or action into the flow. Structured schema capture is useful when the whole model response should conform to a schema without routing through a named tool handler.

Picoflow can also short-circuit the normal second LLM call after tool execution. By default, after a tool call, the framework records tool results and usually re-enters the model so the LLM can explain or continue. But if a tool handler returns an AI message with additional_kwargs.direct = true, LlmRunner appends that message and returns it immediately.

CompareStep.generate_comparison(...) shows this pattern. The first LLM call selects the comparison intent and calls generate_comparison; the tool handler fetches hotel data, generates a comparison chart in code, and returns a direct AI message containing the chart and follow-up question. Picoflow then avoids a second LLM call, which is useful when the deterministic tool result is already the exact response that should be shown to the user.

Logic Steps

LogicStep is a non-LLM step. Its runLogic() returns a LogicResponseType that can route to another step and inject state. LogicRunner can chain multiple logic steps before returning to an LLM-backed step.

In BasicFlow, FooLogicStep routes to GooLogicStep with { fooData: 'fooValue' }, then GooLogicStep routes to FavoritesStep with { gooData: 'gooValue' }. This shows deterministic, code-only routing inside the same step framework.

Memory And Session Persistence

Picoflow persists a full SessionType document. A session includes:

Memory serializes and rehydrates LangChain messages across named namespaces. Steps can share memory, as NameStep, DOBStep, and AddressStep do with default, or isolate memory, as PresidentStep does with president and FavoritesStep does with favorite.

Persistence is selected by CoreConfig.documentDB:

All three adapters implement the same SessionAdaptor interface: fetch, fetchAll, restore, create, save, and delete. SQLite stores the session as JSON in a session table and uses WAL mode. MongoDB and Cosmos DB store the session document directly.

Completed or aborted sessions are not resumed by fetch(...); Picoflow creates a new session instead.

BasicFlow As Feature Demonstration

BasicFlow is the project's feature tour.

It demonstrates:

The normal chat path begins at WeatherStep unless config.isPresident is true. WeatherStep accepts only LA and NYC; once both have been captured, it routes into logic steps and then into the rest of the flow.

The batch path is activated by config._concurrent. Flow.run(...) detects that flag and calls spawnSteps() instead of the normal active-step path. BasicFlow.spawnSteps() activates PresidentStep, then calls concurrentSteps(...) over a list of ordinal presidents. Each item is sent back through the app using SelfClient, with per-item config such as { nth: '10th', isPresident: true }. Responses are saved into PresidentStep state and the session is completed.

This batch design is Picoflow's framework-level parallel processing construct. It is not graph fan-out; it is concurrent orchestration implemented by a flow coordinator that spawns independent flow runs through HTTP and aggregates their results.

The public basic-flow demo source also demonstrates a newer step-level concurrent fan-out path. NameStep still calls runStep(InContextStep) to execute one nested step in context. Inside InContextStep.onEnter(), however, the step calls runSteps(...) with ConcurStep1 and ConcurStep2. The base Step.runSteps(...) implementation creates one nested activation per request and awaits them with Promise.all, so a step can fan out to multiple concurrent child steps and then save their returned results back into its own state.

InvoiceFlow As Non-Chatbot Application

InvoiceFlow shows that Picoflow is not limited to chatbot questionnaires.

The flow is a document extraction pipeline. Its active step, ExtractInvoice3Step, uses a Gemini model, constructs a prompt from config.fileName, asks the model to fetch an invoice image, uploads the local file through LLMFileManager, and then captures extracted JSON through a capture_json tool.

When JSON is captured:

The HTTP layer then returns the model result with JSON content type rather than a plain chat response.

InvoiceFlow also has a _concurrent path. Its spawnSteps() processes invoice image names through concurrentSteps(...), again using Picoflow's own parallel flow pattern rather than a graph-native map/reduce scheduler.

LangGraph Architecture Baseline

LangGraph models workflows as graphs with three primary concepts:

A LangGraph application is usually built by creating a StateGraph, adding nodes and edges, then compiling the graph. Compilation performs structural checks and attaches runtime capabilities such as checkpointers and breakpoints.

LangGraph state channels can use reducers. For example, a messages channel uses a message-aware reducer so new messages append or update by id instead of replacing the whole message list. Other state fields can use default overwrite behavior, custom reducers, or untracked values that are available during execution but excluded from checkpoints.

LangGraph persistence is based on:

With a checkpointer, graph state is saved against a thread_id. Checkpoints are created at graph super-step boundaries. LangGraph can retrieve the latest state, inspect historical state, resume from checkpoints, support fault tolerance, and enable time travel.

LangGraph also supports:

Direct Comparison

Area Picoflow LangGraph
Primary abstraction Flow containing active Step objects Compiled StateGraph
Routing model Code returns or activates the next step Edges, conditional edges, Send, or Command
Transition representation Runtime mutation of isActive plus sequence history Graph topology plus runtime scheduled next nodes
State model Session document with flow memory, per-step JSON state, transient state, context, logs, tokens Schema-defined graph state channels with reducers
Memory model Namespaced LangChain message histories per flow/step Message channels in graph state plus checkpointer/store memory
Model configuration Global model registry plus flow-level and step-level model/parameter overrides Usually configured inside nodes, model factories, or runtime config
Persistence MongoDB, Cosmos DB, or SQLite session document as a rich durable flow snapshot with a document version field; checkpoint-style replay is possible when historical session snapshots are retained Checkpointers for thread state and stores for cross-thread memory
Observability and production debugging Session document includes step memories, execution sequence, step states, token counters, and built-in log/error/warn/debug/verbose reports LangSmith provides rich external tracing and observability when enabled
Deployment and data boundary Self-contained NodeJS service instances can be clustered inside a company firewall with company-owned session storage LangGraph can run self-hosted; LangSmith Cloud observability sends traces/data to LangChain-hosted infrastructure unless disabled or replaced with self-hosted LangSmith
Execution style Turn-based active step loop Graph super-step scheduler
Tool behavior Step methods handle tools and can route directly Tools usually update state and may route with Command
Result capture Tool-call arguments or step-level structured schema output; direct AI messages can short-circuit a second LLM call Node output updates graph state; structured output/tool use is usually implemented inside node logic
Parallelism Built-in _concurrent flow mode and concurrentSteps(...) for batch/parallel flow execution Graph-level parallel node execution and dynamic Send fan-out
Non-LLM logic LogicStep and LogicRunner Any node can be pure code
Compile-time validation Minimal; flow steps are collected at runtime Graph compilation checks structure
Visualization Sequence can be inspected from session; Mermaid docs exist separately Graph topology is explicit and visualizable
Human-in-loop Native to the turn-based request/response contract; the active step waits for the next human turn without a special interrupt construct First-class interrupts, resume, and checkpoint inspection
Time travel and dev replay Possible by copying a production session document into dev, rolling back memory JSON, setting an earlier active step/state, and reexecuting the flow; historical copies provide checkpoint-style time travel Built into checkpoint history model
Best fit Context-enriched conversational agents and purposeful turn-based applications Graph-shaped task workflows needing scheduling, inspection, replay, and branching

Key Architectural Difference: No Edges In Picoflow

Picoflow does not ask the developer to write:

graph.addEdge("NameStep", "DOBStep");
graph.addConditionalEdges("WeatherStep", route);

Instead, a Picoflow step contains routing inside ordinary code:

protected async dob(tool: ToolCall): Promise<ToolResponseType> {
  this.saveState({
    year: tool.args?.year,
    month: tool.args?.month,
    day: tool.args?.day,
  });
  return AddressStep;
}

Or:

protected async address(tool: ToolCall): Promise<ToolResponseType> {
  const response = ValidateAddress(tool.args?.address);
  if (!response) {
    return {
      step: AddressStep,
      tool: 'Invalid address',
    };
  }

  this.saveState({ address: response });
  return {
    step: EndStep,
    prompt: DemoPrompt.AbruptEnd,
    state: { fromAddress: 5 },
  };
}

This makes transitions local, imperative, and close to business validation. The tradeoff is that the flow topology is not fully declared in one place. To understand all possible routes, a developer must read the step code and tool handlers.

LangGraph makes the opposite tradeoff. Routing is explicit in the graph builder, or explicitly returned as Command. This improves graph-level visibility, static validation, visual inspection, and generic runtime behavior. The tradeoff is that developers must express routing through graph primitives even when the business rule is naturally located inside a handler.

Human In The Loop

Picoflow's human-in-the-loop behavior is native to its turn-based design. A flow runs one turn, persists the session document, returns a response, and naturally waits for the next user message. If the active step still needs information, it remains active in the saved session. The next request resumes from that same active step with the same step state and memory.

That means a human pause is not exceptional control flow in Picoflow. It is the ordinary lifecycle of a chatbot or agentic application: ask, persist, wait, resume. A step can collect data over multiple human turns, validate a response, ask again, or transition only after the user supplies what the step needs.

LangGraph supports human-in-the-loop through interrupts and resume semantics. That is powerful for durable graph execution, especially when a graph would otherwise continue running and needs to stop at an explicit checkpoint for human input or approval. Picoflow does not need an equivalent interrupt primitive for normal conversational human-in-the-loop behavior because every turn boundary is already a durable pause point.

Persistence Difference

Picoflow stores the application session as the primary unit. The saved document contains flow state, step state, memories, logs, token counters, and status. The session document is readable as the application record of the interaction.

The session document also carries a version number. In this codebase, new sessions are created with version: K.version, which is currently 1.13. That version identifies the session document/runtime format. It is separate from historical snapshot retention for time travel.

Because the session document carries each step's persisted state, active flag, memory, flow context, and transition sequence, Picoflow can support time travel by resurrecting a prior session document, setting the active step/state to an earlier point, and reexecuting the flow from there. This is a session-document snapshot model: the durable application record is also the material needed to rewind, fork, or reproduce execution.

That makes Picoflow's session document checkpoint-like, but not identical to LangGraph's checkpoint history by default. In the current implementation, saveSession() writes the latest durable flow state back to storage, so the normal session record is a rich mutable snapshot of the flow. If the application retains historical copies of that session document, those copies become checkpoint-style replay points. Picoflow therefore captures the durable changes needed for replay and debugging, while historical snapshot retention policy determines whether the system has a built-in timeline of prior snapshots.

A practical workflow is to take a production session document, copy it into a development database, set the active step to the state where debugging should begin, and roll back the relevant memory namespace by editing its JSON message history. Because memories are stored as ordinary arrays under named namespaces, shared histories and step-specific histories can be trimmed independently. The developer can also adjust step state or flow context in the same document before replaying the flow in dev.

One caveat is transient state. Picoflow's transient state is intentionally useful only during recursive calls in a single turn and is removed before persistence. The session document captures durable flow state, not every temporary in-memory value.

LangGraph stores graph state checkpoints as the primary unit. The checkpointer is optimized for resume, fault tolerance, checkpoint history, pending writes, and time travel. Long-term memory is separated into stores.

In practical terms:

Observability And Production Debugging

Picoflow's session document is also an observability tool. For a production runtime problem, the saved session can show:

SessionLogger writes structured entries directly into the session document. Runtime warnings such as ignored model parameters, missing tool handlers, hallucinated tools, batch errors, and aborted sessions are therefore attached to the same durable record as the user conversation, tool loop, state, and routing history.

This makes the session document useful for post-incident debugging. An operator can inspect what the user and model said, which step was active, which path the flow took, what each state contained, which warnings or errors were recorded, and then resurrect or modify the document to replay the problem from a chosen point.

That replay can happen outside production. Since the session document encapsulates the conversation, step histories, state JSON, flow context, logs, and execution sequence, it can be moved into a development environment without reconstructing the incident by hand. The developer can choose the starting point by changing which step is active, roll back or trim the memory JSON to the desired turn, and run the flow again against dev models, tools, or patched code.

LangGraph plus LangSmith offers richer external tracing, dashboards, and observability workflows. Picoflow's distinction is that the operational evidence is embedded in the application session document itself, so the debugging artifact can remain inside the same company-controlled database as the production session.

Deployment And Data Boundary

Picoflow can be deployed as a cluster of self-contained NodeJS service instances inside a company's firewall. The runtime can use company-controlled MongoDB, Cosmos DB, or SQLite storage for sessions, memory, state, logs, and replay data. In that deployment shape, the orchestration layer does not require an external observability or workflow cloud to run.

LangGraph itself can also run in a self-hosted application process. The data-boundary difference becomes important when LangGraph is paired with LangSmith Cloud for tracing, debugging, evaluation, and observability. LangSmith Cloud is a managed service hosted in LangChain's cloud environment, so traces and associated run data leave the company boundary unless tracing is disabled, redacted, or replaced with self-hosted LangSmith. LangSmith self-hosting exists, but it is an Enterprise deployment option rather than the default managed-cloud path.

For companies with strict firewall or data-residency requirements, Picoflow's default deployment model is simpler: run NodeJS instances and the selected session database inside the controlled network. LangGraph plus LangSmith can meet similar requirements only if the team avoids LangSmith Cloud or procures and operates the self-hosted LangSmith option.

Parallelism: Batch vs. Fan-Out

Picoflow's Two Concurrency Mechanisms

Picoflow has two separate concurrency mechanisms in the basic demo: flow-level batch concurrency for launching independent flow executions, and step-level nested fan-out for launching multiple child steps from within one active step. They are related, but they operate at different scopes and should be described separately.

Diagram showing PicoFlow parallel execution
Two scopes, one clear decision: independent flow runs or child steps inside the active flow.

01 / BATCH

Run independent flows

concurrentSteps(...) coordinates isolated flow executions. Each item carries its own config, can be throttled, and returns a result to the parent.

02 / FAN-OUT

Parallelize inside a flow

runSteps(...) launches child steps from the current parent step and returns their responses for direct aggregation.

Architectural questionPicoFlowLangGraph
What starts parallel work?Explicit helpers: concurrentSteps and runSteps.Graph topology, outgoing edges, or dynamic Send fan-out.
Where does context live?In the isolated flow run or active parent-step scope.Across graph branches that can update graph state.
Who resolves the join?The parent flow or step aggregates returned results in program logic.Reducers may be needed when concurrent branches update the same state key.

The decision is not “can it run in parallel?” It is: where should ownership of context, state, and results live?

Flow-Level Batch Concurrency

The first mechanism is Picoflow's _concurrent flow mode and the underlying concurrentSteps(...) construct. Flow.run(...) checks config._concurrent; if true, it calls spawnSteps() instead of the normal active-step path. concurrentSteps(...) batches items, sends each item as an independent request to SELF_URL, and lets each spawned flow run with its own config. The parent flow collects responses through callbacks.

Step-Level Nested Fan-Out

The second mechanism is more local: concurrent fan-out inside a running step. Step.runStep(...) runs one nested child step under the current step's active context. Step.runSteps(...) generalizes that pattern to multiple child step requests, activates each nested step, runs it with its own optional user message, and waits for all child results with Promise.all.

In the basic-flow demo, InContextStep.onEnter() calls runSteps([...]) to launch ConcurStep1 and ConcurStep2 concurrently, then stores both returned values with saveState({ concurStep1, concurStep2 }). ConcurStep2 further demonstrates that nested fan-out can compose by calling runSteps(...) from its own onEnter(). This is different from _concurrent: it remains inside the current flow session and current parent step context instead of spawning separate flow requests through SelfClient.

LangGraph Graph-Level Parallelism

LangGraph parallelism is part of graph execution. Multiple outgoing edges can schedule multiple destination nodes in the next super-step. Send supports map-style dynamic fan-out where each generated item can be sent to a downstream node with its own state.

So parallel processing is not unique to LangGraph. The difference is the control model:

Developer Experience

Picoflow's style is natural when building a purposeful flow where each step maps to a domain interaction:

The code for prompt, tools, validation, state update, and routing lives together. This is convenient for business workflows and conversational forms.

LangGraph's style is natural when the developer wants the runtime to understand and manage the workflow topology:

The orchestration is more explicit and infrastructure-oriented.

Best Fit Positioning

Picoflow is strongest for context-enriched chatbots and conversational agents. Its natural unit is the active step: prompt, tools, memory namespace, state, model configuration, validation, routing, and human turn handling live together. That makes Picoflow especially effective when an application needs to collect information over multiple turns, enrich context, preserve separate or shared histories, validate user input, and keep the full production session debuggable.

LangGraph is strongest for task-oriented agent workflows. Its natural unit is the graph: nodes perform work, edges route execution, checkpoints preserve graph state, and the runtime can schedule branches, fan-out, interrupts, resumes, and joins. That makes LangGraph especially effective when the problem is less conversation-first and more workflow-first.

Both frameworks can cross over. Picoflow can build task flows, as InvoiceFlow shows, and LangGraph can build chatbots. The distinction is ergonomic fit: Picoflow is optimized around context-rich turn-based conversation, while LangGraph is optimized around graph-shaped task execution.

When Picoflow Is A Better Fit

Picoflow is attractive when:

BasicFlow and InvoiceFlow fit this model well. One is a chatbot-style feature demonstration; the other is a document extraction workflow that still benefits from state, tools, prompt control, and session persistence.

When LangGraph Is A Better Fit

LangGraph is attractive when:

Migration Mental Model

A rough mapping from Picoflow to LangGraph would be:

Picoflow LangGraph
Flow StateGraph plus runtime config
Step Node function or subgraph
Step.state State channel fields
Memory namespace Message state channel or separate channel
Flow.context Runtime config or state input
Tool handler return { step, state } Command({ goto, update })
LogicStep Pure code node
EndStep END
_concurrent + concurrentSteps Batch-style graph invocation or external job fan-out
Step.runSteps(...) Send fan-out or parallel outgoing edges
Mongo/Cosmos/SQLite session doc Checkpointer plus optional store

This mapping is useful conceptually, but a direct translation would change the architecture. Picoflow embeds route decisions in step classes; LangGraph would pull many of those decisions into graph edges, conditional edge routers, or Command returns.

Bottom Line

Picoflow and LangGraph both support stateful agentic workflows, tool use, memory, and code-driven control. They differ in where orchestration lives.

Picoflow puts orchestration inside the application object model: flows, steps, tool handlers, persisted sessions, and active-step mutation. It is compact and domain-friendly for context-enriched conversational agents and purposeful turn-based applications.

LangGraph puts orchestration inside a durable graph runtime: declared state, nodes, edges, super-steps, checkpoints, and graph-level scheduling. It is stronger for graph-shaped task workflows that need topology visibility, replayability, parallel execution, and production-grade durable workflow control.

Picoflow's architecture is deliberately not "LangGraph with different syntax." It is a separate design: a turn-based, persistent, code-routed agentic flow framework that uses LangChain primitives without adopting LangGraph's edge-based graph runtime.