
Node-based workflow canvas embeddable in apps, featuring plugin-driven node specs/executors, MVI state, AI-assisted workflow generation, live-stream execution, subgraphs, customizable UI and themes.
Node-based workflow editor for Kotlin Multiplatform
Drop a canvas into any app — AI pipelines, shader graphs, automation builders, game tools.
Graphyn is a Kotlin Multiplatform library that gives your app a fully-featured node-based canvas — the kind used in Unreal Blueprint, Blender shader editor, or n8n. Wire it up with plugins to define your own node types, connect them, and execute workflows.
Docs site: ronjunevaldoz.github.io/graphyn-editor
executeAsFlow() for real-time progress ┌─────────────────────────────────┐
│ app/shared (Compose UI) │
│ Canvas + Editor Shell │
└────────┬──────────────┬─────────┘
│ │
┌───────▼────┐ ┌──────▼──────┐
│ editor-api │ │ plugin-api │
│ Card UI │ │ Node specs │
│ Panels │ │ Executors │
└───────┬────┘ └──────┬──────┘
│ │
┌──────▼───────────────▼──────┐
│ core/ (folder of modules) │
│ model · execution │
│ serialization · data │
└──────────────────────────────┘
ui/cards (ShapeCardFactory, FieldCardFactory)
plugins/* (node definitions + executors)
core is a folder of focused, layered modules — there is no umbrella :core module. Each consumer depends only on the submodules it uses; everything builds up from core:model.
| Module | Artifact | What |
|---|---|---|
core:model |
graphyn-core-model |
Workflow model, types, validation, registry |
core:execution |
graphyn-core-execution |
Execution engine, executors, events |
core:serialization |
graphyn-core-serialization |
Workflow document codec |
core:data |
graphyn-core-data |
Workflow stores + platform persistence |
core:designsystem |
graphyn-ui-design |
Design tokens, theme, and UI primitives |
editor-api |
graphyn-editor-api |
Canvas card + panel contracts |
plugin-api |
graphyn-plugin-api |
Node spec + executor contracts |
ui/cards |
graphyn-ui-cards |
Ready-made card shapes (Shape, Field, Circle) |
app/shared |
graphyn-editor |
Compose Multiplatform canvas + editor shell |
ai |
graphyn-ai |
LLM workflow generation (Ollama) |
runtime |
graphyn-runtime |
Convenience bundle of all first-party plugins |
server |
graphyn-ktor-plugin |
Ktor execution API + install(Graphyn)
|
mcp |
— (application) | Stdio MCP server — generic workflow CRUD + execute for agents |
plugins/control |
graphyn-plugin-control |
Branch, loop, merge |
plugins/list-ops |
graphyn-plugin-list-ops |
Map, filter, reduce, sort |
plugins/types |
graphyn-plugin-types |
Type conversion and casting |
plugins/text |
graphyn-plugin-text |
Split, join, replace, template |
plugins/io |
graphyn-plugin-io |
HTTP, file read/write, path resolution |
plugins/json |
graphyn-plugin-json |
JSON parse, query, transform |
plugins/preview |
graphyn-plugin-preview |
Live output preview nodes |
plugins/sticky-notes |
graphyn-plugin-sticky-notes |
On-canvas annotation nodes |
plugins/script |
graphyn-plugin-script |
Kotlin Script eval (JVM) |
plugins/media-core |
graphyn-plugin-media-core |
FFmpeg-backed media processing (JVM) |
plugins/media-ai |
graphyn-plugin-media-ai |
TTS / STT / OCR adapters (JVM) |
plugins/gmail |
graphyn-plugin-gmail |
Gmail integration (fetch, send, reply) |
plugins/linkedin |
graphyn-plugin-linkedin |
LinkedIn profile and feed nodes |
// gradle/libs.versions.toml
[versions]
graphyn = "0.9.2"
[libraries]
graphyn-editor = { module = "io.github.ronjunevaldoz:graphyn-editor", version.ref = "graphyn" }
graphyn-ui-cards = { module = "io.github.ronjunevaldoz:graphyn-ui-cards", version.ref = "graphyn" }
graphyn-runtime = { module = "io.github.ronjunevaldoz:graphyn-runtime", version.ref = "graphyn" }
graphyn-plugin-api = { module = "io.github.ronjunevaldoz:graphyn-plugin-api", version.ref = "graphyn" }
graphyn-editor-api = { module = "io.github.ronjunevaldoz:graphyn-editor-api", version.ref = "graphyn" }
graphyn-core-model = { module = "io.github.ronjunevaldoz:graphyn-core-model", version.ref = "graphyn" }
graphyn-plugin-io = { module = "io.github.ronjunevaldoz:graphyn-plugin-io", version.ref = "graphyn" }
graphyn-plugin-gmail = { module = "io.github.ronjunevaldoz:graphyn-plugin-gmail", version.ref = "graphyn" }
# full plugin list: graphyn-plugin-{control,list-ops,types,text,io,json,preview,sticky-notes,script,media-core,media-ai,gmail,linkedin}
# full core list: graphyn-core-{model,execution,serialization,data}// build.gradle.kts
commonMain.dependencies {
implementation(libs.graphyn.editor) // full canvas UI
implementation(libs.graphyn.runtime) // all first-party plugins bundled
}| Use case | Dependency |
|---|---|
| Full canvas editor | graphyn-editor |
| All first-party plugins | graphyn-runtime |
| Card UI kit only | graphyn-ui-cards |
| Build a runtime plugin | graphyn-plugin-api |
| Build an editor plugin | graphyn-editor-api |
| Individual plugin | graphyn-plugin-{control,io,json,…} |
| Workflow model only |
graphyn-core-model (+ -execution / -serialization / -data as needed) |
A runtime plugin defines a node's spec (ports, defaults) and executor (the actual logic):
object MyPlugin : GraphynPlugin {
override val metadata = GraphynPluginMetadata(id = "com.example.my", displayName = "My Plugin", version = "1.0.0")
override fun register(registrar: GraphynPluginRegistrar) {
registrar.registerNodeSpec(
NodeSpec(
type = "my.transform",
label = "Transform",
category = "com.example.text",
inputs = listOf(PortSpec("input", WorkflowType.String)),
outputs = listOf(PortSpec("output", WorkflowType.String)),
defaultValues = mapOf("input" to WorkflowValue.StringValue("hello")),
)
)
registrar.registerExecutor("my.transform") { inputs ->
val value = (inputs["input"] as? WorkflowValue.StringValue)?.value ?: ""
mapOf("output" to WorkflowValue.StringValue(value.uppercase()))
}
}
}Wire it into the editor shell:
@OptIn(GraphynExperimentalApi::class)
@Composable
fun App() {
val runtimeRegistry = remember { DefaultGraphynPluginRegistry().apply { install(MyPlugin) } }
val state = rememberGraphynEditorState()
GraphynEditorShell(
state = state,
dependencies = GraphynEditorShellDependencies(nodeSpecs = runtimeRegistry.nodeSpecs),
branding = GraphynBranding(appName = "My Studio"),
)
}For a custom canvas card, inspector panel, FieldCard's per-WorkflowType input widgets, auto-discovery via ServiceLoader, subgraphs, AI workflow generation, and keyboard shortcuts — see the docs site and Plugin API.
Graphyn separates runtime concerns from editor concerns — you can ship a runtime plugin without any UI dependency.
| Layer | Module | Stable contract |
|---|---|---|
| Runtime | plugin-api |
GraphynPlugin, GraphynPluginRegistrar, NodeSpec, NodeExecutor
|
| Editor cards | editor-api |
NodeCanvasFactory, GraphynEditorPluginRegistrar
|
| Editor panels | editor-api |
EditorPanelFactory, EditorPanelContext (includes onConfigChange) |
| State observation | editor-api |
GraphynEditorStateView — workflow, workflowFlow: StateFlow, selectedNodeId
|
Default* implementations are marked @GraphynExperimentalApi — their signatures may evolve. The interfaces above are stable.
Instead of wiring every plugin by hand, hosts on the JVM and Android can let plugins register themselves via java.util.ServiceLoader. A plugin author ships a resource file:
# src/main/resources/META-INF/services/com.ronjunevaldoz.graphyn.pluginapi.GraphynPlugin
com.example.MyPlugin
Then the host installs everything on the classpath in one call:
val registry = DefaultGraphynPluginRegistry().apply {
install(CorePlugin) // explicit, always present
installDiscovered() // plus any plugin on the classpath (ServiceLoader)
}installDiscovered() skips plugins already installed (matched by metadata.id) and returns the newly-installed ones. On JS, Wasm, and iOS it is a no-op — those hosts register plugins explicitly.
A node may embed a nested WorkflowDefinition via NodeRef.subgraph. The engine runs it recursively (and in parallel where possible), and the subgraph node's resolved inputs are injected into the inner workflow's free input ports (those with no internal connection or config), keyed by port name — so a parent workflow can feed data into a nested one. Explicit inner config and internal wiring always take precedence. A subgraph node's outputs are the inner workflow's free output ports (those nothing inside consumes).
In the editor, select two or more nodes and press Cmd/Ctrl + Shift + G to collapse them into a subgraph node; its boundary ports are derived automatically from the inner workflow. Double-click a subgraph node (or use Enter → in the inspector) to drill into it, and Expand ⤢ in the inspector to inline it again.
The :ai module turns a natural-language prompt into a WorkflowDefinition. WorkflowGenerator has two implementations:
OllamaWorkflowGenerator(OllamaConfig(baseUrl, model)) — calls an Ollama host's /api/generate with format=json. The prompt embeds the node catalog with port types (type — [in:type] -> [out:type]) so the model only emits real node types and fills each node's config with type-matched literals for every unconnected input; WorkflowJsonParser validates the output, coerces config values to each port's WorkflowType, and drops unknown node types / dangling connections rather than failing. Default model qwen2.5-coder:14b.PlaceholderWorkflowGenerator — offline canned output for UI development and no-host scenarios.The editor surfaces this as a docked ✨ AI panel (toggle from the toolbar). Describe a workflow and it generates onto the current canvas, auto-laid-out. The panel keeps a chat transcript so you can iterate, and each result reports what was created plus any unsupported nodes or dropped connections that were sanitized away — so you learn why the graph differs from your request. Generation failures stay inline and retryable.
Every editor action (undo, copy, group, collapse, …) is a rebindable EditorShortcutAction. Open ⌨ Keys in the toolbar, click an action's chord chip, and press the new combination — bindings persist via the settings store and conflicts are rejected. Contextual keys (Escape, Delete) are fixed. See keyboard-shortcuts.md.
| Platform | Status |
|---|---|
| Android | ✅ |
| Desktop (JVM) | ✅ |
| Web (Wasm) | ✅ |
| Web (JS) | ✅ |
| iOS | ✅ XCFramework (SPM) |
| Server (JVM) | ✅ runtime only |
./gradlew :app:desktopApp:run # Desktop
./gradlew :app:androidApp:assembleDebug # Android
./gradlew :app:webApp:wasmJsBrowserDevelopmentRun # Web (Wasm)
./gradlew :app:webApp:jsBrowserDevelopmentRun # Web (JS)Start the Ktor server:
./gradlew :server:runThe server exposes:
| Endpoint | Description |
|---|---|
GET / |
Health check |
GET /nodes |
List all registered node specs |
GET /nodes/{type} |
Get a single node spec by type (404 if unknown) |
POST /validate |
Validate a workflow — returns [ValidationError]
|
POST /execute |
Run synchronously — returns WorkflowExecutionResult
|
POST /executions |
Start async run — returns { runId } (202) |
GET /executions/{id}/events |
SSE stream of per-node events + final result |
GET /workflows |
List persisted workflows (newest first) |
GET /workflows/{id} |
Load a workflow by ID |
POST /workflows |
Save / upsert a workflow (returns WorkflowMeta, 201) |
DELETE /workflows/{id} |
Delete workflow and its version history |
Set GRAPHYN_API_KEY=<secret> in the environment to enable Bearer-token auth. All endpoints except GET / require the token when the env var is present.
:mcp is a stdio MCP server for agents (Claude Desktop, Claude Code, etc.) — generic CRUD + execute over workflows, no template-specific tools. It embeds the engine in-process (FileWorkflowStore, defaults to <project-root>/.graphyn/workflows — override with GRAPHYN_MCP_WORKFLOWS_DIR), no running :server needed.
./gradlew :mcp:installDistAdd it to your MCP client config, e.g. a project's .mcp.json:
{
"mcpServers": {
"graphyn-workflows": {
"command": "./mcp/build/install/mcp/bin/mcp",
"args": []
}
}
}| Tool | Description |
|---|---|
workflow_list |
List all stored workflows |
workflow_get |
Fetch a workflow's full definition by id |
workflow_publish |
Save/update a workflow from raw JSON (validates first, id must start with mcp-) |
workflow_delete |
Delete a workflow and its history (id must start with mcp-) |
workflow_execute |
Run a stored workflow by id, with optional overrides and async
|
workflow_execution_status |
Poll progress for an async run |
workflow_list_node_types |
List registered node types, for authoring workflow_publish payloads |
By default :mcp installs the Shorts, MediaCore, MediaAi, and StableDiffusion plugins on top of the base runtime set. Trim or reorder with GRAPHYN_MCP_PLUGINS (comma-separated, or all):
"env": { "GRAPHYN_MCP_PLUGINS": "shorts,media-core" }workflow_publish/workflow_delete are scoped to ids starting with mcp- — they share :mcp's own store with the desktop editor, and this stops an agent from overwriting or deleting your real workflows by reusing an id. workflow_publish/workflow_execute run against the real engine — nodes like script.eval and io.file_write/io.http_request execute unsandboxed. Tool annotations (destructiveHint/openWorldHint) flag which ones.
Artifacts are published to Maven Central automatically when a version tag is pushed (git tag vX.Y.Z && git push origin vX.Y.Z). For local / manual publishing without CI, use the Doppler-backed script:
# Prerequisites: doppler CLI + DOPPLER_TOKEN in .env or .env.local
# Doppler project "maven-central / prd" must have MAVEN_CENTRAL_USERNAME,
# MAVEN_CENTRAL_PASSWORD, GPG_SIGNING_KEY, GPG_SIGNING_PASSWORD
# Publish all modules at version from gradle.properties
./scripts/publish-local.sh
# Explicit version
./scripts/publish-local.sh 0.9.2
# Single module only (fastest for hotfixes)
./scripts/publish-local.sh 0.9.2 serverSee .env.example for all supported environment variables and docs/reference/compatibility-matrix.md for the full artifact list.
# core is a folder of submodules — check each (core:execution holds the integration test)
./gradlew :core:model:check :core:execution:check :core:serialization:check :core:data:check
./gradlew :app:shared:jvmTest # canvas + editor UI tests (Roborazzi)
./gradlew :plugins:io:jvmTest # I/O plugin tests
./gradlew :server:test # server route testsFull documentation at ronjunevaldoz.github.io/graphyn-editor.
Add the package in Xcode → File → Add Package Dependencies:
https://github.com/ronjunevaldoz/graphyn-editor
Or in Package.swift:
.package(url: "https://github.com/ronjunevaldoz/graphyn-editor", from: "0.1.0")Apache 2.0 © Ron June Valdoz
Node-based workflow editor for Kotlin Multiplatform
Drop a canvas into any app — AI pipelines, shader graphs, automation builders, game tools.
Graphyn is a Kotlin Multiplatform library that gives your app a fully-featured node-based canvas — the kind used in Unreal Blueprint, Blender shader editor, or n8n. Wire it up with plugins to define your own node types, connect them, and execute workflows.
Docs site: ronjunevaldoz.github.io/graphyn-editor
executeAsFlow() for real-time progress ┌─────────────────────────────────┐
│ app/shared (Compose UI) │
│ Canvas + Editor Shell │
└────────┬──────────────┬─────────┘
│ │
┌───────▼────┐ ┌──────▼──────┐
│ editor-api │ │ plugin-api │
│ Card UI │ │ Node specs │
│ Panels │ │ Executors │
└───────┬────┘ └──────┬──────┘
│ │
┌──────▼───────────────▼──────┐
│ core/ (folder of modules) │
│ model · execution │
│ serialization · data │
└──────────────────────────────┘
ui/cards (ShapeCardFactory, FieldCardFactory)
plugins/* (node definitions + executors)
core is a folder of focused, layered modules — there is no umbrella :core module. Each consumer depends only on the submodules it uses; everything builds up from core:model.
| Module | Artifact | What |
|---|---|---|
core:model |
graphyn-core-model |
Workflow model, types, validation, registry |
core:execution |
graphyn-core-execution |
Execution engine, executors, events |
core:serialization |
graphyn-core-serialization |
Workflow document codec |
core:data |
graphyn-core-data |
Workflow stores + platform persistence |
core:designsystem |
graphyn-ui-design |
Design tokens, theme, and UI primitives |
editor-api |
graphyn-editor-api |
Canvas card + panel contracts |
plugin-api |
graphyn-plugin-api |
Node spec + executor contracts |
ui/cards |
graphyn-ui-cards |
Ready-made card shapes (Shape, Field, Circle) |
app/shared |
graphyn-editor |
Compose Multiplatform canvas + editor shell |
ai |
graphyn-ai |
LLM workflow generation (Ollama) |
runtime |
graphyn-runtime |
Convenience bundle of all first-party plugins |
server |
graphyn-ktor-plugin |
Ktor execution API + install(Graphyn)
|
mcp |
— (application) | Stdio MCP server — generic workflow CRUD + execute for agents |
plugins/control |
graphyn-plugin-control |
Branch, loop, merge |
plugins/list-ops |
graphyn-plugin-list-ops |
Map, filter, reduce, sort |
plugins/types |
graphyn-plugin-types |
Type conversion and casting |
plugins/text |
graphyn-plugin-text |
Split, join, replace, template |
plugins/io |
graphyn-plugin-io |
HTTP, file read/write, path resolution |
plugins/json |
graphyn-plugin-json |
JSON parse, query, transform |
plugins/preview |
graphyn-plugin-preview |
Live output preview nodes |
plugins/sticky-notes |
graphyn-plugin-sticky-notes |
On-canvas annotation nodes |
plugins/script |
graphyn-plugin-script |
Kotlin Script eval (JVM) |
plugins/media-core |
graphyn-plugin-media-core |
FFmpeg-backed media processing (JVM) |
plugins/media-ai |
graphyn-plugin-media-ai |
TTS / STT / OCR adapters (JVM) |
plugins/gmail |
graphyn-plugin-gmail |
Gmail integration (fetch, send, reply) |
plugins/linkedin |
graphyn-plugin-linkedin |
LinkedIn profile and feed nodes |
// gradle/libs.versions.toml
[versions]
graphyn = "0.9.2"
[libraries]
graphyn-editor = { module = "io.github.ronjunevaldoz:graphyn-editor", version.ref = "graphyn" }
graphyn-ui-cards = { module = "io.github.ronjunevaldoz:graphyn-ui-cards", version.ref = "graphyn" }
graphyn-runtime = { module = "io.github.ronjunevaldoz:graphyn-runtime", version.ref = "graphyn" }
graphyn-plugin-api = { module = "io.github.ronjunevaldoz:graphyn-plugin-api", version.ref = "graphyn" }
graphyn-editor-api = { module = "io.github.ronjunevaldoz:graphyn-editor-api", version.ref = "graphyn" }
graphyn-core-model = { module = "io.github.ronjunevaldoz:graphyn-core-model", version.ref = "graphyn" }
graphyn-plugin-io = { module = "io.github.ronjunevaldoz:graphyn-plugin-io", version.ref = "graphyn" }
graphyn-plugin-gmail = { module = "io.github.ronjunevaldoz:graphyn-plugin-gmail", version.ref = "graphyn" }
# full plugin list: graphyn-plugin-{control,list-ops,types,text,io,json,preview,sticky-notes,script,media-core,media-ai,gmail,linkedin}
# full core list: graphyn-core-{model,execution,serialization,data}// build.gradle.kts
commonMain.dependencies {
implementation(libs.graphyn.editor) // full canvas UI
implementation(libs.graphyn.runtime) // all first-party plugins bundled
}| Use case | Dependency |
|---|---|
| Full canvas editor | graphyn-editor |
| All first-party plugins | graphyn-runtime |
| Card UI kit only | graphyn-ui-cards |
| Build a runtime plugin | graphyn-plugin-api |
| Build an editor plugin | graphyn-editor-api |
| Individual plugin | graphyn-plugin-{control,io,json,…} |
| Workflow model only |
graphyn-core-model (+ -execution / -serialization / -data as needed) |
A runtime plugin defines a node's spec (ports, defaults) and executor (the actual logic):
object MyPlugin : GraphynPlugin {
override val metadata = GraphynPluginMetadata(id = "com.example.my", displayName = "My Plugin", version = "1.0.0")
override fun register(registrar: GraphynPluginRegistrar) {
registrar.registerNodeSpec(
NodeSpec(
type = "my.transform",
label = "Transform",
category = "com.example.text",
inputs = listOf(PortSpec("input", WorkflowType.String)),
outputs = listOf(PortSpec("output", WorkflowType.String)),
defaultValues = mapOf("input" to WorkflowValue.StringValue("hello")),
)
)
registrar.registerExecutor("my.transform") { inputs ->
val value = (inputs["input"] as? WorkflowValue.StringValue)?.value ?: ""
mapOf("output" to WorkflowValue.StringValue(value.uppercase()))
}
}
}Wire it into the editor shell:
@OptIn(GraphynExperimentalApi::class)
@Composable
fun App() {
val runtimeRegistry = remember { DefaultGraphynPluginRegistry().apply { install(MyPlugin) } }
val state = rememberGraphynEditorState()
GraphynEditorShell(
state = state,
dependencies = GraphynEditorShellDependencies(nodeSpecs = runtimeRegistry.nodeSpecs),
branding = GraphynBranding(appName = "My Studio"),
)
}For a custom canvas card, inspector panel, FieldCard's per-WorkflowType input widgets, auto-discovery via ServiceLoader, subgraphs, AI workflow generation, and keyboard shortcuts — see the docs site and Plugin API.
Graphyn separates runtime concerns from editor concerns — you can ship a runtime plugin without any UI dependency.
| Layer | Module | Stable contract |
|---|---|---|
| Runtime | plugin-api |
GraphynPlugin, GraphynPluginRegistrar, NodeSpec, NodeExecutor
|
| Editor cards | editor-api |
NodeCanvasFactory, GraphynEditorPluginRegistrar
|
| Editor panels | editor-api |
EditorPanelFactory, EditorPanelContext (includes onConfigChange) |
| State observation | editor-api |
GraphynEditorStateView — workflow, workflowFlow: StateFlow, selectedNodeId
|
Default* implementations are marked @GraphynExperimentalApi — their signatures may evolve. The interfaces above are stable.
Instead of wiring every plugin by hand, hosts on the JVM and Android can let plugins register themselves via java.util.ServiceLoader. A plugin author ships a resource file:
# src/main/resources/META-INF/services/com.ronjunevaldoz.graphyn.pluginapi.GraphynPlugin
com.example.MyPlugin
Then the host installs everything on the classpath in one call:
val registry = DefaultGraphynPluginRegistry().apply {
install(CorePlugin) // explicit, always present
installDiscovered() // plus any plugin on the classpath (ServiceLoader)
}installDiscovered() skips plugins already installed (matched by metadata.id) and returns the newly-installed ones. On JS, Wasm, and iOS it is a no-op — those hosts register plugins explicitly.
A node may embed a nested WorkflowDefinition via NodeRef.subgraph. The engine runs it recursively (and in parallel where possible), and the subgraph node's resolved inputs are injected into the inner workflow's free input ports (those with no internal connection or config), keyed by port name — so a parent workflow can feed data into a nested one. Explicit inner config and internal wiring always take precedence. A subgraph node's outputs are the inner workflow's free output ports (those nothing inside consumes).
In the editor, select two or more nodes and press Cmd/Ctrl + Shift + G to collapse them into a subgraph node; its boundary ports are derived automatically from the inner workflow. Double-click a subgraph node (or use Enter → in the inspector) to drill into it, and Expand ⤢ in the inspector to inline it again.
The :ai module turns a natural-language prompt into a WorkflowDefinition. WorkflowGenerator has two implementations:
OllamaWorkflowGenerator(OllamaConfig(baseUrl, model)) — calls an Ollama host's /api/generate with format=json. The prompt embeds the node catalog with port types (type — [in:type] -> [out:type]) so the model only emits real node types and fills each node's config with type-matched literals for every unconnected input; WorkflowJsonParser validates the output, coerces config values to each port's WorkflowType, and drops unknown node types / dangling connections rather than failing. Default model qwen2.5-coder:14b.PlaceholderWorkflowGenerator — offline canned output for UI development and no-host scenarios.The editor surfaces this as a docked ✨ AI panel (toggle from the toolbar). Describe a workflow and it generates onto the current canvas, auto-laid-out. The panel keeps a chat transcript so you can iterate, and each result reports what was created plus any unsupported nodes or dropped connections that were sanitized away — so you learn why the graph differs from your request. Generation failures stay inline and retryable.
Every editor action (undo, copy, group, collapse, …) is a rebindable EditorShortcutAction. Open ⌨ Keys in the toolbar, click an action's chord chip, and press the new combination — bindings persist via the settings store and conflicts are rejected. Contextual keys (Escape, Delete) are fixed. See keyboard-shortcuts.md.
| Platform | Status |
|---|---|
| Android | ✅ |
| Desktop (JVM) | ✅ |
| Web (Wasm) | ✅ |
| Web (JS) | ✅ |
| iOS | ✅ XCFramework (SPM) |
| Server (JVM) | ✅ runtime only |
./gradlew :app:desktopApp:run # Desktop
./gradlew :app:androidApp:assembleDebug # Android
./gradlew :app:webApp:wasmJsBrowserDevelopmentRun # Web (Wasm)
./gradlew :app:webApp:jsBrowserDevelopmentRun # Web (JS)Start the Ktor server:
./gradlew :server:runThe server exposes:
| Endpoint | Description |
|---|---|
GET / |
Health check |
GET /nodes |
List all registered node specs |
GET /nodes/{type} |
Get a single node spec by type (404 if unknown) |
POST /validate |
Validate a workflow — returns [ValidationError]
|
POST /execute |
Run synchronously — returns WorkflowExecutionResult
|
POST /executions |
Start async run — returns { runId } (202) |
GET /executions/{id}/events |
SSE stream of per-node events + final result |
GET /workflows |
List persisted workflows (newest first) |
GET /workflows/{id} |
Load a workflow by ID |
POST /workflows |
Save / upsert a workflow (returns WorkflowMeta, 201) |
DELETE /workflows/{id} |
Delete workflow and its version history |
Set GRAPHYN_API_KEY=<secret> in the environment to enable Bearer-token auth. All endpoints except GET / require the token when the env var is present.
:mcp is a stdio MCP server for agents (Claude Desktop, Claude Code, etc.) — generic CRUD + execute over workflows, no template-specific tools. It embeds the engine in-process (FileWorkflowStore, defaults to <project-root>/.graphyn/workflows — override with GRAPHYN_MCP_WORKFLOWS_DIR), no running :server needed.
./gradlew :mcp:installDistAdd it to your MCP client config, e.g. a project's .mcp.json:
{
"mcpServers": {
"graphyn-workflows": {
"command": "./mcp/build/install/mcp/bin/mcp",
"args": []
}
}
}| Tool | Description |
|---|---|
workflow_list |
List all stored workflows |
workflow_get |
Fetch a workflow's full definition by id |
workflow_publish |
Save/update a workflow from raw JSON (validates first, id must start with mcp-) |
workflow_delete |
Delete a workflow and its history (id must start with mcp-) |
workflow_execute |
Run a stored workflow by id, with optional overrides and async
|
workflow_execution_status |
Poll progress for an async run |
workflow_list_node_types |
List registered node types, for authoring workflow_publish payloads |
By default :mcp installs the Shorts, MediaCore, MediaAi, and StableDiffusion plugins on top of the base runtime set. Trim or reorder with GRAPHYN_MCP_PLUGINS (comma-separated, or all):
"env": { "GRAPHYN_MCP_PLUGINS": "shorts,media-core" }workflow_publish/workflow_delete are scoped to ids starting with mcp- — they share :mcp's own store with the desktop editor, and this stops an agent from overwriting or deleting your real workflows by reusing an id. workflow_publish/workflow_execute run against the real engine — nodes like script.eval and io.file_write/io.http_request execute unsandboxed. Tool annotations (destructiveHint/openWorldHint) flag which ones.
Artifacts are published to Maven Central automatically when a version tag is pushed (git tag vX.Y.Z && git push origin vX.Y.Z). For local / manual publishing without CI, use the Doppler-backed script:
# Prerequisites: doppler CLI + DOPPLER_TOKEN in .env or .env.local
# Doppler project "maven-central / prd" must have MAVEN_CENTRAL_USERNAME,
# MAVEN_CENTRAL_PASSWORD, GPG_SIGNING_KEY, GPG_SIGNING_PASSWORD
# Publish all modules at version from gradle.properties
./scripts/publish-local.sh
# Explicit version
./scripts/publish-local.sh 0.9.2
# Single module only (fastest for hotfixes)
./scripts/publish-local.sh 0.9.2 serverSee .env.example for all supported environment variables and docs/reference/compatibility-matrix.md for the full artifact list.
# core is a folder of submodules — check each (core:execution holds the integration test)
./gradlew :core:model:check :core:execution:check :core:serialization:check :core:data:check
./gradlew :app:shared:jvmTest # canvas + editor UI tests (Roborazzi)
./gradlew :plugins:io:jvmTest # I/O plugin tests
./gradlew :server:test # server route testsFull documentation at ronjunevaldoz.github.io/graphyn-editor.
Add the package in Xcode → File → Add Package Dependencies:
https://github.com/ronjunevaldoz/graphyn-editor
Or in Package.swift:
.package(url: "https://github.com/ronjunevaldoz/graphyn-editor", from: "0.1.0")Apache 2.0 © Ron June Valdoz