I built a food logging system in Obsidian that turns this:
- coffee
- scrambled eggs
- chicken breast with broccoli
Into this:
- coffee (8 oz): 2 cal | 0.3g protein | 0g carbs | 0g fat
- scrambled eggs (2 eggs): 140 cal | 12g protein | 2g carbs | 10g fat
- chicken breast with broccoli (6oz chicken, 1 cup): 350 cal | 45g protein | 25g carbs | 8g fat
With just two commands. Here's how I did it.
The Problem with Traditional Food Tracking Apps
I've tried every food tracking app: MyFitnessPal, Cronometer, LoseIt, you name it. They all have the same issues:
- Context switching breaks flow - Opening a separate app interrupts my workflow
- Manual entry is tedious - Searching databases, scanning barcodes, measuring portions
- Database limitations - Can't easily log custom or combined meals
- Subscription costs - $10-15/month adds up
- Data silos - Your food log is trapped in their system
I wanted something that lived in my note-taking system and was as easy as jotting down what I ate.
The Solution: Obsidian + QuickAdd + Gemini AI
I built a system that combines:
- QuickAdd plugin - Custom commands to run JavaScript macros
- Note Toolbar plugin - Clickable buttons for instant access
- Google Gemini AI - Natural language nutrition lookup
- Obsidian Bases - Dynamic filtered views for tracking
The result: nutrition tracking that's as easy as writing a bullet list.
How It Works
1. Open Today's Food Log (One Command)
Run "Open Today's Food Log" from the command palette (or click a toolbar button), and it instantly opens or creates Health/Food/Log/YYYY-MM-DD.md with this structure:
---
tags: foodlog
calories: 0
fat: 0
carbs: 0
protein: 0
created: 2026-01-29
---
My daily journal note with a button to quickly navigate to the food log for the day
2. Type Your Meals (Natural Language)
Just type what you ate in simple bullet format:
- mushroom coffee
- banana
- chicken breast with broccoli and peas
Each line is one meal. You can be as specific or vague as you want. The AI handles the rest.
3. Calculate Nutrition (One Command)
Run "Calculate Nutrition". The AI:
- Looks up each food item
- Estimates quantities if not specified
- Calculates nutrition for combined meals
- Formats everything consistently
- Updates frontmatter totals automatically
Result:
---
tags: foodlog
calories: 457
fat: 9
carbs: 53
protein: 47
created: 2026-01-29
---
- mushroom coffee (1 cup): 5 cal | 0.5g protein | 1g carbs | 0g fat
- banana (1 medium): 105 cal | 1.3g protein | 27g carbs | 0.4g fat
- chicken breast with broccoli and peas (6oz chicken, 1 cup veggies): 350 cal | 45g protein | 25g carbs | 8g fat
My food log of the day
4. View and Track with Obsidian Bases
Once you have multiple food logs, you can create a dynamic view using Obsidian's built-in Bases feature to see all your logs in one place.
What are Obsidian Bases?
Bases are Obsidian's native feature for creating filtered, sorted views of your notes—like a database query. You can display notes in table format, cards, or lists based on their properties.
Create a Food Log Base:
Create a file called Food Log.base in a Bases/ folder with this configuration:
filters:
and:
- file.folder == "Health/Food/Log"
views:
- type: table
name: All Logs
order:
- file.name
- calories
- protein
- carbs
- fat
- created
sort:
property: created
direction: descending
columnSize:
file.name: 120
calories: 80
protein: 80
carbs: 80
fat: 80
created: 100
What this does:
-
Filters only files in
Health/Food/Log/folder - Displays a table with file name, calories, protein, carbs, fat, and created date
- Sorts by created date (newest first)
- Sets column widths for readability
Embed the Base:
Add this to any note (like your daily note or dashboard):
![[Food Log.base]]
You'll see a table showing all your food logs with their nutrition totals. You can:
- Sort by any column (click column headers)
- Filter by date ranges
- See trends at a glance
- Click any row to open that day's log
Example Output:
Simple base view of my food logs
Advanced: Add Calculated Views
You can create multiple views in the same base:
views:
- type: table
name: All Logs
# ... (as above)
- type: table
name: This Week
filters:
and:
- created >= today() - "7 days"
# ... (same order/sort)
- type: table
name: High Protein Days
filters:
and:
- protein >= 150
# ... (same order/sort)
Then embed specific views:
![[Food Log.base#This Week]]
![[Food Log.base#High Protein Days]]
This gives you a dashboard-style view of your nutrition data, all within Obsidian, with zero external tools.
Technical Implementation
Script 1: open-todays-food-log.js
This script opens or creates today's food log file.
module.exports = async (params) => {
const { app } = params;
// Get today's date in YYYY-MM-DD format
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}`;
// Construct file path
const filePath = `Health/Food/Log/${dateStr}.md`;
// Check if file exists
let file = app.vault.getAbstractFileByPath(filePath);
if (!file) {
// Create template content
const content = `---
tags: foodlog
calories: 0
fat: 0
carbs: 0
protein: 0
created: ${dateStr}
---
`;
// Create the file
file = await app.vault.create(filePath, content);
new Notice(`Created food log for ${dateStr}`);
}
// Open the file
const leaf = app.workspace.getLeaf();
await leaf.openFile(file);
};
Script 2: calculate-nutrition.js
This is where the magic happens. The script processes food entries using Gemini AI.
// QuickAdd Macro: Calculate Nutrition
module.exports = async (params) => {
const { app } = params;
const API_KEY = "YOUR_GEMINI_API_KEY_HERE"; // Get from https://aistudio.google.com/app/apikey
// ====== AI NUTRITION API ======
async function getNutrition(foodDescription) {
const prompt = `You are a nutrition calculator. Return ONLY valid JSON with no markdown formatting.
Food description: "${foodDescription}"
Return this exact JSON structure:
{"description":"food name (quantity)","calories":number,"protein":number,"carbs":number,"fat":number}
Guidelines:
- This may be a single food OR a combined meal
- For combined meals, calculate TOTAL nutrition for ALL foods together
- Parse food and estimate quantity if not specified
- Use standard USDA nutrition data
- Round to 1 decimal place
- Be specific in description
- If quantity is mentioned, include it in description`;
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent?key=${API_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }]
}),
signal: controller.signal
}
);
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
const text = data.candidates[0].content.parts[0].text;
const cleanText = text.replace(/```
{% endraw %}
json\s*|\s*
{% raw %}
```/g, '').trim();
const nutrition = JSON.parse(cleanText);
return nutrition;
} catch (error) {
throw new Error(`Failed to get nutrition: ${error.message}`);
}
}
// ====== HELPER FUNCTIONS ======
function hasDetailedNutrition(line) {
return /- [^:]+: [\d.]+ cal \| [\d.]+g protein \| [\d.]+g carbs \| [\d.]+g fat/.test(line);
}
function isBulletItem(line) {
return /^\s*[-•*]\s+.+/.test(line);
}
function extractFoodText(line) {
return line.replace(/^\s*[-•*]\s+/, '').trim();
}
function parseNutritionEntry(line) {
const regex = /- [^:]+: ([\d.]+) cal \| ([\d.]+)g protein \| ([\d.]+)g carbs \| ([\d.]+)g fat/;
const match = line.match(regex);
if (!match) return null;
return {
calories: parseFloat(match[1]),
protein: parseFloat(match[2]),
carbs: parseFloat(match[3]),
fat: parseFloat(match[4])
};
}
function calculateTotals(content) {
const lines = content.split('\n');
let totals = { calories: 0, protein: 0, carbs: 0, fat: 0 };
for (const line of lines) {
const nutrition = parseNutritionEntry(line);
if (nutrition) {
totals.calories += nutrition.calories;
totals.protein += nutrition.protein;
totals.carbs += nutrition.carbs;
totals.fat += nutrition.fat;
}
}
return {
calories: parseFloat(totals.calories.toFixed(1)),
protein: parseFloat(totals.protein.toFixed(1)),
carbs: parseFloat(totals.carbs.toFixed(1)),
fat: parseFloat(totals.fat.toFixed(1))
};
}
function updateFrontmatter(content, totals) {
const parts = content.split('---');
if (parts.length < 3) {
const frontmatter = `---
protein: ${totals.protein}
carbs: ${totals.carbs}
fat: ${totals.fat}
calories: ${totals.calories}
tags:
- foodlog
created: ${new Date().toISOString().split('T')[0]}
---
`;
return frontmatter + content;
}
let frontmatter = parts[1];
const updateProperty = (fm, key, value) => {
const regex = new RegExp(`${key}:\\s*[\\d.]+`, 'g');
if (regex.test(fm)) {
return fm.replace(regex, `${key}: ${value}`);
}
return fm + `\n${key}: ${value}`;
};
frontmatter = updateProperty(frontmatter, 'protein', totals.protein);
frontmatter = updateProperty(frontmatter, 'carbs', totals.carbs);
frontmatter = updateProperty(frontmatter, 'fat', totals.fat);
frontmatter = updateProperty(frontmatter, 'calories', totals.calories);
return `---${frontmatter}---${parts.slice(2).join('---')}`;
}
function formatNutritionEntry(nutrition) {
return `- ${nutrition.description}: ${nutrition.calories} cal | ${nutrition.protein}g protein | ${nutrition.carbs}g carbs | ${nutrition.fat}g fat`;
}
// ====== MAIN LOGIC ======
const file = app.workspace.getActiveFile();
if (!file) {
new Notice("⚠️ No active file!");
return;
}
let content = await app.vault.read(file);
const lines = content.split('\n');
// Find all bullet items that need processing
const itemsToProcess = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!isBulletItem(line)) continue;
if (hasDetailedNutrition(line)) continue;
const foodText = extractFoodText(line);
if (foodText.length > 0) {
itemsToProcess.push({
lineIndex: i,
originalLine: line,
foodText: foodText
});
}
}
// Process items
let successCount = 0;
let errorCount = 0;
const DELAY_BETWEEN_CALLS = 500; // 500ms delay
if (itemsToProcess.length > 0) {
new Notice(`🔍 Processing ${itemsToProcess.length} food item(s)...`);
for (let i = 0; i < itemsToProcess.length; i++) {
const item = itemsToProcess[i];
try {
new Notice(`Processing ${i + 1}/${itemsToProcess.length}: ${item.foodText.substring(0, 30)}...`);
const nutrition = await getNutrition(item.foodText);
const formattedEntry = formatNutritionEntry(nutrition);
lines[item.lineIndex] = formattedEntry;
successCount++;
content = lines.join('\n');
await app.vault.modify(file, content);
if (i < itemsToProcess.length - 1) {
await new Promise(resolve => setTimeout(resolve, DELAY_BETWEEN_CALLS));
}
} catch (error) {
console.error(`Error processing "${item.foodText}":`, error);
new Notice(`❌ Error: ${item.foodText.substring(0, 30)}...`);
errorCount++;
}
}
}
// Update frontmatter totals
new Notice("📊 Updating total nutrition...");
content = await app.vault.read(file);
const totals = calculateTotals(content);
content = updateFrontmatter(content, totals);
await app.vault.modify(file, content);
// Show summary
if (itemsToProcess.length === 0) {
new Notice(`✅ Totals updated: ${totals.calories} cal | ${totals.protein}g protein`);
} else if (errorCount === 0) {
new Notice(`✅ Processed ${successCount} item(s)!`);
} else {
new Notice(`⚠️ Completed with ${errorCount} error(s). Processed ${successCount} item(s).`);
}
};
Setup Instructions
1. Install Required Plugins
From Obsidian's Community Plugins:
- QuickAdd - For custom commands
- Note Toolbar (optional) - For clickable buttons
2. Get Gemini API Key
- Go to Google AI Studio
- Create a new API key (free tier available)
- Copy the key
3. Create Script Files
Create the directory structure:
YourVault/
└── Scripts/
└── QuickAdd/
├── open-todays-food-log.js
└── calculate-nutrition.js
Copy the scripts above into these files. In calculate-nutrition.js, replace YOUR_GEMINI_API_KEY_HERE with your actual API key.
4. Configure QuickAdd
- Open Obsidian Settings → QuickAdd
- Add a new Macro: "Open Today's Food Log"
- Configure it to run
Scripts/QuickAdd/open-todays-food-log.js
- Configure it to run
- Add another Macro: "Calculate Nutrition"
- Configure it to run
Scripts/QuickAdd/calculate-nutrition.js
- Configure it to run
- Add both macros to the command palette
5. Create Food Log Directory
Create the folder: Health/Food/Log/
6. (Optional) Configure Note Toolbar
If using Note Toolbar plugin, add this to your food log frontmatter:
notetoolbar: Food Log
Then configure the toolbar to show buttons for your QuickAdd commands.
Why This Works
✅ Frictionless - Two commands, that's it. No app switching, no searching databases, no measuring.
✅ Natural Input - Just type food names in plain English. The AI handles quantities, combined meals, and calculations.
✅ Flexible - Works with single foods ("coffee") or complex meals ("salmon with quinoa and roasted vegetables").
✅ Private - All data stays in your vault. No third-party services storing your eating habits.
✅ Cross-Platform - Works on Obsidian desktop and mobile.
✅ Nearly Free - Gemini API costs approximately $0.01/month for typical usage (3 meals/day × 30 days).
✅ Customizable - Pure JavaScript—modify the prompt, change the format, adjust to your needs.
Real-World Results
I've been using this system for several weeks. The difference is dramatic:
Before: Sporadic tracking when I remembered, usually gave up after a few days.
After: Consistent daily logging. It's literally as easy as writing a quick note.
The key insight: Friction is the enemy of consistency. Every extra step—opening an app, searching a database, weighing food—makes it less likely you'll do it. By reducing the workflow to "write what you ate" and "run a command," it becomes sustainable.
Cost Breakdown
- QuickAdd plugin: Free
- Note Toolbar plugin: Free
- Gemini API: ~$0.01/month (free tier: 1,500 requests/day)
- Obsidian: $0-50 (free for personal use, $50/year for commercial)
Compare to:
- MyFitnessPal Premium: $80/year
- Cronometer Gold: $50/year
- LoseIt Premium: $40/year
Limitations & Considerations
Accuracy: AI nutrition estimates are reasonably accurate but not perfect. If you need medical-grade precision, consider a more rigorous tracking method.
Internet Required: The Gemini API requires an internet connection. Food entry works offline, but you'll need to be online to calculate nutrition.
Setup Complexity: Requires basic JavaScript knowledge and comfort with Obsidian plugins. Not a one-click install.
No Barcode Scanning: If you primarily eat packaged foods with barcodes, a traditional app might be faster.
Future Improvements
Ideas I'm considering:
- Voice input integration - Use Obsidian's voice notes to dictate meals
- Photo logging - Take a photo, let AI identify and calculate nutrition
- Meal templates - Save common meals for quick reuse
- Weekly/monthly reports - Auto-generate nutrition summaries
- Recipe integration - Link to recipe notes with calculated per-serving nutrition
Conclusion
This system has completely changed my relationship with food tracking. What used to be a chore that I dreaded (and avoided) is now something I do naturally as part of my daily note-taking workflow.
The magic isn't in the AI or the scripts—it's in the reduction of friction. By meeting me where I already am (my notes) and accepting input in the most natural format possible (plain text), it removes every excuse not to track.
If you're an Obsidian user who's struggled with food tracking consistency, give this a try. The setup takes 15 minutes, and you might finally stick with it.
Resources
Questions? Improvements? Drop them in the comments. I'd love to hear how you adapt this system for your needs!



Top comments (0)