Applications as the Only Mini-App Resource
Status: draft v2, 2026-07-26. Scope: retire workflows.app_doc, make the
applications table (added 2026-07-25, PR #4479) the single place a mini app
lives, and keep apps and workflows orthogonal in the UI: apps get their own
sidebar panel and their own tabs, and a workflow bound to an app opens only
when the user asks for it from the app surface. A new bundle format ships an
app together with its workflows, and the shipped examples become a curated set
of app bundles instead of one generated form per template.
v2 supersedes the v1 draft: the “view any workflow as an app” on-ramp is dropped, and the plan gains the bundle format and the examples overhaul.
Why
A mini app is not a workflow. The document schema already says so:
ApplicationDocument.operations is a list where every operation carries its
own workflowId, so one app can bind several workflows (or the same workflow
twice with different mappings). Storing the document on workflow.app_doc
collapses that into 1:1, gives the app no identity, no version history, no
publish/release cycle, no budget, and no telemetry — all of which exist on the
applications table and none of which make sense on a workflow row.
The UI inherited the same conflation: the app builder is routed by workflow id
(/app-builder/:workflowId), the workspace View tab toggles a workflow into an
app, and viewing a bare workflow silently writes a generated app_doc onto it.
Users never chose to have an app; every workflow grew one. The result is ~40
shipped templates whose apps are copies of the same form, and a live fork bug:
CreateApplicationFromWorkflowButton copies an app_doc into an application
row and nothing syncs or marks either copy as canonical afterward.
Current state
Two storage paths over one document type:
workflow.app_doc— reached by the web builder route (/app-builder/:workflowId), the/miniapp/:workflowIdruntime and the workspace View tab, mobile (read-only), the CLInodetool app debug, the REST workflow API, the ~40 shipped templates (scripts/generate-template-apps.mjs), the app-preview/marketing pipeline, and the agentui_app_*tools. ThehostWorkflowIdoption onparseApplicationDocumentexists solely to serve this storage.applications+application_versions— web + tRPC only (packages/websocket/src/trpc/routers/applications.ts): sidebar Apps panel, workspaceapplicationtab, Design/Run/Settings surface, publish with pinnedworkflow_graphs, budgets, invocation metering inunified-websocket-runner.ts.
Target state
- Storage: the
applicationstable is the only persisted home for anApplicationDocument.workflow.app_docis accepted on import as a legacy field, lifted into an application row, and never written again. - UI orthogonality: no workflow/app switch anywhere. The Apps sidebar panel is where apps are listed and opened — one workspace tab per app. A workflow never presents itself as an app, and an app tab never morphs into a workflow editor. The workflows an app binds are loaded in the background (graphs prefetched for the app’s operations) and surface only through a Linked workflows menu on the app surface; clicking one opens a normal workflow tab.
- Creation is explicit: an app exists because a user (or an example install) created one — “New app” in the Apps panel, or “Create app from workflow” as a one-way scaffold. No auto-generated per-workflow app.
- Portability: an
ApplicationBundleJSON format serializes an app together with the full graphs of every workflow it binds, so apps ship, export, and import as one artifact. - Examples: shipped example apps are curated bundles — each one a real use case carrying one or more workflows. Workflow templates without a good app use case ship no app at all.
The ApplicationBundle format
One JSON document (zippable for assets), defined in packages/app-runtime:
{
"schemaVersion": 1,
"app": { /* ApplicationDocument — operations reference workflows[].key */ },
"workflows": [
{ "key": "draft", "name": "...", "graph": { "nodes": [], "edges": [] } }
]
}
- Inside a bundle,
OperationBinding.workflowIdholds a bundle-localkey; import creates real workflows and rewrites keys to the new ids (the same indirection.nodetoolbundles use forbundle://asset refs). - Asset-carrying bundles reuse the existing
.nodetoolzip machinery: the bundle JSON rides alongside the asset entries, refs staybundle://. - Export pins what
application_versions.workflow_graphspins today — a released app exports reproducibly.
Tasks
Each task is scoped for one subagent: it names its files, has a checkable
outcome, and states its dependencies. Tasks in the same phase are independent
and can run in parallel. Every task ends with npm run check green and, where
it touches a package with tests, tests for the new behavior.
Phase 0 — decisions locked in this doc
No agent work; recorded so tasks below don’t re-litigate them.
- Apps and workflows are orthogonal resources with orthogonal UI. There is
no workflow→app view, toggle, or tab. The workspace View tab and the
auto-generated
app_docare removed, not made ephemeral (supersedes v1). - Application id is the identity on every surface (tabs, routes, agent tools,
CLI, API). Workflow-scoped app routes (
/app-builder/:workflowId,/miniapp/:workflowId) go away; a redirect covers old links where cheap. ApplicationBundle(above) is the interchange format for examples, export, and import. Examples ship as bundles, not asexample.app_doc.workflow.app_docis deprecated on day one, written by nothing after Phase 2, and dropped in Phase 5.
Phase 1 — foundations (parallel)
T1. Lift-on-read helper in app-runtime
Add liftLegacyAppDoc(workflow) → ApplicationDocument | null to
packages/app-runtime: wraps parseApplicationDocument with
hostWorkflowId, returns a document ready to insert as an application row.
Single implementation shared by the server import path, the migration, and
the CLI.
Files: packages/app-runtime/src/document.ts (+ tests in __tests__/).
Done when: unit tests cover schemaVersion-3 docs, legacy {version,data}
docs, and empty-workflowId operations getting the host id.
T2. Server: applications REST surface
The applications router is tRPC-only. Add REST routes mirroring the tRPC
procedures external clients need: list, get, create, update, delete,
releasedDocument. Reuse the router’s handlers; do not duplicate logic.
Files: packages/websocket/src/http-api.ts,
packages/protocol/src/api-schemas/applications.ts (Zod contract exists),
tests in packages/websocket/src/__tests__/.
Done when: REST and tRPC return identical shapes for the same app.
T3. ApplicationBundle format
Implement the schema, parser, serializer, and key-rewriting in
packages/app-runtime (new src/bundle.ts), with the Zod contract in
packages/protocol. Includes bundleFromApplication(app, graphs) and
applyBundle(bundle) → { app, workflows } (pure — persistence stays in the
server/CLI callers).
Files: packages/app-runtime/src/bundle.ts + tests,
packages/protocol/src/api-schemas/applications.ts.
Done when: round-trip test — export an app binding two workflows, import it,
operations point at the newly created workflow ids.
T4. Docs: rewrite the mini-apps pages
docs/mini-apps.md still says the document “lives on workflow.app_doc” and
never mentions the applications table. Rewrite docs/mini-apps.md,
docs/mini-apps-guide.md, docs/mini-apps-reference.md, and the stale parts
of docs/app-builder.md around the target state: apps are their own
resource, opened from the Apps panel, linked workflows on demand, bundles for
sharing. Follow docs/WRITING_STYLE.md.
Done when: no doc presents app_doc as where apps live or describes a
workflow/app toggle.
Phase 2 — UI orthogonality (parallel, after Phase 1)
T5. Apps panel is the front door
Make the existing sidebar Apps panel
(web/src/components/applications/ApplicationListPanel.tsx) the only entry
point: list, search, “New app”, “Create app from workflow” (explicit
scaffold), each app opening one application workspace tab
(WorkspaceTabsStore already has the tab type). Remove the workflow-keyed
entry points: the /app-builder/:workflowId route and container
(AppBuilderPage.tsx), the workspace View tab, and WorkflowAppView.tsx’s
generate-and-save path. /miniapp/:workflowId redirects to the app tab when
a backing application exists, else 404s.
Files: web/src/components/appbuilder/ (delete/reduce AppBuilderPage,
WorkflowAppView, generateAppDoc, persistence.ts),
web/src/index.tsx routes, web/src/stores/WorkflowManagerStore.ts (drop
app_doc from save payloads), PanelLeft.tsx, Jest tests.
Done when: no web code path writes app_doc and no route keys an app by
workflow id.
T6. App surface: linked workflows in the background
On the app tab (ApplicationSurface.tsx), prefetch the graphs of every
workflow the document’s operations bind (TanStack Query, enabled on tab
focus) without opening editor tabs. Add a Linked workflows menu listing
them (name, operation(s) using it, released-pin status); clicking opens a
normal workflow tab via the workspace tab store. Broken links (deleted
workflow) render as errors in the menu, not crashes.
Files: web/src/components/workspace/ApplicationSurface.tsx, a new
LinkedWorkflowsMenu component (ui_primitives only), hooks in
web/src/hooks/useApplications.ts, tests.
Done when: opening an app never opens a workflow tab; the menu opens one on
click; graphs are fetched once and cached.
T7. Agent tools: key ui_app_* by application id
The Puck agent bridge keys editors by workflow id because workflow id is
currently the app identity. Move the tool contract to application id (no
workflow-id fallback — orthogonality applies to agents too; the scaffold
tool covers “make an app for this workflow”).
Files: web/src/lib/tools/builtin/puck.ts,
web/src/components/appbuilder/puck/puckAgentBridge.ts,
puck/PuckAgentBinder.tsx, web/src/lib/chat/uiContext.ts; update the
app-tools eval suite in packages/agents/src/evals/.
Done when: nodetool eval app-tools passes against the new contract.
Phase 3 — bundles everywhere (parallel, after Phase 1; T8 also after T2)
T8. Server + CLI: bundle export/import
API: GET /api/applications/:id/export-bundle,
POST /api/applications/import-bundle (creates the app and its workflows,
rewrites keys via T3). CLI: nodetool apps export-bundle <id> -o my.app.json
and nodetool apps import-bundle my.app.json, direct-DB like the existing
workflows export-bundle. Editor command-menu entries mirror the workflow
bundle ones.
Depends on: T2, T3. Files: packages/websocket/src/http-api.ts,
packages/cli/src/commands/ (new apps.ts), web command menu, tests.
Done when: export → import on a clean DB reproduces a working app with its
workflows.
T9. CLI: nodetool app debug accepts application id and bundle files
Today the harness loads “the app_doc on a workflow”. Accept an application
id (models layer, no server) or an ApplicationBundle JSON file; keep
workflow-id input as a legacy path that lifts via T1. Multi-operation apps
already work in the harness — this is a loader change.
Depends on: T1, T3. Files: packages/cli/src/commands/app.ts,
packages/cli/src/app-debug/app-spec.ts, debug/target.ts, Vitest tests.
Done when: app debug <application_id> and app debug my.app.json produce
the same report shape an app_doc run did.
T10. Mobile: read applications
Mobile parses workflow.app_doc read-only. Point it at the applications REST
API (T2 — mobile has no tRPC client): list apps, open one per screen per
mobile/ARCHITECTURE.md § Documents. Remove the in-memory per-workflow
generateAppDoc path along with the concept it served.
Depends on: T2. Files: mobile/src/components/app_runtime/, data hooks,
Jest tests.
Done when: an app published on the server renders on mobile without the
workflow carrying any app_doc.
Phase 4 — examples overhaul (after Phase 3)
T11. Curate the example apps
Audit the 37 templates (see docs/plans/examples-revamp.md for the shape
census) and produce a curation spec: which use cases deserve an app, which
templates stay workflow-only, and what each example app contains. Selection
criteria — user-centric, in priority order: (1) the app is how a real person
would want to use it (a form/dashboard beats opening a node graph), (2) it
exercises something the platform is good at (multi-workflow operations,
streaming, variables), (3) it runs on a fresh install or degrades clearly
without keys. Target ~8–12 apps, several binding more than one workflow
(e.g. generate + refine, transcribe + summarize + translate). Deliverable: a
table in this file’s companion docs/plans/example-apps.md with per-app
operations, workflows, and widgets.
Done when: the spec is reviewed and merged; no code.
T12. Build the example app bundles
Implement the curated apps as ApplicationBundle files shipped with
packages/base-nodes (new examples/apps/ dir). Replace the per-template
generation in scripts/generate-template-apps.mjs (curation table +
Output-node augmentation) with a build step that validates each bundle
(nodetool app debug --no-run) and emits the preview bundles in
web/public/app-preview/. Server exposes example apps for install; install
goes through T8’s import path.
Depends on: T8, T11. Files: scripts/ (replace generate-template-apps.mjs),
packages/base-nodes/nodetool/examples/, packages/websocket example
routes, tests.
Done when: installing an example app creates the app + its workflows; no
example JSON carries app_doc.
T13. Preview + marketing pipeline on bundles
Point web/src/app_preview/AppPreviewApp.tsx,
web/scripts/screenshot-app-previews.mjs, and
marketing/scripts/generate-miniapp-entries.mjs at the bundle-derived
previews. /apps/* pages regenerate from the curated set only — fewer,
better pages.
Depends on: T12. Done when: npm run gen:apps runs clean and every /apps/*
page corresponds to a shipped example app.
Phase 5 — data migration and removal
T14. One-time lift migration for user data
A models-layer migration (packages/models/src/migrations/versions.ts) that
walks workflows with a non-null app_doc, creates an application row per doc
via T1’s helper, and nulls the value. Where a fromWorkflowId import already
created a (possibly newer) application, the application wins and the
app_doc is archived as a new draft version, not merged.
Depends on: T1; lands only after Phase 2 (no writers left).
Done when: migration test proves a seeded app_doc workflow ends up with an
equivalent application and an empty app_doc.
T15. Drop app_doc from schema and API
Remove the column (SQLite + PG schemas, raw DDL in db.ts), the Zod/API
fields (packages/protocol), the now-unreachable hostWorkflowId lift
paths, and the screenshot-server read. Grep-clean: rg app_doc returns only
migration history and this plan.
Depends on: everything above shipped, and T14 released at least one version
earlier so old clients have upgraded.
Sequencing summary
Phase 1: T1 T2 T3 T4 — parallel
Phase 2: T5 T6 T7 — parallel, after Phase 1
Phase 3: T8(←T2,T3) T9(←T1,T3) T10(←T2) — parallel
Phase 4: T11 → T12(←T8) → T13
Phase 5: T14(←T1, after Phase 2) → T15 (one release later)
Risks
- Fork already in the wild: users who used
create({ fromWorkflowId })have divergent copies; T14’s application-wins rule covers it, but the archived draft version must be visible in the version history or the user’s edits look lost. - Old mobile/CLI clients read
app_docfrom the API. T15 waits a release after T14 for this reason; T2/T9/T10 must ship first. - Removing the View tab regresses a flow people may use today (instant form for any workflow). The mitigation is the explicit scaffold (“Create app from workflow”) being one click from both the Apps panel and the workflow editor — cheap to invoke, but a deliberate act that creates a real resource.
- Example count drops from ~40 auto-generated apps to ~8–12 curated ones;
/apps/*marketing pages shrink accordingly. That is the point, but existing inbound links need redirects (docs/redirects/).