# DreamLake — Full documentation > DreamLake is ML experiment tracking for robotics and embodied AI — record, store, browse, and search multimodal data, publish renderable artifacts, and run remote compute. Generated from https://docs.dreamlake.ai. 111 pages. --- Source: https://docs.dreamlake.ai # DreamLake ML experiment tracking for robotics and embodied AI. Record, store, browse, and search multimodal episode data — video, audio, tracks, logs, and parameters — from one CLI, one Python SDK, and one dashboard. ## Install One command, and there is no runtime to install first. The `dreamlake` CLI is a single native binary that lives in your home directory and keeps itself up to date. **macOS, Linux, WSL** ```bash file="terminal" curl -fsSL https://dl.dreamlake.ai/install.sh | bash ``` **Windows (PowerShell)** ```powershell file="terminal" irm https://dl.dreamlake.ai/install.ps1 | iex ``` **Windows (CMD)** ```batch file="terminal" powershell -NoProfile -Command "irm https://dl.dreamlake.ai/install.ps1 | iex" ``` Then open a new terminal and check it worked: ```bash file="terminal" dreamlake --version ``` > **Note:** Downloads the binary for your platform, checks its SHA-256 against the > release manifest, and installs entirely under `$HOME`: > > - `~/.local/share/dreamlake/versions/` — the binary > - `~/.local/bin/dreamlake` — symlink to the active version > - appends one line to your login shell's rc file if `~/.local/bin` isn't > already on `PATH` > > No `sudo`, and nothing outside your home directory. The installer refuses > to run under `sudo` for exactly this reason. Platforms, release channels, version pinning, troubleshooting, and uninstall steps are in the [install reference](#install-reference) at the bottom of this page. ## Your first episode The walk-through below takes about five minutes. Every command assumes `dreamlake` is on your `PATH`. ### Authenticate ```bash file="terminal" dreamlake login ``` Opens your browser for OAuth device auth and stores a token locally — you only do this once per machine. On headless machines, add `--no-browser` to get a QR code instead. Point at a self-hosted server with `--url `. ### Upload ```bash file="terminal" dreamlake upload ./run01.mp4 \ --episode alice@robotics:run-042 \ --to /camera/front ``` That single command chunks the file, uploads it to S3-backed storage, registers the asset on the server, and (for video) triggers HLS splitting — the episode is immediately browsable in the dashboard. File type is auto-detected from the extension. Large files are chunked (10 MB parts, 4 parallel workers) and uploaded via S3 multipart, so an interrupted upload resumes where it left off. | Extension | Type | |-----------|------| | `.mp4`, `.mov`, `.mkv`, `.webm` | video | | `.wav`, `.mp3`, `.flac`, `.aac` | audio | | `.vtt`, `.srt` | text-track | | `.jsonl`, `.csv` | label-track | ### List and download ```bash file="terminal" dreamlake list --episode alice@robotics:run-042 dreamlake download --episode alice@robotics:run-042 --from /camera/front -o ./video.mp4 ``` ### Organize with bindrs and datasets A **bindr** is a curated collection of files matched by glob; a **dataset** groups bindrs into a training-ready unit: ```bash file="terminal" dreamlake create bindr "front-camera" --project robotics@alice --episode "2026/04/*" dreamlake create dataset "training-v1" --project robotics@alice dreamlake update dataset "training-v1" --project robotics@alice --add "front-*" ``` See the [CLI Reference](/cli.md#collections) for the full create / update / delete / list surface. ### Episode syntax The `--episode` flag everywhere uses `[namespace@]project[:episode]`: | Example | Namespace | Project | Episode | |---------|-----------|---------|---------| | `robotics` | (current user) | `robotics` | — | | `alice@robotics` | `alice` | `robotics` | — | | `alice@robotics:run-042` | `alice` | `robotics` | `run-042` | ### Environment variables | Variable | Description | |----------|-------------| | `DREAMLAKE_REMOTE` | Server URL (overrides the stored login) | | `DREAMLAKE_BSS_URL` | BSS storage URL | | `DREAMLAKE_API_KEY` | API token — skips interactive login, for CI | ## How it fits together ``` dreamlake CLI / Python SDK ├── Upload file ──→ BSS (S3 storage, HLS splitting) └── Register asset ──→ DreamLake Server (catalog, auth) ├──→ Dashboard (browse, render, share) └──→ Qdrant (semantic search) ``` | Component | Role | |-----------|------| | **dreamlake CLI** | Native binary — upload, download, collections, artifacts, workflows — [own docs](https://cli.dreamlake.ai) | | **dreamlake-py** | Python SDK — video slicing, tracks, vector index | | **dreamlake-server** | Fastify API — catalog, auth (JWT), visibility, search routing | | **BSS** | S3-backed binary storage with HLS video splitting | | **Dashboard** | Browse episodes, render artifacts, manage sharing at [dreamlake.ai](https://dreamlake.ai) | | **Qdrant** | Vector index (CLIP embeddings) behind semantic search | | **Lakeshore** | Queue-native remote compute — [own docs](https://lakeshore.dreamlake.ai) | ## Explore the docs Load, slice, and batch video in Python with lazy, NumPy-style indexing — frames to tensors in one line. Query hours of footage with natural language — CLIP embeddings over 2-second chunks, indexed in Qdrant. Push a renderable file, get a live page — versioned, shareable, and safe to delete. Every `dreamlake` command, versioned with each release — full reference at cli.dreamlake.ai. The REST surface behind the CLI and dashboard — auth, episodes, nodes, tracks, search, and artifacts. How the pieces fit: the data model, node tree, auth flow, and upload pipeline. DreamLake's elastic compute fabric — run Python functions on remote workers with a decorator. The dreamlake-ai org — SDKs, server, skills, and example pipelines. Ready to go deeper? Jump straight to the [Architecture](/architecture.md) overview. ## Install reference Everything below is the long tail of installing and maintaining the CLI. You do not need any of it to get started. ### Supported platforms | Platform | Architectures | |---|---| | macOS 13+ | Apple Silicon, Intel | | Linux (glibc) | x86-64, arm64 | | Linux (musl / Alpine) | x86-64, arm64 | | Windows 10+ | x86-64, arm64 | 32-bit Windows is not supported. Inside WSL, use the Linux command rather than the PowerShell one — you are installing into the Linux side. ### Staying up to date At most once every four hours, a command may start a short detached background process that checks for a new release and installs it; the new version takes effect the next time you run a command. To update right now: ```bash file="terminal" dreamlake self-update ``` > **Note:** Windows locks a running `.exe`, so there is no background updater there. > Run `dreamlake self-update` when you want a new version. `dreamlake > self-update --status` reports this under `auto:`. > **Note:** `dreamlake update` edits bindrs, datasets, and projects. Updating the CLI > itself is `dreamlake self-update`. ### Release channels `latest` (the default) gets every release as it ships. `stable` trails by about a week and skips releases with known regressions. ```bash file="terminal" dreamlake self-update channel stable # switch channel dreamlake self-update # apply it ``` ### Pin a version Asking for an exact version pins it: auto-update stops until you choose a channel again. This is what you want in a CI image or when a release regresses. **Windows (PowerShell) tab:** On Windows, pass the target through the script block: **macOS, Linux, WSL** ```bash file="terminal" curl -fsSL https://dl.dreamlake.ai/install.sh | bash -s 0.3.0 dreamlake self-update 0.3.0 # or, once installed dreamlake self-update channel latest # unpin and resume updates ``` **Windows (PowerShell)** ```powershell file="terminal" & ([scriptblock]::Create((irm https://dl.dreamlake.ai/install.ps1))) 0.3.0 ``` `dreamlake self-update --status` shows whether you're pinned. ### Turn updates off | Variable | Effect | |---|---| | `DREAMLAKE_DISABLE_AUTOUPDATER=1` | No background checks; `self-update` and `install` still work | | `DREAMLAKE_DISABLE_UPDATES=1` | Blocks every path that changes your version, including `self-update` and `install` | ### Troubleshooting `dreamlake doctor` prints where the binary lives, which version the launcher points at, your channel, and the last auto-update result — no network calls, no session started. **`command not found` after installing.** The installer appended to your shell rc but the current shell predates it. Open a new terminal, or: ```bash file="terminal" export PATH="$HOME/.local/bin:$PATH" ``` **`dreamlake` still runs an old version.** Something else on your `PATH` answers to the same name — usually an npm install of the CLI, or the retired Python console script. The installer warns about this, and `doctor` names the file under `on PATH:`. Check with: ```bash file="terminal" which -a dreamlake ``` Then remove the other one, or put `~/.local/bin` ahead of it: ```bash file="terminal" npm uninstall -g @dreamlake/dreamlake-cli # if it came from npm ``` This one is worth ruling out first, because it does not look like a failure: the old binary keeps answering and every command appears to work. ### From source For working on the CLI itself: ```bash file="terminal" git clone https://github.com/dreamlake-ai/dreamlake-cli.git cd dreamlake-cli && pnpm install pnpm cli --help ``` ### Uninstall **Windows (PowerShell) tab:** On Windows: **macOS, Linux, WSL** ```bash file="terminal" rm -f ~/.local/bin/dreamlake rm -rf ~/.local/share/dreamlake ~/.local/state/dreamlake ~/.dreamlake ``` **Windows (PowerShell)** ```powershell file="terminal" Remove-Item "$env:USERPROFILE\.local\bin\dreamlake.exe" -Force Remove-Item "$env:USERPROFILE\.local\share\dreamlake" -Recurse -Force ``` Add `rm -rf ~/.config/dreamlake` to also drop your saved logins. On macOS and Linux, remove the `# added by the dreamlake installer` block from your shell rc; on Windows, drop `%USERPROFILE%\.local\bin` from your user `Path` in **System Properties → Environment Variables**. --- Source: https://docs.dreamlake.ai/ml-dash # ML-Dash ML experiment tracking and data storage. Log parameters, metrics, logs, files, and time-series tracks from Python, keep them on disk or send them to a dash.ml server, and browse them in the dashboard. This section documents ML-Dash SDK **0.7.0** and CLI **0.1.1**. The dash.ml web dashboard is documented separately at [docs.dash.ml](https://docs.dash.ml/dashboard/overview). ## Install ML-Dash comes in two parts, installed separately: | Part | What it does | Install | |---|---|---| | **Python SDK** — the `ml_dash` package | Logs from your training code | `pip install ml-dash` | | **CLI** — the `ml-dash` command | Logs in, lists projects, uploads and downloads runs | standalone binary or npm | You only need the SDK to track runs locally. Install the CLI as well to log in to dash.ml. ### Python SDK ```bash pip install ml-dash ``` Python 3.9 or newer. Two optional extras: - `ml-dash[auth]` — reads the login token from the OS keychain. You need it on machines where `ml-dash login` stores the token there (see [Authentication](/ml-dash/get-started/authentication.md#where-the-token-lives)). - `ml-dash[video]` — saves videos from frame arrays (`save_video`). ```bash pip install "ml-dash[auth]" ``` ### CLI **macOS, Linux:** one self-contained binary. You don't need Node or Python. ```bash curl -fsSL https://pub-42e1dcc7de574d4a92984865fdc95f10.r2.dev/install.sh | sh ``` **Windows (PowerShell):** ```powershell irm https://pub-42e1dcc7de574d4a92984865fdc95f10.r2.dev/install.ps1 | iex ``` **If you already have Node.js 20.19 or newer:** ```bash npm install -g @dreamlake/ml-dash ``` The npm package is scoped, but the command it installs is still `ml-dash`. Check it worked: ```bash ml-dash version ``` > **Warning:** From SDK **0.7.0**, `pip install ml-dash` installs only the Python SDK. The > `ml-dash` command is now a separate program and is installed on its own, as > shown above. See [Upgrading from 0.6](#upgrading-from-06). ## Your first experiment ### Track a run locally You don't need an account or a server. By default an experiment writes to `.dash/` in the current directory: ```python from ml_dash import Experiment with Experiment(prefix="alice/tutorial/first-run").run as exp: exp.params.set(learning_rate=0.001, batch_size=32, epochs=10) exp.log("Training started") for epoch in range(10): loss = 1.0 - epoch * 0.08 # your real loss here exp.metrics("train").log(loss=loss, epoch=epoch) exp.log("Training finished") ``` The prefix is `owner/project/experiment`. The run lands on disk as: ``` .dash/ └── alice/ # owner └── tutorial/ # project └── first-run/ # experiment ├── logs/logs.jsonl ├── parameters.json └── metrics/train/data.jsonl ``` ### Log in ```bash ml-dash login ``` The CLI prints a short code and a QR code, and opens your browser to approve it. The token is saved on your machine, and the SDK reads it from there. See [Authentication](/ml-dash/get-started/authentication.md). ### Send runs to dash.ml Pass `dash_url`, and the same code also writes to the server: ```python with Experiment( prefix="alice/tutorial/first-run", dash_url="https://api.dash.ml", ).run as exp: ... ``` When the run starts, the SDK prints a link to it on [dash.ml](https://dash.ml). Runs you already tracked locally can be uploaded with the CLI: ```bash ml-dash upload # everything under ./.dash ml-dash list # confirm it arrived ``` ## How it fits together ``` training script ── ml_dash SDK ──┬──→ .dash/ on disk (local mode) └──→ ML-Dash server ──→ dash.ml dashboard ↑ (remote mode) ml-dash CLI ── login, list, upload, download ``` | Component | Role | |---|---| | **`ml_dash` (PyPI)** | Python SDK. `Experiment` with `params`, `metrics`, `logs`, `files`, and `tracks` | | **`ml-dash` CLI** | Login, projects, bulk upload and download, raw GraphQL. Ships from npm and as a standalone binary | | **ML-Dash server** | REST and GraphQL API at `https://api.dash.ml` that stores runs, metrics, and files | | **Dashboard** | Browse, chart, and compare runs at [dash.ml](https://dash.ml) | ## Explore the docs Prefixes, local, hybrid, and remote mode, and the run lifecycle. Step-indexed scalars: loss curves, accuracy, learning rate. Checkpoints, configs, figures, and videos, with metadata. Timestamped multi-modal streams for robotics and RL. Navigate, chart, and compare runs on dash.ml. Complete training scripts, from a minimal loop to PyTorch MNIST. Every public class and method in the SDK. Every `ml-dash` command and flag. Working with an AI agent? Every page is also available as markdown and as an importable skill. See [LLM-Readable Docs](/ml-dash/reference/llm-readable.md). ## Install reference The rest of this page covers maintaining an install. You don't need it to get started. ### Updating the CLI ```bash ml-dash update # install the latest release ml-dash update --check # only report whether one exists ``` `update` uses whichever channel you installed from. An npm install runs `npm install -g @dreamlake/ml-dash@`. A standalone binary downloads the new build, checks it against the release's sha256, runs it once, and only then replaces itself. If any step fails, your working binary stays as it was. `update` never downgrades. Update the SDK with pip: ```bash pip install -U ml-dash ``` ### Pin a CLI version ```bash curl -fsSL https://pub-42e1dcc7de574d4a92984865fdc95f10.r2.dev/install.sh | sh -s -- --version 0.1.1 ``` ```powershell & ([scriptblock]::Create((irm https://pub-42e1dcc7de574d4a92984865fdc95f10.r2.dev/install.ps1))) -Version 0.1.1 ``` With npm: `npm install -g @dreamlake/ml-dash@0.1.1`. An installed CLI can move to an exact newer release with `ml-dash update --version `. `update` refuses to downgrade, so use one of the commands above to go back to an older release. ### Where the standalone CLI installs The installer checks every download against the sha256 in that release's manifest before writing anything. It installs into `~/.local/bin` (`%LOCALAPPDATA%\ml-dash\bin` on Windows), which you can change with `--install-dir` / `-InstallDir`. It never overwrites an `ml-dash` that npm or pip installed. It reports the conflict on `PATH` and leaves it to you. ### Supported platforms macOS (arm64, x64), Linux (x64, arm64; glibc and musl), and Windows (x64, arm64). The binaries bundle their own runtime. On Alpine, the musl builds need one system library first: ```bash apk add --no-cache libstdc++ ``` ### Upgrading from 0.6 SDK 0.6.27 and earlier installed an `ml-dash` command as part of `pip install ml-dash`. That Python CLI was removed in **0.7.0**: - `pip install -U ml-dash` removes the old `ml-dash` command. Install the new CLI before or right after you upgrade if your scripts call `ml-dash`. - The command names and arguments carry over. The new CLI reads and writes the same keychain entry and `~/.dash/` files, so an existing login normally keeps working. If it doesn't, run `ml-dash login` again. - The `ml_dash.cli` and `ml_dash.cli_commands` modules are gone. Code that imported them should run the `ml-dash` binary instead. - The SDK itself (`Experiment`, `params`, `metrics`, `logs`, `files`, `tracks`) is unchanged. Docs for earlier releases are in the version menu at [docs.dash.ml](https://docs.dash.ml). --- Source: https://docs.dreamlake.ai/notes/linked-items # Linked note items Turn a list item into a note while keeping the parent list compact. **Development preview:** available in the Notes extraction development UI. Production UI release is not yet verified. 1. Click **Turn list item into a note** at the right edge of any numbered, bullet or checkbox item. 2. The item text and nested content move into a new private note. Its first line becomes the title. The parent keeps its number or checkbox. 3. The reference displays a short hash and title, with an ellipsis when the title is too long. 4. Click the reference to open an editable tab in the panel on the right. Create that panel only when absent; subsequent references add tabs there. References opened from a side Note add tabs in that same side panel. An already-open Note is focused instead of duplicated. On a narrow screen, navigation fills the viewport. Closing a tab does not delete the Note. ## Plain Markdown Only the full note ID is saved in the reference: ```markdown 1. :note[6aa9951250d9de84058e8ebb] - [ ] Discuss :note[6aa989ea6aff1e1960afc51f] before the demo. ``` You can edit text before and after the reference. The short hash and title are display values, not stored Markdown. Renaming the child does not change its ID; reopening the parent resolves its current title. References inside code remain literal text. Linked notes retain their own permissions. Creating a reference does not grant access to the child or make it public. A missing or inaccessible note displays **Unavailable note**. If an item changes during creation, the source stays intact and the new note opens separately. If a request fails, inspect Notes before trying again. Undoing the parent replacement does not delete the child. ## Rich-component notation The Note picker, extraction and copy button prefer `:note[]`. The renderer preserves saved `#note:` and `:note{id=""}`. Keep existing source unchanged; never bulk-migrate notation. Display titles and short hashes are never reference IDs. For an artifact, use `:artifact[geyang/pitch-deck]`; saved `:artifact{namespace="geyang" id="pitch-deck"}` and `#artifact:geyang/pitch-deck` remain supported. Brackets hold primary content; braces are only named attributes. This is the remark-directive Markdown extension convention, not core CommonMark; DreamLake defines resource semantics. Bare `:note{ID}` / `:artifact{namespace/id}` are invalid. Other preferred forms are `:bindr[id]`, `:asset-reference[id]{caption="plot"}`, `:placeholder[owner name]` and `:chatgpt-content-reference[0]`. Imported ChatGPT forms remain unresolved; never invent resources. Saved attribute forms and `[ owner name ]` remain supported. The artifact extension is a development preview, not a verified production release. Artifact IDs require their namespace; references grant no access. See [Notes reference syntax](/notes/#artifact-references-development-preview). ## Sync recovery Each open note has its own collaborative state. If sending pauses, preserve the local draft before choosing **Out of sync ▾ → Use server version**. Downloading the parent draft does not back up unsent text in the child editor. Follow the [Notes recovery steps](/notes/#reconnect-and-recovery) for each affected note. Do not retry extraction or rewrite a parent from stale text to bypass a sync hold. ## Agent skill [Download the linked-note skill](/skills/dreamlake-note-references.zip). Extract `dreamlake-note-references` into your agent's skills directory. The skill covers raw token storage, nested item extraction, concurrency checks, and CLI readback. --- Source: https://docs.dreamlake.ai/notes/markdown # Markdown authoring Write Markdown directly in a Note. Live preview renders formatting while retaining the source for editing and collaboration. Use headings for structure, lists for steps, and color sparingly to emphasize a status or phrase. ## Everyday formatting ```markdown # Project update **Decision:** proceed with the pilot. - Owner: Ada - [ ] Confirm the schedule - [x] Review the proposal Read the [project brief](https://example.com/brief). Use `status` for inline code. ``` ## Nested ordered lists Write ordered lists with numeric Markdown markers (`1.` or `1)`) and indent children beneath their parent: ```markdown 1. First step 1. First substep 1. First detail 2. Second detail 2. Second substep 2. Next step ``` Rendered Notes preserve each numeric marker as written, including its number and `.` or `)` delimiter. Nesting does not change the marker style. Each nested list is indented, with wrapped lines aligned beneath the item text. The editor retains the source markers too. Inside an ordered list, a child may start at `2.`, `10)`, or another number; the editor and rendered Notes preserve the same nesting and marker. For example: ```markdown 1. Parent 2) Child 10. Grandchild ``` Press Tab on an item to nest it under the preceding sibling, or Shift+Tab to move it back out. Tab leaves the first item at its current level when there is no preceding sibling; repeated Tab presses do not turn it into a code block. The shortcuts also work with the caret at the start of the item line. Four spaces also work for a child beneath `1. Parent`; the indentation must belong to a parent list item. At the top level, four leading spaces create an indented code block. In the editor, continuation lines keep their structural indentation when the cursor moves away; extra spaces within prose still collapse in live preview without changing the saved source. Typed `a.`, `a)`, `(a)`, `i.`, `ii.`, `一、`, and `(一)` are plain text, not Markdown list markers. Chinese text is supported inside ordinary numeric or bullet lists. A numeric `1)` marker displays as `1)` in rendered Notes. ## Color a phrase Brackets hold the text; braces follow the brackets and hold named attributes: ```markdown :color[Needs review]{color="#b45309"} :color[Ready]{color="green"} :color[Blocked]{color="#ef4444"} ``` These are DreamLake color directives using the Markdown directive convention `:name[content]{attribute="value"}`. Standard Markdown has no text-color syntax. The `:color` directive changes the foreground text color. Use `:highlight` for a background tint; arbitrary CSS is not supported. Use a visible word such as “Blocked” as well as color, and check readability in both light and dark themes. A fixed color stays the same when the theme changes. ### Preview in light and dark themes The same note renders an inline phrase and a colored table status in both themes: ![Light theme: red Important text above a table with a green Ready status.](/images/notes/inline-color-light.png) ![Dark theme: the same red phrase and green table status on a dark background.](/images/notes/inline-color-dark.png) Captured from the deployed app using a sample read-only note. Fixed colors do not adapt to the theme; choose a color with enough contrast in the theme you use. The sample uses `:color[Important]{color="#ef4444"}` and `:color[Ready]{color="green"}`. ### Supported values and text Color values must be double-quoted. Use 3, 4, 6 or 8 hexadecimal digits after `#` (the 4 and 8-digit forms include alpha), or one of these case-insensitive names: `black`, `silver`, `gray`, `white`, `maroon`, `red`, `purple`, `fuchsia`, `green`, `lime`, `olive`, `yellow`, `navy`, `blue`, `teal`, `aqua`, `orange`, `rebeccapurple`. Content is plain text: nested Markdown, HTML and nested directives are not rendered. Escape brackets and backslashes with a backslash. Attribute-only syntax is also accepted: ```markdown :color[Review \[draft\]]{color="orange"} :color{text="Ready" color="green"} ``` Place a directive at the beginning of a line or after whitespace or an opening parenthesis. Select the directive in the editor to reveal and edit its source. Invalid colors, missing text, duplicate attributes and unknown attributes remain literal. A backslash before the colon or a code span keeps the syntax visible. Directives inside Markdown links remain literal. ## Highlight a phrase ```markdown :highlight[Review needed] :highlight[Key finding]{color="#60a5fa"} :highlight{text="Decision" color="orange"} ``` Highlights use a translucent background tint and inherit the surrounding text color in light and dark themes. They accept the same quoted color values, plain-text content and escaping rules as `:color`. Select the highlighted phrase to edit its original source. Omit `color` for yellow; an explicitly empty or invalid color remains literal. `==text==` and raw HTML `` are not supported highlight syntax. Add `user="geyang" comment="Check the source"` alongside `color` to attach plain-text metadata: `:highlight[Key finding]{user="geyang" comment="Check the source"}`. Use the inline/sidebar comments toggle: inline shows no annotation cards or hover popups. Sidebar mode shows compact metadata cards in the table-of-contents column, following passages visible in the current viewport. Click highlighted text in the editor or use the sidebar edit action to reveal its original source. Use canonical public handles, not display names or internal user IDs. Attribution is self-declared; legacy names remain unresolved. See [Highlight metadata](/notes/#highlight-metadata) for escaping and revision-safe agent edits. ### Highlight preview in both themes ![Light theme: yellow, blue and orange highlights in the live editor and rendered Markdown.](/images/notes/highlight-light.png) ![Dark theme: the same highlights inherit readable light text.](/images/notes/highlight-dark.png) Captured from the app's actual editor and Markdown renderer using `scripts/highlight-preview.html` (serve with `pnpm exec vite --config scripts/highlight-preview.config.ts`; add `?dark` for dark mode). ## Use color in tables ```markdown | Item | Status | | --- | --- | | Proposal | :color[Ready]{color="green"} | | Schedule | :highlight[Needs review]{color="yellow"} | ``` Color and highlights render in Notes live preview, table cells and the app’s read-only Markdown views. Saved text, revision handling and collaborative edits retain the exact source. CLI/API HTML snapshots and external Markdown readers may show the directive literally; they do not automatically gain the app’s color renderer. ## Link resources Use resource directives to reference another Note or artifact: ```markdown :note[note-id] :artifact[namespace/artifact-id] ``` See [Notes](/notes/) for resource attributes and API editing, and [Linked note items](/notes/linked-items/) for extracting a list item into a Note. --- Source: https://docs.dreamlake.ai/architecture # Architecture The mental model behind DreamLake: how episodes, files, and collections are organized, how auth works, and what happens to a file between `dreamlake upload` and playback in the dashboard. ## Data model Everything hangs off a **namespace** (a user or an org). Projects group episodes; an episode is one recording session and owns its files, tracks, logs, and parameters. ``` Namespace (user or org) └── Project ├── Episode (a recording session) │ ├── Files (video, audio, labels, text tracks) │ ├── Tracks (time-series scalars/tensors) │ ├── Logs │ └── Parameters ├── Bindrs (curated file collections) ├── Datasets (groups of bindrs) └── Node tree (folder hierarchy, materialized paths) ``` Namespaces also own **artifacts** — versioned renderable documents with per-artifact visibility and share links, kept in their own catalog (see the [CLI Reference](/cli.md#artifacts)). ## Node tree All files and folders are nodes in one unified tree, stored in MongoDB using materialized paths — lookups by path are a single indexed query, no recursion. | Node kind | Description | |-----------|-------------| | `project` | Top-level project node | | `episode` | Recording session | | `folder` | Organizational directory | | `file`, `video`, `audio`, `image`, `text`, `code` | Leaf nodes | Path convention: root = `","`, depth 1 = `",camera,"`, depth 2 = `",camera,front,"`. ## Auth A two-token system separates identity from API access: 1. **Login** — OAuth device flow via vuer-auth → short-lived token 2. **Exchange** — `POST /auth/exchange` → long-lived DreamLake JWT (HS256) 3. **API calls** — `Authorization: Bearer ` First login auto-creates the User and Namespace records — there is no separate sign-up step. ## Upload pipeline ``` CLI → chunk file (10 MB) → S3 multipart upload via BSS → register asset in DreamLake Server (creates node hierarchy) → trigger Lambda for HLS splitting (video/audio) ``` Uploads are resumable — progress is persisted to `~/.dreamlake/uploads/`, so an interrupted transfer picks up at the last completed part. The HLS split is what powers both instant playback in the dashboard and the 2-second chunks that [semantic search](/search.md) embeds. ## Planned DreamDB maintenance During a coordinated storage-format upgrade, the operator can temporarily pause the Artifacts, Annotations, Envs and Workflows APIs, plus managed DreamDB Source creation and its DreamDB write/credential endpoints. These requests return HTTP `503` with code `DREAMDB_MIGRATION_PAUSED`, `Retry-After: 60` and `Cache-Control: no-store`. This is a maintenance failure, **not empty data**: retain the existing dataset and retry after service resumes; do not delete or recreate it to work around the pause. Search and non-DreamDB Source operations are not paused by this switch. The server switch is `DREAMDB_MIGRATION_PAUSED=true` (off by default). It does not revoke credentials already issued to clients or stop direct storage writes. Operators must coordinate a scoped storage-side freeze, compatible client versions and rollback records separately before changing dataset refs. ## Tech stack | Component | Technology | |-----------|-----------| | Server | Fastify, MongoDB (Prisma), JWT | | Storage | S3 via BSS | | Search | Qdrant (CLIP embeddings) | | CLI | Python | ## Next steps The REST endpoints these pieces expose. The vectorize-and-query pipeline built on the HLS chunks. --- Source: https://docs.dreamlake.ai/notes/embeds # Embeds and query arguments Artifacts and previews have three presentation states: - **Inline:** a compact reference in the text flow, with no embedded content. - **Embed:** a block of content inside the Note, enabled with `embed="4:3"` or another supported aspect. - **Open:** the standalone artifact page or destination page, opened in the preview/browser. These states apply to every supported artifact content type; the directive argument is always `embed`, independent of the content language. Use `embed="4:3"` or `embed="16:9"` to render an artifact or web page inside the Note. The value both enables the embed and sets its aspect ratio. `embed="true"` is shorthand for `embed="16:9"`. Omit `embed` (or set it to `"false"`) to keep an inline reference. ## Sizing models **Responsive ratio:** fill the available document width and derive height from the ratio. This is the default model; it responds when the Note panel resizes. ```markdown :artifact[geyang/dashboard]{embed="4:3"} :artifact[geyang/pitch-deck#slide-3]{embed="16:9"} ``` **Fixed width with ratio:** request a pixel width and derive height from the ratio. Width still shrinks to fit a narrower document. ```markdown :artifact[geyang/dashboard]{embed="4:3" width="640"} ``` **Fixed height:** use an explicit height for a scrollable report or web page. Height overrides the ratio; width remains responsive unless specified. ```markdown :preview[https://example.com/report]{title="Report" embed="true" height="480"} :artifact[geyang/dashboard]{embed="4:3" width="640" height="400"} ``` **Content zoom:** the default `zoom="fit"` gives responsive content the embed's viewport dimensions. It does not inspect or automatically shrink a fixed-size third-party page. Use a percentage to scale its content independently of the outer dimensions; 75% provides a larger internal layout viewport. ```markdown :preview[https://example.com/report]{embed="16:9" zoom="75%" border="false"} ``` | Argument | Values | Default | | --- | --- | --- | | `embed` | `"true"`, `"false"`, or a positive integer ratio such as `"4:3"` | `"false"` | | `width` | Positive pixels (bare number or `px`, up to 4096), or 1–100% | `"100%"` | | `height` | Positive pixels (bare number or `px`, up to 4096) | From ratio | | `zoom` | `"fit"` or integer percentages from `"25%"` through `"200%"` | `"fit"` | | `border` | `"true"` or `"false"` | `"false"` | Ratio terms are integers from 1 through 999. Sizing, zoom and border arguments require an enabled embed; invalid sizing values remain literal source. CSS and sandbox permissions cannot be changed through these arguments. Use a standalone line for larger embeds. Hover or focus a reference, then choose the pin + **Embed** bubble below it to embed it as a block. The bubble contains only the pin icon and **Embed**. In an embedded web preview, hovering or focusing its header shows the destination URL beside the preview tag. Drag the bottom capsule to change height, or the left/right capsules to change width. A curved bottom-right handle resizes width and height together. All handles appear when the pointer reaches their edge or they receive keyboard focus. Handles also accept arrow keys (16px steps; Shift for 64px). On the corner handle, left/right change width and up/down change height. A drag saves pixel dimensions and preserves content query arguments. The preview header shows a pinned icon at rest; hovering or focusing it reveals a red unpin icon. Click it to collapse the embed back to a reference; content query arguments are preserved and embed sizing is removed. Edit the directive in source to return to percentage width or ratio sizing. Read-only views do not expose editing controls. Embedded artifacts use the isolated, content-only artifact renderer with the current reader's existing access; embedding does not grant access or create a share link. Web pages must allow iframe embedding. Static HTML snapshots retain inert references and never load embedded content. ## Query pass-through API Keep the artifact reference or page URL in brackets. Put embed options and content-specific query arguments together in braces: ```markdown :artifact[geyang/video-viewer]{embed="4:3" view="contact-sheet" columns="4" frames="12"} :preview[https://example.com/video]{embed="16:9" view="storyboard" start="30"} ``` These resource names are examples, not preinstalled artifacts. The referenced artifact or website must implement the requested views. Notes consumes `embed`, `width`, `height`, `zoom`, and `border`. Earlier `inline` arguments remain readable for compatibility; new embeds use `embed`. Neither key is forwarded to the renderer. Resource identity fields (`namespace`, `id`, `fragment` for artifacts; `url` and `title` for previews) also belong to Notes. All other valid arguments become public query parameters; they are never interpreted as HTML attributes, CSS, or sandbox flags. For the first example, the artifact receives `?view=contact-sheet&columns=4&frames=12`. Its ordinary viewer link uses `?art.view=contact-sheet&art.columns=4&art.frames=12`. Only the viewer URL uses `art.`; do not prefix directive arguments. A fragment stays in the reference: ```markdown :artifact[geyang/video-viewer#scene-3]{embed="16:9" view="player" start="30"} ``` Preview arguments are merged into the URL query. Brace arguments replace an existing value with the same key; unrelated URL parameters and the fragment stay intact. Values use quoted strings and are URL-encoded automatically, including nested URLs. Do not pre-encode them: ```markdown :preview[https://example.com/viewer?theme=dark]{embed="16:9" src="https://example.com/clip.mp4?a=1&b=2" view="contact-sheet"} ``` The same arguments work on clickable tags without `embed`: opening the side panel or the ordinary viewer link carries the query to the renderer. ### Read and update configuration inside an artifact Use the existing artifact route API rather than `window.location`: HTML artifacts run in a nested `about:srcdoc` frame. ```html
``` Route changes stay inside the embed; they do not rewrite the Note or inherit its page query. Dispose subscriptions when a renderer unmounts. See the full [artifact routing contract](/artifacts/#artifact-paths-and-local-routing) for navigation, fragments, lifecycle, and access boundaries. ### Argument validation Names must match `[A-Za-z][A-Za-z0-9_.-]{0,63}`. Values are double-quoted strings; the renderer validates their meaning and parses numbers or JSON. Duplicate arguments are invalid. Reserved route names such as `share`, `token`, `auth`, `authorization`, `cookie`, `project`, `namespace`, `instanceId`, `__proto__`, `prototype`, `constructor`, and `dreamlake` (including their `.`, `_`, or `-` suffix forms) cannot be forwarded. Encoded route state is limited to 8192 characters. Invalid directives remain literal text. Query data is public configuration, not authorization. Notes never forwards its own URL parameters or credentials. References and queries do not grant access. ## Video, storyboard, and contact-sheet contracts A video artifact can define these query arguments without changing Notes: | Argument | Suggested renderer meaning | | --- | --- | | `view` | `player`, `storyboard` (ordered scene cards), or `contact-sheet` (frame grid) | | `src` | A video source supported by that renderer | | `columns` | Number of grid columns | | `frames` | Number of evenly sampled frames | | `interval` | Sample spacing in seconds, instead of a fixed frame count | | `start`, `end` | Sampling or playback range in seconds | This is a renderer API convention, not a built-in Notes video feature. A renderer should bound sampling work, show timestamps, and make a frame open playback at that timestamp. It should reject conflicting `frames` and `interval` options. A future `:contact-sheet[...]` directive could be shorthand for this renderer; it is not currently implemented. The artifact sandbox currently blocks arbitrary remote video-file loading. A self-contained artifact can bundle video or precomputed frames. Passing a `src` URL does not bypass that policy; general remote-video sampling needs an authorized host media bridge. A separately hosted viewer used through `:preview` can implement its own video access, subject to that site's embedding policy and its media origin's CORS rules. Parent-scoped user-data access is tracked in [issue #837](https://github.com/dreamlake-ai/dreamlake-workspace/issues/837). The embedding parent and current viewer must determine the authorized context; the artifact owner's identity alone is insufficient. --- Source: https://docs.dreamlake.ai/annotations {/* The Fig tags come from the site-wide mdxComponents map in site.config.ts — no import needed. Beyond the install commands, this page carries no code: every SDK call it describes is written out in /annotations/reference. */} # Annotations An annotation is where your **labeled data** lives — your workflow does the labeling, the annotation stores its output. One episode is one raw video plus any number of annotation layers: anything you can pin to that video's clock. Layers are independent and open-ended — send what you have, add the rest later. ## Public catalog reads `GET /namespaces/:slug/annotations` accepts requests without an Authorization header. Anonymous callers and authenticated nonmembers receive only live public annotation rows; namespace members retain access to their private rows. A supplied invalid or expired token returns 401. Public annotation details (`GET /namespaces/:slug/annotations/:name`) and content reads (`POST /namespaces/:slug/annotations/:name/presign-read`) also accept anonymous requests. Both check the annotation's current visibility. Private, missing and deleted annotations return 404 to callers without access; namespace members can still read their private annotations. Presigned reads retain the existing path validation and expiry limits. Upload credentials, creation, editing and deletion still require authentication and membership. ## Browsing in the app `//profile?tab=annotations` and `//annotations` share the same catalog. Personal owners and organization members see the resources and operations permitted to them; anonymous visitors and visitors to someone else's namespace see public annotations only. Profile has an identity rail, while the application has resource navigation for the URL's namespace. Public details open without a sign-in redirect and retain the namespace sidebar. The detail header returns anonymous readers to the Profile annotations tab and signed-in readers to `//annotations`. Embedded viewers return to their containing project. See [Profiles and workspaces](/workspaces.md). ## Prerequisites ```bash curl -fsSL https://dl.dreamlake.ai/install.sh | bash # the dreamlake CLI pip install dreamlake dreamdb # SDK + storage engine brew install ffmpeg # transcoding (any install method works) dreamlake login # browser sign-in; CI uses DREAMLAKE_API_KEY ``` Windows, what the installer puts where, release channels, and auto-update: [Install](/index.md#install). ## Upload it One call. The video is transcoded for the browser, each layer is stored against its clock, and adding a layer later just means passing one more argument. Give the episode an id you'll recognise — that's the handle for coming back. ## Keep adding Open an annotation by name on any day and add to it. Revisions never overwrite — newest wins, history stays. | Later, you can | Effect | | --------------------------------- | ---------------------------------------------- | | add another episode | the annotation grows | | revise a layer | newest wins, old versions kept | | add a layer you skipped | it shows up on an episode you already uploaded | | add a layer type that ships later | same thing — just another revision | | add a camera angle | views share one clock; video is add-only | | make it public | anyone can open it, no login | Nothing you upload today has to be re-uploaded to benefit from what ships tomorrow. ## Watch it Open the web app → **Annotations** → your annotation → your episode. How a layer is drawn follows from how it's pinned — which is why the list can grow without the picture changing: | A layer pinned to | Is drawn | Shipping today | | ----------------- | ---------------------------------------------- | ---------------------------------- | | a frame | on the picture, tracking playback | joint skeletons, 3D reconstruction | | an interval | as labeled bars on the timeline; click to seek | action segments | | the whole episode | as episode metadata | task labels | Each is optional. The player shows what you have and skips what you don't — a bare video simply streams. ## When there's no overlay yet Every annotation page has two views, and the second one always works. **Preset view** is what the last section showed: the annotation's type label matches a preset, and you get the player with its overlays. **Raw tracks view** ignores presets and lists every track straight off the manifest — name, kind, values along the timeline. Label, JSON, embedding, video; it doesn't matter. | If your data | You get | | ------------------- | ------------------------------------------------- | | matches a preset | the rendered view, overlays and all | | doesn't, or not yet | the raw track view — nothing hidden, nothing lost | | gets a preset later | the rendered view, with no re-upload | So a new kind of data is **useful the day you upload it**. ## Bring your own schema The same machinery with the lid off: declare any columns you like — sensor readings, JSON, images, embeddings — anchored to timestamps, or to row numbers when your data has no clock. It's also the escape hatch when the layer you need doesn't exist yet. | Rule | What it means for you | | ----------------------- | ----------------------------------------------------- | | append-only, write-once | a value at a given time and column is never rewritten | | one commit per write | send batches, not one point at a time | | one writer at a time | parallel writers to the same annotation will clash | Custom annotations appear in the catalog under their own type label; today you read them back through the SDK. ## Share it Annotations are **private** by default. | | Who can view | | ---------------------- | --------------------------------- | | 🔒 private _(default)_ | you and members of your namespace | | 🌐 public | anyone, no login | Names can be scoped to a team, too: a bare name is yours, an organisation prefix targets that org, and membership is checked per request. ## Let Claude do it Add the [annotations skill](https://github.com/dreamlake-ai/dreamlake-skills) and skip the SDK — describe what you want, it makes the calls: | You say | It does | | ---------------------------------------------------------------- | ------------------------------------------- | | _"upload this clip with its hand skeletons and action segments"_ | creates the annotation, uploads the episode | | _"add the 3D reconstruction to ep-001"_ | fills in the missing layer | | _"add a wrist camera to ep-001"_ | attaches the second view | | _"make this annotation public"_ | flips visibility | ## Next steps The manual — every method, argument and data shape, with the code this page leaves out. Every layer's exact fields, including 3D reconstruction. --- Source: https://docs.dreamlake.ai/ml-dash/get-started/authentication # Authentication You log in once with the `ml-dash` CLI. The CLI saves a token on your machine, and both the CLI and the Python SDK read it from there. Local mode needs no login at all. ## Log in Install the CLI first (see [Install](/ml-dash.md#cli)), then: ```bash ml-dash login ``` The CLI prints a short code and a QR code, and opens your browser at the approval page. Approve the request there. You never type a password into the terminal. Once you approve, the CLI saves the token and tells you where it put it, for example `Your authentication token has been stored (keychain).` | Flag | Use | |---|---| | `--no-browser` | Print the code and URL, but don't open a browser. Use this on a remote machine and approve from any other device. | | `--dash-url URL` | Log in to a server other than `https://api.dash.ml` | | `--auth-url URL` | Use a different authorization server (default `https://auth.vuer.ai`) | The code expires after 10 minutes. Check who you are logged in as, or log out: ```bash ml-dash profile # fetches your profile from the server ml-dash profile --cached # reads the stored token only, no network ml-dash logout # clears the token from every place it could be stored ``` ## Where the token lives The CLI stores the token in the most secure place the machine offers: | Machine | Where `ml-dash login` stores the token | |---|---| | macOS | The login keychain (service `ml-dash`) | | Linux with `secret-tool` (a desktop session) | The Secret Service keyring (service `ml-dash`) | | Windows, or Linux without `secret-tool` (servers, clusters, containers) | `~/.dash/tokens.encrypted`, with its key in `~/.dash/encryption.key` | Set `ML_DASH_NO_KEYCHAIN=1` before `ml-dash login` to skip the keychain and always use the encrypted file. ## Using the token from Python The SDK loads the stored token when an experiment has a `dash_url`. You don't pass any credentials in code: ```python from ml_dash import Experiment with Experiment(prefix="alice/project/run-1", dash_url="https://api.dash.ml").run as exp: exp.log("Authenticated automatically") ``` The SDK reads the same places the CLI writes. If `keyring` is installed, it uses the OS keychain. Otherwise it uses `~/.dash/tokens.encrypted`. So match the SDK install to where your login went: - **Token in the keychain** (macOS, desktop Linux): install `pip install "ml-dash[auth]"`, which adds `keyring`. Without it, the SDK can't see the token. - **Token in `~/.dash/tokens.encrypted`**: a plain `pip install ml-dash` reads it. > **Warning:** Run `ml-dash profile` to confirm the CLI is logged in, then check that the SDK > install matches the list above. A common mismatch: the token is in > `~/.dash/tokens.encrypted`, but another package pulled `keyring` into the > Python environment, so the SDK looks only in the keychain. Uninstall > `keyring` from that environment, or log in again on a machine whose keychain > the SDK can read. ## Servers and configuration Both the CLI and the SDK default to `https://api.dash.ml`. - **CLI:** `--dash-url` (alias `--api-url`) on any command, or `remote_url` in `~/.dash/config.json`. - **SDK:** the `dash_url` argument. `dash_url=True` uses the `ML_DASH_API_URL` environment variable, or `https://api.dash.ml` if it isn't set. For scripts and CI, the CLI also accepts a token written directly into `~/.dash/config.json`. It takes precedence over the stored login: ```json { "remote_url": "https://api.dash.ml", "api_key": "" } ``` The SDK does not read `api_key` from this file. It reads only the stored login described above. ## How login works `ml-dash login` uses the OAuth 2.0 Device Authorization Grant ([RFC 8628](https://www.rfc-editor.org/rfc/rfc8628)) against `auth.vuer.ai`, then exchanges the result for an ML-Dash token: ``` ml-dash CLI auth.vuer.ai Browser |-- POST /api/device/start -->| | | { device_secret_hash } | | |<-- { user_code, verification_uri, expires_in: 600 } | | [prints code + QR, opens browser] | | |<-- user enters code, approves | |-- POST /api/device/poll --->| | |<-- 202 authorization_pending (every 5 s) | |<-- 200 { access_token } | | | | |-- POST /api/auth/exchange ──→ ML-Dash server | |<-- { ML-Dash token } | [stores the token] ``` - The poll is tied to a random per-machine secret, stored in `~/.dash/config.json`. Only its SHA-256 hash is sent to the server, so there is no `device_code` that could be intercepted. - The token from `auth.vuer.ai` is exchanged at the ML-Dash server's `/api/auth/exchange` for the token the CLI stores and the SDK sends. --- Source: https://docs.dreamlake.ai/sim-rollouts {/* Figures come from the site-wide mdxComponents map (SimRolloutFigures) — no import needed. The page is figure-led: each section opens with a diagram, prose stays to captions, and every deep detail links out to Sources and the Dataset Viz docs. */} # Sim rollouts A trained policy is a neural network — nothing to watch. What's worth seeing is the policy **acting**: the agent walking, reaching, flying. This guide turns one rollout into a single **MCAP** DreamLake plays natively — the mesh moving through the scene, every reward and joint a cursor-synced chart. No screen recording, no bespoke viewer. Anything with a **body and a 3D pose** fits — robot, character, drone, hand. DreamLake already ships the last two moves; this guide is mostly the first. ## Prerequisites ```bash pip install foxglove-sdk trimesh # write MCAP; export meshes to OBJ curl -fsSL https://dl.dreamlake.ai/install.sh | bash # the dreamlake CLI dreamlake login # browser sign-in ``` ## Generate the MCAP Roll the policy out and log **three channels**, each a Foxglove well-known schema DreamLake decodes with zero config. ```python import foxglove from foxglove.messages import FrameTransforms, SceneUpdate w = foxglove.open_mcap("rollout.mcap") obs, _ = env.reset() for t in range(STEPS): obs, reward, done, _ = env.step(policy(obs)) ns = int(t * step_dt * 1e9) foxglove.log("/tf", FrameTransforms(transforms=body_transforms(env)), log_time=ns) if t == 0: foxglove.log("/robot", SceneUpdate(entities=robot_meshes(env)), log_time=ns) # once foxglove.log("/metrics", {"reward": float(reward), **joint_angles(env)}, log_time=ns) w.close() ``` The [skill](#let-claude-do-it) ships a runnable template that fills the two helpers above and gets the easy-to-miss details right — quaternion order (`wxyz` → `xyzw`) and exporting meshes in a format the viewer reads today (OBJ or GLB). ## Connect a source The `.mcap` is data like any other — DreamLake reads it in place, never imports. Put it in storage you already reach, then link it on the namespace's **Sources** page ([full rules](/sources.md)): ```bash hf upload your-name/your-repo ./rollout.mcap --repo-type dataset # or S3 / Dropbox ``` ## Watch it A `.dreamrc` beside the file names which channel feeds which view — embedded mesh, a motion trail, the metric charts: ```yaml version: 1 dataset: format: mcap episodes: auto views: - view: recon3d up: z tracks: [{ field: "/tf::*", as: transform3d }] geometry: ["/robot::*"] # the embedded OBJ meshes trail: { ahead: 1, behind: 0.5 } - view: timeline - view: lineChart title: "/metrics" series: ["/metrics"] ``` Name it `.dreamrc` for a folder, or `.mcap.dreamrc` for one mcap when the folder holds several. Keys, options, and the validate loop: the [Dataset Viz docs](https://viz.dreamlake.ai/dataset-viz). ## Bring your own robot Two ways to put a body on the skeleton — pick one. Embed the mesh for a self-contained file, or — if DreamLake's robot registry (`live9080/dreamlake-robots`) has your robot and its link names match your `/tf` frames — emit a `/tf`-only MCAP and let the `.dreamrc` load the URDF. ## Let Claude do it The [sim-to-mcap skill](https://github.com/dreamlake-ai/dreamlake-skills) runs this end to end and hands off to the source and dataset-viz skills: | You say | It does | | --- | --- | | _"visualize my mjlab G1 walking policy"_ | rolls out the checkpoint, writes the MCAP | | _"the robot shows as bare axes"_ | re-exports the mesh in a format the viewer reads, binds `/robot::*` | | _"use the preset G1 model instead"_ | switches to a `/tf`-only file + URDF | ## Next steps Link a bucket or HF repo as a source — providers, layout, verification. Every `.dreamrc` key and view option, next to a live playground. The runnable template and the write → upload → render hand-off. --- Source: https://docs.dreamlake.ai/video # Video SDK Load, slice, and batch video data in Python with NumPy-style indexing. Every operation is lazy — nothing is decoded until you actually access pixels, so slicing an hour of footage costs nothing. > **Note:** **Before you begin** — `pip install dreamlake` for this Python SDK. The > `dreamlake` CLI is a separate native binary > ([install it here](/index.md#install)); use it to authenticate with > `dreamlake login`, which the SDK then reuses. Video ids come from > `dreamlake list` or the dashboard. ## Load ```python import dreamlake as dl video = dl.load_video("v-BV1bW411n7fY9x01") print(video.fps, video.duration, video.width, video.height) ``` ## Slice `float` = time (seconds), `int` = frame number. Returns a lazy `Video`. ```python clip = video[10.0:20.0] # 10s clip frame = video[42] # frame 42 sub = clip[2.0:5.0] # sub-slice → Video(st=12.0, et=15.0) ``` ## Access Frames ```python video[0].image # PIL Image video[0].numpy() # (H, W, 3) clip.numpy() # (N, H, W, 3) clip.tensor() # (N, C, H, W) torch tensor video.thumbnail # middle frame ``` ## Chunk & Batch ```python chunks = video[0.0:2.0].chunk(0.200) # VideoArray of 10 × 200ms chunks[:, 0].numpy() # first frame of each → (10, H, W, 3) chunks[:, 0].tensor() # feed to model ``` ## TextTrack Buffer time-aligned text entries, flush to server: ```python track = dl.text_track(prefix="/run-042/captions", project="robotics@alice") track.add("Robot picks up cup", source=clip) track.flush() ``` ## VectorIndex Store and search embeddings: ```python index = dl.vec_index("my-experiment") index.add(vector=enc(clip[0]), caption="robot arm", source=clip) results = index.search("robot picking up cup", limit=10) ``` ## Prefix Context ```python with dl.Prefix(project="robotics@alice", prefix="/2026/04/run-042"): dl.upload("./video.mp4", path="camera/front") track = dl.text_track(path="captions/llava") ``` ## Next steps Vectorize the clips you just sliced and query them with natural language. Upload and organize the episodes the SDK loads from. --- Source: https://docs.dreamlake.ai/search # Semantic Search Search video content with natural language — "robot picking up cup" returns the exact 2-second clips, with playback URLs and scores. Videos are split into HLS chunks, embedded with CLIP, and indexed in Qdrant. ## Pipeline ``` Upload video → Lambda splits into 2s HLS chunks → dreamlake vectorize (CLIP + LLaVA per chunk) → Qdrant index → Query: text → CLIP embed → nearest neighbor ``` ## Vectorize ```bash dreamlake vectorize --episode robotics@alice:run-042 dreamlake vectorize --bindr "front-camera" --project robotics@alice dreamlake vectorize --dataset "training-v1" --project robotics@alice ``` Add `--zaku-url http://host:9000` for distributed processing via Zaku task queue. Per chunk: CLIP ViT-L/14 (768d image embedding) + LLaVA 13B (caption → CLIP text embedding). ## Search ``` GET /namespaces/:ns/projects/:project/semantic-search?q=robot+picking+up+cup ``` | Param | Description | |-------|-------------| | `q` | Natural language query | | `episode` | Scope to episode | | `bindr` | Scope to bindr | | `limit` | Max results (default 10) | | `using` | `image` (default) or `caption` | Returns matched 2s clips with playback URLs and scores. ## Performance | Step | Time | |------|------| | Vectorize per chunk | ~14.5s (CLIP + LLaVA) | | Search query | ~50ms | | Storage per chunk | ~6KB | | 1 hour video | 1,800 points, ~11MB in Qdrant | ## Next steps Load the matched clips in Python and turn them into tensors. The search endpoints, plus everything else the server exposes. --- Source: https://docs.dreamlake.ai/workspaces # Profiles and workspaces ## One namespace, two layouts `//profile?tab=` and `//` show the same resource catalog with the same permissions. Profile has an identity and avatar rail; the workspace has a resource sidebar. Projects is the first resource after Overview and the default workspace destination. Existing namespace-root links continue to open Profile. Your own profile and profiles of organizations you belong to expose the resources and actions available to you. Creation, editing, deletion and organization administration remain subject to the existing resource and role permissions; being able to browse a catalog does not grant permission to modify every item. Signed-out visitors and signed-in visitors browsing someone else's namespace see public catalogs only. Projects, Notes, Annotations, Envs and Artifacts support public browsing in either layout. Private catalogs such as Sources, Connections and Vault are not advertised to visitors. A directly shared resource can still be opened according to its existing server permissions; a share does not turn its owner's private catalog into a public list. Public views omit private counts, trash, Shared with me, organization updates and creation/editing controls. All/Public filters are omitted when every result is public; meaningful filters such as artifact kind remain. The member views retain useful catalog filters and permitted management actions. List/grid, search, sorting and resource cards are shared across the two layouts. Overview keeps recent projects and annotations and its list/grid preference. It does not fetch every resource catalog merely to display counts. The global **Cmd+Shift+D** (or **Ctrl+Shift+D**) developer switch controls discovery of experimental features, not access rights. Turning it on never grants access to private resources. ## Resource owner and signed-in account The sidebar separates your signed-in account, global navigation and the namespace you are browsing. Above the DreamLake brand, a plain username and chevron open your account menu; the sidebar collapse control shares that row. Dashboard opens your own recent work from any namespace. Gallery opens the same public gallery for everyone in a new browser tab. In your own personal namespace, a plain **Workspace** heading folds and unfolds the resource navigation, just like Bindrs. It scrolls with the list. Other namespaces instead show an avatar, display name and **Workspace @namespace** heading. This heading stays at the top of the scrolling sidebar; clicking the avatar/name area folds its resource links. The plus/minus indicator appears on hover. The separate **@namespace** link opens that owner's Profile in the current tab. A collapsed sidebar shows only the owner avatar and resource icons, and restores the previous resource-fold state when expanded. Folding Workspace does not hide Pinned or Bindrs. When available, **+New** sits above the Workspace group. Resource links and permitted Settings, Members and Teams entries belong to the URL's namespace. Your signed-in identity above the brand does not turn into an organization when you browse one. The account menu lists your personal namespace and organizations, then **My Profile**, **My Dashboard**, **New organization** and **Sign out**. Both personal destinations remain available on every signed-in surface: My Profile opens your own profile, and My Dashboard opens `/dashboard`, in the current browser tab. Profile has no separate Enter workspace button or Dashboard shortcut beside the account menu. Choosing a namespace normally opens its corresponding resource list. From a detail it returns to the list without carrying the previous resource ID; unavailable destinations fall back to Projects. From Profile it retains that layout and an available tab. From the personal Dashboard, choosing an organization opens its Projects list. Browser history and refresh follow the URL's namespace. There is no remembered workspace overriding the owner shown in navigation. Membership loss removes member-only navigation and content without changing the owner of the page. Signed-out visitors see a brief sign-in invitation between the brand and Gallery, with a primary **Sign in** button and a **New to DreamLake? Sign up** link. Gallery and public workspace navigation remain available, while Dashboard is hidden. A collapsed sidebar keeps a sign-in icon accessible. While authentication is being checked, a neutral placeholder reserves the account area instead of flashing sign-in buttons. Profile retains its landing-style authentication buttons. Explicit sign-in return addresses preserve the pathname, query and fragment. The footer pairs a small **Theme** label with the light/system/dark segmented control. The label is hidden and the control turns vertical when the sidebar is collapsed. Profile keeps its existing horizontal theme control. Profile owners can edit their identity through the existing pencil/avatar dialogs; organization identity editing remains restricted to its owners. Organization profiles retain member avatars and directory views. Public directories exclude secret teams and private member fields; governance remains permission-controlled. ## Your Dashboard `/dashboard` is the signed-in user's personal home, with the application sidebar. It combines recent projects and annotations in your own namespace with your recently visited projects across namespaces. Visit history stays private to your account and each project links to its real owner. Other people and organizations are browsed through Profile and their resource lists, not public Dashboards. The global Dashboard entry stays reachable while browsing someone else's namespace. On Dashboard, the Workspace section offers your own namespace's resource shortcuts. Profile exposes My Dashboard in the account menu; Gallery retains its signed-in Dashboard shortcut. Signing in without an explicit return destination opens `/dashboard`; a preserved resource destination still takes priority. Legacy `//dashboard` links redirect to `/dashboard`, which requires authentication and always resolves the signed-in user's own data rather than the namespace in the old address. ## Resource lists and shared links Public resource lists and public Projects, Annotations, Envs, Artifacts and Notes details can be opened without signing in. Private-only pages require the appropriate identity and permissions. The sidebar remains available while moving between application lists and details, with the current owner's permitted links. Loading or an error in the resource does not replace the navigation. Project files inherit their containing project's visibility; separately filed Notes, Artifacts and Annotations retain their own permissions. A public project does not publish private resources filed inside it or their private counts. An Env or Artifact share token can authorize an anonymous read. A private Note share link requires sign-in because it creates a grant for that person. Invalid or revoked links retain the server's not-found or access-denied behavior. The detail header returns signed-out readers to `//profile?tab=` and signed-in readers to `//`. Embedded resources return to their containing project first. Browser Back continues to follow actual history. There is no extra sign-in strip above the detail. The namespace list APIs for Notes, Projects and Annotations accept requests without an Authorization header. Anonymous users and nonmembers receive only live public rows; authenticated namespace members retain their existing access. Pagination totals use the same visibility filter. Supplied invalid or expired credentials return 401. Anonymous project summaries omit internal bindr/dataset counts. Anonymous Notes searches do not activate or flush collaborative rooms. Creating, modifying, sharing and deleting remain authenticated operations. ## Organization updates Your own Envs and Notes catalogs include a separate **Recent in your organizations** section. Each organization shows up to six recent resources and a **View all** link. Personal results and Shared with me remain separate. Visitors and organization catalogs do not show this personal aggregation. Organizations load in batches of six, with at most three simultaneous catalog requests. A failed organization can be retried without reloading your personal resources. Notes requests are bounded by the existing API; Envs currently reads the organization's full catalog and displays the six most recent entries. --- Source: https://docs.dreamlake.ai/artifacts {/* FigWhat/FigPush/FigVersions/FigTrash come from the site-wide mdxComponents map in site.config.ts — no import needed. */} # Artifacts An artifact is one renderable file — an HTML page, a React component, a Markdown doc, an SVG, a Mermaid diagram, or a code snippet — that you push from the terminal and view as a live page in the dashboard. ## Preview spacing Artifact previews start with zero page margin and padding. This applies to the shared renderer and the default HTML document inside its isolated iframe. HTML and React authors can add spacing explicitly within their content; the preview does not add an outer gutter. To show an image edge to edge, remove padding from its authored container and use `display: block` on the image. Fit preserves the image's aspect ratio, so a differently shaped viewport can still leave unused space; zero padding does not crop or stretch the image. ## Reference an artifact from a Note (development preview) ```markdown :artifact[geyang/pitch-deck] ``` In the development UI, click the `#…` ID badge in the artifact header to copy the complete `:artifact[namespace/id]` reference. The badge displays only the last six ID characters; the copied reference includes the owner namespace and full ID. This copy action does not create a share link or change permissions. Use the owner namespace and stable artifact ID returned by the artifact CLI. Brackets hold primary content; optional named attributes belong in braces, following the [remark-directive extension](https://github.com/remarkjs/remark-directive), not core CommonMark. DreamLake defines the resource semantics. Saved `:artifact{namespace="geyang" id="pitch-deck"}` and `#artifact:geyang/pitch-deck` also work in the local development UI. The tag resolves the title and opens the artifact panel to the right of the Note without changing permissions or visibility. Static API HTML preserves the reference as an unresolved, atomic source span; it does not embed artifact content or share-token URLs. Production deployment is not yet verified. See [Notes reference syntax](/notes/#artifact-references-development-preview). ### Slide and section reference syntax `:artifact[geyang/pitch-deck#slide-3]` retains an existing target ID; `#/3` is valid only if the artifact defines that route. Clicking sends the fragment to the reusable panel through `dreamlake.route.hash`, retaining its running iframe. See [fragment syntax](/notes/#fragment-reference-syntax-development-preview). ## Browsing in the app `//profile?tab=artifacts` and `//artifacts` reuse the same Artifacts catalog. Your own namespace and organizations you belong to show resources and actions allowed by your permissions. Signed-out visitors and signed-in visitors to other namespaces see public resources only, without creation or modification controls. Profile uses an avatar rail; the application uses resource navigation for the namespace in the URL. Shared with me, trash and modification controls remain restricted to permitted member views. Public artifacts can be read without sign-in. Private share links require the recipient to sign in. The application sidebar shows the resource owner's avatar and links, including for anonymous public readers. Your signed-in identity and personal/organization switcher are separate from that owner. See [Profiles and workspaces](/workspaces.md). The detail header returns anonymous readers to `//profile?tab=artifacts` and signed-in readers to `//artifacts`. Details opened inside a project retain their return-to-project action. There is no extra sign-in navigation bar. ## Push one ```bash dreamlake artifact push ./report.html ``` That's it. The CLI prints an **open link** — click it to see your artifact rendered at `dreamlake.ai//artifacts`. | Kind | File | |------|------| | `html` | Any self-contained page | | `react` | A component file that defines `App` | | `markdown` | `.md` docs | | `svg` / `mermaid` | Diagrams | | `code` | Anything else — rendered with syntax highlighting | ## Version it Push the same `--id` again and you get a new version — nothing is overwritten, and the viewer has a version picker. ## Share it Artifacts are **private** by default. Two ways to open them up: | | Command | Who can view | |---|---------|--------------| | 🔒 private | *(default)* | Only you and namespace members | | 🔗 share link | `push --share` | Signed-in recipients with a valid share token | | 🌐 public | `push --visibility public` | Anyone, no login | You can also toggle visibility and copy links from the artifact's page — the Share button does the same thing. ## Delete it (safely) `delete` is a soft delete: the artifact moves to your gallery's **trash** tab and any share link stops working. Restore it any time — or erase it forever. ```bash dreamlake artifact delete report # → Trash (restorable) dreamlake artifact restore report # bring it back dreamlake artifact delete report --permanent # erase forever — no undo ``` ## Let Claude do it Add the [artifacts skills](https://github.com/dreamlake-ai/dreamlake-skills) and ask Claude to *"push this dashboard as a dreamlake artifact and share it"*: ```bash git clone https://github.com/dreamlake-ai/dreamlake-skills.git ~/dreamlake-skills mkdir -p ~/.claude/skills ln -s ~/dreamlake-skills/dreamlake-artifacts ~/.claude/skills/ ln -s ~/dreamlake-skills/dreamlake-artifact-authoring ~/.claude/skills/ ``` Install both skills: one publishes, the other prepares content for the offline rendering frame. To update both, run `git -C ~/dreamlake-skills pull --ff-only`. Use a project's `.claude/skills/` directory for project scope. If a skill already exists, inspect and preserve local edits before replacing it with a symlink. ## Next steps Every flag — ids, kinds, namespaces, visibility. The REST endpoints behind push, share, and trash. ## Artifact paths and local routing The artifact render iframe uses a meaningful resource path and ordinary local query/fragment state: ```text https://artifacts.dreamlake.ai/geyang/pitch-deck?slide=3&view=chart#overview ``` There is no internal instance ID in its URL. The path identifies the resource; it is **not authorization**. The DreamLake viewer performs the authorized content read, then sends the content to this isolated frame through a validated handshake. The frame never receives the viewer's authentication or share token. The corresponding shareable **viewer** link is: ```text https://dreamlake.ai/geyang/artifacts/pitch-deck?art.slide=3&art.view=chart#overview ``` Only this outer viewer query uses `art.` to distinguish artifact state from host parameters such as `share`. The fragment is ordinary `#overview` on both URLs. Earlier development links using `#art=overview` are still read, but new links use the plain fragment. The `art.` prefix never reaches the frame query. Opening the frame address directly shows an explicit embed-only page with a link to the authorized DreamLake viewer. It does not fetch private content, invent a public read endpoint, or copy capability query fields into that link. Public/private/share-link rules continue to be enforced by the viewer and API. Ad-hoc Note/file previews have no catalog identity and use `/_preview`; catalog thumbnails and artifact detail viewers use the actual namespace/artifact path. Inside HTML and React artifacts this becomes `dreamlake.route.search === '?slide=3&view=chart'` and `dreamlake.route.hash === '#overview'`. Read strings with `new URLSearchParams(dreamlake.route.search)`. Values can contain Unicode, spaces, JSON text, or other strings; use `URLSearchParams` to encode queries and `encodeURIComponent` for a fragment when building a URL. Repeated keys and empty values are supported. Parse numbers/JSON and validate their meaning in your artifact. Route data is public, user-controlled state, never a secret. ```html
``` `navigate({search?, hash?}, {replace?: boolean})` merges omitted fields with the current route and returns a Promise. Set a field to `''` to clear it. It pushes browser history by default; `{replace: true}` replaces the current entry. `subscribe(callback)` returns an unsubscribe function; callbacks read the new snapshot using `getSnapshot()` or the `search`/`hash` getters. React artifacts can use `React.useSyncExternalStore(route.subscribe, route.getSnapshot)`. Use an effect cleanup for other subscriptions. Back/forward and incoming route changes update the running artifact without reloading its iframe or resetting forms, React state, or WebGL scenes. The outer render frame's `location.pathname`, `location.search`, and `location.hash` reflect the clean artifact address. Use `dreamlake.route` for navigation without reloads and for a common API across HTML and React. HTML still runs inside an opaque, sandboxed `about:srcdoc` child; its own `location` is not the outer frame URL. The route API supplies the same values without weakening that sandbox. The host owns browser history; the frame mirrors route changes with `replaceState`, preventing duplicate history entries. Reloading the iframe performs a fresh handshake and reloads authorized content for the same path. Directly assigning `location.search` in React reloads the frame; prefer `route.navigate` to preserve component state. Native React fragment changes mirror to the host using replace semantics; native HTML anchors stay in the opaque child. Use the route API when an HTML route should survive sharing. Only the standalone artifact detail page binds this API to browser history. Gallery thumbnails, file/Note previews, and project-embedded viewers do not inherit the surrounding page's query/fragment. Interactive previews can use the route API locally. The detail Share/Copy link includes the current route. Public links contain no share token; private sharing adds only the intended read-capability token. Updating a route preserves host authorization in the address bar but never exposes it to artifact code. Parameter names (after removing `art.`) must match `[A-Za-z][A-Za-z0-9_.-]{0,63}`. Reserved names, case-insensitively, are `share`, `token`, `auth`, `authorization`, `cookie`, `project`, `namespace`, `instanceId`, `__proto__`, `prototype`, `constructor`, and `dreamlake`, including names followed by `.`, `_`, or `-`. Search plus hash is limited to 8192 characters. Invalid API navigation rejects; invalid link parameters are ignored and an oversized link route becomes empty. Host parameters such as `share`, auth, and project fields are never blanket-forwarded. A parameter is ordinary data, not permission to query private resources or escape the sandbox. The existing query/download bridge and its authorization/confirmation rules still apply. For a deep link from a Note, use an ordinary Markdown link with this URL, or `:artifact[namespace/id#slide-3]` to open its hash route in a right-hand panel. Do not put a share token inside rich-reference attributes. This contract requires the companion frame and app changes. Release the frame first, including its SPA fallback (`/* /index.html 200`), then the app. The new frame accepts old root `/#af...` handshakes for existing hosts; only legacy root URLs interpret the fragment as protocol state. Modern paths use a per-document boot challenge and host instance identity exchanged exclusively in messages, checked together with the source window and allowed/pinned parent origin. Repeated readiness for the same document does not reinitialize the artifact. After reload, messages from the previous document cannot complete new requests. The new host can answer an old frame's ready message if that renderer loads, but older frame deployments may lack the clean-path fallback and route API. Do not deploy the host before the new frame is verified. Source validation does not mean this feature is deployed. No server API or CLI change is required. ## Artifact panels in Notes (development preview) Use `:artifact[namespace/id]` to open the shared artifact viewer beside a Note, including Notes within a project. Append the artifact's own hash route to select a slide: `:artifact[geyang/landing-pages#slide-3]`. Clicking a second reference to the same artifact selects its existing panel and applies the new fragment. The artifact receives it through `dreamlake.route.hash`; its own code defines what that fragment means. The surrounding Note/project URL stays unchanged. This is the same viewer used by standalone artifact pages and project items, including version selection, preview zoom, and permission-checked management controls. Closing the panel leaves the Note open. Tags grant no access, and never contain a share capability. Full artifact links remain available with Control/Command-click. These panel changes require the companion UI release; publishing these docs or uploading an artifact alone does not deploy them. ## Manage an existing link CLI 0.32.4 adds `artifact share get/create/revoke` and `artifact visibility` without another upload. Inspect with `get` before changing access and after the requested change. Follow the [complete CLI sharing recipe](https://cli.dreamlake.ai/artifacts#manage-existing-share-links). Omitting `--share` on a later push preserves the existing token; it does not stop sharing. Use `dreamlake artifact share revoke ` to clear the token. Revocation blocks link-derived access while sharing is disabled; recorded recipients may regain access if sharing is enabled again. Public visibility and namespace membership are independent of the link. --- Source: https://docs.dreamlake.ai/hosted-pages # Generate & Hosted Pages A hosted page is a React app written by an AI agent and kept alive in its own container. There is no build and no deploy: the page runs a dev server, so an edit made from chat appears in the open browser within seconds — at the same URL, forever. ## Quick experience The whole loop from the command line — no build, no deploy, one URL throughout. ```bash # 1. Install and sign in curl -fsSL https://dl.dreamlake.ai/install.sh | sh dreamlake login --env prod # 2. Generate — the URL is printed within seconds dreamlake page generate \ --description "Three stat cards: Total, Reviewed, Pending." # https://host.dreamlake.ai/page/eba91469e21e/ # 3. Edit — applied over HMR, same URL dreamlake page edit --workspace-id eba91469e21e \ --description "Switch to a dark theme with a blue accent." # 4. Inspect and remove dreamlake page status --workspace-id eba91469e21e dreamlake page delete --workspace-id eba91469e21e ``` > **Note:** Run step 3 with the page open in another window: nothing reloads, nothing > rebuilds, and the URL never changes. ## Generating a page The agent writes an ordinary Vite React project. A container starts, runs `pnpm dev`, and the page is reachable. Nothing is compiled, uploaded, or published. ## Serving Each page has its own container and its own dev server. The router maps the URL path to the right one. ## Editing a live page This is the part that replaces "rebuild and redeploy". The agent edits the source code **inside** the container. Vite notices, and pushes just the changed module to every browser that has the page open. A page being watched on a wall display updates without anyone reloading it. ### When an edit breaks the page The agent can read the dev server's compile output and the browser's runtime errors, so it sees its own mistakes and fixes them before reporting success. ## Idle pages A page nobody is looking at costs nothing. It stops after 30 minutes and comes back on the next visit. ## Security model ### Isolation Edit instructions come from end users, in plain language. So the agent never touches the host. A malicious or simply confused instruction can corrupt one page's source code — which the same mechanism can then repair. It cannot reach the machine, the other pages, or anything sensitive. ### Kernel isolation Containers run under **gVisor** (`runsc`) rather than the normal runtime. gVisor implements Linux itself, in user space. The container's syscalls are answered by that kernel — the host kernel sits behind it and is never called directly. The container even reports its own kernel version, `4.19.0-gvisor`. The usual way out of a container is a host-kernel bug. Here the host kernel is not the thing on the other side of the syscall boundary, so that class of escape has nothing to aim at. ### Shell / data separation Two modes. A page built from a plain description has no business data to protect. A page wired to real records does — and even then, its source code contains **no business data and no credentials**. Only two things are written into the page — the API base URL and the annotation result ID. Everything else is fetched at runtime. Access control lives in the API layer, not in the page, so a leaked URL exposes an empty shell. ### Not embeddable by third parties When the page loads, it asks its parent window for the current user's token, then calls the DreamLake API with it. The token never appears in the page's source code. **If no parent responds** — the URL is opened directly, or embedded in someone else's site — the page shows a blank auth-required screen. There is nothing to extract without a live token from a trusted parent. | Layer | Mechanism | Description | |---|---|---| | Outer | frame-ancestors CSP | A response header restricts embedding to `*.dreamlake.ai` — the browser rejects third-party embeds before any JavaScript runs | | Middle | Origin allowlist | The console's token responder only replies to requests from trusted origins, so a page embedded elsewhere never receives a token | | Inner | API JWT auth | Every API call carries the user's JWT; the server verifies it and enforces namespace access control — the same rules as everywhere else in DreamLake | --- Source: https://docs.dreamlake.ai/notes # Notes Read [Markdown authoring](/notes/markdown/), [Embeds and query arguments](/notes/embeds/), [Panels](/notes/panels/), and [Linked note items](/notes/linked-items/) for focused guides. This page retains the complete CLI/API reference and existing section links. A note is a collaborative Markdown document. This is how a script — or a coding agent working through bash — edits one while people have it open. See [Panels and agent control](/notes/panels.md) for artifact previews, pinned tabs, and programmable native layouts. In live preview, an opening H1 with content below it is positioned above the viewport once, before interaction. Scrolling back to the title keeps it visible; typing, blur, and idle time do not automatically hide it again. Raw Markdown, title-only notes, and explicit search or section navigation retain their existing behavior. Formatting remains enabled while editing. Vim visual selections remain visible in both rich and raw views. Each connected browser session shares its cursor and selection, including other sessions of the same account. Clearing a selection updates it immediately; leaving editor focus removes its shared cursor and selection. Hidden tabs leave visible presence. Visible sessions renew presence every 30 seconds; peers expire after 90 seconds without renewal. Cursor labels size to their names, capped at 20 characters of display width with ellipsis. Remote text updates preserve the visible text position in the note pane; a new scroll gesture, keystroke, or selection takes precedence over a pending viewport correction. Use a collaborator avatar to navigate to its current cursor when the location is available. A cursor at the beginning of the note shows a popup saying **Cursor is at the start of the note** and leaves your view in place. An avatar without a current cursor shows a popup saying **No location available**. Other available cursor locations support the existing jump action. History timeline previews return to the current working draft when the pointer leaves the timeline. An explicitly placed edit marker or selected change range keeps its historical view open; clicking a version label alone does not pin it. ## Experimental native rich Markdown editor In **Settings → editor experiments**, enable **Native Markdown editor (experimental)** to switch the main Notes editor to the native rich Markdown editor. The setting is off by default, applies only in this browser, and takes effect immediately. Other browsers and teammates keep their own choice; disable it to return to the CodeMirror editor. The native editor keeps canonical Markdown in the same collaborative RTC document. It supports rich Markdown presentation, formatting and insertion commands, comments and suggestions, references, folding and section navigation, Vim mode, audio controls, and collaborator cursors and selections. Notes keep their existing save, sync, version history, and recovery controls. Switching between rich presentation and raw Markdown does not change the note source. In rich presentation, native headings align with the surrounding prose: the heading marker and its separator whitespace do not add a visual indent. Paragraphs and lists use consistent spacing, and blank lines retain a visible editing position. These presentation rules preserve the original Markdown, including heading spaces and line breaks; raw Markdown keeps the source visible. ### Stable prefixes and structural Backspace The native rich editor keeps heading (`#`), list (`-`, `1.`), and quote (`>`) prefixes in a fixed left gutter. Activating a line keeps its body text in the same position and preserves its wrapping. Nested lists keep a stable indent at each level, and task items keep a stable checkbox slot. With a collapsed caret at the start of visible text, **Backspace** converts an `#`-style (ATX) heading to a paragraph; outdents a nested list or task item one level together with its subtree; converts a root list or task item to a paragraph; or removes one quote level. Within text or at a soft wrap, Backspace performs ordinary character deletion. **Undo** restores the structural edit as one action. Raw Markdown mode keeps literal deletion behavior. These changes apply only to the native experiment. Underlined (Setext) headings have no leading marker and retain ordinary deletion behavior. This editor remains experimental, and CodeMirror remains the default. The browser preference does not change the CLI, API, note format, or permissions. Use the DreamLake CLI for supported operations. Use Python or TypeScript APIs only when a required operation is unavailable through the CLI or the task explicitly requires SDK integration. {/* */} ## Read Notes directly **For normal reads, run the bare command and inspect its output directly:** ```bash # Set NOTE_ID to the note ID, slug, or exact title you want to read. dreamlake notes read "$NOTE_ID" ``` Do not add `--json` or `--view` for ordinary human or agent reads. The default output includes canonical source, a revision, and a content hash. Retain the revision and hash with the source when preparing safe edits; agent convenience is not a reason to switch to JSON. Use JSON only for an explicitly requested structured integration. The scripted concurrency examples below demonstrate that compatibility path; they are not the default reading procedure. Read normal collaboration and selection receipts directly too. {/* */} ## Public catalog reads `GET /namespaces/:slug/notes` accepts requests without an Authorization header. Anonymous callers and authenticated nonmembers receive only live public notes; namespace members retain their existing catalog access. Pagination totals use the same visibility filter as the rows. A supplied invalid or expired token returns 401 rather than silently falling back to anonymous access. Anonymous searches do not activate or flush collaborative rooms. Creating, editing and sharing notes still require authentication and their existing permissions. ## Browsing in the app `//profile?tab=notes` and `//notes` reuse the same Notes catalog. Your own namespace and organizations you belong to show resources and actions allowed by your permissions. Signed-out visitors and signed-in visitors to other namespaces see public resources only, without creation or modification controls. Profile uses an avatar rail; the application uses resource navigation for the namespace in the URL. Your own Notes catalog retains Shared with me and recent organization notes. Private Note share links require sign-in under the server's per-person grant rules. The application sidebar shows the resource owner's avatar and links, including for anonymous public readers. Your signed-in identity and personal/organization switcher are separate from that owner. See [Profiles and workspaces](/workspaces.md). The detail header returns anonymous readers to `//profile?tab=notes` and signed-in readers to `//notes`. Details opened inside a project retain their return-to-project action. There is no extra sign-in navigation bar. ### Project and bindr associations Adding a project or bindr from a note changes membership while keeping the current note, URL, search and panes open. Choose a destination project inside the association picker. Bindrs belong to that project; identical bindr names in different projects are separate destinations. A bindr association uses the existing mounted note node, not a new copy or arbitrary filesystem placement. Pending operations disable duplicate submissions. A failed bindr addition may leave a successfully added project association; retry the bindr addition after reviewing the inline status. Removing an association uses the same context preservation behavior and retains the last-project guard. Use the separate **Open project** or **Open bindr** links when you want to navigate. ### Matching passages Searching the Notes catalog shows up to two distinct matching passages beneath each result title, with matching words highlighted. Hover or focus a passage to open a line-based popover, or use **Preview matches** from the keyboard. The popover shows multiple matching paragraphs with their line breaks and all query highlights. Repeated excerpts appear once with an occurrence count; expand them to choose the exact section and occurrence. Selecting a passage opens the note in the existing pane and selects the matching occurrence when its current source still agrees with the result. Identical wording at different source positions remains distinct. A changed source reports **Match changed** and asks you to choose a current occurrence instead of using stale offsets. Title-only matches open the note normally. The current catalog query and list scroll survive opening a passage. Source-only matches that cannot be mapped safely to rendered Markdown remain visible in the explicit source excerpt; the interface does not guess a rendered position. ### Resource subviews Project file and folder details use native draggable sibling view tabs. Files offer **Preview** and **Details**; folders offer their available **Files**, **README**, **Visualize** and **Episodes** views. Each tab identifies its resource and subview. In the project view, selecting another resource reuses the existing resource tab slots instead of accumulating README/Files pairs for visited folders. Open note tabs remain in place. Drag a tab to an edge to compare views side by side, or into a panel's center to group it. Closing a subview leaves its siblings open; **Views** reopens closed views. Tab switches retain mounted view state. Existing source-browser URL-owned view controls keep their navigation behavior. ML-Dash run inspectors use the same native tabs for **Params** and **Log**, while detail pages keep their existing separate log panel. ## Install **CLI** ```bash curl -fsSL https://dl.dreamlake.ai/install.sh | bash dreamlake login ``` **Python** ```bash pip install dreamlake dreamlake login ``` Both read the same saved login. `pip install dreamlake` installs the Python SDK; install the standalone CLI using the CLI tab before running `dreamlake login`. The Python package is not the supported CLI installer. ## Collaboration and sync People can edit the same note simultaneously. Edits merge through the existing CRDT; checking sync does not lock the note or wait for other editors to stop. The status beside the note title describes this tab: | Status | Meaning | |---|---| | **Synced** | This tab matched the server revision and text checksum. | | **Syncing** | Changes or verification are still in progress. | | **Out of sync** | Sending is paused; inspect the recovery dropdown. | A connected socket alone does not prove that text matches. The browser requests a checksum calculated by the RTC server and compares it only when both hold the same revision and no local changes are pending. Different revisions, slow acknowledgements and delayed checks remain **Syncing**; they are not proof of corruption. New edits invalidate the previous verification. Typing and selection replacement stay local while the edit is applied, so intermediate delete/insert events do not reset the cursor. Composition finishes before an incoming update changes the editor. Updates from other people continue to merge normally. ### Reconnect and recovery During a temporary disconnect, the tab keeps pending operations and retries on reconnect. It replays their original identities only against a compatible server checkpoint. A proven text mismatch, an update that cannot be applied, or a changed checkpoint that prevents safe replay pauses sending and retains a local draft. It does not automatically send that held draft after a refresh. Open **Out of sync ▾** beside the note title: 1. Choose **Download local draft** to save your text before discarding anything. 2. Choose **Use server version** to discard the held local draft and fetch current server content. This does not overwrite the server note. 3. Compare your downloaded draft with the note, then reapply any missing changes in the editor. **Download edit data** saves `note-edit-data.json` with the local editing and sync state for inspection. It does not send edits, discard the draft or load a server version. This is diagnostic data, not an automatic restore/import action. A retained draft normally survives refresh in the same browser tab. Browser storage can be unavailable; follow the warning to copy or download it before closing or refreshing. Do not clear browser storage as a recovery shortcut. Legacy CLI/Python edit helpers retain their conditional revision checks below. The v2 patch interface uses merge mode by default and opt-in exact mode. They cannot read or recover an unsent draft held in another browser tab. A fresh CLI read describes server content, not proof that every open editor matches it. ## Time travel Click the **Time travel** history icon beside the title to open a near-full-screen, resizable viewer. Use **Play/Pause**, previous/next step, or the ticked slider to browse retained versions. Each tick selects one recorded edit; the slider snaps to those ticks. The viewer is **view only**. It captures the checkpoint and retained edits when opened, renders them separately, and does not replace the live editor or publish changes. Close and reopen it to include newer edits. There is no restore action. When you step or play through versions, added rendered text briefly glows green. Removed text appears in red with a strikethrough, fades, then disappears. Colors compare the view you left with the view you entered: stepping backward reverses which text appears and disappears. A jump compares the two selected views rather than replaying every intermediate edit. Formatting-only changes update normally. Pausing playback freezes an active highlight; resuming continues it. Stepping or scrubbing cancels the old transition so ghosts never pile up. Faster playback uses shorter fades. Reduced-motion preferences use static highlights that clear without fading. Very large comparisons display the version without highlights to keep navigation responsive. Deleted ghosts are presentation-only, excluded from accessibility output, selection, and the code-copy action. They never alter saved text or the live editor. Scroll position stays under your control. History starts at the retained checkpoint: compaction can remove older versions. This is not a complete archive, and playback is not a recovery tool for an unsent local draft. Download a held draft through the sync dropdown instead. ## For coding agents ```bash git clone https://github.com/dreamlake-ai/dreamlake-skills.git ~/dreamlake-skills mkdir -p ~/.claude/skills ln -s ~/dreamlake-skills/dreamlake-notes ~/.claude/skills/ ``` `git pull` then updates it. Use `.claude/skills/` for one project. `dreamlake-cli` in the same repo is the full CLI reference. The Notes skill's procedure is generated from this guide. Correct examples here first, then regenerate the docs reference and synchronize the public skills repository using the [docs-to-skills procedure](/dev/skills.md). ## Placeholders Use square brackets with whitespace immediately inside both brackets for text that still needs to be filled in: `[ xxxxx ]`, `[ owner name ]`, or `[ launch date ]`. Notes show these as blue highlighted inline boxes with visible brackets and inner spacing: `[ owner name ]`. Hover over a box to see **placeholder**. This works in the editor, table cells and read-only view. Keep the brackets until you replace the placeholder with its final value; the saved Markdown remains plain text. `[text]`, `[ text]`, and `[text ]` are plain text, not placeholders. Empty brackets and whitespace-only content are not placeholders. ```markdown Owner: [ owner name ] Launch: [ launch date ] Review #note:6aa9951250d9de84058e8ebb before publishing. ``` References take precedence: Markdown links such as `[guide](https://docs.dreamlake.ai/notes/)`, reference links with a definition (`[guide][docs]` or `[docs]`), images, and `#note:` keep their reference behavior. Do not turn a reference into a placeholder. Task markers (`[ ]` and `[x]`) and brackets inside code are not placeholders. Escape the opening bracket (`\[literal]`) when you want ordinary bracketed prose without a highlight. ## Create and list **CLI** ```bash dreamlake notes create "Design Doc" dreamlake notes create "Design Doc" --file draft.md dreamlake notes create "Design Doc" --text "# Design Doc" dreamlake notes create "Design Doc" --public # default is private dreamlake notes list dreamlake notes list --limit 20 dreamlake notes list --shared # what others sent you dreamlake notes list --json ``` **Python** ```python import dreamlake as dl note = dl.create_note("", "Design Doc", text="# Design Doc\n") note.id, note.namespace, note.etag dl.list_notes("") dl.list_notes("", limit=20, offset=20) dl.shared_with_me() ``` Titles may repeat — the slug takes a suffix — so keep `note.id` rather than the name you passed. ## Link to a note in the browser Use the note's full `id` in browser links: ```text https://dreamlake.ai//notes?note= ``` Read `namespaceSlug` and `id` from `dreamlake notes create --json` or `dreamlake notes list --json`. Do not substitute the title or human-readable slug in this URL: the browser detail route expects the ID, even though the CLI accepts slugs and titles. Use the returned owner namespace rather than assuming your personal namespace. The ID is sometimes called the note hash; it is the `note` query parameter, not a `#` URL fragment. In the development preview, the path controls the list pane independently of the active note: - `//notes` lists notes. - `//projects` lists projects; `/projects/` opens a project. - `//bindrs` lists Bindrs; `/bindrs/` opens a Bindr. Append `?note=` to any of these paths to open a note. Switching list context keeps that note open. The note header's contextual list button hides or restores the list pane. Older note and project links redirect to these routes. List search includes ordering; default status/category chips are omitted from the compact panes. Project and Bindr member ordering is applied before pagination so it covers the entire result set. Inside another DreamLake note, prefer `:note[]` (development preview) for a native note reference. A browser link does not change visibility or grant access to a private note. ## Inline text color and highlights Use a color directive to style an inline span in Notes previews, table cells, and the app’s rendered Markdown: ```markdown :color[Important]{color="#ef4444"} :color[Ready]{color="green"} :highlight[Review needed] :highlight[Key finding]{color="#60a5fa"} :color{text="Review needed" color="#f90"} ``` The content is plain text, including any Markdown markers. Escape brackets and backslashes with a backslash in bracket content. Color values must be quoted: 3, 4, 6 or 8-digit hex colors, or `black`, `silver`, `gray`, `white`, `maroon`, `red`, `purple`, `fuchsia`, `green`, `lime`, `olive`, `yellow`, `navy`, `blue`, `teal`, `aqua`, `orange` or `rebeccapurple`. Unknown attributes and invalid colors remain literal. Code, escaped directives and Markdown links remain literal too. Selecting a directive in the editor reveals its original editable source; saved Markdown is unchanged. `:color` changes the foreground; `:highlight` adds a translucent background tint and keeps the surrounding text color. Omit the `color` attribute to use yellow: `:highlight[Important]` or `:highlight{text="Important"}`. Highlights accept the same colors and plain-text content as color directives, including the attribute-only form `:highlight{text="Review needed" color="yellow"}`. Raw HTML and arbitrary CSS styles are not enabled. See the [Markdown authoring guide](/notes/markdown/) for formatting examples, color choices, tables and portability. CLI/API HTML snapshots currently keep color directives as source text. ### Highlight metadata Attach optional `user` and `comment` strings to a highlight: ```markdown :highlight[Review needed]{user="geyang" comment="Confirm the delivery date"} :highlight[Key finding]{color="#60a5fa" user="geyang" comment="Check the source"} :highlight[重点 🤖]{comment="First line\nSecond line"} ``` Highlight annotations reuse the Notes inline/sidebar comments toggle. Inline mode shows no annotation cards or hover popups. Click highlighted text in the editor to reveal its editable source. Sidebar mode shows the handle and comment in compact cards in the table-of-contents column, with an edit action for writers. Cards follow the passages visible in the current viewport; a dense group scrolls inside the column. Narrow panes fall back to inline mode. Read-only sidebar cards show metadata without edit controls. `user` is the canonical public user handle, such as `geyang`, not an internal user ID or a display name. A single leading `@` is accepted; the saved source is not rewritten. Autocomplete inserts the canonical handle. Compact sidebar cards show the handle. Legacy display-name values remain literal; the app never guesses an account from a name. Attribution is self-declared and does not verify authorship or grant access. These are plain-text annotations on a highlight, not saved comment threads. Either field may be omitted; empty strings add no label. Existing plain highlights and colors keep their behavior. Select the directive in the editor to edit its source, including metadata. Metadata does not change the highlighted text or its source offsets, including in table cells and read-only app views. Attribute values use JSON string escaping: `\"` for a quote, `\\` for a backslash and `\n` for a newline. HTML in metadata stays text. Unknown or duplicate attributes and malformed quoting leave the whole directive literal. Use `user`, not `author`; only `color`, `user` and `comment` are accepted secondary attributes. Agents should first read the note and retain its revision, then replace the exact existing directive using `--if-match` and read it back. For example, set `NOTE_ID` to the target note ID and read its legacy ETag (the replacement helper uses an ETag, not a v2 `rtc:` revision): ```bash # NOTE_ID is the ID returned by create/list; this edits an existing highlight. SNAPSHOT=$(mktemp) dreamlake notes read --legacy --note "$NOTE_ID" --json > "$SNAPSHOT" REV=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["etag"])' "$SNAPSHOT") dreamlake notes replace ':highlight[Review needed]' \ --text ':highlight[Review needed]{user="geyang" comment="Check the source"}' \ --note "$NOTE_ID" --if-match "$REV" dreamlake notes read "$NOTE_ID" --json rm "$SNAPSHOT" ``` The existing `notes create --text` / `--file` commands also accept this syntax. There is no dedicated highlight command: these are ordinary Markdown directives, so matching text with `notes replace` is enough; no line numbers are needed. Do not overwrite the whole note to update one annotation. CLI/SDK storage already accepts this Markdown; no new client method or package version is required. CLI/API HTML snapshots retain rich directives as source text; the app renders them. ### Web preview tags Open a web page beside a Note with a preview tag: ```markdown :preview[https://example.com/deck/#slide-3]{title="Slide 3"} ``` The URL is required; the title is optional. Tags work in the editor, tables and rendered Markdown. A normal click opens the built-in web preview panel beside the Note. New targets open as tabs in the existing panel on the right; a right panel is created only when one is absent. Reopening the exact URL reuses its tab; different URL fragments retain separate preview tabs. Note references follow the same rule. References opened from a side Note add tabs in that same side panel, preserving the current Note and its edits. Modified clicks keep ordinary browser link behavior. Only absolute HTTP(S) URLs without embedded credentials are accepted. Invalid syntax, duplicate or unknown attributes, unsafe schemes, code and escaped tags remain literal text. The saved source is unchanged by rendering. A target must allow iframe embedding. Its own authentication and framing policy still apply. A temporary tunnel URL works only while its tunnel and server run. Browser static rendering retains an inert label before hydration; server CLI HTML snapshots currently leave preview directives as literal source with the existing source mapping. They do not load the target or create a panel. ### Embeds See [Embeds and query arguments](/notes/embeds/) for responsive ratios, fixed sizes, zoom, and the artifact/preview query API. Inline references stay in the text flow; `embed` shows a content block; opening a reference shows its standalone page in the preview/browser. ### Artifact references (development preview) Use Markdown directive notation for new references: ```markdown :note[6ab5aaeed3b4339ea2f4c162] :artifact[geyang/pitch-deck] :bindr[bindr-id] :asset-reference[asset-id]{caption="plot"} :placeholder[owner name] :chatgpt-content-reference[0] ``` This follows the [remark-directive convention](https://github.com/remarkjs/remark-directive), a Markdown extension, not core CommonMark. Brackets hold primary content; braces hold optional named attributes. Resource semantics are DreamLake-specific. The namespace and artifact ID are both required because artifact IDs are scoped to their owner; note IDs resolve globally. The Note picker, extraction and copy reference button now prefer `:note[]` in the development UI. Both Note and artifact headers show a clickable `#…` badge with the last six ID characters. Clicking copies the complete bracket reference, including the owner namespace for artifacts; it does not create a share link or change access. Saved `#note:`, `#artifact:geyang/pitch-deck`, `:note{id="note-id"}`, `:artifact{namespace="geyang" id="pitch-deck"}` and all existing attribute-only rich components remain accepted. Do not bulk-rewrite stored notes. Bare `:note{ID}` and `:artifact{namespace/id}` are invalid. Secondary attributes currently include asset `caption`; values are double-quoted JSON strings. Unknown/duplicate attributes, conflicting primary values, missing fields and invalid IDs remain literal. Preserve exact source on reads and patches. Placeholder content can escape brackets and backslashes with a backslash. References grant no access and never change sharing. Missing/inaccessible resources remain unavailable. Code, escapes and Markdown links stay literal. Imported ChatGPT citation forms stay unresolved and retain their original source; never invent a Note or artifact to replace them. Existing `[ owner name ]` placeholders remain supported. Click an artifact tag in a Note or a project's Note pane to open the reusable artifact panel to the right of that note. The note stays open. Clicking another reference to the same artifact reuses its panel, including references to a different slide. Panels retain the artifact viewer's preview, version, zoom and authorized sharing controls, and use the common draggable tabs and close controls. Control/Command-click keeps the ordinary artifact link behavior. The current implementation is a development preview until the companion UI is deployed. The browser resolves titles through authorized artifact metadata. API HTML previews map each full token atomically and leave it unresolved, without fetching private metadata or embedding capability URLs. Artifact tags are implemented in the local development UI/API; production deployment is not yet verified. There is no artifact insertion picker yet: type/paste the complete token. ### Fragment reference syntax (development preview) A reference can retain a slide or section target as a URL fragment: ```markdown :note[6ab5aaeed3b4339ea2f4c162#overview] :artifact[geyang/pitch-deck#slide-3] :artifact[geyang/pitch-deck#/3] ``` The optional named form `:note[id]{fragment="overview"}` is also accepted. Specify the fragment only once. The parser separates it from the resource ID and preserves the exact raw token, including percent encoding. Malformed fragments remain literal. Static API HTML maps the complete reference atomically without fetching metadata or creating capabilities. Artifact references pass their fragment to the panel's local `dreamlake.route`. For example, `:artifact[geyang/landing-pages#slide-3]` opens the landing-page copy at stage 3. Selecting another slide updates the existing iframe without reloading it or changing the surrounding Note/project URL. Note-section scrolling remains separate from this artifact behavior. An existing artifact ID or an author-defined hash route must supply the target; do not infer slide numbering or invent a section. ## Suggested edits Use three tags for reviewable edits stored directly in the note: ```markdown :insert[new text]{user="geyang"} :delete[existing text]{user="geyang"} :replace[existing text]{with="replacement text" user="geyang"} ``` `user` is optional display attribution. `replace` requires `with`; an empty replacement is allowed. Add optional `reason="Why this change helps"` to explain a suggestion. Attribute values are JSON strings. Escape literal brackets and backslashes in the bracket body with a backslash. Insertion-menu choices fill the signed-in user's name; scripts can supply attribution explicitly. The bracket form is canonical. The browser also accepts a curly-body alias for all three kinds; optional named attributes follow in a separate pair of braces: ```markdown and I:insert[ think this works] and I:insert{ think this works} :delete{old text}{reason="No longer needed"} :replace{old text}{with="new text" user="geyang"} ``` A suggestion can directly follow ordinary text without an intervening space. Leading and trailing spaces inside its body are preserved when accepted. Curly bodies support balanced nested braces; escape a literal brace or backslash with a backslash. Canonical bracket bodies retain their existing bracket escaping. These aliases apply to suggested edits, not other directive types. Insertions are underlined and deletions struck through in the note. A replacement shows both. Inline mode shows these text changes without cards or hover popups. Switch to Sidebar in a wide pane for **accept · reject** actions. Compact cards replace the table of contents in its existing column and follow passages visible in the current viewport. Dense groups scroll inside that column. Hovering or focusing a card or text anchor highlights the corresponding annotation. Narrow panes fall back to Inline while retaining the Sidebar preference. Accept applies the proposed text: insert keeps new text, delete removes old text, and replace substitutes its `with` value. Reject removes an insertion or restores the original text of a deletion/replacement. Each decision replaces only that exact tag in one undoable editor operation and uses the note's normal collaborative save. If the source changed before the action, it refuses the stale operation. Note writers can accept/reject; read-only views show the proposal without write controls. No separate suggestion collection or replies are added. Incomplete or malformed tags remain literal. Tags inside code, Markdown links, or comments do not become suggested edits. Supported kinds are intentionally limited to insert, delete, and replace; use comments for questions or discussion. ## Comments (development preview) Comments use `:comment[text]` for text stored in the note and `:comment[cmt_<24 hex digits>]` for a saved comment reference. Both accept optional `{user="geyang"}` attribution and `mode="inline"` to keep an occurrence inline. Braces after a bracket contain metadata only; there is no `type`, `text`, `ref`, or `userId` field. Attribution is a display label; the server records the authenticated creator separately. Escape brackets and backslashes with a backslash. Use `\cmt_...` inside brackets when an ID-shaped string should be literal text. Code spans and fenced code remain literal. In the rich editor, typing `:comment{` starts a saved-comment draft. Keep typing in the note: its side box mirrors the body. The editor supplies a hidden draft key for retry safety; this key does not create a saved comment object. Closed drafts first save after 800 ms of inactivity once their body contains at least two non-whitespace characters, or with any nonempty body when the caret or focus leaves the comment. Empty and whitespace-only drafts never create objects. IME composition defers writes. Saving never moves the caret or replaces active text. Once the caret leaves and the latest body is acknowledged, source becomes `:comment[cmt_...]{user="..."}` (the optional user attribute is retained when supplied). Newly typed comment brackets and brace drafts automatically include the signed-in user's namespace as `user`. Opening a saved comment edits its object while the reference stays fixed. In Sidebar view the borderless editor and its Save action share the comment container; Save waits for the latest save before closing. Comments have no replies; conversations belong in chats. The sidebar starts with Comments when review annotations are present. In **Settings → Sidebar suggestions**, Automatic learns a small preference model from comment use and corrections; fixed Comments and Contents modes disable that automatic choice. The model makes at most one decision per note visit, when preview is enabled and the pane has room. Choosing a tab or collapsing the sidebar takes priority for the rest of that visit. Narrow panes keep annotations inline. An empty Comments view falls back to Contents without erasing the choice. New choices do not create per-note preference records. Existing saved note choices remain readable for compatibility. Learning is saved per account in this browser, with eight numeric context features, at most 32 recent feedback records, and an 8 KiB total storage cap. Records exclude note identifiers, titles, authors and bodies. Settings shows observation counts, storage use, recent outcomes and selection probabilities. You can choose how often the other view is tried, stop keeping recent records, clear records, or reset learning. Disabling or clearing the record history does not erase the aggregate model; Reset learning clears both while preserving your settings. The initial exploration rate is 5%. Comment use is a small positive signal; dismissing Comments or manually opening it after Contents corrects the model. Inactivity is never positive feedback. A decision without an explicit correction is evaluated after 60 visible, focused seconds; incomplete visits are discarded. These signals estimate interface usefulness, not user satisfaction. **Comments → Inline / Sidebar** changes the current view, independently of storage. Inline comments show the author label and italic text in the author's collaboration color, with faint brackets around the body. Sidebar comments use `[…]` anchors and compact bracketed cards. Short comments wrap in full; longer comments show four lines with **Read more** to expand a scrollable reading view. **Edit** opens the saved-comment editor separately. The editor grows with its text up to a bounded height, then scrolls. Cards replace the table of contents in the same column, follow visible passages, and scroll within the column when densely packed. Hovering or focusing the anchor or card highlights its matching annotation. Inline mode has no annotation cards or hover previews; explicitly opening a saved comment opens its editor beside the clicked comment, within the visible window, without scrolling the note to the top. The editor's **Resolve** action saves pending changes before removing that comment occurrence from the note; **Save** closes the editor without removing it. The **Inline this** button sits after the resolve checkmark. At rest it is a Lucide chevron; hover or keyboard focus animates it into a left arrow pointing at a vertical line. Reduced-motion preferences show the same states without animation. The action saves pending edits and adds `mode="inline"` to that occurrence, keeping it in the paragraph with the Comments sidebar open. Use the right chevron in its editor to return it to the sidebar. Both actions use editor history, and Undo restores the previous occurrence. Readers without note-edit permission do not see these actions. Narrow panes fall back to Inline while retaining the Sidebar preference. Read-only readers can open accessible saved comments but cannot change them. Rendering, loading, and remote text replay never create comment objects. A brace draft pasted by a script without an editor creation key remains source text; use the API to create a saved object deliberately. The same completion menu handles supported tag names after `:`, accessible resource targets within `[`, and supported attributes within `{`. The `user` attribute offers people lookup. Free text stays valid; searching does not save or convert it. Comment bodies are free text: typing inside `:comment[` does not search saved comments. Existing saved-comment references still render normally. ### Collection API These endpoints require the matching server version. They are not a CLI release claim. Paths are relative to the DreamLake API base. | Method and path | Contract | |---|---| | `POST /namespaces/:slug/notes/:noteId/comments` | Body `{body, creationKey, user?}`; authenticated origin-note writer only. Key is 16–128 ASCII letters, digits, `_`, or `-`. | | `GET /namespaces/:slug/notes/:noteId/comments?q=...` | Up to 30 accessible origin-note comments, newest first; optional body substring search. | | `GET /namespaces/:slug/comments/:commentId` | Read the object under its original note's permissions. | | `PATCH /namespaces/:slug/comments/:commentId` | Body `{body, revision}`; compare-and-swap update; stale revision returns 409. | Returned objects include `id`, `noteId`, `body`, optional `user`, `createdBy`, `revision`, timestamps and `canEdit`. Bodies are nonempty and at most 20,000 characters. Retrying creation with the same note/key returns the existing object without overwriting it. A different key deliberately creates a different object. Reads of public origin notes allow anonymous callers; private origins and deleted origins do not become visible through a copied reference. Invalid credentials are rejected, and mutations still require authentication and write permission. Retain local text on a failed save or revision conflict. Retry uncertain creation with the same key, and never overwrite a newer object from an older draft. The editor retains recovery state for the current browser session; the keyed body remains in the note until acknowledged and collapsed. A changed object requires reconciliation, with the local draft available to copy. A lost response can safely be retried. Deleting an anchor does not delete its saved object. ## Name a note **CLI** ```bash dreamlake notes read --legacy design-doc # slug dreamlake notes read --legacy 6aa948b3fea6e541282b747e # id dreamlake notes read --legacy "Design Doc" # exact title # Every example below uses $NOTE_ID. Set it once — any of the three forms: NOTE_ID=design-doc ``` **Python** ```python dl.note("/design-doc") dl.note("6aa948b3fea6e541282b747e") # id alone, no namespace ``` A note you may not read answers exactly as one that does not exist. ## Read ### Prefer the smallest relevant read For a targeted question or edit, read the relevant section or passage instead of the entire note. Use `toc` or `find` to locate it when needed, then request that section or line range. Read the whole document when reviewing the whole document, establishing a required v2 baseline, or recovering a missing or expired baseline — not on every small edit or verification. Keep the baseline and its tokens across calls. Once a v2 baseline is available, use [incremental reads](#keep-incremental-reads-compact) to refresh the cached source and check edits; inspect only the relevant changed passages. A section read is not a complete v2 baseline. Do not replace the whole body with a partial read, or substitute a legacy ETag for an opaque v2 revision. With the current CLI, `--section`, `--start-line`, `--end-line` and `--numbered` require `--legacy`. V2 supports complete source and `--since` deltas, not section snapshots. Prefer the scoped compatibility read for inspection; obtain one full v2 baseline only when the planned patch workflow needs it and none is cached. Attributed reads also affect what collaborators see. A full-body read selects the full source; a scoped read selects its returned passage, and a delta read does not claim a whole-document selection. Match the requested range to the work you are doing. Do not issue repeated full reads just to refresh presence; use a heartbeat for an active session or let recent presence expire. ### Read commands The existing examples below use the explicit `--legacy` CLI contract (source `text` and content-hash `etag`). The v2 interface later in this guide uses `content`, `hash` and an opaque RTC `revision`. Do not mix their tokens. **CLI** ```bash dreamlake notes read --legacy "$NOTE_ID" # whole body dreamlake notes read --legacy "$NOTE_ID" --section install # one section dreamlake notes read --legacy "$NOTE_ID" --start-line 40 --end-line 80 dreamlake notes read --legacy "$NOTE_ID" --start-line 40 --end-line 80 --numbered dreamlake notes read --legacy --note "$NOTE_ID" --json # body + revision dreamlake notes toc --note "$NOTE_ID" # the outline dreamlake notes toc --note "$NOTE_ID" --json ``` **Python** ```python note = dl.note("/design-doc") note.text # whole body note.read_section("install") # one section part = note.read_lines(1, 40) part.truncated # there is more below part.total_lines doc = note.read() doc.toc() # anchor, title, level, line range, ind range ``` A **section** is a heading plus everything under it. Anchors are title slugs (`setup`, `setup-2` when repeated); text above the first heading is `preamble`. `toc` gives each heading an anchor, a **line range** and a **character range** — enough to edit a part without reading the whole note. A ranged read reports the **whole** note's revision. Writing a range back as the body deletes everything outside it — use `replace` instead. ## Edit by what it says Line numbers move when anyone edits above them; text does not. A query matching **twice is refused**, not applied to the first match. The examples below are independent recipes, not a script to run in sequence. Before each revision-checked edit, read the note and capture its revision (the CLI recipe uses `jq`): ```bash REV=$(dreamlake notes read --legacy "$NOTE_ID" --json | jq -er .etag) ``` Inspect the returned body before choosing the edit. After a successful write, read again before the next edit; do not reuse the old revision or bypass a conflict with `--force`. **CLI** ```bash dreamlake notes find "Draft" --note "$NOTE_ID" dreamlake notes find --regex '\bTODO\b.*' --flags im --note "$NOTE_ID" dreamlake notes replace "Draft" --text "Published" --all --note "$NOTE_ID" --if-match "$REV" dreamlake notes insert --text "New line" --line 10 --note "$NOTE_ID" --if-match "$REV" dreamlake notes delete "obsolete paragraph" --note "$NOTE_ID" ``` **Python** ```python doc = dl.note("").read() # local snapshot doc.find("Draft") doc.find(regex=r"\bTODO\b.*", flags="im") updated = doc.replace("Published", query="Draft", all=True) print(updated) # the full new source, not just the part changed doc.insert("New line", line=10) doc.delete(query="obsolete paragraph") print(doc.diff()) # what would change doc.save() # one conditional write ``` Replacement text is the **first** argument; `query` or `regex` names what to change. Each edit returns the whole updated source, not just the part it touched. Exactly one match is expected. `--all` / `all=True` takes every match; `--count N` / `count=N` requires exactly N. A failed edit changes nothing. Python edits are local until `save()`, which writes one patch against the revision `read()` returned. If the note moved, the save is refused. `doc.revert()` throws the local edits away. ### Patterns Regex is explicit — never inferred from the query. JavaScript syntax in both clients: `(?…)`, `$1`, `$`, `$&`, `$$` for a literal `$`. Capture expansion is automatic; there is no flag for it. **CLI** ```bash dreamlake notes replace --regex '(\w+)=(\d+)' --text '$1: $2' --all --note "$NOTE_ID" dreamlake notes replace --regex '(?timeout|retries)=(?\d+)' \ --text '$: $' --all --note "$NOTE_ID" --if-match "$REV" --dry-run ``` **Python** ```python doc.replace("$1: $2", regex=r"(\w+)=(\d+)", all=True) doc.replace("$: $", regex=r"(?timeout|retries)=(?\d+)", all=True) ``` `--dry-run` prints the result without writing. Every mutation takes `--if-match`. ### By line or character range When the text is awkward to name, address it by position instead. Positions come from a read or from `toc`. **CLI** ```bash dreamlake notes replace --text "Updated line" --line 10 --note "$NOTE_ID" --if-match "$REV" dreamlake notes replace --text "New block" --line 10:15 --note "$NOTE_ID" dreamlake notes replace --text "replacement" --ind 120:145 --note "$NOTE_ID" dreamlake notes insert --text "inserted text" --ind 120 --note "$NOTE_ID" ``` **Python** ```python doc.replace("Updated line", line=10) doc.replace("New block", line=(10, 15)) doc.replace("replacement", ind=(120, 145)) doc.insert("inserted text", ind=120) ``` Lines are **1-based and inclusive**. `ind` is **0-based and end-exclusive**, counted in Unicode code points — so an emoji or a CJK character is one unit, not two. `toc` returns both. A line edit keeps the line ending; a character edit adds nothing. ### HTML Markdown may contain HTML, and a note's body can be an HTML document outright. When it does, address an **element** rather than raw text — the same sentence often appears in several places, and a plain query would refuse the edit as ambiguous. **CLI** ```bash dreamlake notes select "#contact" --note "$NOTE_ID" # where it is, and its text dreamlake notes replace "Contact us" --text "Talk to sales" --selector "#contact" --note "$NOTE_ID" dreamlake notes insert --text "
  • New
  • " --selector "#list" --position append --note "$NOTE_ID" ``` **Python** ```python doc.select("#contact") doc.select("#contact").replace("Talk to sales", query="Contact us") doc.select("#contact").update(attrs={"href": "/sales"}) doc.select("#list").insert("
  • New
  • ", position="append") ``` A selector must match **exactly one** element; zero or several is an error. `select` on its own only reports — the element's source range and its decoded text — and changes nothing. Only the addressed characters change; entity spelling, attribute quoting and whitespace elsewhere survive. ## Write whole parts For replacing a section or the whole body outright, rather than editing text in place. **CLI** ```bash dreamlake notes write "$NOTE_ID" --file whole.md dreamlake notes write "$NOTE_ID" --section install --file install.md dreamlake notes append "$NOTE_ID" --text "one more line" dreamlake notes append "$NOTE_ID" --file more.md dreamlake notes add-section "$NOTE_ID" --file trouble.md --after install dreamlake notes rm-section "$NOTE_ID" troubleshooting ``` **Python** ```python note.write(whole_body) note.write_section("install", "## Install\n\npip install dreamlake\n") note.append("\n## Changelog\n\n- shipped\n") note.insert_section("## Troubleshooting\n\nCheck the logs.\n", after="install") note.delete_section("troubleshooting") note.refresh() # re-read after someone else wrote ``` Anyone with the note open **sees the change appear** — your edit merges with what they are typing. Nothing is locked. ### Changes since a read or edit **Legacy ETag interface:** available alongside v2 in CLI 0.26.2 and Python SDK 0.20.0. Select `--legacy` for these CLI recipes and `legacy=True` for remote Python patches. The CLI returns the same ref in `read --json` and accepts it via `--since`: ```bash # Requires jq. Keep the snapshot and ref for a later shell session. NOTE_ID=design-doc dreamlake notes read --legacy "$NOTE_ID" --json > note-snapshot.json REV=$(jq -er .etag note-snapshot.json) dreamlake notes diff --legacy "$NOTE_ID" --since "$REV" dreamlake notes diff --legacy "$NOTE_ID" --since "$REV" --json # Apply a diff prepared against that saved body: dreamlake notes patch --legacy "$NOTE_ID" --file change.patch --if-match "$REV" --json ``` `notes diff` requires `--since`; it does not keep a hidden local baseline. Plain output is the diff on stdout and current ETag on stderr. `--json` also returns `from` and `to`. The CLI help includes this workflow: `dreamlake notes diff --legacy --help`, `notes read --help`, and `notes patch --help`. Reads return `doc.etag`, a quoted SHA-256 hash of the complete note text. Keep that ref to see changes since your own read or successful edit across sessions: ```python note = dl.note("") doc = note.read() ref = doc.etag print(note.diff(since=ref)) # current text versus that snapshot print(note.diff()) # defaults to this handle's last ETag result = note.patch(my_diff, if_match=ref, legacy=True) ref = result.etag # reference for the successful edit ``` Fetching a diff does not advance the cached revision or write precondition. Call `read()` or `refresh()` to adopt the latest state. Identical text has the same hash; the ref identifies content, not an RTC operation index. Unknown refs return an error, including older refs whose snapshots were never retained. The HTTP endpoint is `GET /namespaces/:slug/notes/:noteId/diff?since=`. It returns `diff`, `from`, `to`, and `etag` (the current ref). Snapshots are scoped to the note and require current read access. Optional `context` accepts 0–100 lines, default 3. Partial reads return a ref for the complete body. For local draft changes, use `doc.diff()` for all unsaved changes, `doc.diff(since="last_edit")` for the latest local operation, and `doc.patch(unified_diff)` to apply a patch before `doc.save()`. ### Patch This legacy patch helper validates unified-diff context against current text and rejects mismatched context. Its optional ETag rejects any intervening revision. The v2 merge workflow below instead addresses the saved original native identities and preserves compatible concurrent edits. **CLI** ```bash diff -u before.md after.md | dreamlake notes patch --legacy "$NOTE_ID" --file - dreamlake notes patch --legacy "$NOTE_ID" --file change.patch --dry-run ``` **Python** ```python note.patch(unified_diff, legacy=True) note.patch(unified_diff, if_match=doc.etag, legacy=True) ``` ## Search **CLI** ```bash dreamlake notes grep "TODO" -C 2 dreamlake notes grep --regex '\bFIXME\b' --case-sensitive --glob 'spec-*' dreamlake notes grep "Draft" --json # revision + character range per hit ``` **Python** ```python for hit in dl.grep_notes("TODO", namespace="", context=1): print(f"{hit.note_slug}:{hit.line}:{hit.column} {hit.text}") ``` Output is `slug:line:column`, like `rg` — which note, and where inside it. Literal and case-insensitive by default, so `v1.2` does not match `v1x2`. Each hit carries its revision and `ind`, the character range — enough to edit without re-reading: ```python hit = dl.grep_notes("Draft", namespace="").hits[0] doc = hit.open().read() doc.replace("Published", ind=hit.ind) doc.save() ``` ## Files Files inherit the note's permissions. **CLI** ```bash dreamlake notes files upload ./report.html --note "$NOTE_ID" dreamlake notes files list --note "$NOTE_ID" dreamlake notes files list 'assets/*.png' --note "$NOTE_ID" dreamlake notes files cat config.json --note "$NOTE_ID" dreamlake notes files write config.json --text '{}' --note "$NOTE_ID" dreamlake notes files download report.html --note "$NOTE_ID" -o ./report.html dreamlake notes files mv old.txt new.txt --note "$NOTE_ID" dreamlake notes files cp a.txt b.txt --note "$NOTE_ID" dreamlake notes files rm old.txt --note "$NOTE_ID" # trash dreamlake notes files list --trashed --note "$NOTE_ID" # ids of trashed files dreamlake notes files restore --note "$NOTE_ID" ``` **Python** ```python note.files.upload("diagram.png", path="assets/diagram.png") note.files.create("config.json", text='{"enabled": true}\n') note.files.list("assets/*.png") f = note.files.find("assets/diagram.png") f.download("./local.png") f.move("assets/new.png") f.copy("assets/copy.png") f = f.trash() # each returns the updated file f = f.restore() ``` Bytes are **streamed** both ways and never decoded, so a file larger than memory still round-trips intact. `cat` refuses a binary rather than printing mojibake. `rm` moves a file to the trash. Restoring takes the file **id**, not its path — two trashed files can share a path, so the path alone would be ambiguous. `files list --trashed` prints the ids. ### Attach an image and get its path With the CLI installed and signed in, set `NOTE_ID` to your note's full ID and upload a local PNG. No JSON flag or parsing is needed for interactive use: ```bash dreamlake notes files upload ./diagram.png --note "$NOTE_ID" --path assets/diagram.png ``` The output shows `assets/diagram.png`, the size, content type and file ID. You chose that logical path with `--path`; without it, the path defaults to `diagram.png`. To get just the authenticated **preview page URL** on stdout: ```bash dreamlake notes files preview assets/diagram.png --note "$NOTE_ID" ``` For scripts that need to capture the returned attachment path, use the structured response below (`jq` required): ```bash set -e NOTE_ID="" dreamlake notes files upload ./diagram.png --note "$NOTE_ID" \ --path assets/diagram.png --json > attachment.json IMAGE_PATH=$(jq -er '.path' attachment.json) printf '%s\n' "$IMAGE_PATH" # assets/diagram.png ``` The Python equivalent returns a file object with the same logical path: ```python import dreamlake as dl note = dl.note("") image = note.files.upload("./diagram.png", path="assets/diagram.png") print(image.path) # assets/diagram.png ``` `path` is relative to the note's attachment collection, not a browser image URL. Uploading an attachment does not insert it into the note body. The file preview link is a viewer page, not a URL to use in an image's `src`. ### Get an image URL for Markdown or HTML For an inline image, upload the bytes to the media endpoint and keep the returned `url`. This is a separate upload from the permission-inheriting attachment above; you can skip the attachment step if you only need an inline image. Media is not listed by `notes files list`. CLI 0.37.0 and later return that URL directly using your saved login: ```bash dreamlake notes media upload ./diagram.png ``` Or capture it for insertion into a document: ```bash IMAGE_URL=$(dreamlake notes media upload ./diagram.png) printf '\n![Architecture diagram](%s)\n' "${IMAGE_URL:?Upload the image first}" > image.md printf 'Architecture diagram\n' "${IMAGE_URL:?Upload the image first}" > image.html ``` Choose either the direct upload or the capture command; each call uploads a new media object. No note ID or namespace is needed, and upload alone does not edit a note. Optional `--json` returns the full receipt. This command requires CLI 0.37.0 or later; check `dreamlake notes media upload --help` for availability. For installed versions without it, use the HTTP example below. Set `DREAMLAKE_TOKEN` to a valid bearer token for the API environment you are using. The following uses `curl`, `jq` and an existing `./diagram.png`; set `API_URL` for another environment before running it: ```bash set -e : "${DREAMLAKE_TOKEN:?Set a valid DreamLake API bearer token}" API_URL="${API_URL:-https://api.dreamlake.ai}" curl --fail-with-body --silent --show-error \ -H "Authorization: Bearer $DREAMLAKE_TOKEN" \ -F 'file=@./diagram.png;type=image/png' \ "${API_URL%/}/notes/media" > media.json IMAGE_URL=$(jq -er '.url' media.json) printf '%s\n' "$IMAGE_URL" # Save snippets to local files, ready to paste or insert. printf '\n![Architecture diagram](%s)\n' "$IMAGE_URL" > image.md printf 'Architecture diagram\n' "$IMAGE_URL" > image.html ``` For one-liners without `--json` or an intermediate JSON file, use the same token and local image setup above. The upload response is still JSON; `jq` extracts its URL. This is the fallback for CLI versions without `notes media upload`. ```bash IMAGE_URL=$(set -o pipefail; curl --fail-with-body --silent --show-error -H "Authorization: Bearer ${DREAMLAKE_TOKEN:?Set a valid DreamLake API bearer token}" -F 'file=@./diagram.png;type=image/png' "${API_URL:-https://api.dreamlake.ai}/notes/media" | jq -er '.url') ``` After that upload succeeds, choose either one-liner to write the image markup: ```bash printf '\n![Architecture diagram](%s)\n' "${IMAGE_URL:?Upload the image first}" > image.md printf 'Architecture diagram\n' "${IMAGE_URL:?Upload the image first}" > image.html ``` The response contains `url`, `contentType` and `sizeBytes`. The URL has the form `https://api.dreamlake.ai/notes/media/`. Keep that returned URL rather than the temporary storage URL it redirects to. Do not construct it from the attachment path or ID. Insert the Markdown snippet into your existing note with the append helper: ```bash dreamlake notes append "$NOTE_ID" --file image.md ``` Use `image.html` in an HTML document; for the Notes Markdown body, use `image.md`. Raw HTML is not enabled in the Notes Markdown renderer. Attaching an HTML file with `notes files upload` is also separate from editing the note body. The static `notes read --view html` preview excludes network-loaded media, so verify the image in the interactive app. Anyone holding a media URL can load it without signing in. Making a note private does not revoke that URL. For an image that must inherit the note's permissions, keep it as a file attachment and use the authenticated file preview instead of publishing an inline media URL. ### Look at one **CLI** ```bash dreamlake notes files preview report.html --note "$NOTE_ID" --open dreamlake notes files preview report.html --note "$NOTE_ID" --share dreamlake notes files preview report.html --note "$NOTE_ID" --revoke ``` **Python** ```python print(note.files.find("report.html").preview_url()) print(note.files.find("report.html").preview_url(share=True)) note.files.find("report.html").unshare() ``` The default link needs a signed-in reader. `--share` opens without signing in and does not expire; `--revoke` kills every copy at once. Uploaded HTML renders in a separate origin, never the dashboard's. Markdown, SVG, code and images render too; anything else offers a download. A file with no rendered form is refused rather than linked. ## Legacy revision-checked edit helpers The legacy whole-body/local-document helpers below carry the revision they were based on. A note that changed in between is **refused** rather than overwritten: **CLI** ```bash REV=$(dreamlake notes read --legacy "$NOTE_ID" --json | jq -r .etag) dreamlake notes write "$NOTE_ID" --file new.md --if-match "$REV" ``` **Python** ```python doc = dl.note("").read() doc.replace("Published", query="Draft", all=True) doc.save() # refused if the note moved since read() ``` | Python | CLI exit | Means | Do | |---|:---:|---|---| | `NoteChanged` | `3` | It changed since you read it | Re-read, redo. Retrying fails again. | | `NoteBusy` | `4` | The realtime outcome is unavailable | Preserve the draft and baseline; read and reconcile before resubmitting. | | `PatchFailed` | `5` | Your diff no longer applies | Re-read, regenerate it. | | `NoMatch` | `6` | Nothing matched | Widen the query. | | — | `7` | Refused to overwrite a local file | Pass `--overwrite`. | `--force` / `force=True` skips the check — deliberately, so overwriting a colleague is something you typed rather than something that happened. Verify a write landed by reading it back against the revision it produced: ```python rev = note.patch(diff, if_match=doc.etag, legacy=True) check = note.read(if_match=rev.etag) # refused if anything changed since ``` ## Which addressing to reach for 1. **Exact text** (`query=`) — what you can state reliably; ambiguous matches fail. 2. **A section anchor** from `toc` for a heading and its contents. 3. **A line or character range** from a current read, TOC or grep hit. 4. **A unified patch** for coordinated changes in several places at once. ## Next steps Every `dreamlake notes` command and flag. The endpoints underneath, if you need them directly. ## Notes v2: merge and exact patches Released September 24, 2026 with CLI 0.26.2, Python SDK 0.20.0 and the Notes API backed by RTC server 0.5.1. The API retains native baselines and the authority supplies unlocked baseline observations. Upgrade older clients before using these examples. Deployment evidence and manual acceptance are tracked in [the Notes master plan](https://github.com/dreamlake-ai/dreamlake-workspace/issues/706). A v2 source read returns `note`, `hash`, `revision` and `content`. `hash` is `sha256:` plus the exact UTF-8 source digest. `revision` is an opaque RTC write baseline; equal text does not imply an equal revision. Keep the original source and token together until the edit is verified. Never fetch a fresh token merely to make an old patch pass. ```bash set -euo pipefail NOTE_ID=design-doc dreamlake notes read "$NOTE_ID" --json > baseline.json BASE_HASH=$(jq -er '.hash' baseline.json) BASE=$(jq -er '.revision' baseline.json) jq -jr '.content' baseline.json > base.md dreamlake notes read "$NOTE_ID" --since "$BASE_HASH" --format inline-dff dreamlake notes read "$NOTE_ID" --since "$BASE_HASH" --format diff # Example requires saved source exactly Hello world. without a final newline. dreamlake notes patch "$NOTE_ID" --format inline-dff --base-revision "$BASE" --json > committed.json <<'PATCH' @@ chars 0:12 @@ ~ Hello [-world-]{+team+}. PATCH REVISION=$(jq -er '.revision' committed.json) dreamlake notes read "$NOTE_ID" --if-match "$REVISION" --json > verified.json ``` Use a quoted heredoc delimiter absent from the patch body. Multiline stdin is the default; `--file` is optional. Both reads and uploads select `inline-dff` or `diff` independently. Full reads and incremental reads have self-contained text metadata by default; JSON is opt-in. `--legacy` selects the previous text/ETag contract. `notes diff` is the incremental-read alias in the matching CLI. Inline ranges count Unicode code points, zero-based and end-exclusive. Literal marker punctuation uses backslash escaping, with `\n`, `\r`, `\t` and `\\` for controls/backslashes. Unified line patches preserve exact line endings and `\ No newline at end of file` markers. Invalid patches apply nothing. The default patch sends `{format, patch, baseRevision, mode: "merge"}` without `If-Match`: the server retrieves the original identity-bearing snapshot and journal, validates the original source, and compiles the sparse edits into ordinary native RTC operations. Concurrent changes merge by native character identity; the server never re-diffs an old target against freshly read text. The source hash alone cannot identify this baseline. Missing or expired native baselines fail explicitly and require a new read and a reviewed patch. A room reset invalidates prior generations even in merge mode. The default mode is `merge`. Add `--exact --base-revision "$BASE"` to require the original authoritative revision at commit (`mode: "exact"` in the API). For compatibility, `--if-match` alone supplies both the original baseline and exact mode; if both flags are provided they must agree. An explicit API `mode: "merge"` conflicts with `If-Match` and is rejected. Python uses `base_revision=BASE` with optional `exact=True`. Patch receipts include `mode` (`merge` or `exact`), the original `baseRevision`, and the observed resulting `hash` and `revision`. That observation may already include later concurrent edits. Only exact mode uses the conditional commit protocol and may return 412 when another writer changed the room. Merge-mode patches use the existing `crdt`/`ack` protocol. RTC outages produce errors, never an archive-only replacement. `--since` accepts a retained hash, ISO timestamp/date, or positive integer `second(s)`, `minute(s)`, `hour(s)` or `day(s) ago`. Unzoned timestamps and dates use UTC; relative times resolve once at server request time. Time lookups select the latest **retained source observation** at or before that instant, not every browser keystroke or an audit history of all commits. Retention starts when this interface records snapshots; no earlier history is invented. Unknown or expired bases return an explicit error. Apply a returned patch only to its exact `base` source, and verify the resulting hash. A no-op source diff may carry a newer RTC token; it never advances an existing draft automatically. The default `diff` read aligns source lines directly, so substantial rewrites do not consume the character-alignment budget used for merge-safe write patches. `inline-dff` still requires bounded character alignment. Invalid references return `400 bad_reference`, missing retained snapshots return `404 revision_not_found`, diff-generation limits return `422 diff_failed`, and retained-storage or observation failures return `503 diff_unavailable`. Preserve the original baseline on failure. A display diff does not guarantee that a later merge patch fits the write limits; merge remains the default and exact mode remains opt-in. Native CLI 0.33.0 has a redirected-file input defect: `--file - < edit.dff` can send an empty patch and receive a successful no-op receipt. Until a release containing the stdin fix is installed, use `--file edit.dff` and inspect `--dry-run --json` to verify `payload.patch`. The corrected reader preserves redirected input and rejects empty patches before sending. This does not change merge semantics; always verify the requested text in the acknowledged snapshot. Stop on failure and preserve the patch, working copy and original baseline. A missing acknowledgement can mean a commit occurred. The backend reconnects at most once within the same request and resends the identical native message and operation IDs. The CLI does not automatically retry the HTTP patch. A new HTTP invocation is an independent operation, with no cross-request idempotency receipt: read and reconcile the result before resubmitting. Exact readback can itself return a conflict if another writer has already changed the acknowledged revision. ### Agent presence and activity (opt-in) #### Agreed lifecycle and identity **Presence is opt-in.** Agents can read and edit using normal authentication and revision checks without any agent ID, join, heartbeat, or leave. A runner opts in by supplying a stable session ID. The client generates it (a UUID once per task), not the server. Names and IDs are self-reported, grant no permissions, and are not verified audit identity. Two clients under the same authenticated owner can reuse an ID; random UUIDs prevent accidental collisions, not deliberate impersonation. Presence means **active in this note recently**, for both humans and agents. It is not proof that an agent is continuously watching, reading, or typing. Reuse the existing RTC awareness channel and header badge; do not add a status row below the bindr row. Keep three identities separate: | Identity | Purpose | |---|---| | Agent identity / display name | Identifies the agent; its name is a supplied label, not verified model identity. | | Task/session ID | Stable across all CLI calls in one task; distinct for concurrent sessions, even for the same agent. | | Human owner | Derived from authentication, not an agent-supplied owner field; attribution does not imply the owner is present. | The runner creates the task/session ID once, retains it across tool invocations, and passes it to every Notes command. Do not generate an ID on every command or use a shared display name as the session key. Resume the same ID for the same task; use a new ID for a new or concurrent task. Socket reconnections have their own transport IDs and must not create a new logical participant. The current `DREAMLAKE_AGENT_ID` / `X-DreamLake-Agent-Id` field carries this **task/session ID**, despite its name. It does not yet represent a separate, persistent agent-account ID. The roster key is scoped by note, authenticated owner, and session ID; the display name is never the key. | Event | Intended behavior | |---|---| | First attributed read/edit | Implicitly join and start the recent-presence timeout. | | Later read/edit with the same session ID | Refresh the existing badge, without adding another participant. | | Inactivity | Remove the badge after expiry; no cleanup command is required. | | Explicit leave/unjoin | Remove that session from that note immediately; do not erase its identity. | | Interaction after leave or expiry | Implicitly rejoin using the same session ID. | | Optional explicit join or maintained session | Support clients that need sustained presence; ordinary CLI use does not require it. | Human clients can send leave on navigation away or note closure. Abrupt tab close, crash, or network loss may prevent delivery, so expiry is still required. Agents have the same optional leave and expiry fallback. A session ID is identity, not a live lease: storing the ID does not keep a badge alive. Neither a TUI nor a continuous edit stream is required for recent presence. Read activity uses the **existing human selection and cursor display**, with the agent label and participant color. It selects the returned source range; a full-body read selects the full source, and incremental changes do not claim a whole-source selection. New read/focus activity replaces the previous selection. Search does not add a separate seek event. Do not render a second agent-specific selection style or a tool-call log. Insertion/replacement text uses a fading highlight only after acknowledgement. Deletion has no special marker in this iteration. Failed writes and dry runs never produce success highlights. Selections expire after 8 seconds; completed edit highlights fade over 5 seconds. Neither heartbeat nor badge renewal extends those lifetimes. A completed edit may finish fading after its author leaves. Human cursors retain relative CRDT anchoring; CLI selections are exact-source-hash bound and disappear on source changes, rather than guessing a new position. People and agents share participant-color rules, not action-specific colors. Use an agent icon and agent/owner labels to distinguish them; do not rely on color alone. Concurrent sessions must remain distinguishable even when names match. The defaults are a 60-second recent-presence timeout and a 5-second completed-edit fade. Optional passage activity has an independent 8-second expiry. These are DreamLake choices, not asserted Google Docs, iMessage, or Claude Tag timing constants. #### Availability and optional controls An attributed operation uses the lifecycle above and implicitly joins or renews the same 60-second presence entry. Anonymous agent identity is not inferred from ordinary API calls. A separate persistent agent-account identity is not yet part of the wire contract. Deployment and client release status must be checked independently of this source documentation. CLI 0.27.0+ and Python SDK 0.21.0+ support attributed reads and edits. CLI 0.29.0+ adds `visit` and `read --linger`; CLI 0.31.0+ adds the read-only `presence` command and event-driven selections. These require matching server capabilities; a successful read does not prove presence or event support. The CLI uses the active login's API; running a locally installed binary does not select a local server. Use `--remote ` to test a specific API or `--debug` for the local development server. To check a matching API, use an accessible test note and the stable task identity below. Run a read, inspect `notes presence`, then start `read --linger` and stop it with Ctrl-C to verify automatic session cleanup. Verify patch support separately on a disposable note with a merge patch, an exact readback, and a stale exact request that must fail without changing the source. Do not use an existing user document as a write-test fixture. #### Read and linger in the foreground CLI 0.29.0+ adds `notes read --linger` and `notes visit`. They use the deployed Notes v2, presence-roster and agent-activity endpoints. Python has no corresponding convenience method yet. Set `DREAMLAKE_AGENT_ID` once to a unique, stable task-session identity (and optionally `DREAMLAKE_AGENT_NAME`) as described below. `NOTE_ID` must identify a note you can access as a member or explicitly shared reader. ```bash # NOTE_ID and the stable task identity must already be set. dreamlake notes read "$NOTE_ID" --linger ``` **Use the default text output for people and coding agents.** It shows readable diffs, participants, and quoted selections. JSON is optional and intended only for a program that explicitly needs to parse structured events. The command registers presence automatically, prints the complete source with its hash/revision and the other current participants, and stays in the foreground. A separate `visit` is optional. Interrupt with Ctrl-C or SIGTERM to stop and send leave. No background daemon is spawned. Presence expires after its server lease if the process is killed or cannot send leave. Use one linger process per note/task identity; multiple keepers using the same identity share one lease. Edit delivery is **debounced**: wait until the observed source has been quiet for `--debounce` (default `2s`), then deliver **one unified diff** from the last emitted content baseline through the end of the burst. Each newly observed edit restarts that quiet timer. Continuous editing keeps the diff pending; there is no forced maximum-wait flush. Stopping before the quiet period ends discards the pending notification, not any document edits. All output batches are **throttled** by `--throttle` (default `2s`): no two update batches are emitted closer together than that interval. Presence and activity can still be delivered while edits continue; they do not reset the edit quiet timer. Once a diff is ready it joins the next eligible output batch, so a recent presence batch can delay it until the throttle expires. The first source snapshot is immediate. No new batch is emitted merely because a timer elapsed. Both flags require `--linger` and accept explicit `ms`, `s` or `m` units, including fractions, from `250ms` through `5m`. Bare numbers, zero, negatives and out-of-range values are rejected before presence registration. For example: ```bash # One second of edit quiet; no more than one output batch every two seconds. dreamlake notes read "$NOTE_ID" --linger --debounce 1s --throttle 2s # Slower text output for a coding agent or a quieter session. dreamlake notes read "$NOTE_ID" --linger --debounce 2s --throttle 5s ``` **CLI 0.42.0+:** `--intent "…"` publishes a self-reported purpose with the session — one short, specific sentence in the agent's own voice, shown to collaborators in the agent's presence card. It is sent once at join; heartbeats preserve it, and leave or lease expiry removes it. The flag requires `--linger` (`notes visit` also accepts it for one-shot presence). The server trims the text and rejects empty values and more than 280 Unicode code points. ```bash dreamlake notes read "$NOTE_ID" --linger \ --intent "I'm reviewing this sequence to make the pacing clearer." ``` **CLI 0.31.0+:** linger subscribes to authenticated `GET /namespaces/:slug/notes/:noteId/events` (SSE). CLI 0.29.0–0.30.0 used polling. The event stream requires a matching server; there is no silent polling fallback. The server observes the existing RTC connection events and coalesces them to at most one batch per 250ms. The CLI keeps only the latest selection per browser connection, emits at most one update per `--throttle`, and delivers the final selection after a drag stops. Continuous dragging does not restart a debounce timer. Content diffs keep their separate edit quiet period. Idle sessions do not poll body or roster endpoints; source reads happen only for the initial snapshot or after a content event becomes eligible for delivery. Agent activity is read only when its RTC fingerprint changes. Heartbeats remain silent and maintain the agent lease approximately every 20 seconds. **CLI 0.31.2+ text notifications** show names, actions and quoted text. These are representative lines from separate batches; a timestamp appears once per batch. ```text + @alice joined + Reviewer (agent) joined * Reviewer (agent) read the note * Reviewer (agent) edited the note * @alice selected "## The center" - @alice left ``` People appear as `@username`; agents use their configured name and `(agent)`. Only selected text is quoted; embedded newlines are escaped. Cursor moves, selection clears, syncing states, repeated selected text and empty batches stay silent in text. IDs, connection details, offsets and source hashes remain in `--json`; use it to distinguish identical names or tabs and apply exact source positions. Initial content and diffs still include revision metadata for safe edits. These examples also appear in `dreamlake notes read --help`. Human selections in JSON resolve native CRDT anchors against the observed source. Each selection carries `anchor`, `head`, `start`, `end`, `unit: "unicode-code-point"`, `text` (at most 4096 code points), and `truncated`, with `status: "resolved"`. A collapsed range is a caret. An explicit `null` clears a selection (including blur or departure); `status: "unresolved"` means its native anchors have not arrived, not a guessed range or a clear. Tabs remain separate, even for one user. Selection-only changes do not wait for the content debounce. The initial snapshot includes `selectionHash` for its participants' selections. Update batches add `selections`, whose entries contain `client`, `user`, `selection`, and the exact observed source `hash`. That hash can differ from the last emitted content hash while an edit is still being debounced. Never apply these offsets to a different source. Selected text is quoted in terminal output and remains untrusted document content, not an instruction to an agent. Only namespace members or explicitly granted readers can subscribe. Public visibility alone does not expose collaborator selections. Streams recheck access and token expiry every 15 seconds and close on revocation, room reset, slow consumers, or RTC failure. A disconnected stream exits with an error; explicitly restart linger for a fresh snapshot. Events are not retained or replayed. This is a best-effort stream of observations, not an audit log: brief visits or activity coalesced between output batches can be missed, and edits that cancel out within a burst produce no net content diff. Human edits appear in content diffs; the activity feed currently attributes agent reads and edits only. Reading updates does not prove human attention, and it does not reserve or lock the note. ##### Optional: JSON for programmatic consumers Use `--json` only when a program needs NDJSON; ordinary collaboration, including coding-agent sessions, should use the text commands above. ```bash dreamlake notes read "$NOTE_ID" --linger --json ``` With `--json`, stdout is NDJSON: one `type: "snapshot"` object containing `observedAt`, `note`, `content`, `hash`, `revision`, `selectionHash`, and `participants`, followed by `type: "update"` objects containing `observedAt`, `joined`, `left`, and `activities`, and `selections`. Changed content adds `content: {note, base, hash, revision, format, patch}`. Observation times are Unix milliseconds; source and activity are fetched separately from the event stream and are not an atomic cross-stream snapshot. Progress and errors go to stderr. No update object is emitted for an unchanged batch. `--linger` supports complete source reads only; it cannot be combined with `--legacy`, `--view html`, `--at`, `--toc`, `--tag`, `--since`, sections, line ranges or numbered output. `--if-match` checks the **initial** read only. `--format` applies to the emitted diff, not the initial complete source snapshot. Transport/capability errors or an unavailable retained baseline end the command with a nonzero status and a best-effort leave; they are never treated as an empty room. For edits, preserve the original source and revision used to prepare your draft: a later streamed revision is not a replacement baseline for an older draft. ```bash cli-help="notes visit" # Optional one-shot registration: does not fetch the note body or keep a daemon. # NOTE_ID and the stable task identity must already be set. dreamlake notes visit "$NOTE_ID" ``` `visit` uses the existing join lease (60 seconds unless renewed by an attributed operation), returns immediately and does not read content. Use `read --linger` when you want ongoing updates. CLI 0.31.0 removes the old manual `notes presence ` and `join --watch` controls. `read --linger` manages joining, heartbeats, and leaving automatically. Stop it with Ctrl-C when finished; one-shot reads and `visit` expire naturally. There is no manual lifecycle sequence to run alongside it. Never start an untracked helper that outlives the task. ##### Read who is present In CLI 0.31.0+, `presence` reads the current roster without joining or refreshing your session. It does not require an agent ID. Text is the default: ```bash cli-help="notes presence" dreamlake notes presence "$NOTE_ID" ``` Add `--json` only for a program consuming the roster. There are no direct `notes join`, `notes heartbeat`, or `notes leave` commands. Use `visit`, `read --linger`, and Ctrl-C for participation. ##### Low-level HTTP lease protocol SDK integrations and linger use the HTTP protocol below internally. It is not a manual CLI workflow. API: `POST /namespaces/:slug/notes/:noteId/presence` accepts `{action, hash?, ranges?: [{start,end}], intent?}`, bearer authentication, `X-DreamLake-Agent-Id`, and optional `X-DreamLake-Agent-Name`. Actions are `join`, `heartbeat`, `read`, `edit`, `seek`, `clear`, `leave`. Response is `{state}` or `{state:null}` after leave. Only authenticated members or explicitly shared readers may publish; `edit` also requires write permission. Public visibility alone does not grant presence access. Controls do not mutate document content or revision. Owner metadata comes from authenticated lookup. `intent` is the agent's self-reported purpose — one short plain-text sentence in the agent's own voice, such as "I'm reviewing this sequence to make the pacing clearer." Omitting the field preserves the current purpose, `{"summary": "…"}` replaces it, and an explicit `null` clears it, on any action. Summaries are trimmed; empty strings and more than 280 Unicode code points are rejected with 400 `invalid_presence`. The server stamps `updatedAt`; clients cannot supply owner attribution or timestamps. Intent lives with the presence lease: leave or lease expiry removes it, and a later join never resurrects an expired purpose. `clear` keeps its existing meaning — it clears passage activity, not intent. Intent is a self-reported claim, not observed activity, progress, or a permission grant; the roster's authorized readers can see it. #### Read who is present (HTTP API) `GET /namespaces/:slug/notes/:noteId/presence` returns the current RTC awareness roster, including humans and agents. Use bearer authentication as a namespace member or explicitly shared reader. Public visibility alone is insufficient. Agent identity headers are not required, and the observer does not publish presence, renew an agent lease, or edit the note. Successful reads default to `text/plain; charset=utf-8` (also available with `?format=text`): ```text Observed at: 2026-09-26T08:00:00.000Z - human: "Ge" (id: "ge"; client: "browser-session-123") - agent: "Codex" (id: "agent:owner-id:agent-id"; client: "note-agent-session-456"); owner: "Ge" (id: "owner-id"); intent: "I'm reviewing this sequence to make the pacing clearer."; expiresAt: 1790409660000 ``` An empty text roster says `No participants present.` after the observation time. Client-declared strings are quoted and escaped to keep each connection on one line. Use `?format=json` for structured output; other format values return 400 `invalid_format`. Error responses remain JSON for either format. The opt-in JSON response is `{participants, observedAt}`. `observedAt` is Unix time in milliseconds. Each participant has `client` (connection ID) and `user` with `id`, `name`, and `kind` (`human` or `agent`), plus optional `color` and `avatar`. Agents can include `user.owner`, their lease's `expiresAt`, and `intent` (`{summary, updatedAt}` — the self-reported purpose last published with presence). Expired agent leases and internal observer connections are excluded. Multiple browser tabs remain separate entries. To identify other agents, compare `user.id` against `agent::`; the caller is not automatically excluded. Human identities without a kind field are normalized to `human`. These are client-declared display identities, not verified authorization claims. An inactive note returns an empty roster. An active room whose connection fails or times out returns 503 `presence_unavailable`, not an empty roster. Responses are not cached. This requires a server with the GET endpoint and an RTC server supporting awareness rosters; a 404 can also mean the caller lacks access. There is no CLI or Python convenience method for roster reads yet. Using any HTTP client, send an authenticated GET to the path above. The roster is an observation of presence, not a lock or a guarantee that another editor is idle; continue to use the normal conditional-write contract. Explicit ranges require the exact source SHA-256 and zero-based, end-exclusive Unicode code point offsets. Stale or out-of-bounds locations are refused. A collapsed seek is a caret, not a claim that text was read. Source changes invalidate hash-bound markers. Deletion-only edits do not invent insertion ranges. Errors include missing identity/invalid input (400), denied access (404), stale range (409), expired heartbeat (410), and unavailable relay (503). Identity values accept 1–128 ASCII letters, digits, dots, colons, underscores and hyphens; names accept at most 64 printable ASCII characters. CLI 0.27.0+ and Python SDK 0.21.0+ attach identity headers to Notes body/section/diff operations when the environment variables below are set. `visit` and `read --linger` require CLI 0.29.0+ and an active collaborative room; read-only `presence` requires CLI 0.31.0+. The old manual action commands were removed in 0.31.0. Updating a skill does not update a binary or deploy a server. Python has no presence convenience method yet; use the HTTP contract when available. The authorized agent-activity feed retains operation observations, not an online roster. Observation failures must not turn an acknowledged edit into an apparent failed edit. Live source-range decorations currently require the collaborative editor; read-only views do not run an RTC client. #### Identity, names and photos Use a readable agent prefix plus a UUID as the task ID, for example `codex:7b52e4d1-3ac9-4d88-b2a6-61f03c927ea5`. Generate it once per task and reuse it across reads, edits and linger sessions. Concurrent tasks need different IDs; a display name such as `Codex` does not distinguish their sessions. The CLI sends `DREAMLAKE_AGENT_ID` as `X-DreamLake-Agent-Id` and `DREAMLAKE_AGENT_NAME` as `X-DreamLake-Agent-Name`. No separate identity-registration request is required. | Display field | Humans | Agents | | --- | --- | --- | | Presence identity | User namespace slug | `agent::` | | Display name | Profile name from `/auth/me`, falling back to namespace slug | `DREAMLAKE_AGENT_NAME`, falling back to `AI agent` | | Header avatar | Profile photo, or initials when absent | Bot icon | | Tooltip | Name and presence | Agent name, owner name, stated purpose (intent), activity and task/session ID | The server derives an agent's owner from authentication and looks up the owner's profile name/photo; an agent cannot assign an owner through these headers. The owner's photo is carried in metadata but is not currently rendered as the agent's header avatar. Owner attribution does not mean that the owner is present. The header deduplicates connections by identity: multiple browser tabs show one human avatar, while unique agent task IDs show separate agent avatars. It shows up to four avatars plus an overflow chip. Colors are derived from identity. The HTTP roster still returns one entry per connection. Browser awareness names and photos are display metadata, not an authorization source. #### Agentic usage pattern: one identity, normal commands For agent-driven Notes work, set both identity variables before the first live read or edit, unless the user explicitly requests unattributed work. This is a workflow prerequisite for attribution, not a requirement for saving content. Without `DREAMLAKE_AGENT_ID`, an edit can save successfully while producing no agent presence or attributed fading edit highlight. `DREAMLAKE_AGENT_NAME` provides the readable label. Initialize once in the runner's task environment. For separate shell tool calls, the runner must inject the same saved values each time; an export in one shell does not propagate into later independent shells. No explicit join is required. ```bash export DREAMLAKE_AGENT_ID="${DREAMLAKE_AGENT_ID:-codex:$(python3 -c 'import uuid; print(uuid.uuid4())')}" export DREAMLAKE_AGENT_NAME="${DREAMLAKE_AGENT_NAME:-Codex}" NOTE_ID="" ``` Run ordinary commands with that identity. An attributed `read` automatically registers or refreshes presence; a preceding `visit` is never required. Reading without agent identity does not invent or register an agent session. Retain the generated ID in the task context and inject that same literal value into each later shell; rerunning the UUID fallback in a new shell would create a different session. After the first intended live read, verify attribution with `dreamlake notes presence "$NOTE_ID"` (CLI 0.31.0+). This only inspects the roster; it does not register an agent. Check the task ID and display name, not merely another session named Codex. If absent, check the environment passed to the actual read/edit process before diagnosing a UI regression. Do not repeat a successful edit to trigger its highlight. Presence expires about 60 seconds after the last activity; completed edit highlights fade over 5 seconds and require an exact matching live document revision. A roster entry verifies presence only: report a highlight as visually verified only after observing it in the collaborative editor. | Command | Reads content | Presence lifetime | | --- | --- | --- | | `notes read "$NOTE_ID"` | Once | Registers/refreshes presence, then the lease expires naturally | | `notes read "$NOTE_ID" --linger` | Initial source and ongoing updates | Registers automatically and maintains presence until stopped | | `notes visit "$NOTE_ID"` | No | Registers once, returns immediately, then the lease expires naturally | The last two commands require CLI 0.29.0+. A normal read does not mean the agent remains actively reading between commands. ```bash dreamlake notes read "$NOTE_ID" --json > baseline.json dreamlake notes find lighthouse --note "$NOTE_ID" --json # Draft a reviewed patch against baseline.json, using the patch workflow below. # Retain its revision, apply the patch, and read back the acknowledged revision. ``` The conditional patch and exact-readback examples elsewhere in this guide remain required; presence does not relax concurrency checks. Do not retry an old patch with a newly fetched revision merely to force it through. When finished, stop `read --linger` with Ctrl-C; it sends leave automatically. One-shot operations expire naturally. Keep the same session ID between commands, and remove the task identity from the runner's environment when the task ends. Lease expiry handles abrupt exits without requiring remembered cleanup. ### Keep incremental reads compact For an agent following a note, reuse the saved full `read --json` baseline; obtain one only if none is available. Then use `read --since "$BASE_HASH"` for subsequent checks instead of repeatedly downloading the full document. For one-off passage inspection, use the scoped reads above without fetching a full baseline. Differential reads default to unified line diffs. Use `--format inline-dff` explicitly when character edits are useful. On servers with localized unified-diff generation, this returns changed lines with up to three unchanged context lines on each side; nearby changes share a hunk and distant changes use separate hunks. Older servers may still return a whole-document replacement; a docs or skill update alone does not change server output. Both formats preserve exact source, including CRLF and a missing final newline. An unchanged source returns an empty `patch`, possibly with a newer RTC `revision`. Keep `base`, `hash`, and `revision` with the response. Apply the patch only to the saved source matching `base`, verify its resulting hash, and never replace an existing draft's original revision just to make it pass. Unknown or expired bases remain errors. Use `--json` and extract `.patch` when a consumer needs patch text alone; normal text output includes metadata. #### Verify edits without rereading the whole note After a successful v2 patch, verify the acknowledged revision with a delta from your saved baseline. This uses the existing `baseline.json` from the pre-edit read and `receipt.json` from the successful patch; `NOTE_ID` is the same note used for both operations, as set in [Name a note](#name-a-note). ```bash BASE_HASH=$(jq -er .hash baseline.json) ACK_REVISION=$(jq -er .revision receipt.json) dreamlake notes read "$NOTE_ID" --since "$BASE_HASH" \ --if-match "$ACK_REVISION" --json > verified-delta.json ``` Check that the returned `base` matches the cached source hash and its `hash` matches the receipt. Apply the returned patch to that cached source, verify the resulting hash, and inspect the intended changes. Preserve unrelated changes when the write used merge mode. Save the reconstructed source and returned revision as the next baseline only after verification succeeds. An empty delta from the receipt's hash checks for changes after the write; it does not by itself verify the edited text. A stale `--if-match` fails rather than silently accepting a newer revision: retain the receipt and original baseline, then inspect an incremental read without that condition to reconcile later edits. Do not retry the write using a newly fetched revision merely to force it through. If the cached source or retained base is unavailable, take a new full snapshot explicitly; do not claim exact verification of an expired revision. The full-read examples below remain useful as standalone demonstrations and recovery checks. They are not a requirement to reread the entire note after every targeted edit. ### Reproduce a concurrent merge and an exact conflict Use a new private fixture with the compatible releases, an authenticated CLI, `jq`, and Python `dreamlake` configured for the same account/namespace. These examples deliberately issue an exact request first, inspect its rejection, and then make a separate, explicit merge request. There is no automatic downgrade. **CLI** ```bash set -euo pipefail dreamlake notes create "Merge/exact example" --text 'Hello world.' --json > fixture.json NOTE_ID=$(jq -er '.id' fixture.json) dreamlake notes read "$NOTE_ID" --json > baseline.json BASE=$(jq -er '.revision' baseline.json) jq -jr '.content' baseline.json > original.md cat > agent.patch <<'PATCH' @@ chars 0:12 @@ ~ Hello [-world-]{+team+}. PATCH # Simulate a second participant inserting a prefix after the agent's read. dreamlake notes patch "$NOTE_ID" --base-revision "$BASE" --json > human.json <<'PATCH' @@ chars 0:0 @@ ~ {+Human: +} PATCH # Expected conflict: stdout stays empty; stderr explains the failure; exit is 3. set +e dreamlake notes patch "$NOTE_ID" --base-revision "$BASE" --exact \ --file agent.patch --json > exact.stdout 2> exact.stderr STATUS=$? set -e test "$STATUS" -eq 3 test ! -s exact.stdout cat exact.stderr # A separate explicit choice to merge the ORIGINAL patch and identities. dreamlake notes patch "$NOTE_ID" --base-revision "$BASE" \ --file agent.patch --json > merged.json jq '{note, mode, baseRevision, hash, revision}' merged.json dreamlake notes read "$NOTE_ID" --json > observed.json jq -er '.content == "Human: Hello team."' observed.json ``` **Python** ```python import json import os from pathlib import Path import dreamlake as dl from dreamlake import NoteChanged note = dl.create_note(os.environ["NAMESPACE"], "Merge/exact example", text="Hello world.") baseline = note.read_snapshot() patch = "@@ chars 0:12 @@\n~ Hello [-world-]{+team+}.\n" Path("baseline.json").write_text(json.dumps(baseline.to_dict(), indent=2), encoding="utf-8") Path("original.md").write_text(baseline.content, encoding="utf-8") Path("agent.patch").write_text(patch, encoding="utf-8") # A separate native edit arrives after this baseline. note.patch("@@ chars 0:0 @@\n~ {+Human: +}\n", base_revision=baseline.revision) try: note.patch(patch, base_revision=baseline.revision, exact=True) except NoteChanged: print("Exact request rejected; original files retained.") else: raise AssertionError("Expected a stale exact request") # Explicitly choose merge; never do this automatically inside the except block. receipt = note.patch(patch, base_revision=baseline.revision) print(receipt.mode) # merge print(receipt.base_revision) # the ORIGINAL opaque token print(receipt.hash) # observed resulting source hash print(receipt.revision) # observed resulting RTC revision print(json.dumps(receipt.to_dict(), indent=2)) assert note.read_snapshot().content == "Human: Hello team." ``` ### Complete an exact edit and return to merge mode Continue with the fixture above, now containing `Human: Hello team.`. Read a fresh baseline before making this new edit. Exact mode applies only to its individual request; the following empty patch uses the default merge mode. **CLI** ```bash dreamlake notes read "$NOTE_ID" --json > exact-baseline.json EXACT_BASE=$(jq -er '.revision' exact-baseline.json) cat > exact.patch <<'PATCH' @@ chars 18:18 @@ ~ {+!+} PATCH dreamlake notes patch "$NOTE_ID" --base-revision "$EXACT_BASE" --exact \ --file exact.patch --json > exact-success.json jq '{note, mode, baseRevision, hash, revision}' exact-success.json dreamlake notes read "$NOTE_ID" --json > after-exact.json NEXT_BASE=$(jq -er '.revision' after-exact.json) printf '' | dreamlake notes patch "$NOTE_ID" --base-revision "$NEXT_BASE" \ --json > merge-after-exact.json jq -er '.mode == "merge"' merge-after-exact.json ``` **Python** ```python exact_baseline = note.read_snapshot() exact_patch = "@@ chars 18:18 @@\n~ {+!+}\n" Path("exact-baseline.json").write_text( json.dumps(exact_baseline.to_dict(), indent=2), encoding="utf-8") Path("exact.patch").write_text(exact_patch, encoding="utf-8") exact_receipt = note.patch(exact_patch, base_revision=exact_baseline.revision, exact=True) print(exact_receipt.mode) # exact print(json.dumps(exact_receipt.to_dict(), indent=2)) after_exact = note.read_snapshot() assert after_exact.content == "Human: Hello team.!" merge_receipt = note.patch("", base_revision=after_exact.revision) assert merge_receipt.mode == "merge" assert merge_receipt.hash == after_exact.hash assert merge_receipt.revision == after_exact.revision ``` If another writer changes the exact baseline first, stop on the conflict and retain these files. The examples do not retry the HTTP request or switch modes after a failure. The successful follow-up merge above is a separate no-op request with its own saved baseline. Successful exact output captured from the matching isolated API fixture with the candidate CLI (exit 0, empty stderr; these are fixture tokens): ```text note: 507f1f77bcf86cd799439099 hash: sha256:2b8c0f5475f23494272f3800abb11a7680b3360bc2e927763c10aec9cf4398f5 revision: rtc:63834c3821f7b76779815f3a670449268711811e69f86f6b208fb52236713484 mode: exact baseRevision: rtc:71371be6e3227abca241161ebb7d8d65e2d851b8872b5a5d51ce7ec80369d95e ``` With `--json`, the same exact receipt is: ```json { "note": "507f1f77bcf86cd799439099", "hash": "sha256:2b8c0f5475f23494272f3800abb11a7680b3360bc2e927763c10aec9cf4398f5", "revision": "rtc:63834c3821f7b76779815f3a670449268711811e69f86f6b208fb52236713484", "baseRevision": "rtc:71371be6e3227abca241161ebb7d8d65e2d851b8872b5a5d51ce7ec80369d95e", "mode": "exact" } ``` Python's captured `PatchReceipt` attributes match that JSON: ```text mode: exact base_revision: rtc:71371be6e3227abca241161ebb7d8d65e2d851b8872b5a5d51ce7ec80369d95e hash: sha256:2b8c0f5475f23494272f3800abb11a7680b3360bc2e927763c10aec9cf4398f5 revision: rtc:63834c3821f7b76779815f3a670449268711811e69f86f6b208fb52236713484 ``` The following empty patch defaults back to merge and returns this actual CLI text receipt (exit 0, empty stderr). Python reports `.mode == "merge"`, and `.to_dict()` returns the same fields with `baseRevision` in JSON: ```text note: 507f1f77bcf86cd799439099 hash: sha256:2b8c0f5475f23494272f3800abb11a7680b3360bc2e927763c10aec9cf4398f5 revision: rtc:63834c3821f7b76779815f3a670449268711811e69f86f6b208fb52236713484 mode: merge baseRevision: rtc:63834c3821f7b76779815f3a670449268711811e69f86f6b208fb52236713484 ``` The local API/RTC/Mongo fixture verifies `Hello world.` → `Human: Hello team.`: the unrelated prefix survives, original native identities address the replaced word, and the exact rejection appends zero operation batches. The matching source hashes observed in that fixture are: ```json { "originalHash": "sha256:aa3ec16e6acc809d8b2818662276256abfd2f1b441cb51574933f3d4bd115d11", "mergedHash": "sha256:163af32ec4bc25f27e3b9ae68fe85c75e5b4436a769cc82a4050692643ce92cf" } ``` Candidate CLI output captured by replaying that isolated API fixture (exit 0, empty stderr; these IDs are fixture values, not production): ```text note: 507f1f77bcf86cd799439099 hash: sha256:163af32ec4bc25f27e3b9ae68fe85c75e5b4436a769cc82a4050692643ce92cf revision: rtc:71371be6e3227abca241161ebb7d8d65e2d851b8872b5a5d51ce7ec80369d95e mode: merge baseRevision: rtc:870fa6d8ca75e1ae8c89b0e4abf08fd8a2c222c99ac698227f25c94b8f5b41e7 ``` With `--json`, the corresponding stdout is: ```json { "note": "507f1f77bcf86cd799439099", "hash": "sha256:163af32ec4bc25f27e3b9ae68fe85c75e5b4436a769cc82a4050692643ce92cf", "revision": "rtc:71371be6e3227abca241161ebb7d8d65e2d851b8872b5a5d51ce7ec80369d95e", "baseRevision": "rtc:870fa6d8ca75e1ae8c89b0e4abf08fd8a2c222c99ac698227f25c94b8f5b41e7", "mode": "merge" } ``` The exact conflict replay exits 3 with empty stdout and this stderr: ```text ✗ the original revision or RTC identities are no longer valid for this request; keep the original baseline and draft, inspect the note before resubmitting ((412) stale) ``` Opaque note/revision values vary. A successful JSON receipt contains exactly `note`, `mode`, `baseRevision`, `hash`, and `revision`; normal CLI text prints those metadata fields. Python exposes the corresponding attributes, with `base_revision` in Python and `baseRevision` in `to_dict()`. These describe a coherent authoritative observation after persistence was acknowledged. They do not mean every peer has synchronized or that the document will remain unchanged. Another edit can arrive before the verification read; `read --if-match "$REVISION"` / `read_snapshot(if_match=receipt.revision)` makes that verification exact rather than silently accepting a newer state. The fixture observed these HTTP errors (no success receipt on failure): ```json {"error":"stale","message":"The RTC baseline changed"} {"error":"revision_not_found","message":"Original RTC baseline is not retained"} {"error":"patch_failed","message":"Expected inline header and one record"} ``` They correspond to exact conflict **412** (CLI exit **3**, Python `NoteChanged`), missing original identity baseline **404** (CLI exit **1**, Python `NoteNotFound`), and malformed patch **422** (CLI exit **5**, Python `PatchFailed`). CLI failures leave stdout empty and explain the failure on stderr, without replacing local files. A destructive room reset or history rewrite can expire native identities even when the retained baseline envelope still exists; merge then returns **412** (`stale`, CLI exit **3**, Python `NoteChanged`) without applying the patch. Ordinary concurrent editing alone does not cause that merge rejection. A missing retained envelope instead returns **404**; malformed patches return **422**. The baseline, draft and patch remain the caller's files on all failures. Neither client retries a failed HTTP patch or silently changes exact mode to merge. A transport/acknowledgement failure can be ambiguous even when a write persisted: read, reconcile, and deliberately decide what remains to be submitted. ### HTML snapshot preview With the compatible v2 server and CLI, `dreamlake notes read "$NOTE_ID" --view html` returns a complete inert HTML document. Root `data-note`, `data-hash`, `data-revision`, `data-source-type`, `data-offset-unit` and `data-source` attributes contain the exact canonical source and its baseline. Element `data-char="start:end"` ranges address that source in Unicode code points; `data-map` marks linear text, atomic syntax or generated presentation. Decode the source attribute once to recover canonical source, including original entity spelling and line endings. Patch that source using the embedded revision; never upload generated wrappers or mapping attributes. HTML reads are snapshot views; `--view html --since` is rejected. HTML-looking source is rendered as HTML, other source as Markdown. Rich or restricted structures may map atomically; no editable range is guessed from generated text. Scripts, active attributes and network-loaded media are excluded from this static preview. ### Literal Markdown for agents With CLI 0.34.4 and a compatible server, `--view html` on a Markdown note is an **agent format**: HTML-like tags supply structure and addresses; their contents are the exact original Markdown. There is one `data-char` source range, including the construct's syntax. There is no inner/outer split. ```text
  • - [ ] Ship
  • ``` Keep Markdown literal: `- [ ]`, `**bold**`, `:comment[...]`, backslashes, `<`, `&`, and Unicode remain exactly as saved. Do not add HTML escapes, Markdown escapes, or Unicode escape sequences to element contents. Do not strip escapes that are already present in canonical source. No display-text index conversion is needed: ranges address the source text inside the wrappers. A parent item's range includes its nested source. This is not browser HTML. Do not render it or use a DOM parser to recover its body. CLI 0.34.4 requests `contentFormat=literal-markdown` automatically; direct API clients add that parameter to a v2 HTML read. Existing clients keep the prior rendered contract. The API serves Markdown agent markup as `text/plain` and marks the root `data-content-format="literal-markdown"`. Generated heading numbers and other preview decoration are absent. The separate visual preview is unchanged. Metadata attributes still use transport encoding: decode the root `data-source` attribute once for an exact machine-readable source slice, and use the trusted root `data-addresses` index rather than finding tags inside arbitrary Markdown. CLI `--view markdown` handles this and prints literal source with address hints. Keep the original revision with the source and verify the acknowledged edit. Older servers may return rendered HTML; do not assume literal bodies without the format marker. Canonical HTML notes retain their existing HTML source mapping. ### Focused and historical reads `read` returns the current snapshot. Use `--at REVISION` for a retained snapshot; `--since HASH` remains a unified line-diff read. Snapshot selectors are mutually exclusive, and cannot combine with `--since` or `--linger`: ```bash # NOTE_ID identifies an accessible note; copy REVISION from its read receipt. dreamlake notes read "$NOTE_ID" dreamlake notes read "$NOTE_ID" --at "$REVISION" --toc dreamlake notes read "$NOTE_ID" --at "$REVISION" --section s1.1 dreamlake notes read "$NOTE_ID" --at "$REVISION" --tag s1.1.p1 ``` Selectors return mapped HTML. Nested `section` tags have content-derived IDs and `data-index="s1.1"`; headings use `s1.1.h`. Paragraphs (`p`), unordered lists (`ul`), ordered lists (`ol`) and all list items (`li`) share one counter per section, in document reading order. List and item IDs include their containing list/item path: `s1.p1 → s1.ul2 → s1.ul2.li3 → s1.ul2.li4 → s1.p5`. A nested ordered list under the fourth element is `s1.ul2.li4.ol5`, and its next item is `s1.ul2.li4.ol5.li6`. The suffix is the shared section counter, not an item-local position. Checklist items use the same `li` prefix and expose `data-checked="false"` or `data-checked="true"`; ordinary items omit that attribute. Adding, checking or removing a checkbox does not change the item's prefix or its container's type. There is no `tl`, `tli` or `cli` type. HTML tags remain `ul`, `ol` and `li`. A list consumes a number before its items; nested lists and items continue that same counter depth-first. Paragraph wrappers inside list items do not consume another number. Numbering restarts in each section; content before the first heading uses `s0`. A list target includes its entire subtree, and an item target includes its continuation lines and nested lists. Markdown task markers (`[ ]`, `[x]`, `[X]`) and leading HTML checkbox inputs identify checklist items. Read IDs from the returned snapshot rather than calculating them. `--tag` is an exact element ID; a section ID selects its entire subtree. IDs are local to one revision. Unknown IDs and missing snapshots return 404; every read checks current permissions. `data-char="start:end"` are absolute, zero-based, end-exclusive Unicode code-point ranges in original source. `data-lines` is one-based and inclusive. A scoped root contains only the selected `data-source`, with its global `data-source-start` and `data-source-end` and its own `data-source-hash`. The root's `data-hash` and `data-revision` still identify the complete document. Subtract `data-source-start` when slicing local source; keep absolute offsets in the patch. TOCs carry exact heading source on each heading and empty root source. Never upload a slice or rendered HTML as the complete note. ```bash # edit.dff is prepared from the exact source at REVISION. dreamlake notes patch "$NOTE_ID" --file edit.dff --base-revision "$REVISION" --exact # NEXT_REVISION comes from that write receipt. dreamlake notes read "$NOTE_ID" --at "$NEXT_REVISION" --tag s1.1.p1 ``` Exact mode refuses concurrent edits with 412; native merge mode remains available by omitting `--exact`. `--if-match` checks the current revision, while `--at` retrieves history: do not combine them. Preserve an existing draft's original baseline even after another read or linger update. Linger continues to emit source snapshots and line diffs; inspect a streamed revision using a separate pinned read. Pinned reads do not overwrite live presence with historical offsets. See the [addressed-read specification](https://docs.dreamlake.ai/dev/notes/addressed-reads/) for ID generation, ranges, examples, efficiency limits, and the executable acceptance harness. Use a CLI/server build supporting the addressed-read options. ### Comment targets in HTML reads Closed comment directives render as individually addressable elements with `data-rich-kind="comment"`. Their `id` uses `sN.cK` (for example, `s1.c2`), sharing the section's reading-order counter with paragraphs, lists and items. Use the returned ID with `--view html --at "$REVISION" --tag s1.c2` to read one comment's exact canonical directive. The atomic `data-char` and `data-lines` cover the complete directive, including attribution attributes. These HTML addresses are revision-local; read them from the snapshot, rather than guessing. Saved references such as `:comment[cmt_0123456789abcdef01234567]` additionally carry `data-comment-id="cmt_0123456789abcdef01234567"`. That persistent resource ID survives moves and edits and can be used with the comment API. Repeated references to the same saved comment get distinct HTML target IDs but retain the same `data-comment-id`. Keyed drafts expose `data-comment-key`; inline text comments have a target address but no invented persistent resource ID. Static HTML shows source text or the saved reference ID; it does not fetch a comment's private body. Code examples, escaped directives, malformed comments and Markdown links remain literal. When normalization prevents an exact range, the surrounding block remains the edit target instead of a guessed comment range. ### Rich tokens in HTML reads The v2 HTML renderer recognizes strict Markdown source tokens for `:placeholder[owner]`, `:asset-reference[asset-id]{caption="plot"}`, `:note[note-id]`, `:artifact[geyang/pitch-deck]`, `:bindr[bindr-id]` and `:chatgpt-content-reference[0]` in the development preview, plus every saved legacy form. Attribute values use double quotes; unknown/duplicate attributes and malformed tokens remain literal source. Code, escaped punctuation, Markdown links and URL paths keep their ordinary interpretation. Existing `[ owner ]` placeholders retain blue boxes, visible brackets, inner spacing and the **placeholder** hover label. Recognized components carry atomic `data-char` and `data-map` attributes addressing the complete token in canonical Unicode-code-point source. The root `data-source` remains exact. When Markdown normalizes a region so an exact token range cannot be proven, its enclosing block remains atomic; the renderer never guesses an editable token range. Static previews show assets, Notes and bindrs as unresolved labels. They perform no metadata lookup and include no download URL or authorization capability. Labels come only from source the reader can already read. The interactive application separately resolves resources through authorized APIs. Imported ChatGPT citation tags render as unresolved broken-link icons with their original source in the hover label. Neither form implies that the reference is valid or accessible. Placeholder CSS is static and hash-authorized by the preview's CSP; source-provided styles and active HTML remain inert. This capability is included in the September 24 release. Browser rich components were delivered separately in [UI PR #415](https://github.com/dreamlake-ai/dreamlake-ai/pull/415). ### Heading numbering in HTML reads Markdown Notes can place the following options in an initial YAML front-matter block. The same policy is used by the editor/outline and the server HTML preview: ```markdown --- render: headings: numbering: hierarchical startLevel: 2 --- # Design notes ## First #### Deeper ### Next ## Last ``` The displayed numbers are `1`, `1.1`, `1.2`, `2`. Number only actual ancestors: skipping a heading level does not insert zero components. A heading above `startLevel` stays unnumbered and resets the sequence. `numbering` accepts `off` or `hierarchical` (default `off`); `startLevel` is an integer from 1 through 6 (default 2). ATX and setext headings share the policy; code fences do not count. Front matter remains part of canonical source, source hashes and patch offsets, but does not render as body text. Unknown YAML fields survive unchanged. Invalid supported values or malformed YAML produce a visible diagnostic and default options; no source rewrite occurs. Unterminated front matter stays literal body source with a diagnostic. Numbers are display-only spans marked `data-map="generated"` with no editable source range. Heading source/anchor behavior stays unchanged. Body and rich-token source ranges still count from the start of the full document, including front matter and CRLF. These options apply to Markdown source; canonical HTML is not interpreted as Markdown front matter. Server HTML reads and the browser editor/outline support this policy in the September 24 release. ## Select an agent passage by matching text **CLI 0.32.0+:** `notes select --text` publishes an agent selection; section selection requires the server update adding `hash` and `range` to section reads. Older server responses fail explicitly. ```bash export DREAMLAKE_AGENT_ID="review-session-42" export DREAMLAKE_AGENT_NAME="Codex" NOTE_ID="your-note-id" dreamlake notes select --text "The next step is tested in simulation." --note "$NOTE_ID" dreamlake notes select --text "simulation" --section next-steps --occurrence 2 --note "$NOTE_ID" dreamlake notes select --text "simulation" --section next-steps -o -1 --note "$NOTE_ID" ``` Use a stable task identity and your normal authenticated Notes access. The command matches exact canonical source text, including whitespace and markup. It refuses missing or ambiguous matches. `-o` aliases `--occurrence`: `1` selects the first match, `-1` the last, and `-2` the second-last within the chosen scope. Zero and out-of-range values fail without publishing. Receipts report the resolved positive 1-based occurrence. A section match downloads only that section, not the entire document. A whole-note match reads source internally without printing it. Target resolution suppresses read highlighting until a unique match is found. It then sends `POST /namespaces/:slug/notes/:noteId/presence` with `{action:"seek", hash, ranges:[{start,end}]}`. Ranges are half-open Unicode code-point offsets in the whole canonical source. Section reads now return an additive `range` in code points and the whole-source `hash`; legacy `start`/`end` stay UTF-16. Old servers without section metadata fail explicitly. The server validates current source and collaboration access. No source write or human-cursor change occurs. **Plain text is the default for selection commands and agent workflows.** Omit `--json` in normal tool calls and examples. The receipt confirms server acceptance and returns the quoted matched text, scope, resolved match number/count, code-point range and separate expiry times. Multiline excerpts escape newlines. Only an explicit machine integration should request `--json`; that optional receipt includes exact `text` and `scope` (`{kind:"note"}` or `{kind:"section",anchor:"next-steps"}`), alongside `note`, `hash`, `range`, `occurrence`, `matches`, `published`, `selectionExpiresAt` and `presenceExpiresAt`. It does not return the surrounding section or document. Browser rendering still requires an active compatible RTC room and editor. Selection activity lasts eight seconds and presence lasts sixty; a heartbeat renews presence only. Users can navigate to the agent's selected passage through its location control. Pass a retained `--hash "$HASH"` (`sha256:…`) to require the same source. If the source changes before publication, `stale_range` fails without a guessed retry: read the section again and select its current text. Duplicate/missing matches publish no selection. Legacy `notes select "#contact" --note "$NOTE_ID"` and `notes find` remain lookup operations, not explicit visible seek commands. For address hints while reading Markdown, use `read NOTE --view markdown` with the addressed-read CLI/server build. It preserves the selected source text and inserts generated address/character/line comments. List-item targets use hierarchical `li` IDs, such as `s1.ul2.li3`, including nested items. Containers use `ul` or `ol`; every numeric suffix shares paragraph reading order. This reading view is not canonical source and must not be written back as a complete note. ## Manage existing share links Available in CLI 0.32.4 and later; check `dreamlake notes share --help` for installed support. Requires an authenticated login, an existing resource, and permission to manage its sharing. Run these mutation steps only when the user has asked to grant or revoke access. These commands change metadata only; they do not upload content or create a new version. ```bash # Find the release plan and inspect its source and current sharing. dreamlake notes search "release plan" NOTE="release-plan" # Replace with the id or slug from search. dreamlake notes read "$NOTE" dreamlake notes share get "$NOTE" # Give signed-in recipients read access, then verify the returned link. dreamlake notes share create "$NOTE" --role read dreamlake notes share get "$NOTE" # When link access is no longer needed, revoke it and verify. dreamlake notes share revoke "$NOTE" dreamlake notes share get "$NOTE" ``` `get` never enables sharing. It reports the resource URL, visibility, and existing share URL. A resource URL alone does not grant access. `--json` provides structured link metadata; `shareStatus: unavailable` means the server did not expose the token to this caller, not that sharing is disabled. ```bash NOTE="release-plan" # Your existing note id or slug. dreamlake notes visibility "$NOTE" public dreamlake notes visibility "$NOTE" private ``` Visibility and sharing are independent. Making a resource private does not revoke links or accepted access. Revoking a link does not make a public resource private. Use `--namespace ` for another namespace. Only the namespace owner or an eligible Note creator may manage sharing. `create --role write` enables editing; the default is `read`. Updating the role reuses the token and changes the role evaluated on subsequent requests for everyone admitted through the link. Note IDs resolve their owning namespace automatically. ```bash NOTE="release-plan" # Your existing note id or slug. dreamlake notes share revoke "$NOTE" --revoke-accepted ``` Ordinary revocation clears the link and blocks subsequent link-derived access, including for prior recipients. Their acceptance records remain: enabling sharing again restores access under the current link role. `--revoke-accepted` also deletes those records, so recipients must accept a valid link again. A collaborator who already has the room address may keep editing until the room is rotated; this command does not rotate rooms. ```bash NOTE="release-plan" # Your existing note id or slug. # List acceptance records and stored roles as a readable table. dreamlake notes share access "$NOTE" ``` ```bash NOTE="release-plan" # Your existing note id or slug. USER_ID="user-id-from-access-list" dreamlake notes share remove "$NOTE" "$USER_ID" ``` The access list defaults to a readable table; use `--json` for a structured integration. It returns stored roles, which may lag behind the live link role. Use `share get` to inspect the current link role. Removing an acceptance record does not invalidate a circulating link; that link can admit the user again. Membership and public access are unaffected. ## Saved versions The version tag in the toolbar opens a compact revision graph. The right sidebar uses one toolbar toggle for **Comments**, **Table of contents**, and **History**. Choose **History** (the GitGraph icon) to see the graph there. Contents is the default; the note remembers your chosen sidebar. **Working Draft** sits directly above its base version, with an edit count and a GitCommitVertical save icon on that row. A small solid dot marks the draft endpoint; saved-version waypoints are hollow. Choose the icon, enter an optional title/tag and summary, then save. Notes continues to autosave while you work; metadata does not appear in the note body. New milestones receive stable numbers such as `v3`, independent of their titles. Numbers can have gaps after failed saves. Saved versions show their parent connections, including forks from a shared base. The save form defaults to your draft's base; choose another **Base version** to record a different ancestry. This records the relationship without replacing or merging the live draft. The working draft follows the newest saved version when history refreshes, including versions saved by another collaborator, so it stays at the top and its edit count uses the latest checkpoint. This changes only the history display and default save parent, not the note text or saved ancestry. Older versions without recorded parents remain unconnected. The current edit marker shows its one-based index and total within that version interval (for example, **Edit 439 of 443**), including when selected from a grouped tick. Historical previews use the title-row status slot: **e439**, **preview** for a saved version, or **e430–439** for a selected range. Version tags use a lowercase **v** and are hidden while an intermediate edit or range is displayed. Hovering the preview label turns it red with a strikethrough; clicking it returns to the working draft. The chevron opens history. The full preview label remains in the tooltip. The history dropdown fits its content, capped at the remaining viewport height with a 16px bottom gap; longer timelines scroll inside it. The sidebar and dropdown share the editor selection, including resets and selected ranges. The magnifier appears above the selected marker and displays its own red edit-index line on hover. Only an explicitly selected edit or range creates a persistent marker. The lens follows the pointer immediately; document and range previews settle after a short pause, reusing a bounded cache of recent historical text. Each small dot represents one retained intermediate edit; all retained edits are shown. Hover or keyboard-focus a dot to preview its exact text directly in the main body. The historical preview is read-only and isolated from live sync; editor controls and saving are disabled while it is displayed. Leaving the dot restores the prior selection, while clicking the dot keeps that edit selected. There is no separate edit list. The magnifier's right edge stays fixed against the timeline panel as the pointer moves horizontally. A larger magnified region spreads nearby dots apart for selection. Scrolling previews nearby snapshots; clicking version text or activating it with the keyboard selects that revision. Leaving a transient preview restores the last selection. **Back to draft** returns to the still-mounted live editor. The sidebar's **Contents** view follows Dockit's **On this page** format: compact heading links, monospace subheadings, an accent-colored active heading, and a curved progress rail. Section chevrons collapse or expand their child headings; clicking heading text jumps directly to that section. The separate minimap column is omitted. Each saved version retains the exact server-confirmed text, author and date, plus the available collaboration checkpoint and journal. It remains readable after live history is compacted or a room is recreated. Compacted edits that were already missing at save time cannot be recovered; partial counts say **retained edits**. In a saved-version preview, **Compare / link** opens side-by-side comparison and **Copy version link**. Saving never replaces the current note. Tags may repeat; the version ID is unique and immutable. Summaries are written by the person saving the version; automatic AI drafting is not included. Saved history requires edit access, including accepted write-share access. A public note or read-only share does not expose earlier text that may have been removed. Version links do not grant access. If the note changes or is still syncing while you save, review the current text and retry; no version is created from a mismatched browser/server state. ### Version API These authenticated endpoints are scoped to `/namespaces/:slug/notes/:noteId`: - `POST /versions` accepts `{hash, tag?, summary?, parentId?}`. `hash` is the lowercase SHA-256 of the UTF-8 body the user intends to save. The server compares it with a coherent current read and returns `409 note_changed` on mismatch. Tags are at most 80 characters and summaries at most 2,000 characters. A successful `201` returns `id`, `tag`, `summary`, `hash`, `createdAt`, `createdBy`, `author`, `number`, and `parentId`. The optional nullable `parentId` must identify a version in this note; a missing/foreign parent returns `404 parent_not_found`. Omitting it records no parent. RTC-backed versions also include `revision`, `clock`, `editCount`, and `historyComplete`. The count measures retained content-edit messages, not keystrokes. - `GET /versions` returns `{versions, nextCursor}` with up to 50 metadata entries, newest first. Send `?before=` for older entries. - `GET /versions/:versionId` returns the metadata plus `text`. - `GET /versions/:versionId/history` returns the retained `{snapshot, journal}` for read-only replay, with the same editor access requirement. Legacy versions return `{snapshot: null, journal: []}`. This is not a content-write endpoint. The content hash identifies text, not the identity-bearing RTC baseline used for collaborative patches. Saving a version is a retained snapshot operation, not a content write. There are no new CLI flags or Python SDK methods for this surface yet; use the UI or authenticated REST API. --- Source: https://docs.dreamlake.ai/sources {/* The FigSrc* tags come from the site-wide mdxComponents map in site.config.ts — no import needed. */} # Visualize a source A **source** is DreamLake's connection to your data — an S3 bucket or a Hugging Face repo linked into your namespace. Drop one `.dreamrc` file at a dataset's root and the app renders every episode in it: cameras, time series, timelines, 3D. Nothing is imported or converted — the app reads your storage in place. ## Connect your data Linking happens in the dashboard: open **Sources** in your namespace (`/source/`) and connect the storage. Credentials stay server-side, encrypted — never in files or URLs. | Storage | Connect with | Get bytes in today | | --- | --- | --- | | S3 bucket | bucket credentials | `aws s3 cp --recursive ./my-dataset s3://bucket/prefix` | | Hugging Face repo | OAuth | `hf upload you/dataset ./my-dataset --repo-type dataset` | Then verify from a shell that the source holds what you think: ```bash dreamlake source list # sources you can reach dreamlake source browse [path] --source # list a directory dreamlake source download --source -o ./out # spot-check a file ``` ## Shape the dataset Most robot-learning exports work **as-is** — a published dataset renders unmodified, with one file added: | Your dataset root holds | `format` | Anything to do first? | | --- | --- | --- | | `meta/info.json` (LeRobot v2.x / v3.0) | `lerobot` | no | | `*.zarr.zip` or a `.zarr/` directory (UMI) | `umi` | no | | `*.mcap` files — Foxglove schemas or plain ROS 1 / ROS 2 bags | `mcap` | no | | `model/` + `episodes/*/frames.parquet` + `episode.json` | `folder` | no — a DreamLake sim-playback dataset renders as-is | | anything else — videos, images, CSV, parquet | `folder` | one folder per episode | Layout details and shape rules: [prepare your data](https://viz.dreamlake.ai/dataset-viz/requirements). ## Make it render The `.dreamrc` names which fields feed which **views** — that's the whole job. Open the source in the app and the dataset renders: ```yaml file=".dreamrc" version: 1 dataset: format: lerobot # lerobot | umi | mcap | folder episodes: auto views: - view: videoStack cameras: ['observation.images.*'] - view: lineChart series: [{ field: [action, '*'] }] ``` The full authoring workflow — reading the field inventory, every view and its options, a live gallery of finished files — [starts here](https://viz.dreamlake.ai/dataset-viz/start). ## Single MCAP files Selecting one `.mcap` file in the browser renders it as a one-episode dataset. Which config drives the render is a fixed chain — the first hit wins: 1. **`.dreamrc`** beside the file — `test.mcap` looks for `test.mcap.dreamrc`. A config written *for this file*: its views render with the selected file as the one episode. 2. **The folder's `.dreamrc`**, when its `dataset.format` is `mcap` — the unified config for a folder of recordings, applied to just this file. 3. **The source root's `.dreamrc`**, same condition. 4. **No config at all** — the file still renders: a default view generated from its [Foxglove or ROS well-known schemas](https://viz.dreamlake.ai/dataset-viz/reference#the-default-mcap-view). Camera topics become a frame stack, point clouds a 3D pane, tf a moving robot or skeleton (below), numeric channels charts. The header marks it as generated, and **copy .dreamrc** hands you the exact YAML — save it as `.dreamrc` (step 1) and the layout is yours to edit. The more standard the recording, the more the no-config default shows; channels outside the Foxglove / JSON / ROS schema set fall back to the metadata table with the reason stated. ## ROS bags and robots Plain ROS bags need no conversion — ROS 2 (rosbag2's own MCAP output) and ROS 1 alike. Images, point clouds and poses land in the same views as their Foxglove twins, and the `/tf` tree renders in 3D with Foxglove semantics: transforms compose down the declared parent chain, so a tf-only recording plays as a **moving skeleton** — joints, bones, no model required. When the names a recording publishes — tf frame ids, or the joint names on a `/joint_states` topic — carry a known robot's own naming, the default view goes further and renders **the robot itself**: a Unitree G1 recording opens as the G1, a DROID episode as a Franka Panda with its Robotiq gripper, meshes streamed from the [robot preset registry](https://huggingface.co/datasets/live9080/dreamlake-robots). Unknown structures keep the honest skeleton — nothing is guessed. To put a robot on a recording yourself, name its URDF in the 3D view: ```yaml file=".dreamrc" - view: recon3d up: z tracks: [{ field: '/tf::*', as: transform3d }] models: - https://huggingface.co/datasets/live9080/dreamlake-robots/resolve/main/g1/g1_29dof.urdf ``` The viewer reads URDF directly — meshes resolve relative to the file (or through a `packages:` map for `package://` references), each link drives from its `/tf::` track by name, and links the recording never publishes ride their nearest tracked parent. A bag that logs only **joint angles** and no per-link poses — very common, since the angles are the state and the poses are derivable — is 3D too: bind the topic with `joints:` beside a URDF and the viewer runs the forward kinematics itself, gripper linkages included. Full detail: [ROS bags & robot models](https://viz.dreamlake.ai/dataset-viz/robots). ## Sim recordings (MuJoCo & URDF) A teleop recorder that runs MuJoCo can write a DreamLake **sim-playback dataset** directly — one folder per episode, plus the exact model the session ran snapshotted into `model/`. Each `frames.parquet` stores **named channels** — one column per joint, seven per free body, keyed to MJCF names by `episode.json` — so the data survives model recompilation and outlives any single scene version. The `mujoco` view loads the snapshot and plays the channels back **kinematically**: qpos in, forward kinematics out, never stepping physics — scrubbing the shared timeline is random access, frame-exact in either direction. Because channels are name-keyed, an `env:` override plays the same data on a scene-only [env](/envs.md): hand channels with no match there are skipped and listed, while objects and fixtures replay on their own. `ctrl.*` columns carry the action stream for a `lineChart`. The recorder drops this `.dreamrc` at the root: ```yaml file=".dreamrc" version: 1 name: sharpa-teleop dataset: format: folder episodes: "episodes/*/" views: - view: mujoco qpos: [frames] # named channel columns meta: [episode] # episode.json — channels map + model ref height: 420 # env: ns/laundry # optional: play on a scene-only env - view: timeline # shared clock + playback bar - view: lineChart series: [{ field: [frames, 'ctrl.*'] }] ``` The same contract has a **URDF flavor** for motion that isn't an MJCF scene — a retargeted mocap clip, a policy rollout, anything that is "a robot description plus per-frame joint values". `episode.json` says `sim: "urdf"`, `model.path` names the dataset's URDF (with `model.files` listing every mesh), and channels bind **URDF joints by name** — one column per revolute/continuous/prismatic joint — plus one `freejoint` channel naming the **root link** with seven columns (x, y, z, qw, qx, qy, qz — quaternion **w-first**, meters, Z-up world) for the floating base. The `urdf` view drives `setJointValue` and the base pose directly; joints the robot lacks are listed, not fatal, and the same `env:` override plays the data on a pushed URDF env: ```yaml file=".dreamrc" version: 1 name: g1-dance dataset: format: folder episodes: "episodes/*/" views: - view: urdf joints: [frames] # URDF joint columns, keyed by joint name meta: [episode] # episode.json — sim "urdf", model + channels height: 480 - view: timeline - view: lineChart series: [{ field: [frames, left_knee_joint] }] ``` Playback lives here, in sources; the envs you curate stay untouched — pushing and versioning scenes is the [envs guide](/envs.md). ## Let Claude do it Add the two [source skills](https://github.com/dreamlake-ai/dreamlake-skills), point at a folder, and say *"visualize this dataset in DreamLake"*: | Skill | Does | | --- | --- | | `dreamlake-source` | inspects the data, preps the layout, gets it linked, verifies the source | | `dreamlake-dataset-viz` | writes and debugs the `.dreamrc` — bindings, validation, iteration | ```bash git clone https://github.com/dreamlake-ai/dreamlake-skills mkdir -p ~/.claude/skills cp -r dreamlake-skills/{dreamlake-source,dreamlake-dataset-viz} ~/.claude/skills/ ``` Install both — source gets the bytes connected, dataset-viz makes them render. ## Next steps The five-step authoring workflow, every view, and the .dreamrc gallery. The DreamLake-hosted flavor — collections with schemas you push to from the terminal. --- Source: https://docs.dreamlake.ai/workflows {/* FigWf* come from the site-wide mdxComponents map in site.config.ts — no import needed. */} # Workflows A workflow is a typed graph of data production: ordered **stages** whose member nodes do the work — deterministic **compute** (UDFs), judgment-making **agents**, statistical **samplers**, and **control flow** — joined by typed data edges. You push it from the terminal and it renders as a live canvas. > **Note:** This page is about the **WorkflowSpec** — the typed stage/node graph you push > as JSON and run on a canvas. Claude Code's JS orchestration scripts and Python > pipelines are also called workflows, share the `Workflow` server model, and > answer to the same CLI. If that is what you came for, start at > [CJS Workflows vs Python Pipelines](/workflows/cjs-vs-python.md). ## How you use it One round, from a sentence to a published dataset. Skills live in [dreamlake-skills](https://github.com/dreamlake-ai/dreamlake-skills). 1. **Install the CLI and the three skills.** [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) is where you talk to it — install that first. | Skill | Does | |---|---| | `video-labeling-workflow` | fills the template, drives the whole round | | `remote-source-check` | proves your source really holds those paths | | `workflow-publish` | validates and pushes, and asks which namespace | Install all three — the first invokes the other two by name. `--env prod` is worth typing. Without it `login` targets whichever environment is **active on that machine**, so anyone who has ever logged into staging lands there silently — and the failures that follow (an empty `source list`, a push to the wrong place) never point back at the cause. `dreamlake auth env list` shows which login environment is active. Install the standalone CLI from [the CLI guide](/cli.md) and check `dreamlake workflow --help`. The Python `dreamlake` package provides the SDK; it is not the supported CLI installation path. 2. **Say what you want — it asks for the data, verifies it, shows you what it found.** 3. **It pushes — validated against the schema and the graph rules first.** 4. **Review on DreamLake — iterate, hot-reload.** 5. **Run it — nodes light up, output streams.** 6. **Review at the gate, then approve.** The run publishes its dataset and stops. Select the gate node: **↗ Review dataset** opens what was produced, **✓ Approve & continue** completes the run. The link stays after approval — it is the way back to what was signed off. ## What a spec is made of | Part | What it is | |---|---| | **Stages** | phases that group and order — not barriers; execution follows the edges | | **Nodes** | the members of a stage, from four families | | **Edges** | typed connections between ports (`samples`, `dataset`, `metrics`, …); types must match end to end, and a `collect` port fans several streams into one input | | Family | Does | Runs today | |---|---|---| | `compute` | deterministic work — filter, transcode, train, publish (any UDF) | ✅ | | `uda` | agent judgment — labeling, review, curation | ✅ | | `control` | human **approval** gates | ✅ approval only | | `sampler` | statistical subset selection | ⏳ spec-only | The schema also accepts `condition`, `switch`, `loop` and `foreach` control types. They pass validation, then stop a run with `not supported yet` — vocabulary to design against, not behaviour to rely on. A `uda` node declares the tools and **`permissions`** its agent may use, so judgment never runs on raw credentials. Intermediate results stay in the worker's run directory; what reaches DreamLake is what a `publish` node writes. ## Version it Pushes never overwrite. Each one appends an immutable version, and the **spec picker** on the workflow page pins the canvas to any of them — pick the latest to resume following pushes. Editing a node's settings on the canvas and pressing **Apply** does the same thing: the edited spec is validated and appended as the next version. Nothing is edited in place, so a run always names the version it executed. ## Next steps The spec graph, Claude Code's JS orchestration scripts, and Python pipelines side by side — and the seven places where one of them does not exist yet. The full vocabulary — every family's inputs, outputs, and configuration, rendered with the real components. The grant registry uda nodes draw from — domains, verbs, and scopes. `dreamlake workflow push` and `list` — flags, namespaces, versioning. --- Source: https://docs.dreamlake.ai/relay # LLM Relay Agents need a model. Handing every user a provider API key does not scale and does not revoke. The relay puts the key on the server: you authenticate with the DreamLake token you already have, and DreamLake swaps credentials on the way out. ``` POST /relay/anthropic/v1/messages ← what you call ↓ DreamLake token verified, dropped, provider key injected POST https://api.anthropic.com/v1/messages ``` The provider's wire format is preserved **byte-for-byte in both directions**. That is the whole design: the official SDKs, Claude Code, and the Claude Agent SDK work against the relay unmodified. You change a base URL — nothing else. ## Quick start Point any Anthropic client at the relay and give it your DreamLake token: **Python tab:** The same two variables work for anything built on the Anthropic SDK — your own agent loop, a script, a CI job. **curl tab:** Or with no SDK at all: **Claude Code** ```bash export ANTHROPIC_BASE_URL=https://api.dreamlake.ai/relay/anthropic export ANTHROPIC_AUTH_TOKEN=$(dreamlake token) # your DreamLake token, not a provider key claude "summarize the last three runs in this namespace" ``` **Python** ```python import anthropic, os client = anthropic.Anthropic( base_url="https://api.dreamlake.ai/relay/anthropic", auth_token=os.environ["DREAMLAKE_API_KEY"], ) response = client.messages.create( model="claude-opus-5", max_tokens=16000, thinking={"type": "adaptive"}, messages=[{"role": "user", "content": "What changed in this run?"}], ) print(next(b.text for b in response.content if b.type == "text")) ``` **curl** ```bash curl https://api.dreamlake.ai/relay/anthropic/v1/messages \ -H "authorization: Bearer $DREAMLAKE_TOKEN" \ -H "content-type: application/json" \ -d '{ "model": "claude-opus-5", "max_tokens": 1024, "messages": [{"role": "user", "content": "ping"}] }' ``` Streaming works the same way — set `"stream": true` and read the SSE events. The relay does not buffer streams; chunks reach you as the provider emits them. ## Which model? **Anthropic Claude, default `claude-opus-5`.** Three reasons: 1. **The agents are Claude-shaped.** CLI agents, Claude Code, and the Agent SDK all speak the Anthropic Messages API. Relaying that format verbatim means they need no adapter — just `ANTHROPIC_BASE_URL`. 2. **The stack already runs on it.** Hosted Dream Chat containers already call Claude. A second provider family would mean two prompt formats, two token accountings, and two sets of model IDs. 3. **Opus 5 is the right default tier** for agentic and coding work. Want cheaper turns? Pass `claude-haiku-4-5` in the body — the relay never rewrites your model, it only reports and (optionally) restricts it. The relay does not pick models, inject system prompts, or count turns. It is a credential boundary and a usage log. Agent behavior belongs in the agent. An `openai` provider exists behind an optional key, because the pass-through is provider-agnostic. It is not configured by default. ## What you can call Everything under `/relay/:provider/` is forwarded unchanged, so the provider's whole API comes along — not just chat completions: | You call | Reaches | | --- | --- | | `POST /relay/anthropic/v1/messages` | `POST https://api.anthropic.com/v1/messages` | | `POST /relay/anthropic/v1/messages/count_tokens` | the token-counting endpoint | | `GET /relay/anthropic/v1/models` | the live model list | The upstream host is fixed per provider, so the relay cannot be pointed at an arbitrary destination. To discover what a deployment actually offers: ```bash curl https://api.dreamlake.ai/relay/providers -H "authorization: Bearer $DREAMLAKE_TOKEN" ``` ```jsonc { "default": "anthropic", "allowedModels": null, // or a list, when the deployment restricts models "providers": [ { "id": "anthropic", "configured": true, // false → that provider answers 503 "baseUrl": "https://api.dreamlake.ai/relay/anthropic", "defaultModel": "claude-opus-5" } ] } ``` `baseUrl` is exactly what goes in `ANTHROPIC_BASE_URL`. ## Authentication Send the DreamLake token on **either** header — the relay accepts both: ``` Authorization: Bearer x-api-key: ``` Both exist because provider SDKs send the key differently depending on which variable you set: `ANTHROPIC_AUTH_TOKEN` produces `Authorization`, `ANTHROPIC_API_KEY` produces `x-api-key`. Either works, so you don't have to remember which one your client picked. Your token is verified the same way as every other DreamLake route, and is **never forwarded to the provider**. ## Errors | Status | Meaning | | --- | --- | | 401 | Missing, invalid, or expired DreamLake token | | 403 | The deployment restricts models and yours isn't on the list | | 404 | Unknown provider — call `GET /relay/providers` | | 503 | That provider has no credentials on this deployment | | 502 | Upstream unreachable | | *anything else* | **The provider's own status and body, verbatim** | That last row matters: a 429 from Anthropic reaches your SDK as a 429 with its `retry-after` header intact, so your client's normal retry logic works. The relay never reserializes a provider response, so no field is ever stripped. ## What it does not do - **No per-user quota.** Any authenticated DreamLake user can spend tokens. A deployment can cap *which* models are allowed, not how many calls. - **No caching, batching, or retries.** Your SDK's own retry logic works. - **No prompt logging.** Request bodies are relayed, never stored — only metadata (user, model, status, duration, token usage) is logged. - **Not a replacement for hosted chat.** [Hosted pages](/hosted-pages.md) run a whole agent container. This is a raw model API for callers running their own loop. ## Next steps The relay endpoints alongside the rest of the REST surface. Where a relayed model call fits in an agentic workflow. --- Source: https://docs.dreamlake.ai/vault/environment-files # Environment files in Vault Store each environment file as one private Vault entry. The file contents, comments and formatting are preserved. These examples use `dreamlake/envs/dev-env` for development and `dreamlake/envs/prod-env` for production. ## Upload Use a signed-in DreamLake CLI. Run these commands from the directory containing your environment files; replace `.env.dev` and `.env.prod` with your actual filenames. ```bash # Development dreamlake vault add --name dreamlake/envs/dev-env --file-name .env --stdin < .env.dev # Production dreamlake vault add --name dreamlake/envs/prod-env --file-name .env --stdin < .env.prod ``` `--stdin` reads the complete file without putting its contents in command arguments. `--file-name .env` saves a suggested filename; it does not create a local file. The Vault path and local filename are independent. Dots in Vault selectors identify fields, so the entry names use `dev-env` and `prod-env` rather than `dev.env` and `prod.env`. Inspect metadata without printing secret values: ```bash dreamlake vault show --name dreamlake/envs/dev-env dreamlake vault show --name dreamlake/envs/prod-env ``` ## Replace an existing entry Read its current revision with `show`, then supply that revision explicitly: ```bash dreamlake vault add --name dreamlake/envs/dev-env --if-match --file-name .env --stdin < .env.dev ``` Replace `` with the returned number. A revision mismatch means the entry changed; inspect it before trying again. Use the production path and file to update production. ## Restore In the destination project directory, run this Python 3 snippet. It fetches the development file without printing its values, creates `.env` with owner-only permissions, and refuses to overwrite an existing file. Retrieval must succeed before the file is created. ```python import os import subprocess data = subprocess.check_output([ "dreamlake", "vault", "get", "--name", "dreamlake/envs/dev-env" ]) fd = os.open(".env", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "wb") as f: f.write(data) print("Restored .env") ``` For production, change the Vault path to `dreamlake/envs/prod-env`. Keep restored files ignored by Git and let your application's usual dotenv loader read them. Saving a file in Vault does not load it into your shell or deliver it to a remote run. `vault get --to-envs` on a whole-file entry exports one string; it does not parse dotenv lines into separate variables. CLI syntax checked with DreamLake `0.23.0`. This guide does not upload any files automatically. --- Source: https://docs.dreamlake.ai/workflows/cjs-vs-python # CJS Workflows vs Python Pipelines A CJS workflow and a Python pipeline are not two dialects of one idea. They share a noun and almost nothing else: one discovers its shape by running, the other is read off the source without running. This page puts them side by side across eleven axes and then names, honestly, the seven places where one of the two does not exist yet. ## Part 1 — the three artifacts Three separate things in this system are called a workflow. Two of them answer to `dreamlake workflow`. | | Artifact | Authored as | Stored as | Executed by | |---|---|---|---|---| | **W1** | CJS workflow — a Claude Code JS orchestration script | a `.js` exporting a `meta` literal plus `phase()` / `agent()` / `parallel()` | `Workflow.script` + `WorkflowVersion`, plus run traces | the Claude Code CLI's Workflow tool, locally | | **W2** | WorkflowSpec v1 — the typed stage/node graph | hand- or agent-authored JSON | a DreamDB dataset; the server holds only `specVersion` / `specMeta` | a `workflow_runtime` worker off a Redis stream | | **P** | Python pipeline | `.py` with `@dl.pipeline` + `@ls.udf` | `Pipeline.sourceCode` + `PipelineVersion.graph` | nothing — the graph is derived, never run | W2 is the artifact the [Workflows guide](/workflows.md) and the [node types reference](/workflows/node-types.md) describe. This page is about the other two: **W1 against P**, with W2 present only where the comparison needs it. ### Two facts that make this genuinely confusing **One CLI verb, two file formats.** `dreamlake workflow create` and `dreamlake workflow update` take `--file ` and parse a `meta` literal out of JavaScript. `dreamlake workflow push` takes a WorkflowSpec v1 JSON file and writes it to DreamDB. Same command group, same workflow name, entirely different artifact on each side of the flag. **One record, two version counters.** The `Workflow` model carries both: - `currentVersionHash` — a twelve-hex hash pointing at a `WorkflowVersion`, and a `WorkflowVersion` holds a `script`. That is the **JS** history. - `specVersion` — a monotonic integer, the latest push counter into the workflow's DreamDB dataset, alongside `specMeta` (`{ description, stageCount, nodeCount, edgeCount }`). That is the **JSON** history. Nothing keeps them in sync. A workflow can be at `currentVersionHash` `a1b2…` and `specVersion` 7 with no relationship whatsoever between the two, because they were written by two different commands describing two different artifacts. **And a third `push`.** `dreamlake workflow push` also exists in the Python CLI, with the same ` [--name]` shape and byte-identical output — both CLIs re-serialize the spec at two-space indent so they append to the same dataset. The TypeScript CLI acknowledges the split in its own error text: its hidden `workflow append-local` subcommand answers `no_writer — append-local needs the canonical Python writer (pip install dreamlake); the TS CLI has no DreamDB writer`. The Python CLI stays the canonical DreamDB writer, and dreamlake-server spawns it by name for spec applies. --- ## Part 2 — eleven axes ### 0. What the name means **W1.** One of three artifacts, and the least documented of them. `dreamlake workflow` covers W1 (`create`, `update`, `show`, `push-run`, `watch-run`) and W2 (`push`) from the same command group, so the verb alone never tells you which one you are operating on. **P.** One artifact. `dreamlake pipeline` means exactly one thing: a Python file with a `@dl.pipeline` entry point. ### 1. Where the graph comes from **W1 — a runtime trace.** There is no static DAG for a CJS workflow. Phases and agents are captured as the run executes; the renderable graph is derived from a run's `trace` (`phases` + `agents`). Before the first run, only the phase skeleton exists, built from the current version's `meta.phases`. With no version at all there is nothing to render. **You cannot know the shape of a CJS workflow without running it.** **P — static derivation from source.** The tracer walks the Python AST. It never imports the module and never executes a line, which is why every UDF body in the canonical examples is a literal `...` and traces perfectly well. The graph is a property of the text, available before anything has ever run — and, as Part 4 records, that is just as well, since nothing runs it. ### 2. The unit of work **W1.** An agent call: ```js await agent(promptString, { label, phase, schema, effort }) ``` The prompt is a string. The options carry the display label, the phase the agent belongs to, an output schema, and an effort level. **P.** A decorated function: ```python @ls.udf(kind="review") def review_boxes(labels) -> Mask["R", "N"]: ... ``` The parameters are the node's input ports; the return annotation is its output schema. ### 3. Typing of the connection **W1 — none.** The prompt string *is* the edge. Whatever one agent produced reaches the next one only because the script interpolated it into the next prompt. The `schema` option types an agent's **output**, not the connection: it constrains what that one agent returns and says nothing about who consumes it or whether the consumer expects that shape. **P — typed on both ends.** The tracer reads input ports off the function's parameter list, gives every UDF that returns something exactly **one** output port (`out`), and reads the return annotation's names into `columns` — `Tuple["boxes", "classes", "confidence"]` becomes three column names on the node. A sink UDF with no return annotation gets no output port at all. Every edge additionally carries `kind: "data" | "mask"`. ### 4. Fan-out **W1.** An array of thunks: ```js const findings = await parallel( targets.map((t) => () => agent(`Scout ${t} and report`, { label: t, phase: 0 })), ) ``` `targets` is an ordinary JavaScript array, usually parsed out of a previous agent's output. **The width is computed at runtime.** Nothing before the run knows whether this is three agents or thirty. **P.** Call the UDF more than once, or write a comprehension. Each call mints a fresh node id from the function name — `detect_objects`, then `detect_objects_2`, then `detect_objects_3`. This is static, and it has a hard edge: **loops never unroll.** The tracer walks a `for` body exactly once, so ```python for i in range(3): x = refine(x) ``` produces **one** `refine` node, not three. Three calls written out give three nodes; three iterations of one call give one. ### 5. Fan-in **W1.** Ordinary array handling, then string concatenation into the next prompt: ```js const notes = findings.filter(Boolean).map((f) => f.summary).join('\n\n') await agent(`Synthesize these scout reports:\n\n${notes}`, { phase: 1 }) ``` The join is the fan-in. There is no merge construct — the next agent simply receives a longer prompt. **P.** Pass several results into one UDF, and one edge per argument lands on that node. The tracer then labels it: an untagged UDF whose **data** fan-in comes from two or more distinct non-source nodes is re-kinded `merge`. Only `data` edges count, and inputs drawn from a `source` node are excluded, so a side input such as a prompt table does not accidentally promote a stage to a merge. ### 6. State between steps **W1.** Ordinary JavaScript variables in one module scope. The script is a normal ES module: `const` bindings hold agent results, and they reach the next step as **interpolated prompt text**. There is no store, no artifact registry, and no way to inspect an intermediate other than reading the run trace. **P — provenance sets.** The tracer tracks, for every Python local, the set of nodes its data descends from, each tagged `data` or `mask`. When both tags reach the same node, **`data` dominates `mask`**. Consuming a value in mask position downgrades its whole provenance. Method calls (`review.all(axis=0)`), subscripts (`labels[consensus]`) and operators (`~consensus`) **pass provenance through without creating a node** — they are how a value is reshaped, not a stage of work. ### 7. Control flow **W1.** Plain JavaScript `if`. It is untracked and invisible to the trace: the trace records phases and agents, so a branch that skips an `agent()` call simply leaves that agent out of the trace, and a branch that fires no `phase()` leaves no mark at all. You cannot read a CJS workflow's branching structure off its graph, because the graph is only what happened. **P.** The tracer walks **both** branches of an `if/else` — an intentional over-approximation, so every path's nodes appear. A variable assigned in both branches gets the **union** of the two provenance sets, so a downstream node connects to both and nothing dangles. The `else` branch walks from the pre-`if` bindings rather than the `if` branch's, because the branches are independent. And **the condition is discarded**: `stmt.test` is evaluated in mask role for its provenance only. The graph shows what a branch might touch, never which way it went. ### 8. Failure **W1.** A failed agent yields a falsy entry rather than throwing, which is why `.filter(Boolean)` is idiomatic on every `parallel` result. That idiom silently swallows the failure. Partial failure is entirely the calling script's problem to notice and report; nothing in the runtime stops the workflow because two of five scouts came back empty. **P.** The opposite discipline. Any undecorated call inside a `@dl.pipeline` **raises at trace time**: ``` pipeline calls undecorated function 'to_dataset' — every function called in a @dl.pipeline must be a @ls.udf / @dl.node (wrap framework helpers like to_dataset/requeue in a udf) ``` No hidden helpers, no silent passthrough. The one exemption is a `for` loop's *iterator* expression (`batch(src, n=64)`, `stream(…)`), evaluated leniently so a structural helper there does not raise; the loop **body** is strict again. ### 9. Execution **W1.** The Claude Code CLI's Workflow tool runs the script locally and rewrites a `wf_.json` run file under the session's `workflows/` directory as it goes. `dreamlake workflow push-run` and `watch-run` reduce that file and PUT it to the server, which is how a local run becomes a visible run. **P.** Nothing executes it. `dreamlake pipeline create --file` stores the source and the derived graph; that is the entire lifecycle. There is no runner, no scheduler and no queue on the pipeline side. ### 10. Storage, versioning, renderer **W1.** `Workflow` (identity, metadata) → `WorkflowVersion` (an immutable snapshot of `script` plus the parsed `meta`) → `WorkflowRun` (a mutable trace snapshot, upserted while the run progresses). Rendered as a metro map: phase bands with agent cards strung along a spine. **P.** `Pipeline` → `PipelineVersion` (`sourceCode` plus the derived `graph`) → `NodeState` for runtime status. Rendered by uikit's `` as node cards joined by typed edges, solid for `data` and dashed for `mask`. Both version immutably, and both mint the same kind of identifier: a **twelve-character hash**, `SHA256(id:timestamp:nonce).slice(0, 12)`. --- ## Two real samples ### W1 — a CJS workflow The `meta` literal and its storage contract are defined by dreamlake-server and are exact. The `phase` / `agent` / `parallel` bindings come from the Claude Code CLI's Workflow tool, which is not part of this repository — treat the body below as the shape of the fan-out, not as a normative signature. ```js file="scout.js" export const meta = { name: 'workflow-feature-scout', description: 'Map the five subsystems a feature touches, then synthesize.', phases: [ { title: 'Scout', detail: '5 parallel readers' }, { title: 'Synthesize', detail: 'one writer' }, ], } export default async function run({ phase, agent, parallel }) { await phase(0) const targets = ['server', 'cli', 'uikit', 'docs', 'schema'] const findings = await parallel( targets.map((t) => () => agent(`Read the ${t} and report what it owns.`, { label: t, phase: 0, effort: 'medium', })), ) await phase(1) const notes = findings.filter(Boolean).map((f) => f.summary).join('\n\n') return agent(`Synthesize these scout reports:\n\n${notes}`, { phase: 1 }) } ``` > **Warning:** Both the Studio parser and dreamlake-server lift `meta` out of the script text > **without executing it** — a balanced-brace scan from the first `{` after > `export const meta`, then a constrained recursive-descent read of objects, > arrays, strings, numbers, `true` / `false` / `null`, comments and trailing > commas. Identifiers, calls and arrow functions are treated as unparseable and > skipped, never evaluated. So `name: WORKFLOW_NAME` does not resolve to a > string — it resolves to nothing, and the server returns **`400 > invalid_script`** because `meta.name` must be a non-empty string. The same > applies to `meta.description`. `phases` is tolerant by comparison: it defaults > to `[]`, non-object entries and entries without a string `title` are dropped > silently, and `detail` is kept only when it is a string. ### P — a Python pipeline ```python file="image_object_annotation.py" import dreamlake as dl import lakeshore as ls from dreamlake import batch, requeue, to_dataset from lakeshore.types import Mask, Tensor, Tuple @ls.udf(kind="source") def load_images() -> Tuple["images"]: """Pull the batch of images to annotate.""" ... @ls.udf def detect_objects(images: Tensor["N", "H", "W", 3]) -> Tuple["boxes", "classes", "confidence"]: ... @ls.udf(kind="review") def review_boxes(labels) -> Mask["R", "N"]: """R reviewers pass/fail each box.""" ... @ls.udf(kind="sink") def save_dataset(rows): to_dataset(rows) @ls.udf(kind="sink") def rework(rows): requeue(rows) @dl.pipeline def image_object_annotation(): src = load_images() for items in batch(src, n=64): labels = detect_objects(items.images) review = review_boxes(labels) consensus = review.all(axis=0) # method call: provenance passes through save_dataset(labels[consensus]) # subscript: the mask gate rework(labels[~consensus]) # unary op: still the same provenance ``` Read the last three lines against axis 6. `review.all(axis=0)` and `~consensus` create no nodes — they carry `review_boxes`'s provenance forward with a `mask` tag. `labels[consensus]` mixes a `data` provenance (the subscripted value) with a `mask` provenance (the slice), so `save_dataset` ends up with a solid edge from `detect_objects` and a dashed one from `review_boxes`. That is the whole of "tapping data off the main stream" in the pipeline model — no sampler function is involved, because there isn't one. --- ## Part 3 — the CLI, both sides Neither command group is documented at [/cli](/cli.md) today, so both are recorded here. ```bash # W1 — the JavaScript orchestration script dreamlake workflow create --file [--message ] dreamlake workflow update --file [--message ] dreamlake workflow show dreamlake workflow list # W1 — run traces, pushed from the local wf_*.json the Workflow tool writes dreamlake workflow push-run dreamlake workflow watch-run [--interval 5] # W2 — a WorkflowSpec v1 JSON, NOT the JS dreamlake workflow push [--name ] # P — the Python pipeline dreamlake pipeline create --file dreamlake pipeline update --file dreamlake pipeline version list dreamlake pipeline version show dreamlake pipeline node list dreamlake pipeline node show ``` Two details worth knowing before you use them. **`workflow push` takes the file positionally, not the name.** The workflow name comes from the spec's own `name` field unless you override it with `--name`. This differs from every other verb in the group, which takes `` first — another consequence of `push` operating on a different artifact from its siblings. **`watch-run` is designed to be backgrounded** alongside the Workflow tool. It polls the local run file and pushes a trace snapshot on each tick until the file leaves `running`, then pushes once more so the terminal state lands. Every push is size-budgeted below the server's body limit and degrades deterministically — free-text previews are capped first, then log lines, then `result` is replaced with a size marker. The status, the phase/agent skeleton and the run totals always survive, because those are what the run page renders. If the terminal push were dropped, the server-side run would never leave `running` and the detail page would poll it forever. --- ## Part 4 — the honest gaps This is the part that makes the page worth writing. Everything above describes what the two systems *mean*. This describes what is not there. > **Warning:** The `.py` files above trace correctly and will not import. The shipped > `dreamlake` package exports no `pipeline`, no `node`, no `batch`, no `stream`, > no `to_dataset` and no `requeue`. The real `udf` signature is > `udf(fn=None, *, queue=None, transport="auto", config=None)` — > **no `kind=`**. `kind` is notation the tracer reads out of the decorator's > keyword arguments; the runtime never sees it. `lakeshore.types` does not exist, > so `Mask`, `Tensor` and the string-literal `Tuple` are annotations with no > implementation behind them. And the canonical import for the real package is > `import dreamlake.lakeshore as dls`, not `import lakeshore as ls`. > > The conclusion follows directly: **those `.py` files are a specification format > that happens to be Python syntax.** They work precisely because the tracer > parses text and never imports a module. Do not read them as runnable code. > **Warning:** dreamlake-server calls an external tracer at `PIPELINE_PARSER_URL/trace`. When > that variable is unset it falls back to `parsePython()`, an in-process mock > that matches only a **bare `@ls.udf` line** — not `@ls.udf(kind="source")` — > infers `kind` from whether the function name starts with `tr_`, and returns > `inputs: []` and `edges: []`. That is an empty graph with a few disconnected > nodes in it. Real graphs require the external `dl_trace` service; without it, > a pushed pipeline renders as unconnected cards. > **Warning:** There is no way to see a CJS workflow's shape without running it, and > `meta.phases` does not substitute for one. It is a hand-written label list > with **no relationship to the `agent()` calls the script actually makes** — > nothing validates that a phase exists for every `phase: n` an agent declares, > or that a declared phase is ever entered. It is display metadata that happens > to be ordered. > **Warning:** The edge type system documented at [/workflows/node-types](/workflows/node-types.md) > — typed ports, `samples` / `table` / `dataset` / `model` / `metrics`, branch > ports, collect ports — applies to **W2 only**. None of it exists for a JS > script, where the only connection between two steps is a string that one of > them wrote into the other's prompt. > **Warning:** No `uda`, no permission grants, no `tools` declaration. A "review" step in a > pipeline is a UDF with `kind="review"` — an ordinary function node with a > cosmetic label, which the renderer draws differently and the tracer treats > identically to a transform. The whole `uda` concept, and the grant taxonomy at > [/workflows/agent-permissions](/workflows/agent-permissions.md), lives only in W2. > **Warning:** Neither a CJS script nor a Python pipeline has one. W2 has a real > `approval` control node and a matching server endpoint > (`POST …/runs/:runId/approve`, which resumes from a checkpoint against the > same spec version the run started on). A CJS workflow that needs a human in > the loop has to arrange it in JavaScript; a Python pipeline has nowhere to put > it, because nothing executes the pipeline in the first place. > **Warning:** dreamlake-server enqueues a run onto a Redis stream: `XADD` to `WF_QUEUE`, > defaulting to `labeling_task`, carrying the full spec inline so the worker > never has to read DreamDB. The comment in the route names a `workflow_runtime` > worker as the consumer. **No such consumer was found anywhere in this > checkout** — not the string `workflow_runtime`, not a reader of > `labeling_task`. It may be a separately deployed service. Until that is > confirmed, W2's "push it and it runs" story is unproven, and this page will not > assert it either way. --- ## Related The W2 vocabulary — every node family's inputs, outputs and configuration, and the artifact type system on every edge. The grant registry uda nodes draw from — domains, verbs, and scopes. W2 only. The Python authoring vocabulary, one construct per page, each with a live node view. The rest of the `dreamlake` command surface. --- Source: https://docs.dreamlake.ai/envs {/* FigEnv* come from the site-wide mdxComponents map in site.config.ts — no import needed. */} # Envs An env is a **simulation environment** — a directory holding one MJCF scene or one URDF robot, plus its assets — that you push from the terminal and open as a live, interactive 3D page in the dashboard. ## Browsing in the app `//profile?tab=envs` and `//envs` reuse the same Environments catalog. Your own namespace and organizations you belong to show resources and actions allowed by your permissions. Signed-out visitors and signed-in visitors to other namespaces see public resources only, without creation or modification controls. Profile uses an avatar rail; the application uses resource navigation for the namespace in the URL. Your own Envs catalog also includes recent environments from your organizations. Public environments and valid Env share-token links open without sign-in. The application sidebar shows the resource owner's avatar and links, including for anonymous public readers. Your signed-in identity and personal/organization switcher are separate from that owner. See [Profiles and workspaces](/workspaces.md). The detail header returns anonymous readers to `//profile?tab=envs` and signed-in readers to `//envs`. Details opened inside a project retain their return-to-project action. There is no extra sign-in navigation bar. ## Generate with chat Members can also create and edit envs from the browser, by prompting an embedded agent that runs the [scene-generation skill](/scene-generation/quickstart.md). The chat panel ships with the web app — it appears once an app deployment that includes env chat reaches your server: 1. In your namespace's Envs catalog, click **+ new env** — in the page header next to the Environments/Libraries switch, and offered again by the empty catalog's placeholder. One click opens a fresh *draft* with an auto-minted name (`env_xxxxxx`) — no dialog, nothing to type first, and nothing exists on the server until the agent's first successful push. 2. The draft opens straight into the **intro composer** — describe the scene there; the agent sources assets, measures, composes, validates, and publishes with `dreamlake env push`. 3. On the first successful push the draft becomes the **saved preview** — the interactive viewer on the pushed version, chat still beside it. 4. **Follow-up prompts create new versions** of the same env. The viewer tracks the latest; picking an older version in the header pins it, and a "vN available" button jumps back to latest. 5. Editing an **existing** env works the same: open its page as a member and prompt the chat panel next to the viewer. Read-only viewers, share links, and anonymous visitors never see the chat. 6. To grow a **new** env out of an existing one, start from the catalog, not from the source env's page — the chat edits the env whose page it sits on. Click **+ new env** for a fresh draft, then ask it to start from the source env at a pinned version; the source env keeps its own versions, untouched (details in the [Quickstart](/scene-generation/quickstart.md)). The env chat opens in **Auto** permission mode, so one prompt can carry through the whole workflow — the agent runs the scene tools and `dreamlake env push` without per-command approval. The composer's permission control still works as usual: switch to **Ask permissions** or **Plan mode** for a read-only conversation (the agent inspects and answers but does not generate or push), and switch back to Auto when you want it to build. A read-only choice is respected for as long as you keep it — the page sets the mode only once, on open, and never overrides your selection. Env membership still governs what a push may touch: the mode routes what the agent may *run*, not what your account may *write*. The install-and-use guide for the same workflow from a local agent — ordinary create / edit / reuse prompts included — is the [Scene Generation Quickstart](/scene-generation/quickstart.md). **Troubleshooting the preview when running the app from source.** If the viewer panel shows an error fallback instead of the scene and the console logs `R3F: Hooks can only be used within the Canvas component!`, that is a dev-server fault, fixed in the app source on 2026-09-29: the dev server could load two copies of the 3D renderer and which copy a session got depended on load order — so the crash appears and disappears between checkouts, and its absence on one run proves nothing. If you see it, your source checkout predates the fix: update to a checkout that carries it and restart the dev server. In that source's dependency graph the production build resolves a single renderer copy, which bounds the fault to the dev server — an inference about the current source only, not an audit of anything previously deployed. Count the scene as loaded only when its actual geometry is visible and the simulation controls respond; that console error is failure evidence, and a page shell or a bare `` is not success — the canvas can mount and then unmount again while the model loads. ## Push one ```bash dreamlake env push ./cassie ``` The entry file is auto-detected — the one root-level `*.xml`/`*.mjcf` containing ``, or (with no MJCF around) the one root-level `*.urdf` containing ``, which also sets the env's type to `urdf`. Relative ``, `` and `package://` references keep working exactly as they do on disk. The CLI prints an **open link**. | Flag | What it does | |------|--------------| | `--entry scene.mjcf` | Pick the entry when there are several candidates | | `--type isaaclab` | Simulator family — stored and pullable; `mujoco` and `urdf` have viewers | | `--visibility public` | Anyone can open the viewer, no login | | `--name`, `--title` | Identity (defaults to the directory name) | | `--thumbnail cover.png` | Set the gallery cover yourself (PNG ≤ 512 KiB) — see [Thumbnails](#thumbnails) | ## Version it — only changes upload Push the same env again and you get **v2**. Files are content-addressed, so unchanged meshes and textures are never uploaded or stored twice — the CLI tells you exactly what moved: ```txt ✓ pushed you/cassie v2 — 20 files (1 uploaded 12.4 KB, 19 reused) ``` ## Drive it For a **mujoco** env the viewer is a real simulation, not a screenshot: the engine runs on load, and the toolbar gives you **play / pause / reset / speed**. - **Sliders** — every actuator (`ctrl`, with its real range) and every hinge/slide joint, straight from the model. - **t = 0 is your authored initial state** — when the model defines a ``, the viewer opens at (and **reset** returns to) keyframe 0, including its `ctrl`; without one it uses the compiled defaults (qpos0). *Keyframe honoring is a source change awaiting the next viewer deployment: the prior source baseline (and its wasm runtime) opens at qpos0 regardless of keyframes, so until the deploy — verify in the app — expect qpos0.* - **⌥ / Alt + drag** — pull any body with a mass-scaled spring force, like the MuJoCo app's perturbation. - **files** — the version's full file listing, entry highlighted. - The first member visit renders the gallery **thumbnail** — see [Thumbnails](#thumbnails) for how the angle is chosen and how to control it. A **urdf** env opens as a poseable robot instead — no physics, pure kinematics: every non-mimic revolute / continuous / prismatic joint gets a slider with its real limits, **dragging a link in the scene rotates its joint directly** (orbit pauses while you hold it), and **reset** returns the pose to zero. Meshes referenced as relative paths or `package://` URIs resolve against the pushed directory. ## Thumbnails The env grid's cover image is rendered in the browser the first time a member opens a new version: a fixed 16:10 PNG with a **transparent background**, at the scene's initial (t=0) pose — physics is held until the capture, so the cover shows the authored t=0 state (see [Drive it](#drive-it)), not half a second of free fall. The camera angle is chosen from the scene itself, in priority order: 1. **A camera named `thumbnail`** — add one to your MJCF and the cover (and the viewer's opening shot) is exactly that camera: ```xml … ``` 2. **The first `` in the model** (including ``d files) — scenes authored with a hero camera need no rename. 3. **The free-camera stance MuJoCo's own viewer would use** — `` plus `` when authored. This is the cheapest lever: two lines, and the cover matches what you see in `simulate`: ```xml ``` 4. **A geometry fit** — bounding sphere of all geoms (ground planes excluded), from a three-quarter angle. URDF robots always use this fit. Two overrides beat all of the above: - **The camera button in the viewer toolbar** (members only, latest version): orbit to any angle and click it — that exact view becomes the cover, and the angle is remembered, so **future versions auto-capture from it** too. - **`dreamlake env push --thumbnail cover.png`** stores your own image (≤ 512 KiB PNG) and marks it manual, so the viewer won't overwrite it for that version. Works on a no-change push (thumbnail-only update). ## Pull it back ```bash dreamlake env pull cassie # latest → ./cassie/ dreamlake env pull cassie@1 -o v1 # any version ``` Every file is verified against its content hash on the way down — the directory you get is byte-identical to the one that was pushed. ## Env layers An env can also be **composed from other envs**. A layer stack (`dreamlake.layers.json`) is an ordered list of **ops** — one flat object per line, in the vocabulary of vuer's imperative updates — and materializes into an ordinary `mujoco` env. No layer has a special role, and later ops win; swapping the gripper is editing one line and re-pushing. The pushed version carries both the flat artifact (what every viewer and SDK reads) and the pinned stack, so any composed env can be reopened, edited, and recomposed later. ```json file="dreamlake.layers.json" { "schema": "dreamlake.env-layers/v3", "layers": [ { "tag": "Merge", "src": "you/kitchen" }, { "tag": "Attach", "src": "you/sharpa-right", "key": "right", "at": "world", "pos": [0.45, -0.12, 0.30], "joint": "free-anchored" }, { "tag": "Update", "key": "obj/mug", "pos": [0.30, 0.10, 0.02] }, { "tag": "Remove", "key": "body:fixture/plant" } ] } ``` ```bash dreamlake env compose # materialize ./dreamlake.layers.json locally dreamlake env push # push artifact + pinned stack as one version ``` | op | what it does | |----|--------------| | `Merge` | union the env's MJCF into the stack — a same-name collision is an error | | `Attach` | graft the env's subtree at a target under the identity root `key` (`right` makes `right:palm`), with a mount pose and joint mode | | `Update` | inline sparse opinions: `key` addresses one element, every other prop is an MJCF attribute — nothing else is touched | | `Remove` | delete the addressed element and its subtree — drop the line and it's back | | `Patch` | `Update`-style opinions from a sparse-MJCF file or env, for big opinion sets | `src` is one string: `ns/name[@v]` is a registry env (unversioned floats), a `./`-prefixed path is a local directory or file. > **Note:** Materialization is a real MuJoCo build, so `dreamlake env compose` > delegates it to the engine in **dreamlake-py** — install it with > `pip install "dreamlake[compose]"` (Python + MuJoCo) on the composing > machine. Push / pull / list need no Python. Every field, the exact semantics, and six annotated stacks: [Env Layers Reference](/envs/layers.md). ## Under the hood Uploads go straight to storage with short-lived scoped credentials, and reads are presigned URLs — **env bytes never pass through the API server**. The read side is two plain REST calls (`…/versions` and `…/versions/:v`), so any HTTP client can list and download an env without special tooling. ## Delete it (safely) ```bash dreamlake env delete cassie # soft delete (restorable) dreamlake env restore cassie # bring it back dreamlake env delete cassie --permanent # erase storage — no undo ``` ## Let Claude do it Add the [envs skill](https://github.com/dreamlake-ai/dreamlake-skills), point at a repo, and say *"push the MuJoCo scenes in here to DreamLake"*: ```bash git clone https://github.com/dreamlake-ai/dreamlake-skills.git ~/dreamlake-skills mkdir -p ~/.claude/skills ln -s ~/dreamlake-skills/dreamlake-envs ~/.claude/skills/ ``` Update with `git -C ~/dreamlake-skills pull --ff-only`. If a skill already exists, preserve local edits before replacing it with a symlink. The `dreamlake-envs` skill does the part that isn't a single command: extracting a **self-contained directory** from a scene that references meshes outside itself (collecting the robot folders, rewriting the relative paths), verifying the result still compiles before pushing, and running the pull-back round-trip check after. ## Next steps Recorded teleop episodes don't live here — they play back inside **sources**, each dataset carrying its own model snapshot, with the env itself unchanged: [visualize a source](/sources.md). One skill install, then build / edit / reuse scenes with ordinary prompts — locally or from the env page's chat. The `dreamlake.layers.json` contract — merge / attach / override, field by field, with annotated stacks. Every flag — names, types, entries, visibility, limits. The REST endpoints behind push, versions, and presigned reads. --- Source: https://docs.dreamlake.ai/vault/shared-scopes # Organization and team vaults In the Vault dashboard, use **Save in** to choose Personal, an organization, or a team before creating an entry. The selector lists organizations you belong to and teams where you are a direct member. Personal remains the default on your own profile. Organization members can save and read organization entries. Team entries require both organization membership and direct team membership. Team visibility, organization ownership, and membership in a parent team do not by themselves grant access to team secrets. Manage membership through the existing organization and team settings. Each scope has separate storage. Identically named organization and team entries remain independent. Changing the selector opens another vault; it does not move or share existing entries. Leaving the organization or team removes access on subsequent requests. Deleting an organization, team, or its namespace also removes access. Shared vaults support entry creation, metadata listing, reading, replacement, retirement, restoration, and write-outcome recovery. Access keys, OTP generation, remote setup, host-credential bindings, and encryption-key administration remain personal-only. Existing CLI and SDK commands continue to use the personal vault by default. The API selects a shared scope with `X-Vault-Scope: org:` or `X-Vault-Scope: team:` on every request. Authenticated account sessions can discover their available scope IDs and entry prefixes through `GET /v1/vault/scopes`. Omitting the header selects the personal vault. The server validates current membership rather than trusting the supplied scope or entry path. --- Source: https://docs.dreamlake.ai/workflows/node-types # Workflow Node Types A workflow is a spine of ordered stages; typed member nodes fan out from their stage and connect through typed data edges. This page defines every node family and the data types edges can carry — rendered with the real canvas components. The vocabulary is grounded in established systems: the artifact type system follows Kubeflow Pipelines v2 and Flyte, control flow follows BPMN 2.0 and the workflow control-flow patterns literature, and samplers use statistical naming with Spark/SQL parameter conventions. ## Stage A **stage** groups the work of one phase and is rendered as a node on the spine. Stages are **hubs, not barriers**: work converges into a stage node and fans out to its members, but data edges may cross stage boundaries freely. Stage order (the array order in the spec) is the spine. | Field | Type | Notes | | --- | --- | --- | | `id` | string | `^[a-z0-9][a-z0-9_-]{0,63}$`, unique | | `title` | string | display name | | `detail` | string? | one-line description | ## Compute (UDF) A **compute** node runs a user-defined function on Lakeshore. The `provider` block follows the Lakeshore provider schema: a **launcher class** (`SSH` · `SLURM` · `EC2` · `GCE` · `Kube`) or a named server-stored provider, plus per-machine RunConfig fields. | Field | Type | Notes | | --- | --- | --- | | `compute.udf` | string | UDF reference, e.g. `pipelines.bimanual_filter` | | `compute.params` | object? | static UDF arguments — small inline values are node **parameters**, never port data (the KFP/Snakemake split) | | `compute.provider` | ProviderRef? | `provider` (named) XOR `launcher` + `kwargs`; per-machine: `instance_type`, `image`, `partition`, `resources`, `runner` | | `compute.dispatch` | `direct` \| `daemon` | how the launch happens; defaults per launcher | | `execution` | ExecutionPolicy? | `retry {max_attempts (1 = no retry), retry_on, backoff {initial, factor, max}}`, per-attempt `timeout`, `cache {enabled, version}` | Secrets in provider kwargs must use `{"$secret": "name"}` markers — never inline credentials. ## UDA (User-Defined Agent) A **uda** node runs a remote agent. Field names follow agent-framework conventions (OpenAI Agents SDK, Claude Agent SDK, A2A): | Field | Type | Notes | | --- | --- | --- | | `uda.instructions` | string | the system prompt (fixed persona/behavior — distinct from any per-run input) | | `uda.description` | string? | *when to delegate* to this agent (routing metadata) | | `uda.model` | string? | model id | | `uda.tools` | string[]? | tool grants by name — tools are **not** permission strings | | `uda.permissions` | string[] | data/resource grants — see [Agent Permissions](/workflows/agent-permissions.md) | | `uda.max_turns` | number? | turn budget | | `uda.output_schema` | object? | JSON Schema for the structured final output | | `uda.provider` XOR `uda.queue` | ProviderRef / string | where the agent runs: a provider/host, or a Lakeshore queue | | `execution` | ExecutionPolicy? | retry + timeout only — **`cache` is forbidden on agents**: agent runs are non-deterministic; caching one replays a stale answer | ## Sampler Sampler strategies use statistically correct names. The result of `bernoulli` is **probabilistic in count** — that is what SQL `TABLESAMPLE BERNOULLI` and Spark `df.sample(fraction)` actually do. | Strategy | Parameters | Semantics | | --- | --- | --- | | `bernoulli` | `fraction` ∈ (0, 1], `min_size?`, `seed?` | each item kept independently with probability `fraction`. `min_size` is a DreamLake extension: if `fraction·N < min_size`, degrade to an exact-size random sample | | `random_n` | `size`, `with_replacement? = false`, `seed?` | exact-size simple random sample (reservoir sampling is the streaming implementation) | | `stratified` | `stratify_by`, `fraction?` or `fractions?` (per-stratum map), `min_size?`, `seed?` | per-stratum sampling, Spark `sampleBy` shape | | `first_n` | `size` | head/LIMIT — deterministic and order-dependent; **not** a statistical sample | **Signature**: samplers take exactly one input, restricted to the collection-like types (`samples` · `table` · `directory`), and are **type-preserving** — the output port is derived with the same type (a sample of a clip collection is a clip collection). **Determinism**: identical input + identical `seed` ⇒ identical sample (SQL `REPEATABLE` semantics). Absent seed ⇒ non-reproducible (the run records the effective seed). ## Control flow | Type | Out ports | Semantics | | --- | --- | --- | | `condition` | `true: T`, `false: T` | binary exclusive choice (`expression`) | | `switch` | one per case + **required `default`**, all `T` | n-way exclusive choice (WCP-4); first matching case wins; `default` keeps routing total | | `loop` | `out: T` | `mode: 'while'` — condition-bounded, `until` + `max_iterations` **required** (no unbounded loops); `mode: 'foreach'` — collection-driven (`over`, `max_concurrency`) | | `approval` | `out: T` | human gate with Argo-suspend semantics: pauses until a decision; timeout without decision ⇒ `error`, **never auto-approve** | Control nodes are **type-preserving pass-throughs**: every derived out port carries the input port's type `T`. Plain fan-out and fan-in are **implicit in edges** — there are no AND-gateway nodes. An input port accepts **one inbound edge** by default; fan-in is either a `collect: true` port (ordered collection of same-type producers) or the XOR-merge exception (branches of the same condition/switch may share a target port, since at most one fires). ## Edge data types Every port carries a type from a closed **artifact lattice**. `artifact` is the root and doubles as "any": any subtype is accepted where `artifact` is expected. | Type | Meaning | | --- | --- | | `artifact` | root — compatible with everything | | `file` / `directory` | opaque blob, single vs multipart; format is metadata, not more types | | `table` | schema-carrying tabular data (a *shape*) | | `dataset` | a versioned data *product* — shards + manifest | | `model` | trained model / checkpoint | | `metrics` | evaluation metrics | | `samples` | DreamLake domain type — an addressable collection of media samples / episodes | Incompatible connections are flagged on the canvas before anything runs. Small inline values (strings, numbers, JSON config) are **node configuration**, not edge data — a type never exists on both sides of the parameter/artifact boundary. ## Run states During a run, each node carries a state — `idle` · `queued` · `progress` · `done` · `error` · `skipped` — tinting the same card. Agent instances fan out under their uda node: ## The canvas Stages are hubs the work flows through — members fan out from their stage node and converge into the next. Two orientations, one card style: --- Source: https://docs.dreamlake.ai/libraries # Asset Libraries A **library** is a collection of reusable assets — MuJoCo models, URDF robots, meshes, textures, or any files at all — stored exactly as you pushed them and searchable per asset. Find a model, preview it in 3D, download just its files, and drop them into your own scene. Libraries exist for the *"I need a mug for this scene"* problem: scene generation feeds on big open asset repos (Google Scanned Objects is 1,030 models across ~36,000 files; MuJoCo Menagerie is 88 robots), and what you want from them is never the repo — it's **one asset**, found by name or meaning, with all of its files and none of the rest. Your own models come first: a library is where your team's robots and props live; the curated mirrors of open repos are a convenience on top. Two ideas make everything below fall out naturally: 1. **Your directory is the format.** A library source is your files plus one *optional* `dreamlake.yml`. There is no manifest to write and no hash to compute — `push` discovers assets by convention and generates every piece of bookkeeping itself. 2. **Files are stored verbatim.** The original relative path *is* the storage key. Nothing is rewritten, rehashed into opaque blobs, or wrapped — so a pull is byte-identical and an incremental push only uploads what changed. Everything generated — the wire manifest, rendered thumbnails, search embeddings — lands in a reserved `.dreamlake/` area in platform storage, never in your directory. ## Browsing in the app Libraries share the **Environments** surface: `//envs` has an `Environments | Libraries` segment switch (`?tab=libraries`); there is no top-level Libraries nav entry, and `//libraries` redirects to that tab. A library card opens `//libraries/` — a filterable grid of its assets, thumbnails only, no live 3D. Clicking a card does not navigate: the asset opens as a resizable panel beside the grid at `?asset=`, so scroll position and filters survive. That URL is shareable and loads the panel directly. `/libraries` is the global search page: it lists every library you can see (public ones plus your own), and searches across whichever of them you select. Visibility works like envs: libraries are **private by default**, members see everything in their namespaces, everyone else sees `public` libraries only, and a private library can be opened through a `?share=` link. Hidden and missing look identical from outside (404) — private names are not probeable. ## Your directory is the format A pushable library is any directory. The conventions: ``` menagerie-lib/ unitree_go2/ ← one top-level directory = one asset, id "unitree_go2" scene.xml ← entry point, auto-detected (MJCF scene) go2.xml assets/… ← everything inside belongs to the asset aloha/ ← another asset turntable.glb ← a loose top-level file = a single-file asset dreamlake.yml ← optional curation metadata (below) ``` - **One top-level directory = one asset.** The directory name is the asset `id` (path-safe: `^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$`) — it is what search returns and what `pull --asset` downloads. Every file inside belongs to the asset. - **Loose top-level files become single-file assets.** - **Kind, format and entry are detected**, first match wins: `mjcf` (an `.xml` containing ` **Warning:** `mesh`, `splat` and `image` fire only when the asset contains **exactly > one** file of that family — two `.ply` files fall through to `file`. (The > families are counted separately, so a `thumbnail.png` beside one `.ply` > does not spoil the splat count.) The multi-file splat layouts are the > exception: SOG and SOG-LOD directories are recognized by convention, so > they need no `dreamlake.yml`. SOG is content-checked, not name-checked — a > `meta.json` without the plane files, or one that is not a splat > descriptor, stays a plain `file`. An asset is one *logical* thing — a model with its meshes, textures and collision geometry, or a single file. ### `dreamlake.yml` — optional curation Zero-config push works with no metadata at all. One optional `dreamlake.yml` at the root adds what the CLI cannot infer — every key is optional: ```yaml file="dreamlake.yml" library: # identity for the catalog card title: MuJoCo Menagerie description: High-quality MJCF robot models curated by Google DeepMind. provider: Google DeepMind homepage: https://github.com/google-deepmind/mujoco_menagerie license: Apache-2.0 # library-wide default, per-asset overridable tags: - robots - mujoco upstream: repo: https://github.com/google-deepmind/mujoco_menagerie commit: abc123… discover: # override the discovery conventions (globs, * and **) assets: - "*" # which top-level entries become assets (default) exclude: - "docs/**" # never assets, never uploaded defaults: # category / license / tags only; tags merge, the others fall back category: robot assets: # per-asset overrides, keyed by asset id unitree_go2: title: Unitree Go2 description: Quadruped robot with detailed collision geometry. category: quadruped tags: - quadruped - unitree license: BSD-3-Clause attribution: "© Unitree Robotics" # required credit line (CC-BY etc.) kind: mjcf # override detection (kind / format / entry) entry: unitree_go2/scene.xml # override the detected entry entryPoints: scene: kind: scene file: unitree_go2/scene.xml go2: kind: robot file: unitree_go2/go2.xml thumbnail: unitree_go2/go2.png # your own image instead of a render ``` Per-asset overrides also accept `files:` globs to reshape which files belong to an asset. Paths are relative to the library root. `dreamlake.yml` is a normal source file — it is pushed, pulled, and versioned with your tree. You never write hashes, sizes, or file inventories: that is the wire manifest's job, and the CLI generates it at push time (see [Under the hood](#under-the-hood)). ### Start from a known repo `dreamlake-py` ships importers that turn well-known repo layouts into a clean source directory — the asset files plus a generated `dreamlake.yml`, nothing else: ```bash # MuJoCo Menagerie — 88 robots, scene/robot entry points, per-model licenses python -m dreamlake.assets_tools.import_menagerie \ ~/Code/asset_library/mujoco_menagerie ./menagerie-lib \ --subset aloha,agility_cassie,unitree_go2 # Google Scanned Objects (mujoco_scanned_objects mirror) — 1,030 household objects python -m dreamlake.assets_tools.import_mujoco_scanned_objects \ ~/Code/asset_library/mujoco_scanned_objects ./gso-lib ``` Importers only prepare source. Thumbnails and embeddings are push-time steps now — the flags below. ## Push it ```bash dreamlake library push ./menagerie-lib --namespace my-team --library menagerie # ✓ my-team/menagerie revision 1 — 197 uploaded, 0 unchanged, 0 removed ``` That is the whole workflow: the CLI discovers the assets, detects kinds and entry points, applies `dreamlake.yml` if present, hashes your files (with a local cache, so re-pushes don't re-read an unchanged tree), builds the wire manifest, and uploads. In order: 1. It fetches the remote manifest and **diffs by sha256** — only new or changed files upload. The server mints presigned upload URLs in batches (≤ 1,000 files / 10 GiB per batch, looped); bytes go straight to storage, never through the API server. 2. It calls **register**: the server validates the uploaded manifest, bumps the library `revision` (a compare-and-swap counter — concurrent pushes lose cleanly with a 409 and retry), and updates the catalog row. 3. After register, the server **reconciles** storage: any file outside the new manifest is deleted server-side. Clients never hold delete permission — storage always equals exactly what the manifest declares. Re-push after deleting one model from a 1,030-model library and the plan reads: *2 to upload, 171 unchanged, 24 removed* — seconds, not a re-upload. Push treats your directory as the source of truth — which matters once a library is also edited remotely ([add / rm](#add-or-remove-one-asset)). If the remote has moved since this directory last registered **and** the plan would delete assets, push stops and lists exactly what it would delete: pull to merge, or re-run with `--force` to delete them deliberately. A push that only adds or updates files never triggers the guard. Flags that add work at push time: ```bash dreamlake library push ./menagerie-lib --thumbnails --embed ``` - `--thumbnails` renders 640 px WebP previews of your models (offscreen MuJoCo rendering with auto-framing). Renders land as **platform artifacts** under the reserved `.dreamlake/` area in storage — your directory stays untouched. Search cards and the library grid live on these — ship them. A re-push without the flag carries the existing thumbnails forward; an asset that already has a thumbnail (its own `thumbnail.*`/`preview.*` file, or a `thumbnail:` in `dreamlake.yml`) uses that instead of a render. - `--embed` computes the vectors that turn on [semantic search](#search-it) — image vectors of the thumbnails, text vectors of the metadata. It requires `--thumbnails` in the same push (the image vectors embed the fresh renders). Vectors are content-hash cached locally, so re-running after an edit only re-encodes what changed. - `--model ` picks the encoder `--embed` uses: `siglip2` (the default, and the only space DreamLake searches), `clip` (legacy), or a raw `open_clip` model name. Omit it and the Python tool decides, which is where `DREAMLAKE_EMBED_MODEL` is still read. Passing it without `--embed` is an error. - `--dry-run` prints the plan (uploads, unchanged, removals) and exits; `--verify` re-hashes every local file and re-downloads the remote manifest, bypassing both local caches. `--force` overrides the drift guard, and `--json` puts a machine-readable result on stdout. Both `--thumbnails` and `--embed` shell out to the `dreamlake` Python tooling — `pip install "dreamlake[compose]"` for rendering (MuJoCo), `pip install "dreamlake[embed]"` for vectors. Everything else is pure CLI, and a missing interpreter is a warning, not a failed push. > **Warning:** The renderer loads models in MuJoCo, so `mjcf` assets are the only ones it > can render today; `mesh`, `splat` and `image` assets are skipped and > counted in the push summary's `skipped`. Put a `thumbnail.png` (or > `preview.png`) at the asset root, or name one with `thumbnail:` in > `dreamlake.yml`. This matters beyond the grid: `--embed` builds its image > vectors from thumbnails, so an asset without one contributes text vectors > only. > **Warning:** Search always sees the latest revision, and pulls fetch the current files. > When an asset matters to a scene, pull it and vendor it into the scene — > the platform keeps libraries current, not archival. ## Search it Three scopes, one behavior: ```bash # inside one library curl "$API/namespaces/my-team/libraries/menagerie/search?q=cassie&category=biped" # across chosen libraries (the /libraries page and the CLI use this) curl "$API/library-search?q=gripper&libraries=my-team/menagerie,acme/gso&kind=mjcf" # from the terminal — scope defaults to every library you can see dreamlake library list --all dreamlake library search "coffee mug" --library fortyfive/scanned-objects ``` Keyword search scores `title`, `tags`, `id` and `description` with filters on `category` / `kind` / `license` / `tag`. Empty query = browse mode (filters still apply). With no `--library`, the CLI searches the 50 most recently updated libraries you can see; the API caps an explicit list at 50 too. Two public libraries to try: `fortyfive/menagerie` (68 MJCF robots) and `fortyfive/scanned-objects` (129 household objects). **Semantic search** turns on per library when you push with `--embed`: ```bash dreamlake library push ./menagerie-lib --thumbnails --embed # vectors: .dreamlake/vectors.json + .dreamlake/vectors.f32 (open_clip/ViT-B-16-SigLIP2/webli, 768-d) ``` The embeddings are SigLIP2 image vectors of the thumbnails and text vectors of the metadata (768-d), stored as a platform artifact alongside the manifest. At query time the server embeds your query text in-process — an fp16 ONNX SigLIP2 text tower ships inside the server, no external service — then fuses vector similarity with the keyword ranking and marks the response `semantic: true`. Because SigLIP2 is multilingual, non-English queries work: keyword tokenization is ASCII-only, so a Chinese query carries no keyword tokens and ranks by vector alone. Semantic ranking is used only when the library's sidecar was produced by the **same model** as the active query encoder — `open_clip/ViT-B-16-SigLIP2/webli`. The sidecar records its model id and the server compares it; a mismatch falls back to keyword-only rather than scoring vectors from a different space. Both SigLIP2 and the retired CLIP encoder are 768-d, so dimensionality cannot catch this — only the model id can. You do not normally have to think about it: `dreamlake` ≥ 0.25.0 embeds with SigLIP2 by default, the push prints the encoder it actually used (the line above), and a sidecar outside the query space warns loudly while still completing the push. When you do want to be explicit, `--model siglip2` pins it. `--model clip` is the legacy escape hatch — useful if you embed for something other than DreamLake, and keyword-only here by design. That makes `semantic` a three-way answer, on a library row and in `library info`: `true` (vectors present and compatible), `false` (no sidecar, or one the active encoder will not use), `null` (registered before the flag existed). Searching never fails because embeddings are missing or mismatched — it quietly degrades, so check the flag rather than inferring it from result quality. Search hits return `namespace/library/assetId` plus a presigned thumbnail, the effective `license`, and two pull-cost signals: `files` (`{count, bytes}`) and `digest`, a `sha256:…` over the asset's sorted path/hash lines. The digest identifies the exact file set — equal digests mean a pull would transfer nothing — so a hit alone is enough to decide whether to download. In the terminal each hit is one grep-friendly line — `ns/lib/asset score kind license — description` — closed by a `12 of 40 hits · semantic on` footer; `--license` and `--offset` complete the filter set, and `--json` returns the raw response: ```bash dreamlake library pull acme/gso --asset Cole_Hardware_Mug_Classic_Blue -o ./scene/assets ``` ## Check before you download Two commands answer questions a download shouldn't be the way to ask: ```bash dreamlake library info acme/gso # the library's shape dreamlake library info acme/gso --asset Cole_… # one asset's decision card dreamlake library stat acme/gso --asset Cole_… -o ./scene/assets ``` `info` reads the catalog row and the manifest — nothing else — and prints either the library summary (asset and file counts, total size, kind / category / license counts, whether semantic search is on) or one asset's card: entry points, file count and bytes, effective license, `meta`, and a content `digest` that identifies the asset's exact file set. Both commands keep the downloaded manifest in a local cache keyed by the library's `revision`, so repeated checks against an unchanged library cost one small catalog read and no manifest download — which is what makes them practical against a 60 MB manifest. `--verify` re-downloads. `stat` compares that manifest against a local directory and answers with exactly one of `up-to-date` (exit 0), `stale: 2 files changed, 1 missing — pull would fetch 812 KB`, or `absent: full pull = 34 files, 4.8 MB` (both exit 1) — without transferring a byte. The exit code makes it scriptable: ```bash dreamlake library stat acme/gso --asset $ID -o ./scene/assets \ || dreamlake library pull acme/gso --asset $ID -o ./scene/assets ``` ## Preview it The asset panel mounts a viewer chosen by `kind`: | `kind` | Viewer | |---|---| | `mjcf` | The interactive MuJoCo viewer (same engine as [Envs](/envs.md)): physics, actuator sliders, alt-drag forces. | | `urdf` | The URDF poser with joint gizmos. | | `mesh` | GLB/GLTF, OBJ, STL and mesh PLY. | | `splat` | 3D Gaussian splats — see below. | | anything else | A file listing with per-file downloads. | Entry points render as a switcher — flip between `scene.xml` and the bare robot without leaving the panel. Files load through short-lived presigned URLs and are cached per content hash, so assets sharing files download them once. ### 3D Gaussian splats Splat assets render through [Spark](https://sparkjs.dev). Supported: `.ply` (including PlayCanvas `.compressed.ply`), `.splat`, `.ksplat`, `.spz`, plus two multi-file layouts — **SOG** (a `meta.json` with sibling WebP planes) and **SOG-LOD** (a `lod-meta.json` naming per-level nodes). The viewer picks the flavor from the wire `format` when it is `sog` or `lod`, otherwise from the entry file's extension, otherwise from the entry filename (`meta.json` → SOG, `lod-meta.json` → LOD). Push a SuperSplat export as-is and all three are filled in for you: the CLI detects the directory layout, sets `kind: splat` with `format: sog` or `lod`, and points `entry` at the meta file. `library add` does the same for a single asset. Override it in `dreamlake.yml` only when your layout is unusual enough that detection misses it: ```yaml file="dreamlake.yml" assets: courtyard: kind: splat format: sog # or: lod entry: courtyard/meta.json # or: courtyard/lod-meta.json ``` LOD assets render the **coarsest level only**, as a static preview — enough to recognize a scene without pulling a multi-hundred-megabyte tree. There is no distance-based level switching. ### Coordinate conventions 3DGS trainers emit the COLMAP/OpenCV camera frame — **Y-down**, Z-forward. three.js is **Y-up** right-handed. The viewer therefore applies exactly one π rotation about X to every splat mesh; Spark itself imposes no convention, and nothing else in the pipeline rotates the data. The practical consequence: assets exported by SuperSplat (`.compressed.ply`, SOG, SOG-LOD) are Y-down and render correctly. Polycam's raw `.ply` exports are **already gravity-aligned to Y-up** at export, so the flip double-corrects them and they appear upside-down. Rotate such an export before pushing it; a provenance-based exemption is not implemented. ## Pull it back ```bash dreamlake library pull my-team/menagerie -o ./menagerie-copy # clean source tree dreamlake library pull my-team/menagerie --asset unitree_go2 # one asset's files dreamlake library pull my-team/menagerie --all -o ./mirror # source + platform artifacts ``` Pulls verify every file against its manifest sha256 and materialize original relative paths. The default pull returns exactly the **clean source directory** — platform artifacts under `.dreamlake/` (and the generated files of legacy pushes) are skipped — so a full pull followed by `diff -r` against your source directory is byte-identical, `dreamlake.yml` included. `--all` adds the platform artifacts (wire manifest, rendered thumbnails, vector sidecars) for mirroring or debugging. Pulls are also incremental: files already on disk with the right sha256 are skipped, and pulling into a non-empty directory syncs it — only stale or missing manifest files are written, extra local files are never touched. Re-running a pull you already have costs one manifest read and zero bytes. ## Add or remove one asset You don't need the whole library on disk to change one thing in it: ```bash dreamlake library add fortyfive/props ./my-mug --title "Blue mug" --category household # ✓ fortyfive/props revision 8 — asset my-mug added (34 files, 4.8 MB) dreamlake library rm fortyfive/props my-mug old-chair # ✓ fortyfive/props revision 9 — 2 assets removed, 41 files reclaimed ``` `add` runs the same discovery as push on one directory (or one loose file), takes its metadata from flags (`--id`, `--title`, `--description`, `--category`, `--tags`, `--license`, `--attribution`), and appends the asset to the remote manifest. An existing id is an error unless `--replace` — which is how you update one asset in place. Files the library already holds (shared meshes, unchanged bytes under `--replace`) are not re-uploaded. `rm` never transfers a byte: it removes the manifest entries, and the server reclaims every file no remaining asset references. It refuses to empty a library — delete the library itself instead. Both commands converge under concurrency through the same revision compare-and-swap as push, and both are what the [push drift guard](#push-it) protects: a wholesale push from a stale directory will stop rather than silently undo a remote `add`. ## Under the hood ``` libraries/// files/ ← your files, verbatim (dreamlake.yml included) files/.dreamlake/manifest.json ← wire manifest — generated, machine-owned files/.dreamlake/thumbnails/.webp ← rendered by push --thumbnails files/.dreamlake/vectors.{json,f32} ← embedding sidecar from push --embed ``` One prefix per library in platform storage. `.dreamlake/` is a **reserved dot-directory**: only platform-generated artifacts live there, user asset paths may never enter it, and generated paths may never leave it — that is what keeps your files tree clean. ### The wire manifest — generated, you never edit this `.dreamlake/manifest.json` (schema `dreamlake.assets/v1`) is the API contract between the CLI and the server: the single source of truth every catalog row, search result, and preview derives from. The CLI regenerates it wholesale on every push — treat it the way you treat a lockfile you didn't write. If you integrate over HTTP instead of through the CLI, this is the document you produce and register. | Field | Meaning | |---|---| | `schema` | Always `dreamlake.assets/v1`. | | `library` | Catalog identity: `name`, `type` (default `3d`), `title`, `description`, `provider`, `homepage`, `license`, `tags`, `upstream`. | | `assets[].id` | Unique within the library, path-safe. | | `assets[].kind` / `format` | Viewer dispatch only. Any token matching `^[a-z0-9][a-z0-9._-]{0,31}$`; `kind` defaults to `file`. `format` is a free sub-type — detection sets it for the splat container layouts (`sog`, `lod`), `dreamlake.yml` can set any value. | | `assets[].entry` / `entryPoints` | The file(s) a viewer opens; must be listed in the asset's `files`. | | `assets[].files` | `{path, size, sha256}` objects, paths relative to the library root. sha256 drives the incremental push diff and pull verification. | | `assets[].thumbnail` | One of the asset's own files, or a platform artifact listed in `generated[]`. | | `assets[].meta` | Free-form JSON (`dof`, `triCount`, mass…), ≤ 8 KB. | | `generated[]` | Platform artifacts the push uploaded (thumbnails, vectors) — every path lives under `.dreamlake/`. **Declaring an artifact here is what makes it exist**: the reconcile keeps only what the manifest lists, and the vectors sidecar is read for search only when both halves are declared. | Limits: ≤ 50,000 assets, ≤ 1,000,000 file entries, ≤ 60,000 `generated[]` entries, manifest ≤ 100 MB. For scale, a production library of 14,355 Gaussian-splatting scenes produces a manifest of roughly 60 MB. Assets may share files (a common mesh pool is fine); the same path with two different hashes is rejected. > **Warning:** Only relevant if you integrate over raw HTTP; the CLI does this for you. > If you upload `.dreamlake/vectors.json` and `.dreamlake/vectors.f32`, list > **both** in `generated[]`. An undeclared sidecar does not exist: register > reports `semantic: false`, search stays keyword-only, and the > post-register reconcile — which keeps storage equal to the manifest — > deletes the files. One rule, three places, no partial state. Libraries pushed under the old model (a hand- or importer-generated `assets.json` at the files root) keep working: the server reads the legacy location as a fallback, and the first push from the current CLI migrates them to `.dreamlake/manifest.json`. The catalog row (Mongo) holds identity, visibility, the revision counter, and cheap mirrors for filter chips (categories / kinds / licenses / counts). **Per-asset data lives only in the manifest** — search runs server-side over a revision-keyed in-memory cache of it, so a register invalidates everything atomically and nothing can drift. | Call | Purpose | |---|---| | `POST …/libraries/:name/upload-authorizations` | Presigned upload batch (members) | | `POST …/libraries/:name/register` | Validate manifest, bump revision, reconcile files | | `GET …/libraries/:name` / `…/manifest` | Catalog row / full parsed manifest | | `GET …/libraries/:name/assets/:assetId` | One asset + presigned file URLs (the preview payload) | | `POST …/libraries/:name/files-presign` | Presigned GETs for manifest paths (the pull payload) | | `GET …/libraries/:name/search` | Search inside one library | | `GET /libraries` · `GET /library-search` | Visible-library list · cross-library search | Request/response schemas for every call, the full `dreamlake.assets/v1` field contract, and the composed read patterns are in [Libraries Reference](/libraries/reference.md). Soft delete hides a library (restorable); **purge** permanently deletes the row and every stored object. ## Next steps - [Libraries Reference](/libraries/reference.md) — the machine contract: endpoint schemas, the wire manifest, and how an agent composes them. - [Envs](/envs.md) — single runnable environments; a library is where an env's ingredients come from. - [CLI](/cli.md) — install and authenticate `dreamlake`. - [Search](/search.md) — platform-wide search surfaces. --- Source: https://docs.dreamlake.ai/workflows/agent-permissions # Agent Permissions Every user-defined agent (uda node) declares what it may touch. Grants are flat, auditable strings in the IAM style — domain.resource.verb — validated when a workflow version is saved. > **Status**: permission grants are **defined, stored, and validated for > well-formedness**. Runtime enforcement ships with the execution engine. > **Note:** Grants govern **resources** — datasets, nodes, queues. They do not govern > **tools and files**; that is Claude's `allow` / `ask` / `deny` rule syntax, > documented on [Agents](/lakeshore/agents.md#permissions--what-it-may-touch). A > `uda` node and a declared agent both carry a `tools` list and a permission > block, and in both cases the two lists are validated independently. ## Grant grammar ``` grant := domain "." resource "." verb (":" scope)? domain := lowercase identifier e.g. dreamlake, lakeshore resource:= lowercase identifier e.g. datasets, providers, queues verb := read | create | update | delete | scope := resource qualifier e.g. @acme/robotics/grasp-v2, gpu-a10g ``` The format follows Google Cloud IAM (`service.resource.verb`, as in `storage.objects.get`). The verb set follows the Kubernetes/IAM CRUD convention, with `get`/`list` collapsed to **`read`** (we do not enforce them differently). Domain actions that are not CRUD — like publishing a dataset version — are **registered custom verbs on a specific resource** (the way Kubernetes registers `impersonate` or `bind`), never a global verb. ## Registry | Grant | Meaning | | --- | --- | | `dreamlake.datasets.read` | read datasets and dataset versions | | `dreamlake.datasets.create` | create datasets / write new data | | `dreamlake.datasets.update` | modify dataset metadata / annotations | | `dreamlake.datasets.delete` | soft-delete datasets | | `dreamlake.datasets.release` | **custom verb** — publish an immutable dataset version | | `dreamlake.nodes.read` / `.create` / `.update` / `.delete` | the Node tree: episodes, folders, files | | `dreamlake.artifacts.read` / `.create` / `.update` / `.delete` | renderable artifacts | | `dreamlake.providers.read` / `.create` / `.update` / `.delete` | provider administration | | `dreamlake.workflows.read` / `.create` / `.update` / `.delete` | workflow definitions and versions | | `dreamlake.workflows.run` | **custom verb** — launch a workflow run | | `lakeshore.queues.submit:` | **custom verb, scope required** — submit work to a queue | | `lakeshore.queues.consume:` | **custom verb, scope required** — consume work from a queue | Unknown domains, unknown resources under a known domain, and unknown verbs are validation **errors** — the registry is closed, and grows by registration, not convention drift. ## Scoped grants A `:scope` suffix narrows a grant to a resource path: ``` dreamlake.datasets.read:@acme/robotics/grasp-v2 lakeshore.queues.submit:gpu-a10g ``` Unscoped grants apply namespace-wide. Queue verbs always require a scope. ## Tools are not permissions Tool access is declared in the uda node's own **`tools`** field (`tools: ["Read", "Bash"]`), following Claude's agent spec — the same field name, the same semantics, and the same "omit to inherit everything" default. Permission strings govern **data and resources**; the tools list governs **capabilities**. The two lists are validated independently. Which *files* a tool may touch, and which *commands* Bash may run, is a third thing again — Claude's `Read(…)` / `Edit(…)` / `Bash(…:*)` rule syntax. A `uda` node does not carry one today; a declared agent does. See [Agents → Rule syntax](/lakeshore/agents.md#rule-syntax). ```json { "kind": "uda", "title": "vlm_annotator", "uda": { "instructions": "Label task, sub-task and active hand for each clip", "model": "qwen-vl-72b", "tools": ["Read"], "permissions": [ "dreamlake.datasets.read", "dreamlake.datasets.create", "lakeshore.queues.submit:gpu-a10g" ], "queue": "gpu-a10g" } } ``` ## Why not `edit`, `remove`, or `ToolUse.*`? - **`edit` → `update`, `remove` → `delete`**: no major permission system (IAM, Kubernetes RBAC, GitHub fine-grained) uses `edit` or `remove` as verbs; `update`/`delete` map 1:1 to REST methods and to both IAM and RBAC. - **`ToolUse.Bash` → `tools: ["Bash"]`**: tool grants as permission strings would duplicate a concept every agent runtime already models as a first-class field, and would leave the runtime with two sources of truth for the same capability. --- Source: https://docs.dreamlake.ai/scene-generation/quickstart # Scene Generation Quickstart Install one agent skill, then ask for scenes in plain language — *"build a kitchen counter with a mug and a cocoa tin"* — and get back a rendered, physically validated, versioned [env](/envs.md) you can keep editing with more prompts. This page is the install-and-use guide; the full method lives in the [Scene Generation](/scene-generation.md) technical guide. ## What the skill gives you `dreamlake-scene-generation` teaches a coding agent (Claude Code, or any Skills-compatible client) the complete scene workflow: - **Source assets from anywhere** — download models from the internet (the [MuJoCo Menagerie](https://github.com/google-deepmind/mujoco_menagerie) is the curated first stop for robots), use files you already have, author props procedurally as plain MJCF, or pull from a [DreamLake asset library](/libraries.md) when one is available. The library is a convenience — hash-verified pulls and versioned reuse — not a required first step. - **Measure instead of guessing** — bundled inspection tools read each model's real dimensions and bounding boxes, so objects rest *on* tables instead of floating above them or launching out of them. - **Validate the physics** — a short simulation checks penetration, support forces, and that props stay where they were placed, with explicit per-body assertions rather than a vague "looks stable". - **Render and publish** — camera renders for your eyes, then `dreamlake env push` publishes a versioned env with an interactive [browser viewer page](/envs.md), private by default. - **Edit and reuse with more prompts** — a published scene stays editable: the agent pulls it back, changes what you asked for, and pushes a new version; existing scenes can seed new ones. These scenes were produced by exactly this loop, from ordinary prompts, during the skill's evaluation runs: ![A warm kitchen prep counter with a butcher-block top, a cutting board, a red patterned mug, and a cocoa tin under a key light.](/images/scene-generation/kitchen-hero.png) ![A UR5e robot arm work cell: the arm mounted on a workbench with small parts placed on the table surface.](/images/scene-generation/workcell-hero.png) ![A pottery display: bowls, a jug, cups, and a plate resting on a two-tier work table, with a shelf of glazed cups behind.](/images/scene-generation/pottery-hero.png) ## Prerequisites - **`dreamlake` CLI ≥ 0.35**, logged in (`dreamlake login`). - **Python** — how much depends on the route the agent takes: - **Raw MJCF scenes** need only the skill's inspection/validation tools' dependencies: `mujoco` (≥ 3.2) and `numpy`. The layer composer is **optional** — a scene authored as one self-contained MJCF directory is pushed directly, no `dreamlake[compose]` install involved. - **Layer-composed scenes** (`dreamlake env compose`) additionally need the composer: dreamlake-py ≥ 0.23 with **MuJoCo 3.14**. Details in [Scene Generation § Requirements](/scene-generation.md#requirements). - **An agent that reads Skills** — Claude Code is the reference client. A plain `pip install "dreamlake[compose]"` does **not** guarantee MuJoCo 3.14 — dreamlake 0.23.0 declares `mujoco>=3.2` (0.23.1 raises the floor to `>=3.8`), and composition fails below 3.8 either way. Use a fresh venv with pinned versions (the validated combination: Python 3.11, dreamlake-py 0.23.x, MuJoCo 3.14.0, NumPy 2.x), and verify what you got: ```bash python3 -m venv ~/.venvs/dreamlake-scenes source ~/.venvs/dreamlake-scenes/bin/activate pip install "dreamlake[compose]==0.23.1" "mujoco==3.14.0" python -c "import dreamlake, mujoco; print(dreamlake.__version__, mujoco.__version__)" # expect: 0.23.1 3.14.0 dreamlake --version # ≥ 0.35 # the CLI's compose step finds this venv through DREAMLAKE_PYTHON # (there is no --python flag): export DREAMLAKE_PYTHON="$(command -v python)" ``` Raw-MJCF-only setups can install just `pip install "mujoco==3.14.0" numpy` into the venv instead. ## Install the skill The skill is distributed through the public [dreamlake-skills](https://github.com/dreamlake-ai/dreamlake-skills) repository. Each top-level directory there is one complete, self-contained skill folder — `SKILL.md`, action guides, the runnable `tools/`, and the generated reference pages. Install the **whole folder**, and **symlink rather than copy**, so a `git pull` in the checkout updates the installed skill: ```bash git clone https://github.com/dreamlake-ai/dreamlake-skills.git ~/dreamlake-skills # user-scoped — available in all your projects mkdir -p ~/.claude/skills ln -s ~/dreamlake-skills/dreamlake-scene-generation ~/.claude/skills/ # or project-scoped — only inside one repository mkdir -p .claude/skills ln -s ~/dreamlake-skills/dreamlake-scene-generation .claude/skills/ ``` If your clone predates this skill, `git -C ~/dreamlake-skills pull --ff-only` first; a checkout that already carries `dreamlake-scene-generation/` works as-is. Keep the trailing slash on the destination: `ln -s ~/.claude/skills/` refuses to clobber an existing entry of the same name. If `ln` reports `File exists`, inspect the existing skill and preserve local edits before replacing it. **Updating safely.** Because the install is a symlink, updates are just `git -C pull --ff-only`. If you have customized the skill, commit your edits on a local branch first and `git pull --rebase` (or merge) so an update can never silently overwrite them — never edit an uncommitted working tree you also pull into. The agent discovers the skill from its frontmatter and picks it up when a scene request matches — no registration step. ## Use it — ordinary prompts No special syntax. Describe the scene, name the outcome you want: **Create:** > Build a breakfast table scene — a wooden table with two mugs and a bowl > resting on it, warm lighting, a nice camera angle — and publish it to my > namespace as `breakfast-table`. **Edit** (a published scene stays editable — same name, new version): > In `breakfast-table`, move the blue mug 10 cm to the left, dim the key > light a little, and push the result. **Reuse** (an existing scene can seed a new one): > Start from `breakfast-table` and add a robot arm from the MuJoCo Menagerie > on a stand next to the table. Publish it as `breakfast-robot`. Behind each prompt the agent runs the loop from the [technical guide](/scene-generation.md): source and verify assets, measure, derive placements, compose, validate physics, render, push, and report the version it published. ## What a finished run leaves you Ask the agent to keep the **reusable deliverables** with your project, and treat everything else as disposable scratch: **Keep — this is what makes the scene editable and auditable later:** - **The editable source** — the layer stack (`dreamlake.layers.json`) for composed scenes, or the authored MJCF directory for raw scenes. This is what edits are made to; the pushed env embeds a pinned copy. - **Per-model provenance** — for every third-party model: the exact source URL, version or commit, and its license (Menagerie models each carry their own `LICENSE` file — record each one, not one blanket note). - **Component envs** — base room and props published as their own pinned versions, when the composed route was used. - **Initial state** — prop poses live in the stack (`Attach` `pos`/`quat`); an articulated opening pose is a keyframe block kept in a file beside the stack and re-applied after every recompose. - **The published env itself** — the compiled entry XML plus all assets, at an exact version from the push receipt. - **Renders and the validation report** — the images you approved and the physics evidence behind the version. **Disposable** — scratch pull directories, intermediate compose attempts, validation JSON from rejected layouts, probe scenes: safe to delete once the version is published and verified. ## Preview in the browser Every pushed env gets an interactive viewer page — open the link the push prints, look at the framing and materials, and play the simulation ([Envs guide](/envs.md)). ### Generate from the env page Scenes can also be created and edited *in the browser*, from a chat panel next to the 3D viewer. This flow ships with the web app: it needs an app deployment that includes env chat, so if your env page does not offer the chat panel yet, that deployment hasn't reached your server — prompt your local agent and use the browser to inspect the result instead. How it works: 1. Open your namespace's **Envs** catalog (`//envs`) and click **+ new env** (members only) — it sits in the page header next to the Environments/Libraries switch, and an empty catalog offers the same button in its placeholder. 2. One click opens the env page as a *draft* under an auto-minted name (`env_xxxxxx`) — there is no name dialog, and nothing is created on the server yet. 3. **Describe the scene** in the intro composer the draft opens into ("a breakfast table with two mugs and a bowl…"). The prompt goes to the embedded agent, which runs the same skill workflow — source, measure, compose, validate — and publishes with `dreamlake env push`. The chat opens in **Auto** permission mode so this runs end-to-end without per-command approval; switching the composer to Ask/Plan makes it a read-only conversation (no generation, no push) until you switch back. 4. When the first push succeeds, the page swaps the draft placeholder for the **saved preview**: the interactive viewer on the version that was just pushed, with the chat still open beside it. 5. **Keep prompting to edit** — each successful push adds a version. The viewer follows the latest version; picking an older version from the header dropdown pins it (a "vN available" button jumps back to latest). 6. **Edit an existing env** the same way: open its env page (as a member) and use the chat panel beside the viewer. 7. **Reuse starts from the catalog, not from the source env's page.** The chat edits the env whose page it sits on — a "start from `breakfast-table` and add…" prompt *on `breakfast-table`'s own page* pushes new versions of `breakfast-table` itself. To grow a new scene out of it, go back to the Envs catalog, click **+ new env**, and prompt in the fresh draft: "Start from `breakfast-table` v3 and add a robot arm…". The new env references the source at that pinned version; `breakfast-table` keeps its own versions, untouched. ## Next steps Sourcing, measuring, placement math, validation, publish/iterate. Push, version, drive, thumbnails, and pull round-trips. The full v3 stack grammar and compose semantics. --- Source: https://docs.dreamlake.ai/scene-generation # Scene Generation How to go from *"I want a breakfast table with two mugs"* to a pushed, previewable, **physically valid** MuJoCo env: source assets from wherever serves the request — the internet, files you already have, procedural MJCF, or a [library](/libraries.md) — measure them, derive placements instead of guessing, compose the scene, validate the physics, and publish it as a versioned [env](/envs.md) you can keep editing. New here? Start with the [Quickstart](/scene-generation/quickstart.md). The loop this guide teaches: 1. **Source** ingredients — from any origin: download internet models, take user-supplied files, author props procedurally, or search a DreamLake library — then verify whatever you got actually compiles and collides. 2. **Measure** — read each model's real dimensions and bounding boxes; never eyeball a mesh's size or where its bottom is. 3. **Design** the base scene — supporting fixtures, materials, lights, and a hero camera, all derived from the measurements. 4. **Compose** — either author one self-contained MJCF directory directly, or build a v3 layer stack that places components as `Attach` entries with stable keys and pinned versions (the modular, reusable route). 5. **Validate** — simulate briefly; check penetration, support, and that props rest where you put them. Shape/physics validity and visual quality are different questions — check both, separately. 6. **Publish and preview** — push, open the env page in a browser, iterate by editing the stack and re-pushing. ## Requirements - The `dreamlake` CLI, logged in (`dreamlake login`) — env and library commands are in CLI ≥ 0.35. - Python with `pip install "dreamlake[compose]"` for `dreamlake env compose` (installs the layer engine and `mujoco`; dreamlake-py ≥ 0.23). The engine needs **MuJoCo ≥ 3.8** — use 3.14, the version this workflow is validated with; dreamlake 0.23.0 still _declares_ `>=3.2.0` but composition fails on 3.2, and 0.23.1 declares the real `>=3.8` floor (see [layers § requirements](/envs/layers.md#requirements)). A plain unpinned install does not ensure 3.14 — pin it: `pip install "dreamlake[compose]==0.23.1" "mujoco==3.14.0"` in a fresh venv, and verify with `python -c "import dreamlake, mujoco; print(dreamlake.__version__, mujoco.__version__)"` (a step-by-step venv setup is in the [Quickstart § Prerequisites](/scene-generation/quickstart.md#prerequisites)). If the CLI should use a specific interpreter or venv, set `DREAMLAKE_PYTHON` to that python — there is no `--python` flag. - **When `DREAMLAKE_PYTHON` is set, run the scene tools with it too** — `"$DREAMLAKE_PYTHON" tools/scene_report.py …`. Hosted agent runtimes provide their Python this way on purpose: the shared venv is *not* on `PATH`, so an unqualified `python` there is a different interpreter without the tools' dependencies. The commands below write `python` for brevity. - The scene tools below need only `pip install mujoco numpy` (mujoco ≥ 3.2; the tools' test suite passes on 3.2.0, 3.8.1 and 3.14.0) and run on any MJCF file or pulled env directory — no DreamLake account or workspace checkout involved. The 3.2 floor is for these tools only — composition needs 3.8+ as above. ## Source assets that will actually work Assets can come from **any source** — pick whatever serves the request best, in any mix: - **The internet.** For robots, the curated first stop is the [MuJoCo Menagerie](https://github.com/google-deepmind/mujoco_menagerie) — ready-made, maintained MJCF models. Any other MJCF/OBJ/STL you can legitimately download works too. - **Files the user already has** — a repo checkout, an export from a scanning pipeline, meshes from a CAD tool. - **Procedural authoring** — writing MJCF primitives yourself (boxes, cylinders, capsules with materials) is a first-class source, not a fallback. Tables, shelves, walls and many props are *better* authored than downloaded: exact dimensions, clean collision, tiny files. - **A [DreamLake asset library](/libraries.md)** — an optional convenience when one is available: per-asset search, manifest-hash-verified pulls, versioned reuse. There is no required survey-the-libraries step, and an empty search result never means "stop" — switch source. Two distinctions to keep straight while sourcing: - **A visual reference is not a usable model.** Product photos, renders and images are excellent for choosing dimensions, materials and layout — but only a downloaded MJCF (or a mesh you wrap yourself, below) can actually enter the scene. Don't report an image search as having "found an asset". - **A model you saw is not a model you have.** Until the files are on disk, complete, and compiling, treat a candidate as a lead, not an ingredient. ### Record provenance per model For every third-party model, record at sourcing time — it is much harder to reconstruct later: - the **exact source URL** and the **version or commit** you took it from; - its **license, per model** — Menagerie is not one license: each model directory carries its own `LICENSE` file (BSD, Apache, others). Copy the license alongside the model and note it in the scene's provenance record. ### The acceptance checklist — any source Whatever the origin, an asset is usable when: - **The dependency closure is complete.** Every mesh, texture, and `` the XML references is present and resolves (check ``). A model that compiles only inside its original repo layout is not yet yours. - **It compiles** — load it with the tools below (`scene_report` compiles the model to measure it); a broken asset fails here, not mid-composition. - **Units and up-axis are right.** MuJoCo is meters and Z-up. Meshes exported in millimeters need `scale="0.001 0.001 0.001"` on the ``; measure after loading — a 1.8 mm-tall "chair" is a units bug, not a small chair. - **Collision geometry is present.** A visual-only mesh (every geom `contype="0" conaffinity="0"`) falls through the table no matter where you place it. - **Mass is plausible.** Check the tool report's mass column — a 0.2 g table or a 400 kg mug will simulate legally and behave absurdly. - **The right entry file.** Robot models often ship both a bare robot file and a `scene*.xml`; compose with the _robot_ file (the scene drags its own floor and lights into yours). - For layer composition specifically: **exactly one root body** under `` — `Attach` grafts one subtree. Scanned-object exports (one `` holding visual group-2 and collision group-3 mesh geoms) are ideal. The authoritative reference for every MJCF element and attribute is the [MuJoCo XML reference](https://mujoco.readthedocs.io/en/latest/XMLreference.html). ### Raw meshes: wrap OBJ/STL in MJCF yourself MuJoCo loads **OBJ and STL** meshes as `` assets — a bare mesh becomes a scene object by wrapping it in a body you author: the mesh as a visual geom, plus **simple collision primitives** (a box, cylinder or capsule approximating the shape) rather than trusting an arbitrary mesh's convex hull: ```xml ``` Give the visual geom `mass="0"` explicitly: `contype="0" conaffinity="0"` only disables collision — the geom still contributes density-derived mass and inertia to the body, silently doubling it on top of the collision primitive's explicit mass. Then measure the wrapped body with `scene_report` and fix the collision sizes against the printed mesh AABB (and confirm the reported body mass equals the collision mass you set). **GLB/FBX (and other formats MuJoCo does not read) are not directly loadable** — there is no magic conversion step; if you convert with an external tool, verify the result the same way as any downloaded mesh (compile, measure, check collision), and don't present a conversion pipeline you haven't run as if it works. ### Searching a DreamLake library When you do use a library: `dreamlake library list --all` prints every library you can see _with its description_ — pick where to search, then search there. ```bash dreamlake library list --all dreamlake library search "coffee mug" --library acme/props --kind mjcf ``` Read search results skeptically: - **A ranked hit is not a relevant hit.** Semantic search ranks by closeness, so even a query the library cannot serve can come back as a page of confident-looking rows (observed with nonsense queries against indexed libraries). Judge the titles, then inspect the top candidate before building on it. - **`--kind mjcf` filters out meshes, splats and glTF** — for scene composition you almost always want `mjcf` assets (they carry collision geometry and compile as-is). A `mesh` asset is still usable via the wrapping pattern above. - **No results has several causes**: a `--category`/`--tag`/`--kind` filter that nothing carries, a library that simply lacks matching assets, or one you cannot access. Drop the filters and requery; then try another library — or leave the library route entirely and download or author the asset instead. No-results is a reason to switch source, never to give up on the request. - If a library was pushed without embeddings, search silently runs keyword-only (`semantic: false` in the API response) — shorter, literal queries work better there. Inspect a candidate before committing to it — pull just that asset and measure it (tools below): ```bash dreamlake library pull acme/props --asset blue_mug -o ./assets python tools/scene_report.py ./assets/blue_mug --body model ``` Pulls verify every file against its manifest sha256; a partially transferred or corrupted asset fails the pull rather than landing silently broken. Pulls are incremental and never delete: pulling several assets into one directory just works, and re-running a pull you already have transfers nothing. ## Derive placements — never guess The single most common failure is placing an object by eye: it spawns intersecting the table (launches on load) or floating (drops and topples). The tools in the scene-generation skill (also usable standalone) print the numbers to derive placements from. Abridged real output for a scanned grocery-object mug (values rounded; keys are exactly what the tool emits): ```bash python tools/scene_report.py ./assets/blue_mug --body model --json # "pos": [0.0, 0.0, 0.0], ← body origin, world frame # "aabb_min": [-0.063, -0.047, -0.0], ← this mug's base sits AT its origin # "aabb_max": [0.063, 0.044, 0.135], ``` These bounds are **tight**: mesh geoms report the exact bounds of their compiled vertices (what you see rendered), primitives exact analytic support bounds. That matters — a conservative rotated-box bound looks harmless but pads the bottom by 1–2 cm on typical scanned meshes, and a placement derived from it _floats visibly_ at t=0. Never assume where the bottom is, in either direction: scanned objects often sit exactly at their origin (this one), centroid-origin assets have it well below. Both `pos` and the AABB are **world-frame at the current state**, so the placement rule is a _move_, not an absolute coordinate: ``` delta_z = (h + clearance) − aabb_min.z # clearance ≈ 1 mm new body z = pos.z + delta_z ``` For the mug above (origin at `z = 0`, base at `z = −0.0`) on a table whose top is at `z = 0.74`: `delta_z = 0.74 + 0.001 − (−0.0) = 0.741`, so the new origin z is `0.741`. Only when the measured origin sits at zero does the shortcut "new z = h − aabb_min.z + clearance" hold. The same delta logic applies per horizontal axis — keep `aabb` footprints clear of neighbors and inside the supporting surface. Real-world dimension anchors help the scene read believably: dining table top ≈ 0.74 m, kitchen counter ≈ 0.9 m, seat ≈ 0.45 m, door ≈ 2.0 m. ## Design the base scene The base layer is an ordinary env you author directly (see the MJCF patterns in the [Envs guide](/envs.md)). What separates an attractive scene from a gray void: - **Name everything you may later address**: layer `Update`/`Remove` ops and scene edits address elements _by MJCF name_ (`obj/mug`, `fixture/table`, `light:key`). Unnamed elements cannot be targeted, recolored, or removed. - **Lights**: one warm key light (spot or point, positioned like a window or lamp), one dim directional fill; soften the headlight via ``. A gradient `skybox` texture kills the black void. - **Cameras**: author a hero `` — the env page's cover and opening shot use it. Compute the aim, don't guess quaternions: MuJoCo cameras look along local `−z`, and `xyaxes` are the camera's +x and +y axes in the parent frame. From position **p** aiming at target **t**: `z = normalize(p − t)`, `x = normalize(up × z)`, `y = z × x`, then `xyaxes="x y"`. Add `` + `` so free-camera fallbacks frame the scene too. - **Materials**: checker floor textures give scale cues; wood/ceramic tones with modest `specular`/`shininess` read better than saturated primaries. This is visual quality — the physics validator will not judge it, and a physically perfect scene can still look wrong; always render and look. ## Compose the scene — raw MJCF or layers Two equally valid routes to a pushed scene: - **Raw MJCF**: author (or assemble) one self-contained scene directory — entry XML plus every mesh/texture it references — validate and render it with the tools, and `dreamlake env push` it directly. This is a complete, supported workflow, and the right one for a scene you authored as a whole. - **A layer stack**: publish components as their own envs and compose them. What the extra structure buys is **modular editing and reuse** — each prop is a pinned, versioned layer you can move with a one-line `Attach` edit, swap, or reuse in the next scene. Optional, not required. The rest of this section is the layered route. Full grammar and semantics: [Env Layers Reference](/envs/layers.md). The scene-generation specifics: - **Push reusable components as their own envs** (base room, each prop) so the stack can pin them (`you/room@1`). Assets pulled from a library are ingredients — pushing one as an env is what makes it a _versioned layer_. - **`Attach` keys are the instance identity** — `mug1`, `mug2` — and every name inside becomes `mug1:…`. Keep keys stable across edits: change an `Attach`'s `pos`, not its `key`. Renaming a key orphans every `Update` targeting the old prefix and re-identifies the instance in every diff. - The **`Attach` `pos`/`quat` are your persistent initial state** for free-jointed props: they live in the stack, survive recompose, and become the composed model's default pose (qpos0). - **`Attach.pos` is a frame translation, not an override.** The engine mounts the source subtree under a new frame and keeps the source root body's own `pos`, so the composed world position is `source root pos + Attach.pos` (identity rotation, `at: "world"`; verified with the published engine: source root at z = 0.5 attached with `pos` z = 1 lands at z = 1.5). For a source whose root sits at the origin — typical standalone assets — `Attach.pos` reads as the absolute mount pose; otherwise compute the delta as in [derive placements](#derive-placements--never-guess), i.e. `Attach z = (h + clearance) − source aabb_min.z`. With a rotation or a `body:`/`site:` mount, don't hand-derive: compose, then remeasure the composed artifact with `scene_report` and adjust. - Compose floats, artifacts pin: author `you/room`, and the stack embedded in the output is pinned `you/room@N` with the builder versions — that pinned copy is what makes the scene re-openable and editable later. - **Reuse a materialized scene**: a pushed composed env is an ordinary env — `Merge` it as the base of a new stack and add layers on top. It enters as one layer (its own stack is not re-walked). Always pass the stack and output directory explicitly — without `-o` the output directory is derived from the stack's `name` (else the stack directory's basename), so the next commands may point at nothing: ```bash dreamlake env compose ./dreamlake.layers.json -o ./composed python tools/scene_validate.py ./composed --settle mug1:model --support mug1:model python tools/scene_render.py ./composed -o ./shots --camera thumbnail ``` ## Initial state and keyframes What "the scene opens like this" means, precisely: - The scene tools open a model at keyframe 0 when it has one, else qpos0. The browser viewer's t=0 contract — and crucially, **which viewer versions actually honor keyframe 0** (the prior source baseline does not) — is in [Envs § Drive it](/envs.md#drive-it); don't assume the deployed viewer shows your keyframe until you've seen it there. - Env push/pull transfer files verbatim — a `` in your entry XML survives the round trip. - **Merge-only stacks keep keyframes** (they resurrect correctly remapped). **Any `Attach` strips all keyframes** — attaching changes the dof layout, which invalidates them, so the composed artifact has none. - Therefore, in an attach-bearing composition: put prop poses in `Attach` `pos`/`quat` (they persist in the stack). If you additionally need an _articulated_ opening pose — a robot holding a pose via `ctrl` — add a `` block to the **materialized entry XML** after compose and before push. This is a post-compose step on the artifact: recomposing regenerates the entry, so re-apply it after every recompose (keep the block in a file next to your stack). Validate the result with the scene tools — they read keyframe 0 — before pushing. ## Validate physics — and know what a pass means ```bash python tools/scene_validate.py ./composed \ --settle mug1:model --settle bowl:model=0.02 --support mug1:model ``` The validator loads keyframe 0 (else qpos0), holds the authored `ctrl` (`--passive` zeroes it instead), simulates a few seconds, and fails only on objective evidence: initial/peak/final penetration beyond tolerance, non-finite state, MuJoCo warnings, bodies falling below `--floor-z`, and — for bodies you _name explicitly_ — drift, residual speed/spin, net attitude change, and contact-force support (real newtons opposing gravity, not proximity). There is deliberately no scene-wide "stable" verdict: an uncontrolled robot arm is _supposed_ to move, so a universal claim would be noise for one scene and false comfort for another. Name the bodies that must hold still; everything else is reported for you to read. One subtlety a settle-and-support pass **cannot** certify: the authored initial pose. A prop placed 2 cm above the table falls, lands, and passes every end-of-run check — but the model still opens (and renders its cover) at the floating authored pose; simulation never writes back into the artifact. So validate _and_ look at t=0: render the initial state (`scene_render` draws it), and if a named body's settle row shows a straight drop (`start_pos` vs `end_pos`), fold that correction into the persistent pose — the `Attach.pos` in the stack (or your keyframe) — and recompose. The converse holds too: a good-looking t=0 screenshot is not physical validity — run the checks. ## Publish, preview in the browser, iterate ```bash dreamlake env push ./composed --name my-scene --title "My scene" # ✓ pushed /my-scene v3 — … ← save this exact version dreamlake env pull my-scene@3 -o ./readback # empty dir; hash-verified ``` Push privately by default (`--visibility public` is opt-in) and **save the version number from the receipt**. Pull that exact `name@N` into a fresh, empty directory — a bare `env pull ` fetches whatever is latest, which can race a concurrent push. Then compare what matters: the embedded `dreamlake.layers.json` pins, the entry XML (including any keyframe you appended), and a validation run on the readback. If a push fails mid-flight or times out, **check before retrying**: run `dreamlake env list` and compare against your last receipt to learn what actually landed. Retrying an _unchanged_ directory is normally safe — the CLI compares the entry and every file hash against the latest version and reuses or re-registers an identical version (even adopting an identical concurrent push) instead of minting a new one. But those are content checks, not a transaction guarantee: if the directory changed between attempts, or someone pushed different content in the meantime, the retry mints a legitimate new version — blob dedup alone says nothing about version rows. Retry at most 2–3 times, confirm the version on the final receipt, and report ambiguity rather than assuming. Open the printed link and actually look: hero framing, materials under the viewer's lighting, props resting where placed, play/pause behaves. On the staging deployment, CLIs ≤ 0.37.0 print the API host in that link — set `DREAMLAKE_WEB_URL=https://staging.dreamlake.ai` (an override those CLIs already honor) or upgrade to CLI ≥ 0.37.1, whose receipts target the web app directly. The env page renders `mujoco`-type envs in the interactive viewer; `urdf` gets the kinematic poser; other types list files only. The first member visit captures the gallery thumbnail from your `thumbnail` camera. Iterate by editing the **editable source**, and push the same env name for a new version. For a raw-MJCF scene that is the authored directory itself — edit the XML, re-validate, re-render, push. For a composed scene it is the **pinned stack**, not the materialized XML: pull the env (the embedded `dreamlake.layers.json` rides along), edit the op — move a mug's `Attach.pos`, retune a light `Update` — recompose, re-validate, re-render, push. One-line diff in the stack. To re-aim a camera through an `Update` on SDK 0.23.0, state a `quat` opinion — an `xyaxes` opinion is not accepted there (it _is_ fine in a layer's own MJCF, and SDK ≥ 0.23.1 accepts the alternate orientation specifiers in opinions too) — see [layers § orientations](/envs/layers.md#tag-update--the-meta-component). ## When it goes wrong | Symptom | Likely cause → fix | | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | search returns plausible-looking but wrong assets | semantic ranking can return close-but-wrong rows — inspect before use; add `--kind mjcf`; try another library, or source the asset elsewhere | | search returns nothing | drop `--category`/`--tag`/`--kind` filters and requery; then another library — or switch source: download it (e.g. Menagerie), or author it as MJCF | | downloaded model won't compile outside its repo | incomplete dependency closure — copy every mesh/texture/include it references and fix `meshdir`/`texturedir`; re-check with `scene_report` | | downloaded mesh is comically large or tiny | units mismatch — the export is mm (or cm); set `` and re-measure; MuJoCo is meters, Z-up | | `library pull` fails mid-way | reruns are safe and incremental (hash-verified); already-correct files are skipped, extra local files are never touched | | asset falls through the floor/table | no collision geometry (`contype/conaffinity` 0) or a scene-entry attach — check with `scene_report`, attach the robot/object file instead | | props explode or launch at load | initial penetration — recompute the placement delta from `pos.z` and `aabb_min.z`, keep ≥ 1 mm clearance; `scene_validate` reports the offending pair | | compose: "merge name collision" | two layers define the same name — rename in one, or make the change an `Update` (see [layers errors](/envs/layers.md#common-errors)) | | compose: "strict-match miss" | an op targets a pre-attach name — after `Attach key="mug1"`, address `mug1:body`, not `body` | | compose: engine missing | `pip install "dreamlake[compose]"`; point `DREAMLAKE_PYTHON` at that interpreter | | push rejected: unpinned local layers | push each local layer as its own env first (`--push-layers`), then pin and recompose | | upload/readback network errors | check `dreamlake env list` against your last receipt first (an ambiguous failure may have finalized a version), then retry at most 2–3 times | | scene passes validation but looks wrong | that is expected — the validator checks physics only; render and fix lights/cameras/materials | ## Next steps Discovery conventions, search scopes, manifests, pulls. The full v3 stack grammar, semantics, and compose errors. Push, version, drive, thumbnails, and pull round-trips. --- Source: https://docs.dreamlake.ai/cli # CLI Reference The complete `dreamlake` command surface. If you're new, start with the worked examples in Your First Episode — this page is the reference you come back to. > **Note:** The canonical, per-command reference lives at > [cli.dreamlake.ai](https://cli.dreamlake.ai) — it versions with each CLI > release (the version switcher pins any past release), covers the newer > command groups (agents, skills, sources, snapshots, lakeshore resources), > and documents both install channels: the native installer and > `npm install @dreamlake/dreamlake-cli`. The summary below covers the core > commands; for anything not listed here, go there. All commands use the `dreamlake` entry point. Add `--debug` before any command to target local dev servers instead of production. ## Auth ```bash dreamlake login --url # OAuth device auth flow (--no-browser for QR code) dreamlake logout # Remove stored credentials dreamlake profile # Show current user ``` ## Files ```bash dreamlake upload --episode --to # Upload file (type auto-detected) dreamlake upload --episode --to # Upload folder (--yes to skip prompt) dreamlake download --episode --from -o dreamlake list --episode # List assets (--type video to filter) ``` Upload flags: `--type `, `--bindr ` (comma-separated, auto-created). `` uses the [episode syntax](/index.md#episode-syntax) `[namespace@]project[:episode]`. ## Collections ```bash # Bindrs — curated file collections matched by glob dreamlake create bindr --project # --episode to match episodes dreamlake update bindr --project --add dreamlake delete bindr --project dreamlake list bindr --project # Datasets — groups of bindrs dreamlake create dataset --project dreamlake update dataset --project --add dreamlake delete dataset --project dreamlake list dataset --project # Episodes dreamlake list episode --project ``` ## Search ```bash dreamlake vectorize --episode # CLIP + LLaVA on video chunks dreamlake vectorize --bindr --project # Vectorize bindr scope dreamlake vectorize --dataset --project # Vectorize dataset scope ``` Add `--zaku-url ` for distributed processing. See [Semantic Search](/search.md) for the pipeline this feeds. ## Artifacts Renderable documents — HTML, React, Markdown, SVG, Mermaid, or code — rendered live in the dashboard at `//artifacts`. New here? The [Artifacts guide](/artifacts.md) is the two-minute picture-book version. Each artifact is its own versioned dataset; the DreamLake server keeps the catalog (title, kind, visibility) that powers the gallery and `list`. ```bash dreamlake artifact push # kind auto-detected from extension dreamlake artifact push --title --kind --id # --namespace to target another namespace dreamlake artifact push --visibility public # readable without login (default: private) dreamlake artifact push --share # mint a ?share= link (signed-in users only) dreamlake artifact list # list a namespace's artifacts (from the catalog) dreamlake artifact delete # soft delete → Trash (-y to skip the confirm) dreamlake artifact restore # bring a soft-deleted artifact back dreamlake artifact delete --permanent # purge storage + catalog — IRREVERSIBLE ``` Kinds: `html` · `react` (must define an `App` component) · `markdown` · `svg` · `mermaid` · `code`. Re-push with the same `--id` to append a new version — the viewer keeps every version. After a successful push the CLI prints an **open link** to the artifact, so you can click straight through to view what you just uploaded. **Visibility & sharing.** Artifacts are **private** by default (visible only to namespace members). `--visibility public` makes one readable by anyone; `--share` mints a `?share=` link that any **signed-in** DreamLake user can open (anonymous visitors are sent to log in first). Members can also toggle visibility and copy a share link from the artifact's page in the dashboard. **Delete, Trash & restore.** `delete` is a **soft delete**: the artifact moves to your gallery's **trash** tab (share links stop working immediately), and you can bring it back with `restore`, the Restore button in the Trash, or simply by re-pushing the same `--id`. `delete --permanent` (v0.4.14+) **permanently erases** the artifact — every version, its stored content, and its catalog entry. There is no undo; the dashboard's equivalent is **Delete forever** in the Trash. ### Add the artifacts skills (Claude) DreamLake publishes a [Claude](https://claude.ai/code) **skill** that teaches your agent to publish, version, and share artifacts for you. Add it from the public [`dreamlake-skills`](https://github.com/dreamlake-ai/dreamlake-skills) repo: ```bash git clone https://github.com/dreamlake-ai/dreamlake-skills.git ~/dreamlake-skills mkdir -p ~/.claude/skills ln -s ~/dreamlake-skills/dreamlake-artifacts ~/.claude/skills/ ln -s ~/dreamlake-skills/dreamlake-artifact-authoring ~/.claude/skills/ ``` Install both skills: one publishes, the other prepares content for the offline rendering frame. To update both, run `git -C ~/dreamlake-skills pull --ff-only`. Use a project's `.claude/skills/` directory for project scope. If a skill already exists, inspect and preserve local edits before replacing it with a symlink. Then ask Claude something like *"push this dashboard as a dreamlake artifact and make it public"* — it will invoke the skill and run the right `dreamlake artifact` commands. ## Envs Simulation environments — a directory holding one MJCF scene or one URDF robot plus its assets, viewed as a live, interactive 3D page at `//envs`. New here? The [Envs guide](/envs.md) is the picture-book version. Each env stores its files content-addressed, so re-pushing uploads only what changed, and `pull` returns a byte-identical directory (every file is hash-verified). ```bash dreamlake env push # entry auto-detected (MJCF, else *.urdf); prints an open link dreamlake env push --entry scene.mjcf # pick the entry when several qualify dreamlake env push --type isaaclab # simulator family (auto-detected: mujoco | urdf — both have viewers) dreamlake env push --visibility public # viewer opens without login (default: private) dreamlake env push --thumbnail cover.png # set the gallery cover (PNG ≤ 512 KiB) — see the Envs guide's Thumbnails section dreamlake env create # push that FAILS if the name already exists dreamlake env list # a namespace's envs (name, type, files, entry) dreamlake env pull # latest version → .//, hash-verified dreamlake env pull @2 -o --force # a specific version; --force writes into a non-empty dir dreamlake env delete # soft delete (restorable; -y to skip the confirm) dreamlake env restore # bring it back dreamlake env delete --permanent # purge storage + catalog — IRREVERSIBLE ``` Names are `namespace/env-name` with immutable integer versions — re-push the same name to append the next version, and the viewer keeps a version picker. Limits per version: ≤ 1000 files, ≤ 100 MiB per file, ≤ 1 GiB total; dot-files, `node_modules` and symlinks are skipped. > **Renamed:** before envs existed, `dreamlake env` switched login > environments. That command is now `dreamlake auth env list|use|remove` — see > [Environments](https://cli.dreamlake.ai/environments/). ## The CLI itself ```bash dreamlake --version # print the version dreamlake doctor # install + auto-update diagnostics dreamlake self-update # update now (background checks are automatic) dreamlake self-update # pin to an exact version (pauses auto-update) dreamlake self-update channel stable # switch release channel: latest | stable (unpins) dreamlake self-update --status # version, channel, pin, last check dreamlake install [latest|stable|] # re-install the native binary ``` Note the name: `update` edits bindrs and datasets, `self-update` updates the CLI. See the [install reference](/index.md#install-reference) for the install layout, release channels, and the variables that switch auto-update off. ## Legacy ```bash dreamlake video upload --user --project # Direct BSS upload (bypasses server) dreamlake video download --output # Download by BSS video ID dreamlake video list --user --project # List BSS videos ``` ## Next steps The REST endpoints behind every command on this page. Work with the uploaded video from Python — slicing, frames, tensors. --- Source: https://docs.dreamlake.ai/api # API Reference The REST surface behind the CLI and dashboard. Endpoints are grouped by resource; every path below is relative to your server's base URL. Base URL: `https://api.dreamlake.ai` in production, `http://localhost:3001` in local dev. All endpoints require `Authorization: Bearer ` unless noted — see [Architecture § Auth](/architecture.md#auth) for how tokens are issued. ## Auth | Method | Path | Description | |--------|------|-------------| | POST | `/auth/exchange` | Exchange vuer-auth token for dreamlake JWT (no auth required) | | GET | `/auth/me` | Current user profile | | PATCH | `/auth/me` | Update profile | ## Namespaces & Projects | Method | Path | Description | |--------|------|-------------| | GET | `/auth/namespaces` | List public namespaces (no auth) | | GET | `/namespaces/:slug` | Namespace overview with asset counts | | PATCH | `/namespaces/:slug` | Update namespace (owner only) | | GET | `/namespaces/:slug/projects` | List projects | | POST | `/namespaces/:slug/projects` | Create project | | PATCH | `/namespaces/:slug/projects/:proj` | Update project | For GraphQL project/Note reads and equivalent REST association and choice views, see [Project and Note queries](/api/project-queries.md). ## Episodes | Method | Path | Description | |--------|------|-------------| | POST | `/namespaces/:slug/projects/:proj/episodes` | Create/upsert episode | | GET | `/namespaces/:slug/projects/:proj/episodes` | List episodes (paginated) | | PATCH | `/namespaces/:slug/projects/:proj/episodes/:name` | Update episode | ## Nodes (File Tree) | Method | Path | Description | |--------|------|-------------| | POST | `/nodes` | Create node (auto-creates hierarchy) | | GET | `/nodes` | List nodes by namespace/project/kind | | GET | `/nodes/children` | List 1-level children | | GET | `/nodes/lookup` | Resolve node by materialized path | | GET | `/nodes/:id/descendants` | All descendants (recursive) | | GET | `/nodes/:id/contents` | Episodes + files (paginated) | | GET | `/nodes/:id/download` | Presigned S3 download URL | | PATCH | `/nodes/:id` | Update node (name, move, tags) | | DELETE | `/nodes/:id` | Hard-delete node + descendants | ## Files | Method | Path | Description | |--------|------|-------------| | POST | `/episodes/:id/files` | Upload file (multipart) | | GET | `/episodes/:id/files` | List files | | GET | `/episodes/:id/files/:fid/download` | Download file | | DELETE | `/episodes/:id/files/:fid` | Delete file | ## Tracks & Logs | Method | Path | Description | |--------|------|-------------| | POST | `/episodes/:id/tracks/:name/append` | Append data point | | POST | `/episodes/:id/tracks/:name/append-batch` | Append batch | | GET | `/episodes/:id/tracks/:name/data` | Read track data | | GET | `/episodes/:id/tracks` | List tracks | | POST | `/episodes/:id/logs` | Create log entries | | GET | `/episodes/:id/logs` | Query logs (level, time, search) | ## Parameters | Method | Path | Description | |--------|------|-------------| | POST | `/episodes/:id/parameters` | Set/merge parameters | | GET | `/episodes/:id/parameters` | Get parameters | | DELETE | `/episodes/:id/parameters` | Soft-delete | ## Bindrs & Datasets | Method | Path | Description | |--------|------|-------------| | POST | `…/bindrs` | Create bindr | | GET | `…/bindrs` | List bindrs | | GET | `…/bindrs/:name` | Get bindr | | PATCH | `…/bindrs/:name` | Update bindr | | DELETE | `…/bindrs/:name` | Soft-delete | | POST | `…/bindrs/:name/members` | Add members | | DELETE | `…/bindrs/:name/members` | Remove members | | POST | `…/datasets` | Create dataset | | GET | `…/datasets` | List datasets | | GET | `…/datasets/:name` | Get dataset with bindrs | | PATCH | `…/datasets/:name` | Update dataset | | DELETE | `…/datasets/:name` | Soft-delete | All bindr/dataset paths are under `/namespaces/:slug/projects/:proj/`. ## Artifacts Renderable, versioned documents with per-artifact visibility — the surface behind [`dreamlake artifact`](/cli.md#artifacts). Public/list/read routes apply per-artifact policy themselves (member / public / valid share). | Method | Path | Description | |--------|------|-------------| | GET | `/namespaces/:slug/artifacts` | Catalog list (members: all; others: public only). `?deleted=true` lists the Trash (members) | | GET | `/namespaces/:slug/artifacts/:id` | One catalog row + `viewerCanManage` (`?share=` for share links) | | POST | `/namespaces/:slug/artifacts/upload-credentials` | Mint scoped STS creds for a push (member) | | POST | `/namespaces/:slug/artifacts/:id` | Upsert catalog entry — metadata / visibility / share token (member) | | POST | `/namespaces/:slug/artifacts/read-token` | Mint a short-lived per-artifact read token | | GET | `/namespaces/:slug/artifacts/read/:token/*` | Read-proxy: stream artifact objects (Range-aware) | | DELETE | `/namespaces/:slug/artifacts/:id` | Soft delete → Trash (member) | | POST | `/namespaces/:slug/artifacts/:id/restore` | Restore from Trash (member) | | DELETE | `/namespaces/:slug/artifacts/:id/purge` | Permanent purge — storage + catalog, irreversible (member) | | GET | `/me/artifacts` | Signed-in user's artifacts: yours + shared-with-you, tagged by group | ## Envs Versioned simulation environments (MJCF + assets) — the surface behind [`dreamlake env`](/cli.md#envs). Env file bytes never transit the API: uploads go straight to storage with brokered STS credentials, and reads hand back one presigned URL per file. Public routes apply per-env policy themselves; a hidden env answers **404**, never 403. | Method | Path | Description | |--------|------|-------------| | GET | `/namespaces/:slug/envs` | Catalog list (members: all; others: public only). `?deleted=true` lists the Trash (members) | | GET | `/namespaces/:slug/envs/:name` | One catalog row + `viewerCanManage` + `thumbVersion` | | GET | `/namespaces/:slug/envs/:name/versions` | All pushed versions, newest first | | GET | `/namespaces/:slug/envs/:name/versions/:version` | One version's manifest — `entry` + per-file `{path, size, hash, url}` with presigned GETs (`:version` int or `latest`) | | POST | `/namespaces/:slug/envs/upload-credentials` | Mint STS creds scoped to `envs///` for a push (member) | | POST | `/namespaces/:slug/envs/:name` | Register a pushed version (create-or-revive, `latestVersion = max`), or patch title/description/envType/visibility (member) | | POST | `/namespaces/:slug/envs/:name/thumbnail` | Store the browser-captured PNG thumbnail, ≤ 512 KB (member) | | DELETE | `/namespaces/:slug/envs/:name` | Soft delete → Trash (member) | | POST | `/namespaces/:slug/envs/:name/restore` | Restore from Trash (member) | | DELETE | `/namespaces/:slug/envs/:name/purge` | Permanent purge — whole storage prefix + catalog, irreversible (member) | ## Libraries Asset collections stored verbatim under one storage prefix, searchable per asset — the surface behind [`dreamlake library`](/libraries.md). Payload bytes never transit the API: uploads and downloads run on presigned batches straight to storage. Public routes apply per-library policy themselves (member / `public` visibility / valid `?share=`); a hidden library answers **404**, never 403. | Method | Path | Description | |--------|------|-------------| | GET | `/namespaces/:slug/libraries` | Catalog list (members: all; others: public only). `?deleted=true` lists the Trash (members) | | GET | `/namespaces/:slug/libraries/:name` | One catalog row (`LibraryDetail`) + `viewerCanManage` | | GET | `/namespaces/:slug/libraries/:name/manifest` | The full parsed wire manifest (`dreamlake.assets/v1`), served verbatim | | GET | `/namespaces/:slug/libraries/:name/assets/:assetId` | One asset with presigned per-file URLs — the preview payload (`?ttlSeconds`) | | POST | `/namespaces/:slug/libraries/:name/files-presign` | Presigned GETs for manifest paths, ≤ 1,000 per batch — the pull payload (`ttlSeconds`) | | GET | `/namespaces/:slug/libraries/:name/search` | Search inside one library (`q`, `category`, `kind`, `license`, `tag`, `limit` ≤ 200, `offset`) | | GET | `/libraries` | Visible libraries across namespaces, newest first (`limit` ≤ 500) | | GET | `/library-search` | Cross-library search over `libraries=ns/a,ns/b` (≤ 50); inaccessible targets land in `skipped[]` | | POST | `/namespaces/:slug/libraries/:name/upload-authorizations` | Presigned upload batch for a push (member) | | POST | `/namespaces/:slug/libraries/:name/register` | Validate the uploaded manifest, bump `revision` (CAS — stale `expectedRevision` → 409), reconcile storage (member) | | POST | `/namespaces/:slug/libraries/:name` | Patch `visibility` / share token — content metadata is manifest-derived (member) | | DELETE | `/namespaces/:slug/libraries/:name` | Soft delete → Trash (member) | | POST | `/namespaces/:slug/libraries/:name/restore` | Restore from Trash (member) | | DELETE | `/namespaces/:slug/libraries/:name/purge` | Permanent purge — storage prefix + catalog, irreversible (member) | Full request/response schemas, the manifest contract, and agent read patterns live in [Libraries Reference](/libraries/reference.md). ## LLM Relay Pass-through to a chat-model API using your DreamLake token instead of a provider key — the surface behind [LLM Relay](/relay.md). The path under `/relay/:provider/` is forwarded to the provider verbatim, so its whole API is reachable. Authenticate with `Authorization: Bearer` **or** `x-api-key`. | Method | Path | Description | |--------|------|-------------| | GET | `/relay/providers` | Which providers this deployment can relay to, and the base URL for each | | POST | `/relay/:provider/v1/messages` | Relay a chat completion (`stream: true` streams SSE straight back) | | POST | `/relay/:provider/v1/messages/count_tokens` | Relay a token count | | GET | `/relay/:provider/v1/models` | Relay the provider's live model list | ## Search | Method | Path | Description | |--------|------|-------------| | POST | `/projects/:id/search` | Project-wide vector search | | POST | `/projects/:id/episodes/:eid/search` | Episode-scoped search | | POST | `…/assets/search` | Semantic search (CLIP text/image) | ## Health | Method | Path | Description | |--------|------|-------------| | GET | `/health` | Server status (no auth) | ## Next steps The command-line wrapper over these endpoints. The data model and auth flow behind the paths on this page. --- Source: https://docs.dreamlake.ai/annotations/reference # Annotations Reference The complete API of `dreamlake.annotation`: the generic `Annotation` for custom-schema data (with `Schema` and `Track`), the `VideoAnnotation` preset (`video.annotation/v2`) with its `Episode` handle, and the storage rules that explain the constraints. New here? Start with the step-by-step [Annotations guide](/annotations.md). ## The annotation family Every DreamLake annotation is a **catalog row** (name, `schemaType`, visibility) plus a **dreamdb space** (the data). `schemaType` is the one dispatch key, applied the same way everywhere: | consumer | known schemaType | unknown schemaType | | --- | --- | --- | | `Annotation.open` | returns the preset subclass (`video.annotation/v2` → `VideoAnnotation`) | returns the generic `Annotation` | | web app | rich view | catalog entry (generic track browser: roadmap) | | `Annotation.list(schema_type=…)` | server-side filter | same | Unknown never refuses — data written by newer tools stays readable generically. **Names.** Every name-taking classmethod accepts `"name"` (your own namespace) or `"namespace/name"` (an organisation you belong to; the server authorizes per request). A leading `@` on the namespace is tolerated. At most one `/`. ## Annotation (custom schemas) ```python from dreamlake.annotation import Annotation, Schema, Track, sequence_anchors ``` ### Lifecycle (classmethods) | method | meaning | | --- | --- | | `Annotation.create(name, *, schema=None, schema_type=None, visibility="private")` | create. `schema=None` starts empty — declare tracks as you go (embeddings excepted: create-time only). `schema_type` is your own dispatch label, default `"custom/v1"`; a registered preset type errors and points at the preset's own `create` | | `Annotation.ensure(name, *, schema=None, schema_type=None, visibility="private")` | open-or-create, rerun-safe. Existing annotations are **verified, never widened**: an explicitly expected `schema_type` errors on mismatch, and every field of `schema=` must already be declared | | `Annotation.open(name)` | open; dispatches by the catalog's schemaType (see table above) | | `Annotation.list(namespace=None, schema_type=None)` | `list[AnnotationInfo]` — `name`, `namespace`, `schema_type`, `visibility` | | `Annotation.delete(name, *, purge=False)` | remove the catalog row; `purge=True` also deletes storage. A classmethod on purpose: no open, no credentials — and a safe distance from row-level `ann.db.delete(anchors)` tombstones | ### Tracks — the schema, in its live form There is no `ann.schema`: `ann.tracks()` IS the schema — each `Track` handle carries `name` / `kind` / `mime` / `dim`. | method | meaning | | --- | --- | | `ann.add_track(name, kind, *, mime=None) -> Track` | declare a track (evolution is by addition only). Idempotent for a matching re-declaration; a kind change errors. `"embedding"` refused post-create | | `ann.track(name) -> Track` | the handle; an unknown name errors here, eagerly | | `ann.tracks() -> list[Track]` | every declared track | `kind` is the dreamdb vocabulary, verbatim: `"video"` / `"image"` (with `mime=`; JSON documents are `kind="image", mime="json"`) / `"scalar_float"` / `"scalar_int"` / `"scalar_bool"` / `"scalar_string"` / `"scalar_categorical"` / `"scalar_timestamp"`. Track names: `^[a-z0-9][a-z0-9_]*$`, max 64 chars; `anchor`, `_anchor`, `_time_anchors` are reserved. Tracks added through the bare `ann.db` handle are invisible to `tracks()` (the SDK keeps a fields mirror in the space meta, key `dreamdb.dataset.fields`); another process's `add_track` becomes visible after `ann.reload()`. ### Row-wise write / read | method | meaning | | --- | --- | | `ann.append_rows(rows) -> {"rows": n}` | one commit for the batch. Each row: `{"anchor": ns, track: value, ...}`. Sparse rows are the norm — **omit a field rather than passing None** | | `ann.rows(start=None, end=None, tracks=None)` | rows in `[start, end)`, sorted, sparse (absent field = absent key). Video tracks are excluded by default; naming one in `tracks=` errors | **Round-trip contract:** `rows()` returns exactly the shape `append_rows()` accepts, and `t.read()` exactly what `t.append_range()` accepts — `ann2.append_rows(ann1.rows(…))` needs no conversion. ### Introspection and recovery | method | meaning | | --- | --- | | `ann.anchors(start=None, end=None) -> list[int]` | every item anchor, ascending — count (`len`), span (ends), upload verification | | `ann.reload() -> ann` | refresh in place: catalog row, fields mirror, credential lease. Track handles survive. The fix for both documented stalenesses (cross-process `add_track`; the 12 h lease) | | `ann.schema_type` / `ann.namespace` / `ann.name` | identity | | `ann.visibility` / `ann.set_visibility("public")` | public annotations get anonymous presigned reads | | `ann.db` | the live `dreamdb.Dataset` — the escape hatch. The meta keys `dreamdb.schema_type` / `dreamdb.dataset.*` are the SDK's; overwriting them breaks the handle | ### Anchors Absolute int nanoseconds. Everywhere an anchor or range bound is taken, a **tz-aware** `datetime` also works (naive datetimes are refused, not guessed). Sequential data with no clock of its own uses row indices: `sequence_anchors(n, start=0, step=1)`; continue an existing annotation from `ann.anchors()[-1] + 1`. ### Write semantics (read this before scripting) - **Append-only, write-once.** Re-writing the same (anchor, track) is undefined — the engine resolves same-anchor duplicates by content order, not write order. The SDK rejects duplicates within one batch; across calls it is on you. There is no update verb in v1. - **Every `append*` / `ingest` call is one commit** (a new manifest, the ref advances). Batch with `append_rows` / `append_range`; a loop of single-point appends is slow and churns the ref history. - **One writer per annotation at a time.** Concurrent writers lose updates. Readers are unrestricted. - **Credentials are a 12 h lease** brokered at open. A stale handle raises `AnnotationError("credentials expired — call ann.reload()")`. One active platform annotation per process (the lease rides process-level AWS env vars). ## Schema Mirrors `dreamdb.Schema` one-to-one — same method names, same parameters, chainable — with two twists: every declaration is **recorded** (dreamdb's own Schema cannot be introspected once built), and `required` is pinned `False` (a required field could never be added later; all-optional is what keeps `add_track` available). ```python sch = Schema() sch.add_video("cam", mime="h264") sch.add_image("thumb", mime="jpeg") sch.add_image("meta", mime="json") # JSON documents, the dreamdb way sch.add_embedding("clip", dim=512, lsh_bits=14) # create-time only sch.add_scalar_float("temp") # ... _int/_bool/_string/_categorical/_timestamp sch = Schema.from_fields([{"name": "cam", "type": "video", "mime": "h264"}, ...]) sch.to_fields() # round-trip; the wire format ``` `to_fields()` / `from_fields()` carry the `fields` wire format (also stamped into the catalog's `schemaJson` and the space meta). Validation is eager and actionable: bad/reserved/duplicate names, `video` without `mime`, `embedding` without an int `dim`, `required=True`, and `add_audio` (unsupported — the engine cannot ingest it yet) all error at declaration. ## Track Column-wise reads and writes. One verb, `append`, with the shape in the name: no suffix = one point at a specific anchor, `_range` = a stretch of timeline (and `ann.append_rows` = the row-wise member of the same family). | method | meaning | | --- | --- | | `t.append(anchor, value) -> {"items": 1}` | one data point at one anchor | | `t.append_range(items) -> {"items": n}` | `(anchor, value)` pairs — sorted by the SDK, one commit; a duplicate anchor within the batch errors (write-once) | | `t.get(anchor)` | the value at exactly `anchor`, else `None` | | `t.read(start=None, end=None)` | `[(anchor, value)]` in `[start, end)`, sorted. A declared-but-never-written track reads `[]` | | `t.ingest(src, *, anchor, frag_seconds=2.0, height=None)` | video tracks only — see below | | `t.name` / `t.kind` / `t.mime` / `t.dim` | metadata (`dim` on embeddings) | **Values.** dreamdb's native representations pass through untouched: `bytes` for blobs, int ns for timestamps, native scalars, float vectors. On top, one-way input conveniences: file paths (`image`), tz-aware `datetime` (`scalar_timestamp` and every anchor), `dict`/`list` on `mime="json"` tracks, `.npy` paths and ndarrays (`embedding`, dim-checked). Scalars are strict — `bool` into `scalar_float` errors rather than coercing. `None` is always refused: absence is an omitted field, not a null. Reads return the stored representation as-is, with one exception: `mime="json"` tracks decode back to the `dict`/`list` that went in. **Video.** `t.ingest` is the only write path for video tracks (`append` on one errors and says so). `height=None` is a lossless remux — fast, but a CMAF track accepts exactly one codec configuration, so every clip on the track must be identically encoded. `height=N` re-encodes to a uniform h264 profile so mixed sources can share a track. Each clip occupies `[anchor, anchor + duration)`; overlapping an existing span is refused up front. Ranged video *reads* are not in v1 — playback goes through the platform, raw bytes via `ann.db`. ## VideoAnnotation (the `video.annotation/v2` preset) ```python from dreamlake.annotation import VideoAnnotation ``` An annotation of **episodes**: one recording each — one or more **camera** videos on a shared clock, plus per-frame joint annotations and action segments. Each episode owns a one-hour timeline slot; every camera, annotation and search vector of the episode lives inside that slot, which is why multi-camera playback is time-aligned with zero bookkeeping. Per-episode operations live on the `Episode` handle. The preset subclasses the generic `Annotation` and registers its schemaType: `Annotation.open` on one of these returns this class. ### `VideoAnnotation.create(name=None, *, backend=None, visibility=None, preview_height=720, preview_fps=30.0, frag_seconds=2.0)` | param | meaning | | --- | --- | | `name` | platform mode: catalog entry + managed bucket (needs `dreamlake login` / `DREAMLAKE_API_KEY`). Accepts `namespace/name` | | `backend` | self-hosted mode: `file:///abs/path` or `https://…` S3 URL — no platform involved | | `visibility` | `"private"` (default) / `"public"`; public annotations omit absolute source paths from metadata | | `preview_height` / `preview_fps` / `frag_seconds` | the **encoding profile** — playback resolution, frame rate, and fragment length. Chosen once, for the annotation's lifetime, stored in the space meta. Per-episode calls never pass encoding | Creation is never idempotent: an existing name/path errors instead of silently forking a second history. (`VideoAnnotation.ensure(name)` is the open-or-create form; it takes no schema arguments — the preset owns its schema.) ### `VideoAnnotation.open(name=None, *, backend=None, preview_height=None, preview_fps=None, frag_seconds=None)` Open by platform name or backend URI, strictly — a space stamped with a different schemaType is refused (use `Annotation.open` or `dreamlake.db` for those). The optional encoding kwargs **verify, never set**: pass the values your script assumes and `open()` errors on mismatch — so the `try open / except create` idiom cannot silently eat a config edit. ### `ann.add_episode(videos, *, episode_id=None, joints_pose=None, subtasks=None, recon_mesh=None, recon_pose=None, recon_camera=None, recon_hands=None, recon_gravity=None, meta=None, gid=None, raw=False) -> Episode` The upload. One call transcodes every camera for browser playback and commits annotations + metadata atomically. Returns the `Episode` handle; the ingest report (per-camera fragment counts, annotation counts) is on `epo.report`. | param | meaning | | --- | --- | | `videos` | one path (single camera, stored as `main`) or `{camera: path}`. Camera names: `[a-z0-9_-]`, no `__` | | `joints_pose` | a doc (binds to the **primary** camera — `main` if present, else the first) or `{camera: doc}` to annotate several views. Docs may also be JSON file paths | | `subtasks` | episode-level action segments (doc or path) | | `recon_mesh` | 3D-reconstruction: object meshes `{object: obj_text}` (episode-level, no camera) | | `recon_pose` `recon_camera` `recon_hands` `recon_gravity` | 3D-reconstruction, per camera: object 6-DoF poses / pinhole intrinsics / MANO hands / gravity up-vector. Bare doc → primary camera, or `{camera: doc}`. All optional — see [Annotation formats](#annotation-formats) | | `meta` | labels: `{"task": ..., "scene": ...}` — whitelist-only, **nothing is inferred** | | `gid` | pin a specific slot (rarely needed) | | `raw` | `True` additionally archives a lossless remux (roughly doubles the upload; best-effort on mixed sources) | Fails fast, before any transcoding: any camera ≥ 3600 s, duplicate `episode_id`, unknown meta key, joints naming a camera not in `videos`, annotation fps disagreeing with its camera by >10 accumulated frames, or an aspect ratio differing from that **camera's** existing track (aspect is per camera — head 4:3 and wrist 16:9 coexist). ### `ann.episode(episode_id)` / `ann.episodes(after_gid=None, limit=None)` / `ann.episode_count()` Lookup, listing, and count — all scale-safe: id lookups and counts go through a slim per-episode index track (a `scalar_string` of bare ids — one cacheable fetch yields the whole id→slot map; the fat `episode_meta` column is only ranged-read for the slots actually needed). Bare `episodes()` is the full listing (fine up to a few thousand); past that, page: `episodes(limit=100)` then `episodes(after_gid=page[-1].gid, limit=100)` — each page is one slot-window read. `[e.meta for e in ann.episodes()]` recovers plain dicts. ### `ann.cameras() -> list[str]` / `ann.tracks() -> list[Track]` What is in this annotation. `cameras()` is the union over episodes, `main` first. `tracks()` returns the same `Track` handles as the generic layer (name/kind/mime/dim) plus the preset extras `role` (`video_preview`, `joints_pose`, `subtasks`, `recon_mesh`, `recon_pose`, `recon_camera`, `recon_hands`, `recon_gravity`, `episode_meta`, `search`, `user`), `camera`, and the derived `preset` flag. Preset reads still go through the named APIs (`read_joints_pose`, `epo.read_track`, …). ### `ann.add_track(name, kind, *, mime=None) -> Track` Declare a custom column. Same kind vocabulary as the generic layer, with one extra rule: names must match `^x_[a-z0-9_]+$` — the user namespace the preset promises never to claim, so your columns can never collide with a future SDK release. Re-declaring the same name+kind is idempotent; changing a track's kind is refused; embeddings cannot be added after creation. The web viewer does not render `x_*` tracks; they are first-class data for training loaders. Write and read through the Episode handle. ### `ann.embed_episodes(*, camera=None, fps=1.0, source_dir=None, batch_size=32) -> dict` The search sweep: for **every** episode, sample frames at `fps` from one camera's source file (primary unless `camera=`), encode with CLIP, encode subtask texts with BGE, upload. Episodes commit one at a time — interrupt and re-run freely. `source_dir` locates moved files via each camera's recorded `source_rel`. Needs `pip install "dreamlake[search]"`. One episode = `epo.embed()`. ### `ann.search(query, top_k=10, kind="both") -> list[dict]` Natural-language moments: CLIP text tower against frame vectors and/or BGE against subtask texts, fused by reciprocal rank. Returns `[{"episode_id", "time_sec", "score", "source", "subtask"?}]`. `kind`: `"frames"` / `"subtasks"` / `"both"`. ### `ann.encoding` / `ann.db` `encoding` — the stored profile, read-only: the create-time defaults plus `cameras`, each camera track's **adopted playback profile** (`{width, height, fps}`, fixed by the aspect ratio of that camera's first clip; every later clip is validated against it before any transcoding). `db` — the live dreamdb handle: the escape hatch for anything outside the preset (absolute-anchor appends, columnar bulk reads, branching). Layout invariants are yours to respect. ## Episode Obtained from `add_episode` / `ann.episode(id)` / `ann.episodes()`. The identity triple (`episode_id`, `gid`, `anchor`) is immutable — handles never dangle. The meta snapshot is a read convenience; **every write re-reads the stored metadata at call time**, so a stale handle can never clobber a newer revision. | member | meaning | | --- | --- | | `episode_id` / `gid` / `anchor` | identity: your stable id, the slot number, the slot's base time | | `report` | ingest report (only on the handle `add_episode` returned) | | `meta` / `cameras` / `task` / `scene` / `duration_s` | snapshot reads, zero IO | | `refresh()` | re-read metadata, returns self | | `info()` | fresh meta + annotation summaries per camera (does IO) | ### Reads ```python epo.read_joints_pose(camera=None) # default: primary camera; None if unannotated epo.read_subtasks() # episode-level; None if absent # 3D reconstruction — one method per piece (default: primary camera): epo.read_recon_mesh() # episode-level {object: {obj, scale}} | None epo.read_recon_pose(camera=None) # {"frames": {...}} | None epo.read_recon_camera(camera=None) # intrinsics {fx,fy,cx,cy,width,height} | None epo.read_recon_hands(camera=None) # {"faces","frames"} | None epo.read_recon_gravity(camera=None) # {"vec3d": [...]} | None ``` ### `epo.add_cameras(videos, *, joints_pose=None, raw=False) -> dict` Late-arriving cameras — add-only (video fragments occupy their slot and cannot be replaced; re-sending an existing camera errors). Dict form adds N cameras with one metadata write. ### `epo.revise(*, joints_pose=None, subtasks=None, recon_mesh=None, recon_pose=None, recon_camera=None, recon_hands=None, recon_gravity=None, meta=None) -> dict` The one revision verb: everything passed lands in **one committed row**. Storage is append-only — a revision is a new version, readers see the newest, history stays addressable. `meta` takes the same whitelist as `add_episode`; there is no inference, and passing values identical to what is stored is reported as an error rather than a silent no-op. ### Custom-track values (declare with `ann.add_track` first) ```python epo.set_track("x_quality", {"blurry": False}) # one value per episode (t=0) epo.set_track("x_reward", 0.75, t_sec=12.0) # or at any in-slot time epo.append_track("x_reward", [(1.0, 0.1), (2.0, 0.2)]) # batch, one commit epo.get_track("x_quality") # -> value | None epo.read_track("x_reward", start_sec=0, end_sec=None) # -> [(t_sec, value)] ``` Values follow the track's kind (dicts/lists round-trip as JSON on `image/json` tracks). Timestamps are seconds on the episode's own clock and are bounds-checked to the slot. Re-writing the same (track, time) is a revision — the preset's episode clock has revision semantics the generic layer's absolute anchors do not. For high-rate series and cross-episode bulk reads, drop to the engine with `epo.anchor_at(t_sec)` + `ann.db.append_many` / `ann.db.iter_all_batches(fields=[...])`. ### Search, single episode ```python epo.embed(camera=None, fps=1.0, video_path=None, source_dir=None, batch_size=32) epo.add_search_vectors(frame_vecs=[(t, vec512), ...], # CLIP space, L2-normed subtask_vecs=[(t, vec384, label), ...]) # BGE space ``` `embed` resolves the source file `video_path` → `source_dir`/`source_rel` → recorded absolute path, and refuses a resolved file whose duration disagrees with the ingested camera's (wrong file); an explicit `video_path` overrides the check. Vectors are searchable the moment they land — there is no index-build step. ## Annotation formats Wire-compatible with the web viewer's overlays. Joint coordinates live in the pixel space of the camera whose track holds the document. ```jsonc // joints_pose — per camera, one doc per episode { "width": 1920, "height": 1080, // REQUIRED: annotation-time pixel space "src_fps": 29.987, // REQUIRED: frame k renders at k / src_fps "joint_order": ["wrist", ...], // optional: names, index-aligned "bones": [[0, 1], ...], // optional: skeleton edges "frames": { // REQUIRED, sparse: only annotated frames "0": [{"keypoints_2d": [[x, y], ...], // REQUIRED per detection "is_right": 1, "det_conf": 0.9}] // optional } } // subtasks — episode-level, one doc per episode { "task": "wash the dishes", // optional, NOT copied to episode meta "labeled_subtasks": [ // REQUIRED; gaps are fine {"start_sec": 0.0, "end_sec": 2.5, "subtask": "pick up plate"} ] } ``` ### Reconstruction (3D) An optional third modality: 3D hand–object reconstruction, as five flat `recon_*` arguments (one per stored track). Geometry is in the camera's **OpenCV frame** (x-right / y-down / z-forward), metres, quaternion **wxyz**, frame `f` ↔ time `f/fps`; colour is not stored. `recon_mesh` is episode-level; the other four are per-camera (bare doc → primary, or `{camera: doc}`). ```python epo = ann.add_episode( video, subtasks=..., joints_pose=..., recon_mesh={name: obj_text}, # or {name: {"obj","scale"}} recon_pose={frame: {name: {"t":[x,y,z], "q":[w,x,y,z]}}}, recon_camera={"fx":.., "fy":.., "cx":.., "cy":..}, # pinhole intrinsics (→ recon_camera__) recon_hands={"faces": {"left":[[a,b,c]...], "right":[...]}, # optional "frames": {frame: {"left": {"verts":[[x,y,z]...], "joints":[[x,y,z]...]}, "right": {...}}}}, recon_gravity=[x, y, z], # optional; up direction ) epo.revise(recon_pose={"left": ...}, recon_camera={"left": ...}) # fill a camera in later ``` Each is optional; at least one present. Re-passing a piece revises it. `recon_pose` tells a bare per-frame doc from a `{camera: doc}` map by whether the keys are frame indices vs camera names — so don't name a camera a bare number. ## Storage semantics worth knowing (preset) - **Slots.** Episode `gid` occupies `[gid × 1 h, (gid+1) × 1 h)` on one timeline; slots are never reused; any single camera clip ≤ 3600 s. - **Encoding is annotation-lifetime.** Every clip on one camera track shares one init segment, so height/fps/fragment length cannot vary per episode — that is why they live on `create`, not `add_episode`. - **Aspect ratio is per camera track**, not per annotation. - **Revisions are versioned, bounded, and never in-place.** Each episode-level value carries up to 1024 revisions; readers always resolve to the newest; history stays in the timeline. - **Video is add-only.** Cameras can join an episode; fragments are never replaced. - **`x_` is yours, everything else is the contract.** Preset track names and metadata keys evolve only by addition, in SDK releases; tools render what they recognize and skip the rest. --- Source: https://docs.dreamlake.ai/api/project-queries # Project and Note queries This reference describes the implementation branch. It does not establish that these fields are deployed to staging or production. Verify the target server's schema before switching a client. Existing mutation routes remain REST; Note content, CRDT synchronization, and revision-protected editing are unchanged. ## Separate associations from choices | Question | GraphQL | REST | |---|---|---| | Which projects can this viewer access? | `projectCatalog` | `GET /namespaces/:namespace/projects?search=design&page=1&pageSize=50` | | Where is this Note actually associated? | `note.nodes` | `GET /namespaces/:namespace/resources/note/:noteId/projects?view=nodes` | | Which Bindrs can the picker offer in this project? | `bindrs` | `GET /namespaces/:namespace/projects/:projectSlug/bindrs?view=choices&search=design&first=20` | The GraphQL resolvers and these REST views use the same underlying visibility, project permission, resource-readability, and association services. Their wire shapes differ: GraphQL selects fields and wraps results in `data`; REST returns its resource response directly. The `view=nodes` REST association view permits authorized anonymous reads of public resources; the legacy default response requires a signed-in caller. Unreadable resources are not exposed by the shared service. The existing resource-project REST response without `view=nodes` remains a compatibility response that mixes candidate projects with `bound` and `canBind`. New clients should use the views above instead. The existing Bindr response without `view=choices` also keeps its prior tree-list contract. ## Query a Note's locations ```graphql query NoteLocations($namespace: String!, $noteId: ID!) { note(namespaceSlug: $namespace, id: $noteId) { id name nodes { id name path project { id slug displayName } bindrs { id name parentId } } } } ``` Variables below use illustrative values. Replace them with a namespace and Note ID that the caller can read: ```json { "namespace": "example-team", "noteId": "000000000000000000000001" } ``` `nodes` contains actual resource Nodes in visible projects, never candidate projects. An empty list means no visible associations, not proof that no hidden association exists. An unreadable or missing Note returns `note: null`. `Node.path` is the existing comma-delimited **ancestor path**, not the full file path or a parent ID. For example, a Node named `API design` with `path: ",example-project,design-docs,"` sits beneath that ancestor path. Project ownership comes from `projectNodeId` and the resolved `project`, not parsing the path. No folder or storage migration is part of these reads. `node.bindrs` means Bindrs **tagging that Node**, not every Bindr owned by the project. It is resolved through membership records. Project objects retain the GraphQL `Node` type and stable ID for Apollo normalization. The equivalent REST view returns `{ nodes, total }`; each Node includes its project and attached Bindrs. No `bound` or `canBind` field is needed in that view. ## Search project choices ```graphql query ProjectChoices($namespace: String!, $search: String, $page: Int, $pageSize: Int) { projectCatalog(namespaceSlug: $namespace, search: $search, page: $page, pageSize: $pageSize) { projects { id slug displayName permissions { role project { read update delete manageGrants } content { read create delete } } } total page pageSize totalPages canCreate } } ``` ```json { "namespace": "example-team", "search": "design", "page": 1, "pageSize": 50 } ``` The catalog is scoped to one namespace and returns authorized projects, including project-specific grants. Search is performed on the server. The default page size is 50; the maximum is 200. Fetch subsequent pages when needed instead of treating the first page as the complete set of projects. `canCreate` is permission to create a project in the namespace. `permissions.content.create` is permission to add content within a particular project. For an add-project picker, exclude projects already present in `note.nodes` and offer projects with content creation permission. The mutation still checks both project authorization and resource readability; a successful read is not a grant that remains valid indefinitely. ## Search Bindr choices on demand ```graphql query BindrChoices($projectId: ID!, $search: String, $first: Int, $after: String) { bindrs(projectId: $projectId, search: $search, first: $first, after: $after) { nodes { id name parentId projectNodeId } pageInfo { endCursor hasNextPage } } } ``` ```json { "projectId": "000000000000000000000002", "search": "design", "first": 20, "after": null } ``` The default page size is 20; the maximum is 100. Run this only when the picker needs choices. For another page, pass the returned `endCursor` as `after`; reset the cursor whenever the project or search changes. Treat cursors as opaque. REST uses the same `{ nodes, pageInfo }` response shape and accepts the cursor as an URL-encoded `after` query parameter. The REST path identifies the project by namespace and slug; GraphQL uses its Node ID. These are live listings, not transactionally frozen snapshots. Concurrent edits may change choices between requests. Mutations remain authoritative and must revalidate permissions and membership constraints. ## Execute a GraphQL request Save any query above to `query.graphql` and its variables to `variables.json`. Set the API base URL and an existing DreamLake token in your shell environment: ```bash export DREAMLAKE_API_URL='http://localhost:3001' # Set DREAMLAKE_TOKEN securely to your existing token; do not commit it. python3 - <<'PY' import json import os import urllib.request with open('query.graphql') as source: query = source.read() with open('variables.json') as source: variables = json.load(source) request = urllib.request.Request( os.environ['DREAMLAKE_API_URL'].rstrip('/') + '/graphql', data=json.dumps({'query': query, 'variables': variables}).encode(), headers={ 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + os.environ['DREAMLAKE_TOKEN'], }, ) with urllib.request.urlopen(request) as response: result = json.load(response) if result.get('errors'): raise RuntimeError(result['errors']) print(json.dumps(result['data'], indent=2)) PY ``` GraphQL errors can arrive with HTTP 200; inspect `errors` before using `data`. For Apollo clients, select stable IDs and keep project objects as `Node`. Refresh associations after existing REST mutations. Cancel superseded picker requests, prevent stale identity responses from entering the cache, and isolate explicit anonymous previews from authenticated cache entries. --- Source: https://docs.dreamlake.ai/envs/layers # Env Layers Reference The complete contract of `dreamlake.layers.json` (schema `dreamlake.env-layers/v3`): one component grammar, five ops, the composition semantics, six annotated stacks, and the errors you will actually hit. New here? Start with the [Envs guide § Env layers](/envs.md#env-layers). ## The model A layered env is an ordered **stack** of ops. Every stack entry is one flat object — `{"tag": ..., ...props}` — in the vocabulary of vuer's imperative session updates (`add` / `upsert` / `update` / `remove`), where `Update` is a *meta component*: its `key` addresses an element, its props ARE the opinions. No op has a privileged role — "scene", "embodiment", "physics profile" are things you *do* with layers, not schema concepts. Ops apply in order and **later wins, always**. `dreamlake env compose` materializes the stack into an ordinary `mujoco` env directory — entry XML, flat assets, and a fully **pinned** copy of the stack side by side — and `dreamlake env push` publishes it as a normal env version. Consumers (viewer, SDKs, training code) read only the materialized artifact; the embedded stack is what makes the env permanently re-openable: edit a pin, a pose, or an opinion, recompose, push again — content-addressed dedup keeps re-pushes cheap. ## Requirements Composition is a real MuJoCo build (name-aware attach renaming, URDF import, compile validation), not file manipulation. The CLI delegates materialization to the Python engine in **dreamlake-py**: ```bash pip install "dreamlake[compose]" # the engine + mujoco (optional extra, lazily imported) ``` `dreamlake env compose` requires it on the composing machine and says so clearly when it is absent. Push / pull / list need no Python. Registry layers are resolved through the immutable, hash-verified cache at `~/.dreamlake/cache/envs////`. The engine needs **MuJoCo ≥ 3.8** (validated on 3.8.1 and 3.14.0): it drives the modern `MjSpec` class API, which MuJoCo 3.2's early spec bindings do not provide. dreamlake 0.23.0 still *declares* `mujoco>=3.2.0`, so a 3.2 environment installs cleanly and then fails at the first layer with a `from_file(): incompatible function arguments` error — upgrade mujoco. From **dreamlake 0.23.1** the package declares the real `mujoco>=3.8` floor and the engine refuses an older MuJoCo up front with an explicit version error. (The standalone scene-inspection tools are separate and do run on 3.2.) ## The file: `dreamlake.layers.json` ```json file="dreamlake.layers.json" { "schema": "dreamlake.env-layers/v3", "name": "kitchen-g1", "layers": [ { "tag": "Merge", "src": "you/kitchen@3" }, { "tag": "Attach", "src": "you/sharpa-right", "key": "right", "at": "world", "pos": [0.45, -0.12, 0.30], "quat": [1, 0, 0, 0], "joint": "free-anchored" }, { "tag": "Update", "key": "obj/mug", "pos": [0.30, 0.10, 0.02] }, { "tag": "Update", "key": "light:key", "diffuse": [0.3, 0.3, 0.4] }, { "tag": "Update", "key": "option", "impratio": 10 }, { "tag": "Remove", "key": "body:fixture/plant" }, { "tag": "Patch", "src": "./patches/night.xml" } ] } ``` ### Top-level fields | field | type | required | meaning | | --- | --- | --- | --- | | `schema` | string | yes | `"dreamlake.env-layers/v3"`, verbatim | | `layers` | array | yes | ordered ops, applied first → last | | `substrate` | string | no — default `"mujoco"` | the format the stack composes in. `"mujoco"` is the only substrate today; a layer whose envType differs is admitted only if an importer into the substrate exists (`urdf` — see below), otherwise compose rejects it | | `entry` | string | no — default `"scene.xml"` | entry filename of the materialized artifact | | `name` | string | no | MJCF model name of the output | The minimal stack is two keys: `schema` and `layers`. ### `src` — one string, two shapes `Merge`, `Attach` and `Patch` take a source. The rule is ESM-style: | shape | meaning | | --- | --- | | `"ns/name"` or `"ns/name@3"` | a pushed env version. An unversioned ref **floats** — resolved to the latest version at compose time; the copy of the stack embedded in the artifact is always pinned `@v` | | `"./dir"`, `"../dir"`, `"/abs"` | a local path — a directory in the standard env shape, or a single MJCF file (a sparse patch, say), distinguished automatically. Dev-only: composing and previewing are unrestricted, pushing is gated — see [push discipline](#push-discipline) | A local path **must** start with `./`, `../` or `/` — a bare string is always a registry ref, so `you/kitchen` (registry) and `./you/kitchen` (a local directory that happens to have that name) never collide. ### Element addresses — one syntax everywhere `Attach.at`, `Update.key` and `Remove.key` share one address syntax: - `kind:name` — explicit: `body:fixture/plant`, `geom:floor`, `site:right:palm-mount` (everything after the first colon is the name, so attach-manufactured names like `right:palm` address naturally); - bare `name` — allowed when the name is unambiguous across kinds; a name shared by, say, a body and a geom is a compose-time error that lists the candidates and asks you to qualify; - `option` — the singleton (no name); `visual:` — a visual sub-block (`visual:headlight`). ### `tag: "Merge"` ```json { "tag": "Merge", "src": "you/kitchen@3" } ``` Section-wise union of the env's MJCF into the stack: worldbody children, assets, `` classes, tendons / actuators / sensors / contacts. A name collision between merged layers is an **error**, never silent last-wins — changing existing elements is exclusively the opinion ops' job. Exception by design: `