Plank help · updated 2026-09-19

Build a client document: the route

The build route for a client-facing A4 document in one short page: the builder is already in the container at /opt/plank/doc, the build script goes in /workspace/scripts/doc/, the brand kit is one JSON file, compose() reshapes a stranded tail, and the QA gate reads the finished PDF before you hand it over.

Agents: fetch the raw markdown of this page at /en/help/documents.md

Build a client document: the route

A document is read alone at desk width and sent as a PDF or printed on A4 — a brief, a proposal, a report, a contract summary. Not an HTML page, and not a deck.

python3 -c "import plank_doc"                       # already in this container? then go
# write /workspace/scripts/doc/build_acme.py, importing `from plank_doc import *`
python3 /workspace/scripts/doc/build_acme.py
python3 /opt/plank/doc/plank_doc_qa.py /workspace/acme.pdf      # must exit 0

1. The builder is already here, at /opt/plank/doc

It ships inside this container, because it cannot render without WeasyPrint and WeasyPrint ships in the same image. import plank_doc works from any directory. Put your build script in /workspace/scripts/doc/ anyway — that folder is part of the workspace, so it survives restarts and both a private and a shared chat can see it, which a chat's own home folder cannot.

If import plank_doc fails: install it into /workspace/scripts/doc/

That container was built before the builder existed. Downloading it is the upgrade path and the fallback; the destination is /workspace/scripts/doc/ either way. Run this:

mkdir -p /workspace/scripts/doc/plank_doc
K=https://plank.md/help/kits/client-documents/files/scripts/doc
curl -sSfL -o /workspace/scripts/doc/plank_doc_qa.py $K/plank_doc_qa.py
for f in __init__ scale color brand blocks css document; do
  curl -sSfL -o /workspace/scripts/doc/plank_doc/$f.py $K/plank_doc/$f.py
done

It is a package of seven modules plus the checker, not one file. Every file is also listed at plank.md/help/kits/client-documents/manifest.json with its size and digest, if you want to verify a download. A build script sitting in /workspace/scripts/doc/ puts its own directory first on the import path, so a copy fetched there wins over the baked one with nothing to configure.

2. Never write these files yourself

plank_doc/ and plank_doc_qa.py come from the container or from that manifest, and from nowhere else. Do not write your own file under one of those names to get unblocked. A hand-written module wearing our name imports cleanly, measures nothing, and every later build in /workspace/scripts/doc/ silently picks it up. If you cannot get the real one, say so and build the PDF some other way, under a different filename.

3. The brand kit

One JSON file in the workspace, named by your build script. Seven fields and a closed set of logos:

{
  "name": "ACME", "ink": "#101014", "ground": "#FFFFFF",
  "accent": "#FFD400", "accent_ink": "#000000",
  "typeface": "Inter", "confidentiality": "Конфиденциально",
  "logos": { "light_on_dark": "assets/acme-white.png",
             "dark_on_light": "assets/acme-black.png" }
}

accent_ink is what is legible on the accent when the accent is a fill — a brand decision, never guessed. The accent is a fill and a rule and is never type on the paper; the builder derives a darkened accent_text for that itself. Both lockups are required (mark_only is the one exception, for a brand with a single mid-tone mark), and a kit that declares a white wordmark as dark_on_light is rejected on the asset's own pixels. A refusal names the field and the ratio. Fix the kit; do not lower the floor.

4. Build with compose, not build

# /workspace/scripts/doc/build_acme_brief.py
from plank_doc import *
from plank_doc.document import compose

brand = Brand.from_kit("/workspace/scripts/doc/brand/acme.json")
doc = Document(brand=brand, kind="Бриф", footer_left="ACME · проект", footer_right="Конфиденциально")
doc.add(Masthead(kind="Бриф"), Title("Заголовок", "Подзаголовок"), ..., Colophon(["…"]))
compose(doc, "/workspace/acme-brief.pdf")

compose builds, measures its own last page, and reshapes the tail if it is stranded. Choose blocks by MEANING — SpecTable, Callout, Entry, Bullets, KeyFacts — and never by appearance: none of them takes a colour or a size. Eleven repetitions of "bold subhead, then a paragraph" is a table you have not written yet.

5. Run the QA before you hand anything over

python3 /opt/plank/doc/plank_doc_qa.py /workspace/acme-brief.pdf
# or /workspace/scripts/doc/plank_doc_qa.py if you fetched it there

It reads the finished PDF — any PDF, from any producer — and exits non-zero on an error: a brand lockup on a ground it cannot be seen against, a page that stops halfway down the sheet, a word below WCAG AA (which is what a missing accent_ink, or the brand accent used as type, looks like by the time it reaches a reader), ink in the trimmed margin. Each one is a reader seeing something broken. Fix it and run it again.

If you hand over a document the QA condemned, say so in the message with the count and which ones: "QA reports 5 errors — 4 page voids and 1 invisible logo — which I have not fixed." Delivering a condemned document without repeating the number is the one thing this step exists to prevent.

6. A workspace skill does not replace this route

A skill, a template or an AGENTS.md may own the look — the colours, the typeface, which sections to include — and you follow it. It does not own the toolchain: unless it names a different builder and says why, the document is still built with plank_doc from /workspace/scripts/doc/ or /opt/plank/doc, and still checked with plank_doc_qa.py. A skill that says nothing about how to build has not chosen another route.

Screen-first HTML deliverables are a different job with a different template — HTML deliverables. A deck is a third — Build a deck.