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 |
| ③ 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
Batch Processing Strategy
- Merge related notes by topic before processing
- Use incremental sync, only process changed content
- Implement intelligent deduplication to avoid reprocessing
Error Handling
- Implement retry mechanisms for network fluctuations
- Log detailed information for troubleshooting
- Set timeout controls to avoid long-term blocking
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
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 .envNetwork 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:
- Centralized Management: Single source of truth for all markdown content
- Intelligent Processing: Automated summary generation and analysis
- Efficient Distribution: Notion database for team collaboration and knowledge sharing
- Automated Workflow: Scheduled synchronization without manual intervention
Knowledge workers can focus on writing while the pipeline handles organization, summarization, and distribution automatically.