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").
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 runlist_reposfirst 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”- The local path to the exported
.mdfile. - 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)”-
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.
-
The golden rule: search before writing. Run
search_knowledgeon the manual’s topics andlist_reposbefore 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).
-
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)”-
Create the repo if it does not exist yet (
gh repo create <org>/<name> --private), then connect it —connect_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. -
Extract the base64 payloads to
imageN.pngwith a regex over the definition lines, keeping the export’s global numbering. -
Images always go to
assets/<manual-name>/imageN.png, never bareassets/. This is not only about repos with previous history: numbering collides between exports. In one migration,image344–image347from the new manual and theimage344–image347already 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. -
Push all images in ONE git commit (clone, copy, push). Do not use
create_asset_uploadfor 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 1clone cannot rebase (it conflicts on theindex.mdBlenau regenerates). The loop that works:fetch --depth 1→reset --hard FETCH_HEAD→ copy the PNGs →commit→push, with retries. -
Verify BOTH path shapes with
verify_assetbefore launching any agent. From a subfolder,../assets/<manual>/imageN.pngreturnsresolves: truewhileassets/<manual>/imageN.pngreturnsfalse. Without the../, Blenau resolves against the document’s own folder. Read bothexistsandrenderable: 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
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 of409 Conflictthat round produces. Measured: 43 documents and 633 images with a single 409 in the whole migration.
Phase 2 — Migrate the text
Section titled “Phase 2 — Migrate the text”-
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.
-
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. -
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.
-
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]→, 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_existswarning 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_assetsto 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_anchorsandquestions. Without explicit coverage there is no way to tell “reviewed and correct” from “never looked at”.
-
Write the README index and verify:
list_documentsmust show every document asready. -
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 withdry_run=truefirst. It removes the.mdfrom 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. -
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.
Phase 3 — Cross-links (one agent per FOLDER)
Section titled “Phase 3 — Cross-links (one agent per FOLDER)”-
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.mdby script — every document with its title and first-level sections — and pass it whole. The raw material is thepending_anchorseach 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)”-
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.
-
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.
-
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.
-
Demand explicit coverage:
sections_reviewed==sections_total. -
Same anti-overcorrection rule as Phase 2: when in doubt, leave it and report it.
-
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.
-
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.
-
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 theirquestionsfield; what gets lost is what nobody wrote down.
Operational gotchas
Section titled “Operational gotchas”- 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 Conflictfrom GitHub is NOTversion_mismatch. With many agents writing — and also with twoedit_sectioncalls in parallel on the same document — GitHub rejects the commit. Retry with the sameexpected_versionafter 10–20 s. Only a genuineversion_mismatchmeans you must re-read the section.edit_sectionrejects anew_contentthat 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 userename_section.- Duplicate headings:
edit_sectionacceptsposition;get_sectiondoes not — read those withget_document. A section’s version hash issha256(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_crosslinksis useless on migrated manuals: an identical boilerplate## Sourcessection saturates its suggestions.audit_linkscan 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_repomoves 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. SetPYTHONIOENCODING=utf-8andsys.stdout.reconfigure(encoding='utf-8')in every script. In PowerShell, a here-string passed togit commit -mbreaks on inner quotes — write the message to a file and usegit commit -F, with[System.IO.File]::WriteAllText(…, UTF8Encoding($false))so no BOM is added.
Sizing a new migration
Section titled “Sizing a new migration”| Text | Images | Finished documents | Notes |
|---|---|---|---|
| — | 876 | 21 | First run; landed in the wrong repo and had to be moved |
| — | — | 24 | Existing area repo; asset numbering collided |
| 1,700+ lines | 770 | 43 | Editorial pass done as a passenger — had to be redone |
| 340 KB | 633 | 43 | Assets first: no image phase at all, one single 409 |
The last row is what this playbook produces when followed in order.