Skip to content

Your first project

The workspace on its own is a fine scratch space, but LatchAI gets interesting when you point it at a repository you already work in. That is what mounts are for.

A mount is a folder from anywhere on your machine, declared in mounts.json and managed from the Files sidebar’s + button (or the /api/mounts endpoints). The mount’s name becomes a virtual top-level directory: the file explorer, editor tabs, and every agent tool see it through the same multi-root filesystem and the same permission gate, and nothing outside the workspace and its mounts is reachable.

Open the Files view in the activity bar and use one of the two buttons in its header:

  • + opens a native folder picker (macOS).
  • takes a typed path — ~ is expanded — for headless daemons and everywhere else.

Adding a folder to the workspace

A mount appears as a top-level folder beside workspace/.

The mount is named after the folder’s basename, with anything outside [A-Za-z0-9_.-] replaced by _; a name that’s already taken gets a -2 suffix rather than clobbering the existing mount. The result is one line in <home>/mounts.json:

{
"myrepo": "/Users/you/repos/myrepo"
}

Removing a mount (the × on its root row, “Remove from Workspace” in its context menu, or DELETE /api/mounts/<name>) drops that line. Nothing in the folder is touched — a mount is a pointer, not a copy, and LatchAI never moves or rewrites the directory it points at.

Inside LatchAI, the mount name is the first path segment. myrepo/src/index.ts routes to the mounted repo; anything that doesn’t start with a mount name stays in workspace/. That is the same addressing the file explorer, editor tabs, read_file, write_file, edit_file, list_dir, and run_shell’s cwd all use, so a path you can see is a path an agent can name.

The gate is on the way out, not on the way in. A path that resolves outside the workspace and its mounts is refused before anything is read or written, and the refusal is a permission.denied event on the canvas and in the events panel, not a silent skip:

path escapes workspace root: ../../.ssh/id_rsa

Symlinks are checked too — the real path of the target has to land inside one of the allowed roots, so a symlink planted in the workspace can’t be used to walk out of it.

Open the assistant panel with ⌘⇧L. It runs on the same engine as everything else — same provider, same tool registry, same filesystem — so it can read the repo you just mounted, propose edits, and apply them. The file in your active tab is passed along as context, and @ autocompletes a file path to attach its contents explicitly.

Try something that requires it to look around:

@myrepo/package.json what does the build script actually do, and is anything in scripts/ unused?

You’ll watch it work rather than wait on it: tool calls appear as chips in the chat and as full entries in the events panel (⌘J), and if the model publishes a plan, the panel grows a live to-do strip that ticks over as steps complete.

One picker at the bottom of the panel decides who is answering — a model (anything in the Models page Library, on this Mac or connected) or an agent (any persona in agents/, so you can talk to the same reviewer your workflows use). lists saved sessions and reloads one; sessions survive engine restarts because the envelopes live in runs/chats/.

Because every chat turn is checkpointed, the panel’s ± Review changes affordance shows you exactly what the assistant touched, with a revert.

The chat panel alongside the editor

Chat, the editor, and the file tree are one process — the assistant edits what you’re looking at.

+ in the sidebar (or the “New Workflow” command) scaffolds a draft with a trigger node. From there: add nodes from the toolbar palette, drag between handles to connect them, click a node for its type-aware inspector, and press ⌘S to save. Saving validates the graph server-side and writes workflows/<id>.json — a plain file you can read, diff, and commit.

Step by step, for the smallest useful thing — an agent that reads the repo you just mounted and writes notes into the workspace:

  1. + in the Workflows sidebar. You get Untitled 1, one manual_trigger node, and a dirty tab (● in the tab bar). Nothing is on disk yet.
  2. Add an agent node from the toolbar palette, and drag from the trigger’s output handle to the agent’s input handle to connect them.
  3. Click the agent node. The inspector is type-aware: give it a label, a prompt, and a tool allowlist. Click empty canvas instead and the inspector edits the workflow’s own name and description.
  4. ⌘S. The draft is PUT to the engine, validated against the workflow schema, and written to <home>/workflows/untitled-1.json. A validation error comes back into the toolbar rather than writing a broken file.
  5. ⌘⏎. The run starts, nodes light up as they execute, tokens stream into the node, and file.written shows up in the events panel when the agent writes.

The file you just made:

{
"id": "repo-notes",
"name": "Repo notes",
"description": "Summarize a mounted repo into the workspace.",
"nodes": [
{
"id": "trigger",
"type": "manual_trigger",
"position": { "x": 60, "y": 180 },
"config": {}
},
{
"id": "summarize",
"type": "agent",
"label": "Summarize the README",
"position": { "x": 340, "y": 180 },
"config": {
"tools": ["read_file", "list_dir", "write_file"],
"prompt": "Read myrepo/README.md, then write a three-bullet summary of what this project is to notes/myrepo.md."
}
}
],
"edges": [{ "from": "trigger", "to": "summarize" }]
}

Two things in that config are worth knowing. tools as an array is an allowlist; true means every registered tool, and omitting it entirely makes the node a plain single-shot prompt with no tool loop at all. And any {{nodeId}} in a prompt interpolates that node’s output — {{trigger}} is the run payload, which is how a manual run, a webhook body, and a cron fire all reach the same agent.

The { } JSON button in the toolbar toggles the canvas for the raw document. Both views project the same draft, so an edit in one shows up in the other, and invalid JSON shows its parse error instead of committing.

⌘⏎ runs the workflow, saving first if the draft is dirty, because the engine runs the file on disk rather than the buffer in your browser. (If the trigger declares config.params, LatchAI asks for them first and joins the answers into the payload.) The same run, headless:

Terminal window
npm run run ~/LatchAI/workflows/repo-notes.json

The agent says it can’t find your repo. It is addressing a path outside its roots. Ask for list_dir on . in chat — the top-level entries are the mount names plus the workspace’s own contents, and whatever isn’t in that list isn’t reachable. If the mount is in mounts.json but not in that list, the file was edited by hand while the engine was running: restart it.