Johannes Kaindl278 downloadsRun autonomous local LLM agent teams (crews) on your vault via LM Studio
Run autonomous local LLM agent teams ("crews") on your Obsidian vault, powered by a local LLM model (LM Studio or Ollama) — with a deterministic, orchestrator-led pipeline and a snapshot safety net under every run.
Local models are treated as weak, unreliable executors. The orchestrator decides flow, paths and writes; the model only ever decides content, inside narrow, schema-validated contracts. Every output is constrained, then verified, before it ever touches your vault.
🇬🇧 English · 🇩🇪 Deutsch

collector (deterministic context
gathering), llm (one chat completion against a schema-validated contract), and
actions (deterministic application of a validated action list to the vault). The
model never controls flow and never touches the vault directly.run.md
(frontmatter + per-task detail, Bases-compatible) and a machine-readable
state.json, plus a shipped runs.base dashboard.state.json still marked running are
detected on the next plugin load, with one recommended action: finish the run
(keep the partial changes — they remain undoable via the write-ahead snapshot).isDesktopOnly: true — the plugin is built around a locally-hosted
LLM served over HTTP, a desktop workflow).1234) or
Ollama (default port 11434), serving an OpenAI-compatible API.
The endpoint is configurable in the plugin settings; just enter the URL (e.g.,
http://localhost:1234/v1 for LM Studio or http://localhost:11434/v1 for Ollama).
You can list several endpoints (one per line), and the plugin uses the first
reachable one at each preflight. No provider selection needed — the plugin
auto-detects context length and capabilities.XMLHttpRequest
from inside Obsidian's renderer process (requestUrl cannot stream). LM Studio:
Settings → Developer → Enable CORS. Ollama: set the environment variable
OLLAMA_ORIGINS=<your-obsidian-app-url> (optional; without it, the plugin falls back
to non-streaming mode, and results still arrive).Live token streaming needs CORS enabled on your LLM server. The ticker runs as an
XMLHttpRequestfrom Obsidian's renderer, which always sendsOrigin: app://obsidian.md. LM Studio rejects that preflight unless the server was started with--cors(lms server start --cors), Ollama unlessOLLAMA_ORIGINSallows it. Without it the run still completes — the plugin falls back to a non-streaming request through Obsidian's main process — but the panel shows „Waiting for output…" until the task is done.
Canonical repository: git.jkaindl.de/jkaindl/vault-crews, mirrored on GitHub where the releases are published.
Via AnySource Sideloader, which installs and updates plugins from any git forge. Subscribe to the plugin catalog once under Settings → AnySource Sideloader → Catalogs → Add:
https://git.jkaindl.de/jkaindl/obsidian-catalog/raw/branch/main/catalog.json
Vault Crews then appears in the sideloader's plugin list and updates like any other plugin; every download is checksum-verified. To install just this one plugin without the catalog, add its repository URL as a source instead: https://github.com/johannes-kaindl/vault-crews.
Download main.js, manifest.json and styles.css from the latest release into <vault>/.obsidian/plugins/vault-crews/, then enable the plugin under Settings → Community plugins. Each release also ships checksums.sha256, so you can verify what you downloaded with shasum -a 256 -c checksums.sha256.
cp main.js manifest.json styles.css "<your-vault>/.obsidian/plugins/vault-crews/"
git clone https://git.jkaindl.de/jkaindl/vault-crews
cd vault-crews
npm install
npm run build # produces main.js
Copy main.js, manifest.json and styles.css into <vault>/.obsidian/plugins/vault-crews/.
After enabling (whichever route), run the command "Install example crews" to seed _crews/
(default root, configurable in settings) with the four example teams (Task-Triage,
Daily-Briefing, Notiz-Tagger, Reifegrad-Tagger), their agents, and the runs.base dashboard. Installed files are never
overwritten by a second run — edit them freely afterwards.
_crews by default)
with the four example teams, their agents, and the runs.base dashboard.run.md (human-readable, Bases-compatible) and
state.json next to it; Open last run log jumps there, and runs.base lists all
runs at once.Writing your own teams and agents is plain Markdown in the vault — see Writing your own crews below.
Settings → Community plugins → Vault Crews, in four groups:
| Setting | Default | Meaning |
|---|---|---|
| Endpoints | http://localhost:1234/v1 |
One row per endpoint, each with its own URL, optional API key (kept in Obsidian's secret storage, not in data.json) and model; the first reachable one is used per run. Check connections probes each row and reports refused / unknown host / timeout / not-an-LLM-API / access denied separately |
| Denied endpoints | localhost:8080, 127.0.0.1:8080 |
Never contacted — the default keeps the plugin off a port other local model servers commonly claim. A setting, not hardcoded |
| Request | (profile) | What a run sends with each model call: sampling values (temperature, top_p, top_k, …) and the thinking level, from a profile table per model family and backend. Collapsed section; shows what is sent and what has an effect on your backend, takes your own values per model family, lists the last request and any deviations seen this session. Values in an agent note override it for that agent. The row "Level picker in chat" has no effect in Vault Crews |
| Crew root folder | _crews |
Vault-relative folder holding agents, teams and run logs |
| Hide crew folder in the file explorer | off | Purely cosmetic: the crew root disappears from the explorer, its notes stay in the vault |
| Max writes per run | 10 | Plugin-wide cap; a team's own max_writes can only be lower |
| Wall-clock limit | 10 minutes | Aborts a runaway run, leaving its partial writes snapshotted and undoable |
| Undo history depth | 15 | How many run snapshots of the current day are kept. Older days thin out: one snapshot per day for a week, then per week (4 weeks) and per month (6 months), none after that |
| Call timeout | 300 s | Hard limit per model call — generous, because just-in-time model loading takes a while |
| Stall timeout | 60 s | Aborts if no new token arrives; only checked after the first data chunk, so loading is never mistaken for a stall |
| Verbose logging | off | Reserved — the setting persists but nothing reads it yet (see V1 limitations) |
If the LLM Endpoint Manager plugin is installed, the endpoint (and its API key and model) comes from there and the settings show the manager's picker instead of the list above; without it, the list above is used as before.
Endpoint and timeout changes take effect with the next run; no plugin reload is needed.
A run is an orchestrator walking a fixed pipeline, not a model deciding what to do next. Each team is a sequence of exactly three kinds of task:
collector — deterministic gathering. Code, not the model, reads the vault and
assembles the material (e.g. tasknotes.query over a folder, optionally with note
content).llm — exactly one chat completion, answering against a schema declared by the
task's output: block. The model sees only the collected material and produces only
content — never a path, never a flow decision.actions — deterministic application. The validated action list is applied to
the vault by code.Between the model and your notes sit two independent checks. Stage 1
(output-validator) parses the raw response, validates it against a built-in versioned
schema, and binds every path and enum value back to the material actually collected —
a path the model invented has nothing to bind to and is rejected. One repair attempt
handles malformed JSON. Stage 2 (action-executor) re-checks each action immediately
before the write, independently of stage 1: against the write_scope allowlist and the
fixed denylist, the allowed action types and frontmatter keys, and a content hash — if
you edited the file after it was collected, that action is skipped rather than
overwriting your edit. If more than half of a task's actions fall away, the task fails
instead of applying a half-consistent state.
Writes are snapshotted write-ahead: a note's pre-run content is copied into a hidden per-run store through the Obsidian vault/adapter API before it is touched. That is why even a crashed or aborted run stays fully undoable, and why no git repository is needed.
write_scope whitelist, per team, plus a fixed denylist that always wins. Every
team declares the vault-relative globs it may write to. A fixed denylist —
.obsidian/**, .git/**, _crews/**, _vaultrag/**, dotfiles — overrides any
whitelist unconditionally; crews can never read or write their own configuration
(no self-triggering, no prompt-injection path into plugin control)..obsidian/plugins/vault-crews/undo/). Even a failed or aborted run with partial
writes stays fully undoable. Undo last run restores changed notes and trashes
run-created ones, showing exactly what it will undo (team, time, files) before you
confirm — and warns if a note was edited after the run rather than silently
overwriting it. Run-created notes go to the Obsidian trash, never a hard delete.max_writes per run (team-configurable, capped by
a plugin-wide maximum), a hard per-note size cap, an LLM call budget, and a
wall-clock watchdog (default 10 minutes) that aborts a runaway run (leaving its
partial writes snapshotted and undoable) rather than running forever.output-validator) validates the raw
model JSON against a built-in schema and binds every path/enum value to the
material actually collected for that task. Stage 2 (action-executor) re-checks
every action against the path whitelist/denylist, allowed action types, allowed
frontmatter keys, and a content-hash staleness guard (if you edited the file since
it was collected, that single action is skipped, never silently overwritten) —
immediately before the write, independent of stage 1.http://localhost:1234/v1, Ollama
http://localhost:11434/v1). No other host is ever contacted, no telemetry, no analytics,
no update-check pings.data.json keeps only a reference, so a synced vault does not carry them. A key you entered
before 0.14.0 moves there the next time the plugin resolves its endpoints.Bearer tokens and common API-key formats (sk-…, ghp_…, …) in the text a crew sends become placeholders such as [redacted-token-1]; the answer gets the original back before the crew writes it. Notes reach the model as JSON text, so a restored PEM block keeps its line breaks written as \n. E-mail addresses and other personal data are not masked.run.md,
state.json) and everything shown in the panel passes a redaction step first — including
error bodies, which is the realistic leak path: some gateways echo the Authorization
header back in a 401 response.child_process or
node:fs.Documented rather than silently missing:
failed with
its partial writes snapshotted; the plugin does not attempt to reconnect or re-resolve the
endpoint within a run. This is deliberately deferred — a failed run is always
safe (undo snapshot + full log) and cheap to re-run: section.replace is idempotent
and note.create/patch semantics refuse to double-apply, so simply re-running the
same crew after restarting LM Studio is the supported recovery path.state.json still running) is designed for "Obsidian crashed on this machine
mid-run". A vault synced across two concurrently-running Obsidian desktops (e.g.
via iCloud/Syncthing while both are open) is explicitly out of scope for V1 — see
design risk #8.runs/<id>/artifacts/.
Whenever a task's output fails schema/source-binding validation, the raw model
response is written to artifacts/<taskId>-1.txt; if the one repair attempt also
fails validation, its raw response is written to artifacts/<taskId>-2.txt. This
feeds the test-fixture corpus of real broken model outputs and is written under the
run directory — it is never counted as a vault write, never touches max_writes, and
is not snapshotted for undo. Successful runs write no artifacts at all.verboseLogging (Settings → Advanced) is reserved, not yet wired. The setting
exists and persists, but nothing currently reads it; full raw-output recording of
every call (success or failure) — beyond the failure-case artifacts/ capture
above — is not implemented.run.md at the top, not scrolled to the failed task.
"View failure" opens the run's log file via workspace.getLeaf().openFile() with
no ephemeral scroll state — you land at the top of the note and scroll to the
relevant ## section yourself.status: aborted with partial writes (undoable). With a
fast local model a whole run can finish in 1–2 s, so a click can land after the last
checkpoint and the run completes normally — that is correct (the work was already
done), not a lost click. The panel reflects this truthfully: while aborting it shows
"Abort requested…", and if the run finished first it states "the run finished before
the abort took effect — nothing was aborted" rather than freezing on a spinner. There
is deliberately no mechanism to throw away already-completed work.
A crew is Markdown in the vault: a team (crew-kind: team) as a pipeline of
collector → llm → actions, plus agent notes (crew-kind: agent, system prompt in
the body). The crews that ship with the plugin (command "Install example crews") are
editable examples — copy them and adjust.
temperature, max_tokens, thinking)An agent note can set three model values in its frontmatter; all are optional and override the plugin's Request settings for that agent.
temperature is sent as given; without it the profile value for the model family applies (0.1 for crews). max_tokens is the agent's token budget (default 2048); a thinking level can raise it to the model family's reserve so thinking cannot eat the whole budget. thinking is off, on or auto (default): off asks the model not to think, on asks for the medium level, auto follows the Request settings, whose default is off.
output:)An llm task declares its output format through an output: block:
frontmatter.set — the model proposes frontmatter values for source notes.
allowed_keys restricts which fields may be set. Paths are bound to the collected
material (no hallucination); the structural enum constraint binds values to those
actually present in the vault, but only takes effect once the field already has values
in that folder — on the very first run (no values yet, no value table) a restriction
comes from the instruction and the agent prompt alone.output:
family: frontmatter.set
allowed_keys: [tags, kategorie]
section.write — the model writes Markdown text, which section.replace puts
into the target of the following actions task. Optional max_chars
(default 16000).output:
family: section.write
The older names output_schema: triage-v1 / briefing-v1 remain valid as shorthands.
include_content)By default tasknotes.query returns frontmatter only. For crews that need the note
text (taggers, summarizers), set include_content: true:
collector: tasknotes.query
params:
folder: Notizen
where_missing: [tags]
include_content: true
write_scope)write_scope is a glob allowlist: a crew may write there and nowhere else. Set it as
narrowly as possible — and point the collector's folder and write_scope at the same
folder, otherwise proposals outside the write scope are discarded. The plugin-wide limit
"Max writes per run" caps every team's max_writes on top of that.
Code: AGPL-3.0-or-later — see LICENSE for the full text. Documentation: CC BY-SA 4.0 (LICENSE-DOCS). A commercial license is available on request — see LICENSING.md.