Papa Labs

Google Keep migration guide: Takeout export to Markdown

Moving notes out of Keep (into this blog, or into Obsidian) follows a fixed route: Takeout export → conversion script. No third-party service ever touches your data.

Step 1: Export with Takeout

  1. Open takeout.google.com;
  2. Click “Deselect all”, then tick only Keep;
  3. Choose zip as the format and create the export — with a modest number of notes the email arrives in minutes.

Google Keep product page

Everything in Keep — including image attachments and checklist states — ends up in the Takeout bundle

Step 2: Understand the export

After unzipping, each note has two files: .html (for reading) and .json (for conversion). The JSON looks roughly like:

{
  "title": "FortiGate HA heartbeat gotcha",
  "textContent": "note body…",
  "labels": [{ "name": "networking" }],
  "isTrashed": false,
  "isArchived": false,
  "userEditedTimestampUsec": 1719800000000000
}

Key points:

  • Regular note bodies are in textContent; checklist notes use listContent (an array of text + isChecked items);
  • Skip anything with isTrashed: true;
  • Image attachments are in attachments, sitting in the same folder;
  • Keep labels map directly onto blog tags.

Step 3: The batch conversion script

The core logic is a few dozen lines of Node (something like scripts/keep2md.mjs):

import fs from 'node:fs';
import path from 'node:path';

const src = './Takeout/Keep';
const out = './inbox';

for (const f of fs.readdirSync(src).filter(f => f.endsWith('.json'))) {
  const note = JSON.parse(fs.readFileSync(path.join(src, f), 'utf8'));
  if (note.isTrashed) continue;

  const date = new Date(note.userEditedTimestampUsec / 1000)
    .toISOString().slice(0, 10);
  const tags = (note.labels ?? []).map(l => l.name);
  const body = note.textContent
    ?? (note.listContent ?? [])
      .map(i => `- [${i.isChecked ? 'x' : ' '}] ${i.text}`)
      .join('\n');

  const md = `---\ntitle: ${note.title || '(untitled)'}\ndate: ${date}\ntags: [${tags.join(', ')}]\ndraft: true\n---\n\n${body}\n`;
  fs.writeFileSync(path.join(out, f.replace('.json', '.md')), md);
}

Everything lands in inbox/ with draft: true by default — conversion is batch, curation is human. Move the publishable ones into src/content/posts/ and keep the rest as a private archive.

A prediction

Keep notes are mostly one-line reminders; maybe two or three in ten can grow into an article. Don’t expect full automation — the conversion just gets the raw material ready.

← All posts