How the SimStudioAI Workflow Persistence Layer Saves and Loads Workflows
The @sim/workflow-persistence package exposes two primary functions—saveWorkflow and loadWorkflow—that handle normalization, Zod schema validation, and database operations to persist workflow definitions and runtime states through Drizzle ORM.
Within the simstudioai/sim repository, the workflow persistence layer isolates storage concerns from the editor UI and execution engine. It transforms in-memory workflow objects—complete with blocks, connections, and nested sub-flows—into normalized JSON blobs suitable for database storage, and reverses the process during retrieval to reconstruct fully functional workflows.
Core Persistence API
Persisting Workflows with saveWorkflow
The saveWorkflow function in packages/workflow-persistence/src/save.ts handles the complete storage lifecycle when a workflow is modified or an execution run completes.
The process follows four distinct stages:
-
Normalization: The runtime workflow object passes through helpers in [
subflow-helpers.ts](https://github.com/simstudioai/sim/blob/main/packages/workflow-persistence/src/subflow-helpers.ts) and [subblocks.ts](https://github.com/simstudioai/sim/blob/main/packages/workflow-persistence/src/subblocks.ts) to flatten nested sub-flows and strip transient runtime data, producing a minimal storage representation. -
Serialization and Validation: The normalized structure is stringified to JSON and strictly validated against the Zod schema defined in [
types.ts](https://github.com/simstudioai/sim/blob/main/packages/workflow-persistence/src/types.ts), ensuring type safety before database interaction. -
Database Upsert: Using Drizzle ORM from
@sim/db, the function performs an atomic upsert (insert … on conflict do update) into theworkflow_persistencetable. The row key is the workflow UUID generated via@sim/utils/id. -
Cache Refresh: Upon successful commit, the in-memory cache (
@sim/utils/cache) updates to ensure subsequent reads return the latest version without additional database round-trips.
Retrieving Workflows with loadWorkflow
The loadWorkflow function in packages/workflow-persistence/src/load.ts rehydrates workflows for editing or execution through a systematic reconstruction process.
The retrieval pipeline executes these steps:
- Database Fetch: Retrieves the JSON blob from the
workflow_persistencetable using the workflow UUID. - Schema Validation: Parses the JSON and validates it against
WorkflowPersistedSchemato detect data corruption or version mismatches. - Sub-Flow Reconstruction: Invokes
reconstructSubflowsfromsubflow-helpers.tsto restore nested workflow structures from their flattened storage format. - Runtime Hydration: Re-attaches transient fields such as live state containers and unique runtime IDs that are not stored but required by the executor in
apps/sim/executor.
Data Transformation and Integrity
Bidirectional Sub-Flow Handling
Nested workflows are stored as independent JSON fragments to prevent duplication and circular reference issues. The persistence layer uses isolated helpers to manage the bidirectional conversion between runtime objects and storage-ready fragments, ensuring that sub-flows are never duplicated in the database.
Schema Enforcement with Zod
All persisted data conforms to the centralized Zod schema in types.ts. Because the persistence package lives in the packages/ boundary, it safely imports Zod without exposing validation logic to API routes, maintaining strict separation of concerns and guaranteeing that any structural change to workflows is caught at compile-time and runtime.
Database Strategy and Caching
The persistence layer relies on Drizzle ORM for type-safe SQL operations against the workflow_persistence table. The upsert pattern eliminates the need for callers to decide between "create" versus "update" logic, as the database handles row existence checks automatically based on the workflow UUID. After every successful save, the cache layer (@sim/utils/cache) refreshes its entry, minimizing database load during rapid editing sessions or frequent execution cycles.
Implementation Examples
The following patterns demonstrate how the editor and executor consume the persistence layer.
Saving from the Workflow Editor:
import { saveWorkflow } from '@sim/workflow-persistence'
import { useWorkflowStore } from '@/stores/workflows/store'
async function onSave() {
const workflow = useWorkflowStore.getState().currentWorkflow
await saveWorkflow(workflow.id, workflow)
}
Loading for Execution:
import { loadWorkflow } from '@sim/workflow-persistence'
import { executeWorkflow } from '@/executor/engine'
async function runWorkflow(workflowId: string) {
const workflow = await loadWorkflow(workflowId)
const result = await executeWorkflow(workflow)
await saveWorkflow(workflow.id, result.updatedWorkflow)
}
Key Source Files
| File | Responsibility | Source |
|---|---|---|
packages/workflow-persistence/src/save.ts |
saveWorkflow implementation with normalization and upsert logic |
save.ts |
packages/workflow-persistence/src/load.ts |
loadWorkflow implementation with reconstruction and hydration |
load.ts |
packages/workflow-persistence/src/types.ts |
Central Zod schemas for type-safe persistence | types.ts |
packages/workflow-persistence/src/subflow-helpers.ts |
Utilities for flattening and reconstructing sub-flows | subflow-helpers.ts |
packages/workflow-persistence/src/subblocks.ts |
Helpers for extracting block-level persistence details | subblocks.ts |
packages/workflow-persistence/src/index.ts |
Public API exports | index.ts |
Summary
- The
@sim/workflow-persistencepackage centralizes all workflow durability logic throughsaveWorkflowandloadWorkflow. - Normalization in
save.tsflattens sub-flows and strips transient state before Zod validation and Drizzle ORM upserts. - Reconstruction in
load.tsvalidates stored JSON againstWorkflowPersistedSchemaand rebuilds nested structures usingreconstructSubflows. - Atomic upsert operations ensure idempotent saves without requiring create-or-update decision logic in callers.
- An integrated cache layer minimizes database load by refreshing in-memory entries immediately after writes.
Frequently Asked Questions
What database technology does the persistence layer use?
The layer uses Drizzle ORM via the internal @sim/db package to interact with the workflow_persistence table, performing standard SQL upsert operations to handle both new and existing workflow records atomically.
How does the system handle nested sub-workflows without data duplication?
Sub-flows are extracted and normalized into independent JSON fragments during save, then reconstructed during load using utilities in subflow-helpers.ts. This prevents circular references and ensures each sub-workflow definition is stored only once.
Why does the load process require runtime hydration?
Runtime hydration re-attaches ephemeral execution state—such as live state containers and unique runtime IDs—that are intentionally excluded from persistent storage to keep the database records lightweight and environment-agnostic.
How does schema validation prevent corrupted data from reaching the application?
Every save and load operation validates data against strict Zod schemas defined in types.ts. This catches structural mismatches at runtime boundaries before corrupted data can propagate to the workflow editor or execution engine.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →