Skip to content

Migrate a Markdown export into Blenau

Hand this whole page to your agent. Copy it from your Blenau dashboard and it arrives with your workspace’s repos already in it; blenau playbooks install migrate-markdown-export writes it straight into your agent’s instructions folder; or an agent connected over MCP can fetch it itself with get_playbook("migrate-markdown-export").

Download the raw Markdown


Give this whole file to your agent. It is a working procedure, not an explainer: every rule below exists because skipping it cost a real migration real rework.

It has been run end to end on exports of 300–900 images and 20–45 finished documents. The shape it assumes is any Markdown export that carries its images inline or as base64 definitions — a Google Docs “Download as Markdown”, a Notion or Confluence export, a folder of legacy wiki pages concatenated.

Your workspace’s own values are not filled in here. Copy this playbook from your Blenau dashboard (or blenau playbooks get <id>) and it arrives with your workspace slug and your connected repos already in it. Ask your agent to run list_repos first if you are working from this generic copy.

What the agent needs from you before it starts

Section titled “What the agent needs from you before it starts”
  1. The local path to the exported .md file.
  2. The source, for provenance — the URL of the original document if there is one, or the team/author who owns it if there is not (then sources=[{"type": "team", "ref": "<Team>"}]).

If either is missing, the agent must ask rather than guess. It must not go hunting the disk on its own initiative: download folders differ per machine and usually hold several .md files. Offering to look (“want me to check your Downloads folder?”) is fine — but either way it must confirm the exact file back to you: full name, size and date. “Found handbook.md (36 MB, today 12:17) — is this the one?”

Even when you say “there’s no source URL”, the agent should search the brain for the original document’s id before giving up on provenance: if anything was ever migrated out of this same source, it probably links to it. Report what turns up; never adopt it as the source without your confirmation.

Phase 0 — Analyse and propose (STOP and ask before writing anything)

Section titled “Phase 0 — Analyse and propose (STOP and ask before writing anything)”
  1. Split text from images. Exports carry images as trailing definitions ([image12]: <data:image/png;base64,…>) while the body keeps inline markers (![][image12]). Produce a text-only copy by removing the definition lines and measure: text size, image count, and the H1/H2 title map (grep '^#\{1,2\} ').

    In large manuals, H1s with an emoji are usually thematic block dividers rather than chapters — they are the natural candidates for folders.

  2. The golden rule: search before writing. Run search_knowledge on the manual’s topics and list_repos before creating anything. Merge with what exists; do not duplicate.

    Look specifically for documents that already came out of THIS source. It is common for somebody to have migrated one section by hand months ago. Those get merged, not left to coexist (Phase 2 §7).

  3. Propose the destination and the split, then wait for a human OK. Two decisions:

    • Repo and path prefix. One repo per functional area, with a matching Blenau path prefix. Do not reuse a generic catch-all repo just because it exists and looks vaguely right — a repo named for “general docs” is a drawer, and a migration that lands in a drawer has to be moved out later. When in doubt: a dedicated repo, and ask.

    • Flat documents, or one subfolder per thematic block with assets/ at the root. Offer both. Past ~20 documents the flat variant is unreadable — recommend subfolders. From a subfolder, image links are ../assets/… and cross-links between blocks are ../folder/doc.md.

    The area repo is not automatically the right destination — the reason can be PERMISSIONS. A payroll manual looks like it belongs in the HR repo, but access to payroll is usually not granted to everyone with access to HR. Ask in Phase 0 whether this content has a different audience from the rest of the repo. That is a business decision, not a technical one.

    If the target repo already exists, read what its README claims to be. A repo of technical notes that receives a 43-document user manual has stopped being what it says it is. Rewrite its scope statement in the same migration, or the index lies from day one.

Phase 1 — Repo and assets (images go FIRST)

Section titled “Phase 1 — Repo and assets (images go FIRST)”
  1. Create the repo if it does not exist yet (gh repo create <org>/<name> --private), then connect itconnect_repo(repo, path_prefix, installation_id). Skip either step if it is already done. If the Blenau GitHub App cannot reach the repo the call fails with an actionable error: Blenau cannot grant its own App access, so installing or extending the App is a human step.

  2. Extract the base64 payloads to imageN.png with a regex over the definition lines, keeping the export’s global numbering.

  3. Images always go to assets/<manual-name>/imageN.png, never bare assets/. This is not only about repos with previous history: numbering collides between exports. In one migration, image344image347 from the new manual and the image344image347 already in the repo were completely different pictures; without the subfolder, four screenshots of an unrelated document would have been overwritten with nobody noticing.

    Check the tree before copying: gh api repos/<org>/<repo>/git/trees/HEAD?recursive=1.

  4. Push all images in ONE git commit (clone, copy, push). Do not use create_asset_upload for hundreds of images — that path is for a handful; Blenau’s assets are simply files in the repo.

    With Blenau writing to the same repo concurrently, a --depth 1 clone cannot rebase (it conflicts on the index.md Blenau regenerates). The loop that works: fetch --depth 1reset --hard FETCH_HEAD → copy the PNGs → commitpush, with retries.

  5. Verify BOTH path shapes with verify_asset before launching any agent. From a subfolder, ../assets/<manual>/imageN.png returns resolves: true while assets/<manual>/imageN.png returns false. Without the ../, Blenau resolves against the document’s own folder. Read both exists and renderable: they answer different questions.

Why images come first: it deletes an entire phase. With the assets already in the repo, the agent writing each section emits the final ![alt](../assets/…) in the same pass — and the alt text comes out better, because it has the surrounding prose in front of it. It saves a second round of N agents and, above all, the storm of 409 Conflict that round produces. Measured: 43 documents and 633 images with a single 409 in the whole migration.

  1. Slice the text-only copy by line ranges derived from the H1 map — with a script, not by hand. The script must verify coverage: markers covered vs. total, unassigned images, and non-empty lines that fall in no slice. If the numbers do not add up, the slicing is wrong — do not launch agents yet.

  2. Write the rules ONCE into a file (RULES.md) and give each agent only its own fragment plus its own data. Repeating the rules across 43 prompts guarantees they drift; with a single file, a correction reaches every agent that has not started yet.

  3. Run a mandatory pilot on one small section. Besides migrating it, ask it to audit the rules: what was ambiguous or impossible to apply. In one migration the pilot came back with 6 genuine defects in the rules file. Fixing them cost minutes; not fixing them would have cost 43 uneven documents.

  4. One subagent per document, in batches of ~10. The rules file must contain all of the following — every one of them learned from a failure:

    • Total fidelity: do not summarise, rewrite, invent or reorder. When in doubt, leave it alone and report it.
    • Anti-overcorrection: menus, field names, buttons, codes, account numbers and percentages are copied literally even when they contain typos. Without this rule agents “fix” the manual — real reverts included a product term replaced by a different product term, and a button label silently corrected to the one the agent expected.
    • Strip {#…} anchors; self-referential title links (## [X](#x)) become ## X.
    • Title links that point OUTWARD are not discarded: clean the heading and move the link down to the first line of the body. Where this rule was missing, an agent ate a link to another manual and it had to be repaired by hand.
    • Hierarchy: shift down exactly ONE level (###, #####, #######), preserving nesting. Do not flatten everything to H3 — that turns parents and children into siblings. Exception: if the fragment comes from a chapter that is now a FOLDER, its ##s are already the final sections and must not shift. Say so explicitly in those agents’ prompts or they will flatten the document.
    • Drop empty or image-only headings, merging their content into the previous section — unless the prompt names them as a chapter, in which case keep and report them.
    • Callouts: exports degrade boxed notes into a one-column table with no body row (| Note … | + | :---- |) or into a paragraph with a label and a forced line break. Both become a blockquote with the leading word in bold and the text verbatim. Judge by the block’s ROLE, not its shape. Real tables are preserved.
    • Fix export escaping (1\., field\_x, \-\>, \>).
    • Spacing: one paragraph per block; trailing double spaces are forced line breaks.
    • ![][imageN]![descriptive alt](../assets/<manual>/imageN.png), with the alt written in the manual’s own language, not the agent’s. Square brackets are forbidden inside the alt text: they break parsing and the asset disappears.
    • Open EVERY PNG with Read before writing its alt text. Not optional, and the highest-value rule in the whole procedure (see §19).
    • Write with ingest_document(path, title, content, source_type="manual", sources=[…]).
    • A similar_doc_exists warning between sibling sections of the same manual is ignored; if it points at a different repo in the brain, record it as a cross-link candidate.
    • Do not use list_assets to verify — it times out on documents with many images.
    • Mandatory report: sections_reviewed/sections_total (defined as “headings in the finished document, H1 included”), images_with_verified_alt/images_total, pending_anchors and questions. Without explicit coverage there is no way to tell “reviewed and correct” from “never looked at”.
  5. Write the README index and verify: list_documents must show every document as ready.

  6. Merge the pre-existing documents that came from this same source (Phase 0 §2). Hand it to ONE agent — the owner of the destination document — with explicit instructions: which sections to merge, what each version contributes, and which asset path each image uses (the old document’s images are usually in the root assets/, not in the manual’s subfolder).

    Then retire the old document with delete_document(path) — preview it with dry_run=true first. It removes the .md from GitHub and the index in one call, embeddings included, and the delete stays recoverable from git history. (git rm + push still works and syncs back; it is no longer the only way.) Do not delete its images if the merged document still uses them, and do not leave the old document empty or as a redirect stub: merging is supposed to end with fewer documents.

  7. Verify by script, not by eye. Against each document’s raw content on GitHub: count leftover markers, resolve every image path against the repo tree, resolve every ](x.md), and flag cross-document # anchors and alts containing brackets. Image counts must reconcile document by document against the slicing plan, and the total assets used against the export’s image count.

Section titled “Phase 3 — Cross-links (one agent per FOLDER)”
  1. Cross-links are [text](../folder/doc.md) at document level, naming the target section in the link text. Never # anchors BETWEEN documents (anchors within the same document are fine when they resolve against one of its own headings). Clean up the export’s dead anchors. Links to external documents that have not been migrated are kept as they are; links to manuals that have been migrated become references into the brain.

    One agent per FOLDER, as exclusive owner: fewer agents, zero collisions, and it can see the whole block. Inside the agent, sections run in series.

    Give each agent the concrete destinations, not just the index. Agents obey “do not invent links” well, which means that without a list of real targets they leave almost everything as plain text. Generate a TARGETS.md by script — every document with its title and first-level sections — and pass it whole. The raw material is the pending_anchors each Phase 2 agent reported.

Phase 4 — A DEDICATED editorial pass (one agent per DOCUMENT)

Section titled “Phase 4 — A DEDICATED editorial pass (one agent per DOCUMENT)”
  1. The editorial pass is not a passenger on the cross-link pass. Tried once, and it came out uneven: agents work by exception, so wherever there were no links to create they barely read the text. Measured result: 8 of 43 documents received not a single edit, while the largest block — 9 documents and 1,700 lines — got 10 corrections between them and one small folder got 28. It had to be redone from scratch.

  2. One agent per document, with the original extract in front of it. That makes the pass two things at once, and the second is where the value is:

    • Fidelity audit: nothing lost, nothing invented, nothing reordered. This is what caught a migration that had deleted a hyperlink and replaced it with the phrase “in this manual” — a false statement — and a pilot document that had grown an invented introductory paragraph.
    • Editorial: grammar, consistent impersonal tone, exact duplicates, broken markdown, and arithmetic or technical errata that are demonstrable — those get corrected outright, not flagged. The line is whether the error can be shown from the document itself; anything needing product knowledge to judge belongs in §19’s report instead.
  3. Alt texts are verified by opening the PNG, one by one. In one migration ~450 of 770 had to be rewritten for contradicting the screenshot (“generated accounting entry” over an empty field, “option enabled” over an unchecked box, the same image reused in four sections with an alt that fitted only one).

    If §12 was done properly, this is a sampling pass instead. And the side effect is the most valuable finding of a migration: agents surface the contradictions between the manual and the product’s actual behaviour (“the text says to type a value into a field that does not exist in the screenshot”). Those are not corrected — they are reported to the author.

  4. Demand explicit coverage: sections_reviewed == sections_total.

  5. Same anti-overcorrection rule as Phase 2: when in doubt, leave it and report it.

  6. Do not rename headings — the cross-links depend on them. Blocks that are thematically misplaced move to their own document, and the README is updated.

  7. Audit the README and anything the orchestrator wrote. Nobody has reviewed those. One README carried 18 descriptions that did not match the real content, one of them attributing a topic to a document that does not cover it. An index that lies is worse than no index.

  8. Keep a debt inventory throughout (PENDING.md): rules corrected mid-flight that left earlier documents on the old criterion, chapters that arrive empty from the original, duplication between siblings, text/screenshot contradictions, and export junk. Agents report these generously in their questions field; what gets lost is what nobody wrote down.

  • Concurrent-subagent cap. Every agent runtime has one — 20 in the one these migrations were run on — and rejected launches are not retried for you. Launch in batches of ~10 and stay a couple below the cap rather than riding it. Keep a queue file with each document’s state, or you will lose one silently: it happened, and one document had no images until the final verification caught it.
  • 409 Conflict from GitHub is NOT version_mismatch. With many agents writing — and also with two edit_section calls in parallel on the same document — GitHub rejects the commit. Retry with the same expected_version after 10–20 s. Only a genuine version_mismatch means you must re-read the section.
  • edit_section rejects a new_content that opens with a different heading (heading_in_body). To restructure a body with subsections, open with a paragraph and put the ###s after it. It replaces the BODY; to rename a heading use rename_section.
  • Duplicate headings: edit_section accepts position; get_section does not — read those with get_document. A section’s version hash is sha256(section_markdown)[:16].
  • Count markers with !\[[^\]]*\]\[image\d+\], not !\[\]\[image\d+\] — exports emit some markers with alt text already present, and the empty-alt pattern loses them.
  • suggest_crosslinks is useless on migrated manuals: an identical boilerplate ## Sources section saturates its suggestions. audit_links can return cached output. The reliable check is the raw file on GitHub.
  • MCP timeouts on large documents with a busy server: retry, or verify via raw GitHub. Writes are transactional with an optimistic lock, so a timeout never leaves a half-written document.
  • update_repo moves an entire prefix without re-ingesting.
  • Blenau materialises every write as a commit in GitHub (author = the user, co-author Blenau Bot). Seeing commit traffic is normal — that is the brain/repo duality working.
  • Promotional or navigational blocks at the end of an export (“you may also like”, partner tables) are not knowledge: discard them. Their images explain the gap between assets extracted and assets used.
  • Self-references to the source document: the manual links to itself. Those DO become internal cross-links, since the content is now migrated. Only links to other, unmigrated documents are preserved. Watch out when searching for them: the provenance line every migrated document carries (## Sources) also contains the source document’s id, so a naive search returns every file. Filter that line out before counting.
  • Do the bulk verification from a script, not from the shell. Shell tooling mangles the regexes these checks need — a pattern full of brackets and dots reads as a path or a glob, and the run either errors or silently matches nothing, which looks exactly like “no problems found”. Write the checks in Python and print counts.
  • Windows: the console is cp1252 and any emoji or accented character will break a print. Set PYTHONIOENCODING=utf-8 and sys.stdout.reconfigure(encoding='utf-8') in every script. In PowerShell, a here-string passed to git commit -m breaks on inner quotes — write the message to a file and use git commit -F, with [System.IO.File]::WriteAllText(…, UTF8Encoding($false)) so no BOM is added.
TextImagesFinished documentsNotes
87621First run; landed in the wrong repo and had to be moved
24Existing area repo; asset numbering collided
1,700+ lines77043Editorial pass done as a passenger — had to be redone
340 KB63343Assets first: no image phase at all, one single 409

The last row is what this playbook produces when followed in order.