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.