Building advanced apps (Excalidraw / drawdb class)
The most powerful Digitorn apps — a diagram canvas, an ER-schema designer, a
spreadsheet, a document editor — are not written from scratch. Each is a
capable existing frontend adopted whole and bound to the agent through
@digitornai/sdk. The heavy lifting (the editor) is borrowed; the value Digitorn
adds is a thin bridge that makes the agent and the user co-drive the same
live document.
Every claim on this page is anchored to real, shipped apps you can read:
| App | Adopted frontend | Bridge source |
|---|---|---|
| Excalidraw (canvas) | npm @excalidraw/excalidraw | packages/digitorn-sdk/examples/excalidraw/src/App.tsx |
| drawdb (ER schema) | the whole drawdb OSS repo, vendored | .../examples/drawdb/src/digitorn/SyncBridge.jsx |
| Univer (spreadsheet) | npm @univerjs/presets | .../examples/univer/src/App.tsx + agent-actions.ts |
| LaTeX (document viewer) | npm pdfjs-dist | .../examples/latex/src/App.tsx |
Read 48-digitorn-sdk.md for every hook's exact signature,
47-custom-workspace-views.md for the view YAML,
and 44-client-manifest.md for the ui.workspace block.
This page is how you put them together into something advanced. Never rewrite a
capable editor — adopt one.
Two adoption routes
- Wrap an npm component (Excalidraw, Univer, a PDF viewer). The app is a
thin
App.tsxthat renders the component and wires the bridge inline. Fastest and cleanest when a good component exists on npm. - Vendor a whole OSS repo (drawdb). Copy the project's
src/into the app, add a smallsrc/digitorn/bridge folder (drawdb:SyncBridge.jsx,ThemeSync.jsx,main.jsx), and build. Use this when the capability lives in a full app, not a single package.
Either way the built dist/ ships at {app}/web/dist/ and the app declares
ui.workspace.render_mode: react + entry_file: (the document file the editor
centers on — scene.excalidraw, diagram.drawdb.json, workbook.json,
main.tex).
The bridge: bind the editor to ONE workdir file
The core idea (verified in both excalidraw/src/App.tsx and
drawdb/.../SyncBridge.jsx): the editor's document is bound to one workdir
file with useSharedDoc, so it round-trips through the agent — the agent edits
the file → the editor updates; the user edits the canvas → the file updates → the
agent sees it next turn.
const [scene, setScene] = useSharedDoc<Scene>("scene.excalidraw", { elements: [] });
That single line is deceptively simple. Making it not-broken requires four invariants — both example bridges implement all four, independently, because skipping any one produces a write loop or a clobbered document:
- Fingerprint baseline. Compute a
fingerprint()of the meaningful state (elements, not zoom/scroll/selection) and keep it in a ref (appliedFp). It is the shared baseline for both directions. - Drop your own echo. A programmatic update you push into the editor fires
the editor's
onChange; if its fingerprint matchesappliedFp, it is your own echo — drop it, don't write it back. (Excalidraw also sets asuppressref right before a programmaticupdateSceneto catch the echo.) - Read-only until interacted. The editor fires an empty
onChange([])while it mounts. Gate writes behind aninteractedref so that mount-time empty state cannot overwrite a document the agent already wrote to the file. - High-level inputs expand INTO state, never rewritten. An input like
diagram.mmd(Excalidraw) orschema.dbml(drawdb) is the agent's small, human-readable source; you expand it into the real document but treat it as an INPUT you never write back — otherwise expansion loops forever. On a failure, write the error to a file the agent can read (diagram.error.txt) instead of clobbering a good document.
If your bridge scrolls, churns the file on zoom, or wipes the agent's work on load, it is missing one of these four. They are the whole game.
Exploit the SDK to the maximum
An advanced app uses far more than useSharedDoc. The full power surface, each
verified against the SDK source:
The agent drives the LIVE app (the "wow" layer)
useAgentActions lets the agent invoke semantic actions on the running
editor — not just write files. The app defines what each action means; the
agent calls it by name through the preview channel.
useAgentActions({
add_chart: (args) => chartManager.create(toSpec(args)),
switch_sheet: ({ name }) => workbook.setActiveSheet(String(name)),
select: ({ range }) => selection.set(String(range)),
});
This is exactly Univer's edge (examples/univer/src/agent-actions.ts): the agent
creates and updates native charts on the live canvas in natural language,
while the user keeps full manual control of the same engine — no conflict. Any
advanced app should expose its high-value operations this way.
The app asks the agent (select → Ask AI)
A view cannot directly drive the turn — useChat().send, abort, and
useApprovals().approve/reject are exported but rejected by the daemon from
inside a view (its preview token is a read-only identity: "session belongs to
another user"). That limit is real; don't build a UI that pretends to send as
the user.
What a view CAN do is ask the host to relay an instruction, which the host
posts into the chat under the user's own auth: sendToAgent(instruction, context)
and the ready-made askAI({ anchor, context }) bubble. This is the powerful
"select something in the editor → Ask AI → the agent acts on exactly that"
pattern, and the askAI UI ships inside the SDK so every app gets the same flow:
const rect = range.getBoundingClientRect();
askAI({ anchor: rect, context: { kind: "selection", source: "document.docx", text } });
The agent SEES the app
useAgentSnapshot(capture) publishes a real PNG of the view when the agent
requests one (it writes snapshot.request; your capture writes the PNG and
stamps snapshot.ready). Wire it when the app can export its own canvas cleanly
(Excalidraw's exportToBlob) for a sharper image than the generic
preview.snapshot tool's headless shot.
Host chrome, live build, review, theme
useTopBarActions([...])+switchView(id)— put the app's own buttons and menus into the host top bar (Deploy, Export, Refresh); they appear only while this view is active.stylegives a native-looking primary CTA.useCompile()— a one-click rebuild of the current document with no agent turn (the app owns the button viauseTopBarActions, this owns the action).usePreviewAttach()— render your own empty state until the agent has built something, then switch to the live build; keeps switching on every new build. The primitive behind a Lovable-style build→preview.reportProposals(state)/onReviewCommand(handler)— surface a review affordance ("12 suggestions") and accept/reject/next/prev pending changes, for a suggest-then-apply workflow.useTheme()+useThumbnailMode()— match the host's light/dark live, and render clean (no chrome, fit-to-frame) when captured for a gallery thumbnail.
Four complete archetypes
1. Canvas editor — Excalidraw (wrap an npm component)
deps: @excalidraw/excalidraw + @excalidraw/mermaid-to-excalidraw
entry_file: scene.excalidraw # useSharedDoc document
bridge: examples/excalidraw/src/App.tsx
uses: useSharedDoc + all 4 invariants, useFile("diagram.mmd") high-level
input, useAgentSnapshot (exportToBlob), useTheme, useThumbnailMode
The whole editor is the npm component; the bridge binds {elements, appState, files} to scene.excalidraw, expands diagram.mmd into elements, and strips the
stock socials/Export in favor of a Digitorn "Generate PNG/SVG" that writes to the
workdir.
2. ER-schema designer — drawdb (vendor a whole OSS repo)
adopt: the drawdb OSS repo, copied into src/
entry_file: diagram.drawdb.json
bridge: examples/drawdb/src/digitorn/SyncBridge.jsx (+ ThemeSync.jsx, main.jsx)
uses: useSharedDoc + all 4 invariants, useFile("schema.dbml") high-level
input (DBML → diagram), useThumbnailMode (fit on load)
The bridge reads drawdb's own React contexts (useDiagram, useAreas, …), binds
their combined state to diagram.drawdb.json, and lets the agent author with
readable DBML that expands into the visual schema.
3. Spreadsheet — Univer (wrap npm + agent-driven canvas)
deps: @univerjs/presets (+ conditional-formatting, data-validation, …)
entry_file: workbook.json
bridge: examples/univer/src/App.tsx + agent-actions.ts
uses: useSharedDoc (workbook), useAgentActions (create/update native
charts, scroll, switch sheet, select — on the LIVE sheet)
The standout: agent-actions.ts exposes live-sheet operations the agent drives in
natural language, beating a manual-only tool — while the user edits the same
engine simultaneously.
4. Document viewer — LaTeX (wrap a renderer, read-only preview)
deps: pdfjs-dist
entry_file: main.tex
bridge: examples/latex/src/App.tsx
uses: useWorkspaceEndpoint().fileUrl to load the compiled PDF, useWatch to
re-render when the agent recompiles
Not every advanced app is a two-way editor. LaTeX is a render/preview
archetype: the agent compiles main.tex → PDF with its own tools, and the view
shows it live via pdfjs, re-rendering on each rebuild.
Build and ship
Same for all four:
# in .digitorn-work/<app>/ — source stays out of the app bundle
npm install @digitornai/sdk socket.io-client <the-adopted-frontend>
# socket.io-client is a required peer dependency of @digitornai/sdk
# vite.config: base: "./", build.outDir: "dist"
npm run build
mkdir -p ../../web/dist && cp -r dist/* ../../web/dist/ # only the build crosses in
Then declare it:
ui:
workspace:
render_mode: react
entry_file: scene.excalidraw # the document the editor centers on
position: right
default_view: preview
base: "./" is mandatory (the bundle is served from a nested path); without it
the view loads blank. At runtime the app needs no Node — web/dist/ is static.
Caveats (all verified — keep them true)
- A view can't act as the account owner.
useChat().send/abortand approvals are blocked from a view; usesendToAgent/askAIto relay through the host instead. - The four bridge invariants are not optional — skipping any one gives a write loop or a clobbered document.
- Adopt, don't rewrite. If a capable frontend exists (npm or OSS), wrap or vendor it; hand-writing an Excalidraw-class editor is neither realistic nor the point.
- Only claim what you built and verified live in Studio (
preview.inspect): the editor renders, the agent's edits appear, the user's edits reach the file.