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:

  1. 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.

  2. 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.

  3. Database Upsert: Using Drizzle ORM from @sim/db, the function performs an atomic upsert (insert … on conflict do update) into the workflow_persistence table. The row key is the workflow UUID generated via @sim/utils/id.

  4. 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_persistence table using the workflow UUID.
  • Schema Validation: Parses the JSON and validates it against WorkflowPersistedSchema to detect data corruption or version mismatches.
  • Sub-Flow Reconstruction: Invokes reconstructSubflows from subflow-helpers.ts to 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-persistence package centralizes all workflow durability logic through saveWorkflow and loadWorkflow.
  • Normalization in save.ts flattens sub-flows and strips transient state before Zod validation and Drizzle ORM upserts.
  • Reconstruction in load.ts validates stored JSON against WorkflowPersistedSchema and rebuilds nested structures using reconstructSubflows.
  • 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →