picoflow.io Docs

Guides

Persistence and session stores

Pick a session store, configure it, and understand the difference between completing a workflow and deleting its record. Memory is the default and it does not survive a restart.

Every PicoFlow conversation is one JSON document. Choosing where that document lives is a deployment decision you should make before the first real user, because the default is process-local memory and loses everything on restart.

Persistence is also your operational baseline

A durable store retains the active cursor, state, memories, tokens, status, and structured diagnostics together. The store does not create dashboards or alerts for you, but its common document shape makes internal inspection and document-database aggregation straightforward. See Operate and debug session documents for the safe incident workflow.

Choosing a store

Store SESSION_STORE Durable Safe across processes Use for
Memory MEMORY (default) No No Examples, unit tests, throwaway local runs
SQLite SQLITE Yes Yes, over a shared file Local development, single-node deployments
MongoDB MONGO Yes Yes Horizontally scaled deployments
Cosmos DB COSMO or COSMOS Yes Yes Azure deployments

All four implement the same contract, including revision-based compare-and-swap. The difference is where the atomic check happens — see Concurrency and session conflicts.

An unrecognised value fails fast at startup:

No valid session store 'POSTGRES'. Use MEMORY, MONGO, COSMO, or SQLITE.

Configuration

SESSION_STORE=SQLITE
SQLITE_PATH=ignore/session/session.sqlite   # default when unset

# SESSION_STORE=MONGO
MONGODB_URL=mongodb://localhost:27017/?directConnection=true   # required
MONGODB_NAME=picoflow
MONGODB_COLLECTION=sessions

# SESSION_STORE=COSMO
COSMODB_URL=http://localhost:8081/
COSMODB_KEY=...
COSMODB_ID=picoflow
COSMODB_SESSION_ID=sessions
DOCUMENT_DB is dead configuration

The demo's .env-example sets DOCUMENT_DB=COSMO. No PicoFlow source reads that variable. The store is selected exclusively from SESSION_STORE, read in CoreConfig and defaulting to MEMORY. A project that only sets DOCUMENT_DB silently runs on the in-memory store and loses every session on restart.

Configuration is read once, when FlowEngine is constructed, through a ConfigManager whose precedence is explicit values, then the environment, then a dotenv file. Changing an environment variable at runtime has no effect.

What the document contains

Session document
├── id                    uuid, returned as CHAT_SESSION_ID
├── revision              integer compare-and-swap token, incremented on every write
├── version               session schema version (K.sessionDocVersion)
├── runStatus             "running" | "completed" | "aborted"
├── createdOn, saveOn     Date
├── tokens                input/output/total plus reasoning, visible, cached breakdowns
├── log, error, warn, debug, verbose    structured SessionLogger entries
└── flow                  exactly one envelope — never an array
    ├── name              the registered flow name, permanently bound to this ID
    ├── model             { provider, name, params, retryAttempts? } with credential keys stripped
    ├── context           the first request's config
    ├── memory            namespace -> { messages, summary?, summarizedThroughId? }
    ├── steps             [{ name, state, model? }]
    ├── currentStep       the one durable cursor, or null
    └── sequence          [{ level, stepName }] execution trace

revision and version are unrelated. revision guards writes; version describes the schema and drives migration.

Only metadata dates are revived

Stores hydrate createdOn and saveOn back into Date objects. Anything date-shaped inside step state, memory or context is deliberately left as the string you stored. Do not assume getState("dueDate") returns a Date after a restore.

The store contract

export interface SessionStore {
  load(sessionId: string): Promise<SessionType | null>;
  create(flow: FlowType): Promise<SessionType>;
  save(sessionDoc: SessionType, expectedRevision: number): Promise<SessionType>;
  delete(sessionId: string, expectedRevision?: number): Promise<void>;
  close(): Promise<void>;
}

load() applies no policy. It does not check a Flow’s idle rule, run status or schema version — those decisions belong to Flow.onRestoreSessionDoc(). save() must reject a stale revision with SessionConflictError.

Inject an alternative implementation through the engine, which is also how tests substitute a store:

FlowEngine.create({
  flows: [CustomerFlow],
  providers: [...],
  sessionStore: new MyStore(),
});

Then verify it against the shared behavioural suite:

await SessionStoreConformanceUtil.run(() => new MyStore());

The suite covers revision numbering, conflict on a stale save, conflict on a stale delete, preservation of ISO-looking strings in user data, and the requirement that two concurrent saves from one revision produce exactly one winner.

Flow-owned session idle policy

The store does not evaluate session age. A Flow that needs an idle rule calls sessionIdleMs(doc) from onRestoreSessionDoc() and returns null to start a new session. Retention and cleanup remain separate application responsibilities.

Completion versus deletion

These are different operations with different consequences.

Operation What it does Document Session ID
TerminateSessionStep Sets runStatus = "completed" in onEnter(), reports isEnd() Retained Cannot be resumed; a new request with it creates a new session
sessionCompleted() / markCompleted() Sets runStatus = "completed" directly Retained Same
Unhandled error Engine sets runStatus = "aborted" and persists the message Retained Same
deleteSession(id) Removes the row or document, under the same lock and revision check as a write Gone Gone
// Completing a conversation
return go(TerminateSessionStep).withPrompt("Confirm the saved customer.");

// Deleting the record
await flowEngine.deleteSession(sessionId);

A completed session is a record you can audit: transcript, state, token totals, log entries. Delete only when the record itself must not exist — a retention policy, a privacy request.

deleteSession() returns { success, session } and, on failure, a message. Note that it is not a RunResponseType; it has no completed or contentType.

endChat() is deprecated

FlowEngine.endChat(sessionId) now simply delegates to deleteSession(). The name is misleading: ending a conversation does not imply destroying its record. Call deleteSession() directly, and migrate any POST /ai/end style endpoint to an HTTP DELETE route so the API stops conflating completion with deletion.

Failure modes

Symptom Cause
Sessions vanish after a restart SESSION_STORE unset or set to MEMORY
Sessions vanish after a deploy, with a durable store DOCUMENT_DB was set instead of SESSION_STORE
Conversations restart mid-way A Flow-owned restore policy returned null; inspect its idle or validation rule
Conversations restart after a release onRestoreSessionDoc() reset them on a schema version bump
Configuration value 'MONGODB_URL' is required. Mongo selected without a connection string
SessionConflictError on save Another writer advanced the revision first
Session 'x' belongs to flow 'A', not 'B'. A session ID was reused with a different flowName
Session 'x' violates the one-flow invariant The document was hand-edited or written by something other than PicoFlow
Dates come back as strings Only session metadata dates are revived

Related: The session document, Concurrency and session conflicts, and Session stores.