Article · 2024-01-01

Building an Intelligent Content Management Pipeline from Obsidian to Notion: A Three-Layer Architecture for Knowledge Automation

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│  Content Source │───▶│  AI Processing  │───▶│   Sync Target   │
│   Obsidian      │    │ Claude/Gemini   │    │    Notion       │
│                 │    │                 │    │                 │
│ • Unified MD    │    │ • Summarization │    │ • Publishing    │
│   Storage       │    │ • Smart Analysis│    │   Platform      │
│ • Local Editing │    │ • Format        │    │ • Team          │
│ • Version       │    │   Conversion    │    │   Collaboration │
│   Control       │    │                 │    │ • Database      │
│                 │    │                 │    │   Views         │
└─────────────────┘    └─────────────────┘    └─────────────────┘

Layer Responsibility Analysis

Layer Function Key Technologies/Plugins
① Content Source Layer Centralize all .md files in Obsidian Vault and enable external script read/write access • Direct filesystem access (simplest)
• Local REST API Plugin: HTTPS endpoints supporting read/create/PATCH note operations
• Advanced URI Plugin: Append content via obsidian://advanced-uri?...mode=append parameters
② AI Summary Layer Use Claude Code or Gemini CLI to batch read notes and generate summaries • Claude Code + MCP: Wrap Obsidian REST endpoints as tools, Claude can use list_notes / read_note / patch_note
• Gemini CLI: Built-in MCP extension points, can use gemini run --prompt in scripts to generate summaries, supports 1M token context, script automation
③ Sync Target Layer Write summaries to Notion (single pages or database entries) • Official Notion API: POST /v1/pages can create pages with content blocks in one call; only requires "Add connections" permission
• Auto-GPT-Notion Plugin: Pre-wrapped notion_create_page / notion_append_page commands for direct Notion writing

Detailed Technical Implementation

1. Content Source Layer: Obsidian Configuration

Installing Essential Plugins

Local REST API Plugin

# Search "Local REST API" in Obsidian plugin marketplace and enable
# Settings → Local REST API → Generate Key
# Default port: 27123, recommend 127.0.0.1 binding + strong password

Advanced URI Plugin

# Search "Advanced URI" in plugin marketplace and enable
# Supports note operations via URL parameters:
# obsidian://advanced-uri?vault=MyVault&mode=append&file=Note.md&data=New%20content

API Endpoint Configuration

After installing the Local REST API Plugin, you'll have access to these endpoints:

// Get notes list
GET https://localhost:27123/notes?key=API_KEY

// Read specific note
GET https://localhost:27123/notes/{path}?key=API_KEY

// Create new note
POST https://localhost:27123/notes?key=API_KEY
{
  "path": "NewNote.md",
  "content": "Note content"
}

// Update note
PATCH https://localhost:27123/notes/{path}?key=API_KEY
{
  "content": "Updated content"
}

2. AI Processing Layer: Claude Code / Gemini CLI Integration

Claude Code Integration

MCP Tool Configuration

# obsidian_rest.yaml
name: obsidian_rest
description: Obsidian REST API integration
endpoints:
  - name: list_notes
    url: "https://localhost:27123/notes"
    method: GET
    headers:
      Authorization: "Bearer ${OBSIDIAN_API_KEY}"
  - name: read_note
    url: "https://localhost:27123/notes/{path}"
    method: GET
    headers:
      Authorization: "Bearer ${OBSIDIAN_API_KEY}"
  - name: update_note
    url: "https://localhost:27123/notes/{path}"
    method: PATCH
    headers:
      Authorization: "Bearer ${OBSIDIAN_API_KEY}"
      Content-Type: "application/json"

Usage Example

# Register tool
claude tools add obsidian_rest.yaml

# Let Claude process notes
claude chat "Please read all my notes and generate 200-character summaries for each"

Gemini CLI Integration

Installation and Setup

# Install Gemini CLI
brew install gemini-cli  # or go install

# Configure API Key
export GEMINI_API_KEY=your_api_key_here

Batch Processing Script Example

import os
import requests
import subprocess
import json

def process_notes_with_gemini(vault_path, api_key):
    """Batch process notes using Gemini CLI"""
    
    # Get all markdown files
    md_files = []
    for root, dirs, files in os.walk(vault_path):
        for file in files:
            if file.endswith('.md'):
                md_files.append(os.path.join(root, file))
    
    summaries = []
    
    for md_file in md_files:
        # Read file content
        with open(md_file, 'r', encoding='utf-8') as f:
            content = f.read()
        
        # Generate summary using Gemini CLI
        prompt = f"Please generate a 200-word summary in English for the following content, focusing on key information and core viewpoints:\n\n{content}"
        
        try:
            result = subprocess.run([
                'gemini', 'chat', 
                '-i', prompt,
                '-p', 'Summarize in English'
            ], capture_output=True, text=True, timeout=30)
            
            if result.returncode == 0:
                summary = result.stdout.strip()
                summaries.append({
                    'file': md_file,
                    'original_content': content,
                    'summary': summary
                })
                print(f"✅ Processed: {os.path.basename(md_file)}")
            else:
                print(f"❌ Processing failed: {md_file} - {result.stderr}")
                
        except subprocess.TimeoutExpired:
            print(f"⏱️ Processing timeout: {md_file}")
        except Exception as e:
            print(f"🚫 Processing exception: {md_file} - {str(e)}")
    
    return summaries

3. Sync Target Layer: Notion Integration

Notion API Configuration

Create Integration

# 1. Visit https://www.notion.com/my-integrations
# 2. Click "New integration"
# 3. Get Internal Integration Secret
# 4. In target page → ... → Add connections → Check your integration

Permission Configuration

{
  "capabilities": [
    "read_content",
    "update_content", 
    "insert_content"
  ]
}

Notion SDK Usage Examples

Python Implementation

import os
from notion_client import Client

def sync_to_notion(summaries, notion_token, database_id):
    """Sync summaries to Notion database"""
    
    notion = Client(auth=notion_token)
    
    for item in summaries:
        file_name = os.path.basename(item['file'])
        summary = item['summary']
        
        try:
            # Create new page
            new_page = notion.pages.create(
                parent={"database_id": database_id},
                properties={
                    "Name": {
                        "title": [
                            {
                                "text": {
                                    "content": file_name.replace('.md', '')
                                }
                            }
                        ]
                    },
                    "Source": {
                        "rich_text": [
                            {
                                "text": {
                                    "content": "Obsidian"
                                }
                            }
                        ]
                    },
                    "Status": {
                        "select": {
                            "name": "Processed"
                        }
                    }
                },
                children=[
                    {
                        "object": "block",
                        "type": "paragraph",
                        "paragraph": {
                            "rich_text": [
                                {
                                    "text": {
                                        "content": summary
                                    }
                                }
                            ]
                        }
                    }
                ]
            )
            
            print(f"✅ Synced to Notion: {file_name}")
            
        except Exception as e:
            print(f"❌ Sync failed: {file_name} - {str(e)}")

Node.js Implementation

const { Client } = require('@notionhq/client');

const notion = new Client({
  auth: process.env.NOTION_TOKEN,
});

async function syncToNotionDatabase(summaries, databaseId) {
  for (const item of summaries) {
    const fileName = path.basename(item.file, '.md');
    
    try {
      await notion.pages.create({
        parent: { database_id: databaseId },
        properties: {
          'Name': {
            title: [{ text: { content: fileName } }]
          },
          'Summary': {
            rich_text: [{ text: { content: item.summary } }]
          },
          'Created': {
            date: { start: new Date().toISOString() }
          }
        },
        children: [
          {
            object: 'block',
            type: 'paragraph',
            paragraph: {
              rich_text: [{ text: { content: item.summary } }]
            }
          }
        ]
      });
      
      console.log(`✅ Synced: ${fileName}`);
    } catch (error) {
      console.error(`❌ Sync failed: ${fileName}`, error);
    }
  }
}

End-to-End Workflow Example

Complete Automation Script

#!/usr/bin/env python3
"""
Obsidian -> AI -> Notion Automated Processing Pipeline
"""

import os
import sys
import json
import time
import requests
import subprocess
from datetime import datetime
from notion_client import Client

class ContentPipeline:
    def __init__(self, config):
        self.obsidian_api_key = config['obsidian_api_key']
        self.obsidian_base_url = config['obsidian_base_url']
        self.notion_token = config['notion_token']
        self.notion_database_id = config['notion_database_id']
        self.ai_model = config.get('ai_model', 'gemini')  # 'gemini' or 'claude'
        
        self.notion = Client(auth=self.notion_token)
    
    def get_updated_notes(self, since_hours=24):
        """Get recently updated notes"""
        try:
            response = requests.get(
                f"{self.obsidian_base_url}/notes",
                params={'key': self.obsidian_api_key}
            )
            response.raise_for_status()
            
            all_notes = response.json()
            
            # Filter recently updated notes
            cutoff_time = time.time() - (since_hours * 3600)
            updated_notes = []
            
            for note in all_notes:
                if note.get('mtime', 0) > cutoff_time:
                    updated_notes.append(note)
            
            return updated_notes
            
        except Exception as e:
            print(f"❌ Failed to get notes: {str(e)}")
            return []
    
    def read_note_content(self, note_path):
        """Read note content"""
        try:
            response = requests.get(
                f"{self.obsidian_base_url}/notes/{note_path}",
                params={'key': self.obsidian_api_key}
            )
            response.raise_for_status()
            return response.text
            
        except Exception as e:
            print(f"❌ Failed to read note {note_path}: {str(e)}")
            return None
    
    def generate_summary_with_gemini(self, content):
        """Generate summary using Gemini"""
        prompt = f"""Please generate a structured summary for the following content, including:
1. Core viewpoints (2-3 sentences)
2. Key information points (3-5 points)
3. Practical suggestions (if applicable)

Content:
{content}
"""
        
        try:
            result = subprocess.run([
                'gemini', 'chat',
                '-i', prompt
            ], capture_output=True, text=True, timeout=60)
            
            if result.returncode == 0:
                return result.stdout.strip()
            else:
                print(f"❌ Gemini processing failed: {result.stderr}")
                return None
                
        except Exception as e:
            print(f"❌ Gemini call exception: {str(e)}")
            return None
    
    def generate_summary_with_claude(self, content):
        """Generate summary using Claude Code"""
        # This can integrate Claude Code MCP tools
        # or use Anthropic API
        pass
    
    def sync_to_notion(self, note_path, original_content, summary):
        """Sync to Notion"""
        file_name = os.path.basename(note_path).replace('.md', '')
        
        try:
            # Check if already exists
            existing = self.notion.databases.query(
                database_id=self.notion_database_id,
                filter={
                    "property": "Source File",
                    "rich_text": {
                        "equals": note_path
                    }
                }
            )
            
            page_data = {
                "properties": {
                    "Name": {
                        "title": [{"text": {"content": file_name}}]
                    },
                    "Source File": {
                        "rich_text": [{"text": {"content": note_path}}]
                    },
                    "Last Updated": {
                        "date": {"start": datetime.now().isoformat()}
                    },
                    "Status": {
                        "select": {"name": "Processed"}
                    }
                },
                "children": [
                    {
                        "object": "block",
                        "type": "heading_2",
                        "heading_2": {
                            "rich_text": [{"text": {"content": "AI Generated Summary"}}]
                        }
                    },
                    {
                        "object": "block",
                        "type": "paragraph",
                        "paragraph": {
                            "rich_text": [{"text": {"content": summary}}]
                        }
                    },
                    {
                        "object": "block",
                        "type": "heading_2", 
                        "heading_2": {
                            "rich_text": [{"text": {"content": "Original Content"}}]
                        }
                    },
                    {
                        "object": "block",
                        "type": "code",
                        "code": {
                            "language": "markdown",
                            "rich_text": [{"text": {"content": original_content[:2000]}}]  # Limit length
                        }
                    }
                ]
            }
            
            if existing['results']:
                # Update existing page
                page_id = existing['results'][0]['id']
                self.notion.pages.update(page_id=page_id, **page_data)
                print(f"🔄 Updated: {file_name}")
            else:
                # Create new page
                page_data["parent"] = {"database_id": self.notion_database_id}
                self.notion.pages.create(**page_data)
                print(f"✅ Created: {file_name}")
                
        except Exception as e:
            print(f"❌ Notion sync failed {file_name}: {str(e)}")
    
    def run_pipeline(self):
        """Run complete pipeline"""
        print("🚀 Starting content processing pipeline...")
        
        # 1. Get updated notes
        updated_notes = self.get_updated_notes()
        if not updated_notes:
            print("📝 No updated notes found")
            return
        
        print(f"📋 Found {len(updated_notes)} updated notes")
        
        # 2. Process each note
        for note in updated_notes:
            note_path = note['path']
            print(f"📄 Processing note: {note_path}")
            
            # Read content
            content = self.read_note_content(note_path)
            if not content:
                continue
            
            # Generate summary
            if self.ai_model == 'gemini':
                summary = self.generate_summary_with_gemini(content)
            else:
                summary = self.generate_summary_with_claude(content)
            
            if not summary:
                print(f"⚠️ Skipping note with failed summary generation: {note_path}")
                continue
            
            # Sync to Notion
            self.sync_to_notion(note_path, content, summary)
            
            # Avoid API rate limits
            time.sleep(1)
        
        print("✨ Pipeline run completed!")

def main():
    # Configuration
    config = {
        'obsidian_api_key': os.getenv('OBSIDIAN_API_KEY'),
        'obsidian_base_url': 'https://localhost:27123',
        'notion_token': os.getenv('NOTION_TOKEN'),
        'notion_database_id': os.getenv('NOTION_DATABASE_ID'),
        'ai_model': 'gemini'  # or 'claude'
    }
    
    # Validate configuration
    required_keys = ['obsidian_api_key', 'notion_token', 'notion_database_id']
    for key in required_keys:
        if not config[key]:
            print(f"❌ Missing required configuration: {key}")
            sys.exit(1)
    
    # Run pipeline
    pipeline = ContentPipeline(config)
    pipeline.run_pipeline()

if __name__ == "__main__":
    main()

Automation Deployment

cron Scheduled Tasks

# Run daily at 23:30
30 23 * * * /usr/bin/python3 /path/to/content_pipeline.py >> /path/to/logs/pipeline.log 2>&1

# Check for updates every hour
0 * * * * /usr/bin/python3 /path/to/content_pipeline.py --incremental >> /path/to/logs/pipeline.log 2>&1

systemd Service

[Unit]
Description=Obsidian to Notion Content Pipeline
After=network.target

[Service]
Type=oneshot
User=your-username
WorkingDirectory=/path/to/pipeline
ExecStart=/usr/bin/python3 /path/to/content_pipeline.py
EnvironmentFile=/path/to/.env

[Install]
WantedBy=multi-user.target

Key Limitations & Best Practices

Platform Limitations

Platform Limitations & Best Practices
Obsidian REST API default port 27123, recommend 127.0.0.1 binding + strong password; Advanced URI requires Obsidian running
Claude Code 200k-token per request; use stream=true to reduce waiting; avoid processing too many notes at once, merge by topic first
Gemini CLI Free quota 60 req/min, 1000 req/day; login via GEMINI_API_KEY or Google account
Notion children ≤ 100 blocks per request; long summaries need batched append_block_children. Database property keys must match columns

Performance Optimization Recommendations

  1. Batch Processing Strategy

    • Merge related notes by topic before processing
    • Use incremental sync, only process changed content
    • Implement intelligent deduplication to avoid reprocessing
  2. Error Handling

    • Implement retry mechanisms for network fluctuations
    • Log detailed information for troubleshooting
    • Set timeout controls to avoid long-term blocking
  3. Content Quality Control

    • Set content length thresholds to filter invalid notes
    • Implement summary quality assessment to ensure output quality
    • Review summaries before publishing important content

Security Considerations

  1. API Key Management

    # Use environment variables
    export OBSIDIAN_API_KEY="your_key_here"
    export NOTION_TOKEN="secret_token_here"
    export GEMINI_API_KEY="api_key_here"
    
    # Or use .env file
    echo "OBSIDIAN_API_KEY=your_key" >> .env
    echo "NOTION_TOKEN=secret_token" >> .env
    chmod 600 .env
    
  2. Network Security

    • Bind Obsidian REST API to local address only
    • Use HTTPS and strong passwords to protect endpoints
    • Consider using VPN or tunneling for encrypted communication

Quick Start Guide

5-Minute Setup

Step 1: Obsidian Setup

# 1. Search "Local REST API" in plugin marketplace -> Enable
# 2. Settings → Local REST API → Generate Key
# 3. Record API Key and port (default 27123)

Step 2: Notion Setup

# 1. Visit https://www.notion.com/my-integrations → New integration
# 2. Get Internal Integration Secret
# 3. In target page → ... → Add connections → Check integration
# 4. Create database, record Database ID

Step 3: AI Tool Configuration

# Gemini CLI installation
brew install gemini-cli
export GEMINI_API_KEY=your_api_key

# Or Claude Code configuration
claude tools add obsidian_rest.yaml notion_sdk.yaml

Step 4: Run Test

# Download example script
git clone https://github.com/example/obsidian-notion-pipeline
cd obsidian-notion-pipeline

# Configure environment variables
cp .env.example .env
# Edit .env file, fill in your API keys

# Install dependencies
pip install -r requirements.txt

# Run test
python content_pipeline.py --test

Step 5: Automation Deployment

# Set up scheduled task
crontab -e
# Add: 30 23 * * * /usr/bin/python3 /path/to/content_pipeline.py

# Or use systemd
sudo cp pipeline.service /etc/systemd/system/
sudo systemctl enable pipeline.service
sudo systemctl start pipeline.service

Extended Application Scenarios

Bidirectional Sync

If you need to sync Notion updates back to Obsidian later, you can implement a reverse pipeline:

def sync_notion_to_obsidian(self):
    """Sync updates from Notion back to Obsidian"""
    
    # Query recently updated Notion pages
    recent_pages = self.notion.databases.query(
        database_id=self.notion_database_id,
        filter={
            "property": "Last Edited Time",
            "date": {
                "after": (datetime.now() - timedelta(hours=24)).isoformat()
            }
        }
    )
    
    for page in recent_pages['results']:
        source_file = page['properties']['Source File']['rich_text'][0]['text']['content']
        
        # Get Notion page content
        blocks = self.notion.blocks.children.list(block_id=page['id'])
        content = self.extract_content_from_blocks(blocks)
        
        # Update Obsidian note
        self.update_obsidian_note(source_file, content)

Multi-Source Integration

Support collecting content from multiple data sources:

class MultiSourcePipeline(ContentPipeline):
    def __init__(self, config):
        super().__init__(config)
        self.sources = {
            'obsidian': self.process_obsidian_notes,
            'markdown_files': self.process_local_markdown,
            'web_content': self.process_web_bookmarks,
            'email_attachments': self.process_email_pdfs
        }
    
    def run_multi_source_pipeline(self):
        all_content = []
        
        for source_name, processor in self.sources.items():
            try:
                content = processor()
                all_content.extend(content)
                print(f"✅ Processing completed: {source_name}")
            except Exception as e:
                print(f"❌ Processing failed: {source_name} - {str(e)}")
        
        # Process all content uniformly
        self.batch_process_content(all_content)

Conclusion

The three-layer architecture centralizes all markdown content in Obsidian, uses Claude Code or Gemini CLI to generate summaries, and publishes them to Notion:

  1. Centralized Management: Single source of truth for all markdown content
  2. Intelligent Processing: Automated summary generation and analysis
  3. Efficient Distribution: Notion database for team collaboration and knowledge sharing
  4. Automated Workflow: Scheduled synchronization without manual intervention

Knowledge workers can focus on writing while the pipeline handles organization, summarization, and distribution automatically.

© 2026 Yuxu Ge ·