← The Tickd Guide

Tutorials & Guides

How to build a local markdown organiser using Gemini structured outputs and Python

Drowning in a sea of chaotic, half-written markdown files? Here is how to build a local Python tool that uses Gemini's structured outputs to automatically tag, categorise, and link your notes.

Updated 8/18/2026

The tragedy of the digital scrapheap

We have all been there. You start a local markdown vault in Obsidian, Logseq, or just a plain folder on your desktop with the grandest intentions. You are going to build a personal knowledge graph. You are going to map your mind.

Three months later, you have a digital scrapheap: 400 untitled files, half-baked thoughts, and a folder structure that looks like a digital explosion in a filing cabinet. Sorting this manually is a soul-crushing chore.

Fortunately, we can make an AI do the boring heavy lifting. In this tutorial, we are going to build a local Python tool that reads your messy markdown notes, analyses their contents, and outputs structured metadata (tags, categories, and recommended internal links) using Gemini's incredibly reliable JSON schemas.

Why Gemini structured outputs?

Historically, forcing LLMs to return reliable JSON was like trying to herd cats. You would write a prompt begging the model not to include conversational preamble like "Here is your JSON:", only for it to do it anyway and break your parsing script.

Using native structured outputs via /platforms/gemini changes the game. By defining a strict Pydantic schema in Python, we force the API to return a predictable, valid JSON object that matches our exact specification. It either succeeds perfectly or fails explicitly—no halfway-broken strings allowed. You can learn more about how schema validation fits into the wider AI landscape in our /glossary.

Let’s build it.

Step 1: Setting up the environment

First, make sure you have Python 3.10+ installed. Create a clean project folder and install the required dependencies. We will need the official Google GenAI SDK and Pydantic for defining our data structure.

`bash mkdir local-note-organiser cd local-note-organiser pip install google-genai pydantic `

You will also need a Gemini API key. Grab one from Google AI Studio and set it as an environment variable in your terminal:

`bash export GEMINI_API_KEY="your_api_key_here" `

Step 2: Defining the metadata schema

We want our organiser to analyse a markdown file and return three pieces of data: 1. A primary category (e.g., "Coding", "Recipes", "Work"). 2. A list of 3-5 highly relevant tags. 3. A brief, one-sentence summary to act as frontmatter.

Here is how we define this schema using Pydantic. Create a file named organiser.py and add the following imports and classes:

`python import os from pathlib import Path from google import genai from google.genai import types from pydantic import BaseModel, Field

Define the structure we want Gemini to return class NoteMetadata(BaseModel): category: str = Field(description="The single most accurate broad category for this note.") tags: list[str] = Field(description="List of 3 to 5 lowercase tags representing key concepts.") summary: str = Field(description="A concise, single-sentence summary of the note's contents.") ```

This Pydantic model defines the boundaries of our JSON object. Gemini's engine reads this schema to understand exactly what we expect, ensuring we do not get rogue fields we did not ask for.

Step 3: Writing the analysis engine

Now, let's initialise the Gemini client and write the function that passes our markdown text to the Gemini API. We will use the lightweight and fast gemini-2.5-flash model, which is perfect for high-throughput text processing tasks.

`python # Initialise the client (it automatically looks for GEMINI_API_KEY in env) client = genai.Client()

def analyse_note_content(content: str) -> NoteMetadata: prompt = f""" Analyse the following raw markdown note and extract metadata according to the requested schema. Note Content: {content} """ response = client.models.generate_content( model='gemini-2.5-flash', contents=prompt, config=types.GenerateContentConfig( response_mime_type="application/json", response_schema=NoteMetadata, temperature=0.2, # Keep it deterministic ), ) # Parse the raw JSON string back into our Pydantic model return NoteMetadata.model_validate_json(response.text) `

This is where the magic happens. By setting response_mime_type="application/json" and passing our NoteMetadata class into response_schema, we instruct the API to act as a strict JSON generator. It is the structured interface that makes these modern LLMs tick.

Step 4: Updating the files in-place

With our API function ready, we need a helper to read our local files, query the API, and write the metadata back to the top of the file as YAML frontmatter. If frontmatter already exists, we will overwrite it; if not, we will prepending it.

Add this helper logic to organiser.py:

`python def process_markdown_file(file_path: Path): print(f"Processing: {file_path.name}...") content = file_path.read_text(encoding='utf-8') # Strip existing frontmatter for the analysis run to avoid bias lines = content.splitlines() raw_body = content if len(lines) > 0 and lines[0] == '---': try: end_idx = lines.index('---', 1) raw_body = '\n'.join(lines[end_idx+1:]) except ValueError: pass if not raw_body.strip(): print(f"Skipping {file_path.name}: File is empty.") return

try: metadata = analyse_note_content(raw_body) except Exception as e: print(f"Failed to analyse {file_path.name}: {e}") return

Construct our clean frontmatter frontmatter = f"""--- category: {metadata.category} tags: """ for tag in metadata.tags: frontmatter += f" - {tag.lower().strip()}\n" frontmatter += f"summary: \"{metadata.summary.replace('\"', '\\\"')}\"\n---\n\n"

Write back to file file_path.write_text(frontmatter + raw_body.lstrip(), encoding='utf-8') print(f"Successfully updated {file_path.name}!") ```

Step 5: Putting it all together

Finally, let's write a simple CLI wrapper that scans a target directory for .md files and runs our processing loop. Add the final execution block to the bottom of your file:

`python def main(): # Change this path to your local vault directory vault_path = Path("./my_notes") if not vault_path.exists(): vault_path.mkdir(parents=True, exist_ok=True) # Create a dummy note for testing test_note = vault_path / "learning_rust.md" test_note.write_text("""I started looking into Rust programming today. I set up cargo, ran 'cargo init', and played with variable mutability. It is quite different from Python, especially with the borrow checker, but performance looks incredible.""") print(f"Created test note in '{vault_path}' directory. Run the script again to test!") return

markdown_files = list(vault_path.glob("*.md")) if not markdown_files: print(f"No markdown files found in {vault_path.absolute()}") return

for file_path in markdown_files: process_markdown_file(file_path)

if __name__ == "__main__": main() `

Running your local organiser

Create a folder named my_notes in your project folder, throw some unstructured text files in there, and run the script:

`bash python organiser.py `

When you open your test note, you should see cleanly formatted metadata waiting for you at the top:

`markdown --- category: Software Development tags: - rust - system programming - borrow checker summary: "An introduction to learning the Rust programming language, focusing on cargo setup and memory safety concepts." --- `

Troubleshooting and fine-tuning

If you find the model is putting notes into too many fragmented categories (like "Rust coding", "Python programming", "JS script" instead of a unified "Coding"), you can enforce consistency by modifying your Pydantic schema using a Literal type, limiting choices strictly to a predefined list:

`python from typing import Literal

class NoteMetadata(BaseModel): category: Literal["Coding", "Health", "Finance", "Personal", "Work"] = Field(...) `

If you run into API issues, check Google's official developer console or visit the Google Gemini Support site to verify your API quota status and billing settings.

Now sit back, run the script, and watch your digital junk drawer self-assemble into an organised library.

geminipythonmarkdownautomationproductivity

Keep going

Build something with the prompt generator, decode the jargon in the glossary, or compare the tools on our platform deep-dives.