- Add Docker image for the official Obsidian desktop app and CLI. - Start Obsidian headlessly with Xvfb and DBus setup. - Expose a safe structured HTTP command API without shell execution. - Add JWT-based vault, path, and command authorization. - Support single-vault and multi-vault container mounts. - Add TypeScript SDK helpers for Obsidian CLI commands. - Add n8n community node package with Obsidian operations. - Add docs, compose config, tests, and production image workflow.
3.6 KiB
n8n usage notes
Use the TypeScript SDK from an n8n Code node when n8n can import local or bundled npm packages. In self-hosted n8n this usually means installing the SDK where n8n can resolve it and allowing external modules, for example with NODE_FUNCTION_ALLOW_EXTERNAL=@obsidian-api/sdk or NODE_FUNCTION_ALLOW_EXTERNAL=*.
The SDK only sends structured args arrays to the HTTP API. It never sends shell command strings.
Environment variables
Recommended n8n env vars:
OBSIDIAN_API_URL=http://obsidian-api:8080
OBSIDIAN_API_JWT=<jwt-from-scripts/create_jwt.py>
OBSIDIAN_VAULT=work
Recommended: use a restricted JWT for n8n. Create it with:
OBSIDIAN_JWT_SECRET=change-this-long-random-secret \
python scripts/create_jwt.py \
--sub n8n \
--vault work \
--path Inbox/n8n/ \
--path Automation.md \
--command create \
--command read \
--command append \
--command search \
--command search:context \
--days 365
The JWT contains claims like:
{
"sub": "n8n",
"vaults": ["work"],
"paths": ["Inbox/n8n/", "Automation.md"],
"commands": ["create", "read", "append", "search", "search:context"],
"exp": 1791536000
}
If n8n runs outside the Docker network, use the exposed host URL instead:
OBSIDIAN_API_URL=http://localhost:8080
Basic Code node
import { ObsidianCliClient } from '@obsidian-api/sdk';
const obsidian = new ObsidianCliClient({
baseUrl: process.env.OBSIDIAN_API_URL!,
token: process.env.OBSIDIAN_API_JWT!,
vault: process.env.OBSIDIAN_VAULT,
});
const title = $json.title ?? 'n8n note';
const content = `# ${title}\n\nCreated from n8n.\n`;
const result = await obsidian.create({
path: `Inbox/${title}.md`,
content,
overwrite: true,
});
return [{ json: result }];
Append incoming webhook payload to a daily note
import { ObsidianCliClient } from '@obsidian-api/sdk';
const obsidian = new ObsidianCliClient({
baseUrl: process.env.OBSIDIAN_API_URL!,
token: process.env.OBSIDIAN_API_JWT!,
vault: process.env.OBSIDIAN_VAULT,
});
const payload = JSON.stringify($json, null, 2).replace(/```/g, '``\\`');
const result = await obsidian.dailyAppend(
`\n## n8n event\n\n\`\`\`json\n${payload}\n\`\`\`\n`,
);
return [{ json: result }];
Search notes
import { ObsidianCliClient } from '@obsidian-api/sdk';
const obsidian = new ObsidianCliClient({
baseUrl: process.env.OBSIDIAN_API_URL!,
token: process.env.OBSIDIAN_API_JWT!,
vault: process.env.OBSIDIAN_VAULT,
});
const result = await obsidian.search($json.query, { format: 'json' });
return [{ json: { matches: JSON.parse(result.stdout) } }];
Multi-vault usage
If the API container registers multiple vaults, use a scoped client per vault:
const base = new ObsidianCliClient({
baseUrl: process.env.OBSIDIAN_API_URL!,
token: process.env.OBSIDIAN_API_JWT!,
});
const work = base.inVault('work');
const personal = base.inVault('personal');
await work.create({ path: 'Inbox/Work.md', content: 'Work note', overwrite: true });
await personal.create({ path: 'Inbox/Personal.md', content: 'Personal note', overwrite: true });
Without SDK
If package imports are not available in your n8n environment, call the HTTP API directly:
const response = await fetch(`${process.env.OBSIDIAN_API_URL}/commands`, {
method: 'POST',
headers: {
authorization: `Bearer ${process.env.OBSIDIAN_API_JWT}`,
'content-type': 'application/json',
},
body: JSON.stringify({
args: ['vault=work', 'create', 'path=Inbox/n8n.md', 'content=Hello from n8n', 'overwrite'],
}),
});
return [{ json: await response.json() }];