Skip to content

Persistence

Graphyn holds workflow state in memory. Saving and loading is your responsibility — the library deliberately has no opinion on where workflows live (a database, a REST API, local files). This guide shows three common patterns.


Reading current workflow state

GraphynEditorState.workflow is a @Stable observable property. Read it at any time:

val state = rememberGraphynEditorState(initialWorkflow = myWorkflow)

// Read the current workflow (reflects all edits, undo/redo, etc.)
val current: WorkflowDefinition? = state.workflow

Pattern 1 — Auto-save with debounce

Observe state.workflow inside a LaunchedEffect and debounce writes to avoid saving on every keystroke.

@Composable
fun MyEditor(repo: WorkflowRepository) {
    val state = rememberGraphynEditorState(initialWorkflow = repo.load())

    // Auto-save: wait 1 s of inactivity after any change, then save
    val workflow = state.workflow
    LaunchedEffect(workflow) {
        if (workflow != null) {
            delay(1_000)
            repo.save(workflow)
        }
    }

    GraphynEditorShell(
        dependencies = GraphynEditorShellDependencies(nodeSpecs = repo.specs),
        state = state,
    )
}

LaunchedEffect(workflow) restarts the coroutine every time workflow changes. The delay(1_000) is cancelled and restarted on each change, so the save only fires after a 1-second pause.


Pattern 2 — Explicit save button

Let the user control when saves happen. Store a dirty flag and expose a save action.

@Composable
fun MyEditor(repo: WorkflowRepository) {
    val state = rememberGraphynEditorState(initialWorkflow = repo.load())
    val initial = remember { state.workflow }
    val isDirty = state.workflow != initial

    Column {
        if (isDirty) {
            Button(onClick = {
                state.workflow?.let { repo.save(it) }
            }) { Text("Save") }
        }
        GraphynEditorShell(
            dependencies = GraphynEditorShellDependencies(nodeSpecs = repo.specs),
            state = state,
        )
    }
}

Pattern 3 — Load from a REST API

Fetch the workflow from a backend on first composition and pass it to the editor.

@Composable
fun MyEditor(workflowId: String, api: WorkflowApi) {
    var workflow by remember { mutableStateOf<WorkflowDefinition?>(null) }

    LaunchedEffect(workflowId) {
        workflow = api.fetchWorkflow(workflowId) // suspend fun, returns WorkflowDefinition
    }

    // key() recreates editor state when the workflow identity changes
    key(workflowId) {
        val state = rememberGraphynEditorState(initialWorkflow = workflow)
        GraphynEditorShell(
            dependencies = GraphynEditorShellDependencies(nodeSpecs = mySpecs),
            state = state,
        )
    }
}

To save back:

suspend fun saveToApi(state: GraphynEditorState, api: WorkflowApi) {
    val wf = state.workflow ?: return
    api.updateWorkflow(wf.id, wf.toJson())
}


Pattern 4 — Persist to a local file (Desktop / JVM)

This is the pattern used in app/sample. Load the workflow from a JSON file on startup and auto-save on every edit. Works on any JVM target (Desktop, Server).

private val saveFile = File(System.getProperty("user.home"), ".myapp/workflow.json")

private fun loadWorkflow(): WorkflowDefinition =
    if (saveFile.exists()) runCatching { workflowFromJson(saveFile.readText()) }.getOrNull()
        ?: defaultWorkflow()
    else defaultWorkflow()

private fun saveWorkflow(workflow: WorkflowDefinition) {
    saveFile.parentFile.mkdirs()
    saveFile.writeText(workflow.toJson())
}

@Composable
fun MyEditor() {
    val state = rememberGraphynEditorState(initialWorkflow = remember { loadWorkflow() })

    val workflow = state.workflow
    LaunchedEffect(workflow) {
        if (workflow != null) {
            delay(1_000)          // debounce — only write after 1 s of inactivity
            saveWorkflow(workflow)
        }
    }

    GraphynEditorShell(
        dependencies = GraphynEditorShellDependencies(nodeSpecs = mySpecs),
        state = state,
    )
}

Serialization

val json: String = workflow.toJson()                  // WorkflowDefinition → JSON string
val loaded: WorkflowDefinition = workflowFromJson(json) // JSON string → WorkflowDefinition

See Serialization reference for the full JSON schema.


What is and isn't saved

WorkflowDefinition contains:

Field Saved
Node types, IDs, config values
Connections between ports
Workflow name and ID
Canvas positions (node layout) ❌ — viewport/layout state is editor-only
Group frames (Cmd+G) ❌ — editor-only visual state
Undo/redo history ❌ — session-only

Node positions are not part of WorkflowDefinition. If you need to persist layout, read state.layout.nodePositionsByNodeId separately and restore them after loading.