diff --git a/README.md b/README.md
index bbbd069..267e726 100644
--- a/README.md
+++ b/README.md
@@ -56,7 +56,16 @@ cd Blacknode
The launcher installs the local dependencies and opens
`http://localhost:3000`.
-To try the graph first, follow the [Beginner Walkthrough](docs/walkthrough.md).
+In the welcome screen, choose **Open templates**, open **Text Pipeline**, and
+press **Run once** in the top bar. The **Output** node shows `Hello World`.
+Change a Text node's value and run again to see your own result.
+
+For a visual result, open **Generic Point Cloud Viewer** and press **Run once**
+to inspect the included colored point cloud in 3D. Both workflows run locally
+with the built-in nodes.
+
+Follow the [Beginner Walkthrough](docs/walkthrough.md) to save your work and
+choose the next task.
## Pair your first device
@@ -114,6 +123,18 @@ The optional robot-learning starter prepares collection, training, and
simulation workflows as the Project gains the required data and artifacts. It
never starts a physical or compute action automatically.
+## Develop your own App
+
+Build or customize a node workflow, press **Create App**, and choose the
+parameters, results, and run buttons your team needs. **Save & open App** turns
+that workflow into an operator interface. Switch back with **Edit workflow**
+to change its behavior, or use **File → Package App…** to share it.
+
+Projects organize your application alongside its robots, datasets, training
+runs, policies, and deployments. Start with a working workflow and extend it
+for your own task. See [Create your own App](docs/operator-apps.md#create-your-own-app)
+and [Guided Projects](docs/guided-projects.md).
+
## Add capabilities
Blacknode core owns the graph, editor, runtime, replay, exports, package system,
diff --git a/docs/operator-apps.md b/docs/operator-apps.md
index 3df986b..1a4cb9e 100644
--- a/docs/operator-apps.md
+++ b/docs/operator-apps.md
@@ -8,6 +8,36 @@ Pressing a shortcut for a workflow with `metadata.operator_view` opens its App
surface. **Edit workflow** reveals the nodes and connections, and the **App**
control in the workflow tab bar returns to the operator surface.
+## Create your own App
+
+1. Build a workflow or open a template and run it to check its result.
+2. Press **Create App** beside the workflow tabs, or choose **File → Create App…**.
+3. Give the App a name. Use **+ Parameter** to expose a text or numeric task
+ setting, **+ Result** to display a workflow output, and **+ Run button** to
+ cook a chosen output. Choose each target from the workflow's available nodes.
+4. Give the controls labels your operators understand. Results can display
+ text, metrics, status, images, or a web viewer. A run button can run once or
+ start a live service; add a confirmation message when the action requires it.
+5. Press **Save & open App**. Configure the task and press your run button to
+ see the result. Saving and opening the App does not execute the workflow.
+6. Use **Edit workflow** to change the nodes and **App** to return to the
+ controls. Press **Save** after changing parameters. **Edit App** reopens
+ the designer to change the name or controls you added.
+7. Reopen the saved workflow from **Workflows** and select **App**, or use
+ **File → Package App…** to share an installable ZIP.
+
+Parameters connected to other nodes are configured by those upstream nodes;
+the designer offers unconnected text and numeric parameters. Credential fields
+stay in the existing credential configuration. Existing App sections, primary
+run targets, settings, and safety controls are retained when adding controls.
+Advanced field types and direct service actions use the operator-view contract
+below.
+
+For a robot-learning application, link the saved workflow to a Project. Expose
+its task, dataset, and training settings, then display its progress, results,
+and viewer outputs. [Guided Projects](guided-projects.md) describes the existing
+dataset, training, and policy workflow handoffs.
+
## Operator view contract
An operator view is declared inside workflow metadata:
diff --git a/docs/walkthrough.md b/docs/walkthrough.md
index 76eaeff..e43ac69 100644
--- a/docs/walkthrough.md
+++ b/docs/walkthrough.md
@@ -1,8 +1,25 @@
# Blacknode Beginner Walkthrough
-This is the click-by-click path for trying every main Blacknode feature. Start
-with the no-key steps first, then add NVIDIA NIM, MCP, or Docker when the local
-workflow is working.
+Start with a visible result, then choose the capabilities your task needs.
+
+## Your first result
+
+1. Run `.\start.bat` on Windows or `./start.sh` on macOS/Linux. The launcher
+ installs dependencies and opens the editor.
+2. Choose **Open templates** in the welcome screen.
+3. Open **Text Pipeline** under **Core**.
+4. Press **Run once** in the top bar. The **Output** node displays `Hello World`.
+5. Select a **Text** node, change its `value` in **Properties**, and press
+ **Run once** again. Your changed text appears in the result.
+
+For a 3D result, open **Generic Point Cloud Viewer** and press **Run once**.
+The viewer displays the included colored point cloud; right-drag to orbit and scroll
+to zoom. Both starting workflows use built-in nodes on your computer.
+
+Open **Workflows** to name and save your graph, or **Runs** to inspect a previous
+result. For a real robot, continue with [Pair your first device](../README.md#pair-your-first-device).
+For simulation, install the Newton package from **Packages** and open one of
+its templates. The sections below cover the rest of the editor as you need it.
## What You Need
@@ -59,12 +76,10 @@ What this does:
First-run time depends on network speed and whether pip/npm packages are already
cached. Later starts normally complete in less than one minute.
-On the first launch of this Blacknode workspace, a welcome message opens the
-**Packages** tab. Use it to install the official packages needed by robotics,
-ROS 2, vision, CUDA, dataset, and training templates. Choose **Continue with
-core graph** when you want to begin with built-in nodes. Blacknode records the
-choice in `.blacknode/onboarding.json`; Packages stays available in the left
-sidebar.
+On the first launch, choose **Open templates** to start with built-in nodes.
+Choose **Explore packages** when you already know which robotics, ROS 2, vision,
+CUDA, dataset, or training capabilities you need. Blacknode records the choice
+in `.blacknode/onboarding.json`; Packages stays available in the left sidebar.
The welcome message opens only after the backend confirms that onboarding has
not been completed. A backend startup or connectivity error keeps the current
diff --git a/editor-server/server.py b/editor-server/server.py
index 869ee87..c83a303 100644
--- a/editor-server/server.py
+++ b/editor-server/server.py
@@ -38,6 +38,7 @@
from blacknode.learned import registry as learned_registry
from blacknode.mcp import tools as mcp_tools
from blacknode.node import _NODE_REGISTRY
+from blacknode.operator_views import OperatorViewValidationError, validate_operator_view
from blacknode.nodes import ai as ai_nodes
import blacknode.package_index as bn_package_index
from blacknode.packages import MANIFEST_NAME as BN_MANIFEST_NAME
@@ -2757,6 +2758,45 @@ def update_workflow_requirements(req: UpdateWorkflowRequirementsReq):
return {"metadata": dict(metadata)}
+@app.patch("/graph/operator-view")
+def update_workflow_operator_view(view: dict[str, Any]):
+ try:
+ validate_operator_view(view)
+ except OperatorViewValidationError as exc:
+ raise HTTPException(400, str(exc)) from exc
+ if not re.fullmatch(r"[a-z][a-z0-9_-]{0,63}", str(view.get("id") or "")):
+ raise HTTPException(400, "App ID must start with a lowercase letter and use letters, numbers, hyphens, or underscores.")
+ nodes = {node["id"]: node for node in get_graph()["nodes"]}
+
+ def check_references(value):
+ if isinstance(value, list):
+ for item in value:
+ check_references(item)
+ elif isinstance(value, dict):
+ if "node_id" in value:
+ node = nodes.get(value["node_id"])
+ if node is None:
+ raise HTTPException(400, f"App references missing node: {value['node_id']}")
+ outputs = {"value"} if node["type"] == "Output" else set(node["outputs"])
+ if "port" in value and value["port"] not in outputs:
+ raise HTTPException(400, f"App references missing output: {value['node_id']}.{value['port']}")
+ if "param" in value:
+ definition = _NODE_REGISTRY.get(node["type"])
+ inputs = (set(node["inputs"]) | set(node.get("params", {}))
+ | set(getattr(definition, "_bn_inputs", []))
+ | set(getattr(definition, "_bn_input_defaults", {})))
+ if value["param"] not in inputs:
+ raise HTTPException(400, f"App references missing parameter: {value['node_id']}.{value['param']}")
+ for key, item in value.items():
+ if key not in {"value", "payload"}:
+ check_references(item)
+
+ check_references(view)
+ _session.metadata = {**_session.metadata, "operator_view": copy.deepcopy(view)}
+ _save()
+ return {"metadata": dict(_session.metadata)}
+
+
@app.post("/nodes")
def add_node(req: AddNodeReq):
if req.type_name in _SUBGRAPH_NODE_TYPES:
diff --git a/editor/src/App.tsx b/editor/src/App.tsx
index 820cd5b..d779da3 100644
--- a/editor/src/App.tsx
+++ b/editor/src/App.tsx
@@ -41,6 +41,7 @@ const LocalFilePicker = lazy(() => import('./components/LocalFilePicker'))
const WorkflowOperatorView = lazy(() => import('./components/WorkflowOperatorView'))
const CustomerAppShell = lazy(() => import('./components/CustomerAppShell'))
const AppPackageDialog = lazy(() => import('./components/AppPackageDialog'))
+const AppDesignerDialog = lazy(() => import('./components/AppDesignerDialog'))
const NODE_TYPES = {
blacknode: BlackNode,
@@ -273,7 +274,7 @@ function WorkspaceApp() {
beginAltDragCopy, finishAltDragCopy, undoGraph,
checkServer, reset, newTab, insertTab, switchTab, closeTab, duplicateTab,
openGraphAsTab, openWorkflowAsTab, setActiveTabSurface, renameTab, saveActiveWorkflow,
- diveIntoSubnet, exitSubnet, collapseToSubnet, organizeNodes, cookNode, stopCook, stopRuntimeServices, dismissCookStatus, applyRunReplay,
+ subnetStack, diveIntoSubnet, exitSubnet, collapseToSubnet, organizeNodes, cookNode, stopCook, stopRuntimeServices, dismissCookStatus, applyRunReplay,
handleLearnedNodeEvent, updateParam,
} = useStore()
@@ -321,6 +322,7 @@ function WorkspaceApp() {
const [simulationViewerHeight, setSimulationViewerHeight] = useState(loadSimulationViewerHeight)
const [fileMenuOpen, setFileMenuOpen] = useState(false)
const [appPackageDialogOpen, setAppPackageDialogOpen] = useState(false)
+ const [appDesignerOpen, setAppDesignerOpen] = useState(false)
const [openingAppPackageDialog, setOpeningAppPackageDialog] = useState(false)
const [fileMenuPosition, setFileMenuPosition] = useState({ top: 0, left: 0 })
const [simulationViewerMenuOpen, setSimulationViewerMenuOpen] = useState(false)
@@ -2065,6 +2067,13 @@ function WorkspaceApp() {
Import workflow…
JSON or Python
+ {!hostedPreview && (
+
+ )}
{!hostedPreview && (