create_note
Create a new markdown note with schema validation and automatic frontmatter generation. Resolves conventions from note schemas, applies templates, and ensures linting.
Instructions
Creates a new note with convention-aware defaults. Pass { path, content } and optionally frontmatter (overrides), noteSchema (explicit schema name). Resolves the applicable schema, applies its frontmatter template, merges overrides, writes the note, and lints it. Returns { root, path, frontmatter, lintResult }. Fails if the note already exists.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/tools/create-note-tool.ts:26-201 (handler)The main handler function for the create_note tool. Validates input via Zod schema, checks pre-existence, resolves note schema, merges frontmatter template with overrides (with template variable expansion), writes the note, lints it, and returns the result.
function makeCreateNoteTool(container: ServiceContainer): ToolHandler { return { name: "create_note", description: "Creates a new note with convention-aware defaults. Pass `{ path, content }` and optionally `frontmatter` (overrides), `noteSchema` (explicit schema name). Resolves the applicable schema, applies its frontmatter template, merges overrides, writes the note, and lints it. Returns `{ root, path, frontmatter, lintResult }`. Fails if the note already exists.", inputSchema: CreateNoteSchema, async handler(args): Promise<ToolResponse> { const services = requireServices(container); let notePath: string; let content: string; let fmOverrides: Record<string, unknown> | undefined; let explicitSchema: string | undefined; try { const parsed = CreateNoteSchema.parse(args); notePath = parsed.path; content = parsed.content; fmOverrides = parsed.frontmatter; explicitSchema = parsed.noteSchema; } catch (err) { return { content: [{ type: "text", text: JSON.stringify({ root: getRoot(container), error: err instanceof Error ? err.message : String(err), possibleSolutions: ["Check the required fields: path, content", "Ensure frontmatter values are valid types"], }) }], isError: true, }; } log.info({ notePath, explicitSchema }, "create_note called"); await services.schema.refresh(); try { await services.file.readNote(notePath); // If readNote succeeds, file already exists return { content: [{ type: "text", text: JSON.stringify({ root: getRoot(container), error: `Note already exists: "${notePath}". Use write_note with overwrite mode for existing files.`, possibleSolutions: ["Use write_note with mode: 'overwrite' to update an existing note", "Choose a different path for the new note"], }) }], isError: true, }; } catch (err) { // ENOENT = file doesn't exist, which is what we want — proceed // "Path not allowed" = path filter blocks it — writeNote will also fail, proceed const code = (err as NodeJS.ErrnoException).code; const isPathError = err instanceof Error && err.message.includes("Path not allowed"); if (code !== "ENOENT" && !isPathError) { log.error({ err, notePath }, "create_note: pre-existence check failed"); return { content: [{ type: "text", text: JSON.stringify({ root: getRoot(container), error: err instanceof Error ? err.message : String(err), possibleSolutions: ["Check the path is root-relative", "Ensure the path is not blocked (.obsidian, .git)"], }) }], isError: true, }; } } let resolvedSchemaName: string | null; if (explicitSchema !== undefined) { const knownSchemas = services.schema.listSchemas(); const noteSchemas = knownSchemas.filter((s) => s.type === "note"); const found = noteSchemas.find((s) => s.name === explicitSchema); if (!found) { log.error({ explicitSchema }, "create_note: note schema not found"); return { content: [{ type: "text", text: JSON.stringify({ root: getRoot(container), error: `Note schema "${explicitSchema}" not found. Available note schemas: ${noteSchemas.map((s) => s.name).join(", ") || "(none)"}`, possibleSolutions: ["Use list_schemas to see available note schemas", "Omit noteSchema to auto-resolve from the convention cascade"], }) }], isError: true, }; } resolvedSchemaName = explicitSchema; } else { const matchedSchema = services.schema.resolveNoteSchema(notePath); resolvedSchemaName = matchedSchema?.name ?? null; } let finalFrontmatter: Record<string, unknown> | undefined; if (resolvedSchemaName !== null) { const template = services.schema.getTemplate(resolvedSchemaName); const merged = { ...template.frontmatter, ...(fmOverrides ?? {}) }; const overrideKeys = new Set(Object.keys(fmOverrides ?? {})); const ctx = buildTemplateContext(notePath); for (const [key, value] of Object.entries(merged)) { if (!overrideKeys.has(key) && typeof value === "string") { merged[key] = expandTemplateVars(value, ctx); } } // When the caller explicitly chose a schema, persist that binding in // the note's frontmatter so subsequent lints resolve it via the // `note_schema` field — even in folders with no convention mapping. // Respect an explicit override if the caller set note_schema themselves. if (explicitSchema !== undefined && !overrideKeys.has("note_schema")) { merged.note_schema = explicitSchema; } finalFrontmatter = merged; log.debug( { templateKeys: Object.keys(template.frontmatter), overrideKeys: Object.keys(fmOverrides ?? {}), }, "create_note: template merged with overrides and expanded", ); } else { finalFrontmatter = fmOverrides; } try { await services.file.writeNote(notePath, content, finalFrontmatter, "overwrite"); log.info({ notePath }, "create_note: note written"); } catch (err) { log.error({ err, notePath }, "create_note: write failed"); return { content: [{ type: "text", text: JSON.stringify({ root: getRoot(container), error: err instanceof Error ? err.message : String(err), possibleSolutions: ["Check the path is root-relative and not blocked", "Ensure parent directories are accessible"], }) }], isError: true, }; } let lintResult: LintResult | null = null; if (resolvedSchemaName !== null) { try { lintResult = await services.schema.lintNote(notePath); log.info( { schema: resolvedSchemaName, pass: lintResult.pass, checkCount: lintResult.checks.length, }, "create_note: lint complete", ); } catch (err) { log.warn({ err, notePath }, "create_note: lint failed (note was written)"); } } // Read back to capture any YAML round-trip normalization (e.g. date coercion) let writtenFrontmatter: Record<string, unknown> = finalFrontmatter ?? {}; try { const note = await services.file.readNote(notePath); writtenFrontmatter = note.frontmatter; } catch { // Fall back to intended values } log.info({ notePath, hasLint: lintResult !== null }, "create_note complete"); return { content: [ { type: "text", text: JSON.stringify( { root: getRoot(container), path: notePath, frontmatter: writtenFrontmatter, lintResult }, null, 2, ), }, ], }; }, }; } - src/tools/create-note-tool.ts:13-24 (schema)Zod schema defining the input parameters: path (required), content (defaults to empty string), frontmatter (optional key-value overrides), and noteSchema (optional explicit schema name).
const CreateNoteSchema = z.object({ path: z.string().describe("Root-relative path for the new note (e.g. Knowledge/MyNote.md)"), content: z.string().default("").describe("Note body content (default: empty string)"), frontmatter: z .record(z.string(), z.unknown()) .optional() .describe("Frontmatter overrides merged on top of template defaults"), noteSchema: z .string() .optional() .describe("Explicit note schema name. If omitted, resolved from the convention cascade for the note's folder"), }); - src/tools/create-note-tool.ts:207-216 (registration)Registration function that creates the tool handler via makeCreateNoteTool and adds it to the shared registry map under its name 'create_note'.
export function registerCreateNoteTool( registry: Map<string, ToolHandler>, container: ServiceContainer, ): void { const tools = [makeCreateNoteTool(container)]; for (const tool of tools) { registry.set(tool.name, tool); } } - src/tools/index.ts:78-79 (registration)The call-site in registerTools (the central tool registration orchestrator) that invokes registerCreateNoteTool.
registerCreateNoteTool(registry, container); registerLinkTools(registry, container); - src/types.ts:48-50 (helper)The ServiceContainer interface (from types.ts) used to pass services into tool handlers. The container wraps a nullable Services reference that gets reassigned on directory switch.
export interface ServiceContainer { services: Services | null; }