bim-pdf

Work with PDFs your way — split a combined sheet export into individual files, shrink a set before sending to a client, extract element data for a takeoff, or embed structured data for archiving. If you've ever gotten garbled text or wrong element IDs from a PDF tool on a Revit export, it's because most tools don't understand how Revit encodes its output. bim-pdf does.

PDF analysis and manipulation for AEC workflows. Zero dependencies — handles Revit's specific PDF encoding natively.

Revit PDF specifics

Revit exports PDFs with Adobe PDF Library 18.0.5 (PDF 1.6). Every visible element is wrapped in a BMC/BDC tagged content block encoding its ElementId. Text uses Type0 Identity-H encoding — standard extractors return garbage. bim pdf marked and bim pdf text handle both correctly.

Verbs

VerbWhat it does
bim pdf info FILEMetadata, page count, PDF version, XMP namespace map, and embedded attachments. Includes the same attachment list returned by attach.list; for PDF-BIM packages that attachment set also matches bim.list.
bim pdf pages FILE [--detail]Per-page dimensions and OCG layer list. Default: fast path using pdfium for MediaBox/Rotation only. Use --detail for custom page-dict entries and layer visibility (slower on large vector sets).
bim pdf marked FILE [--page N] [--pages PAGES] [--tag TAG]Extract BMC/BDC tagged content blocks with geometry bounds. Use --page for one page, --pages for a range, or omit both to process every page in one call.
bim pdf text FILE [--page N] [--pages PAGES] [--lines]Text extraction; decodes via ToUnicode CMap. Use --page for one page, --pages for a range, or omit both to process every page in one call. Default output: per-word tokens with x/y/fontSize coordinates. With --lines: assemble tokens into reading-order text lines per page (group by Y, sort by X) -- equivalent to pdftotext -layout / pdfplumber extract_text().
bim pdf extract [FILE] [--files FILES] [--dir FILE] [--recursive] [--field FIELD] [--all] [--page N]Extract named field values from PDF reading-order lines using regex capture groups. Each --field is NAME=REGEX where the first capture group is the value. Supply exactly one of: --file for a single PDF, --files for a JSON string or array of paths, or --dir to collect all PDFs in a directory (add --recursive to walk subdirectories). Default output: one summary per file with first match per field. With --all: every per-line match.
bim pdf table FILE [--page N] [--pages PAGES]Extract row/column-structured text from each page. Use --page for one page, --pages for a range, or omit both to process every page in one call. Detects column boundaries from run X positions and returns [{page, rows:[{cells:[...]}]}]. Useful for holdings tables, statement grids, and tabular data without bespoke parsing.
bim pdf search [FILE] [--files FILES] [--dir FILE] [--recursive] PATTERN [--literal] [--page N] [--pages PAGES] [--context N] [--with-runs] [--with-line] [--max N] [--font FONT] [--min-size MIN-SIZE] [--max-size MAX-SIZE]Search text in a PDF using a regexp pattern; returns matches with bboxes
bim pdf optimize FILE [--out FILE]Optimize PDF size and structure
bim pdf render FILE [--out-dir FILE] [--page N] [--format FORMAT] [--dpi N] [--bbox BBOX] [--out FILE]Render pages to images using pdfium (bundled, no Ghostscript required). Falls back to Ghostscript if pdfium renderer is unavailable. --bbox crops a single page region and requires --page.
bim pdf enrich FILE SIDECAR [--tag-pattern TAG-PATTERN] [--id-field ID-FIELD]Join `marked` output with a sidecar JSON lookup table
bim pdf package --file FILE --metadata FILE [--out FILE] [--no-marks]Embed metadata files as PDF/A-3 attachments — produces a PDF–BIM Package
bim pdf viewer FILE [--port N] [--colors FILE] [--agent-color AGENT-COLOR] [--require-data]Local browser viewer for PDF–BIM packages; writes startup response then blocks
bim pdf report [--template TEMPLATE] [--data FILE] [--flood-map FILE] [--school-map FILE] [--parcel-map FILE] --out FILE [--timeout TIMEOUT]Generate a graphical PDF site assessment report
bim pdf doctorDriver health check
bim pdf versionDriver version
bim pdf serve FILE [--local] [--remote] [--server SERVER] [--base BASE] [--name NAME] [--role ROLE] [--qr-out FILE]Start a markup review session on a chosen backend and print the join link + QR (markups.session-new's output). --remote (default): Firebase, as markups.session-new. --local: the local review server (pdf-bim apps/pdfmarkuptool/local; --server, else $BIM_LOCAL_REVIEW_URL, else http://127.0.0.1:4101/local) -- no Firebase; the join base is --base, else $BIM_PDF_JOIN_BASE, else the SPA origin the server advertises (its --spa). Later markups.* calls reach the same local session with BIM_LOCAL_REVIEW_URL set.

annot

VerbWhat it does
bim pdf annot.list FILE [--page N]List all annotations
bim pdf annot.remove FILE [--page N] [--type TYPE] [--out FILE]Remove annotations
bim pdf annot.bake FILE --scene FILE [--out FILE]Write /Annot dicts + Named Destinations from a scene JSON into a new PDF
bim pdf annot.review FILE --review FILE [--out FILE]Write a review into a PDF as STANDARD annotations Acrobat / Bluebeam read: each markup (pdfmarkuptool canvas points, as markups.list prints them) becomes an Ink / Square / Circle / Polygon / Line annotation (or a Text note for point markups) with /NM = markup id, /T author, /M date, /Contents and a stroked appearance; each reply becomes a /Text annotation with /IRT = the markup (the review thread), and a reply with a state is a review-state annotation (/State Accepted|Rejected|Cancelled|Completed|None, /StateModel Review, hidden like Acrobat's). --review JSON: {markups: [{id, page (1-based), type, points [{x,y}], color, line_width, author, subject, contents, created_at, modified_at, replies: [{id, author, at, contents, state}]}], attachments: [{path, name, description, relationship (default Supplement), mime}]}. Attachments are embedded as PDF/A-3 associated files (appended to the catalog /AF, existing entries such as a bim-pdf package's sidecars kept), so the output still passes bim.validate. Output: {out, annotations, replies, states, attachments, skipped}
bim pdf annot.export-xfdf SCENE [--out FILE]Export scene annotations to XFDF (importable in Bluebeam / Acrobat)

attach

VerbWhat it does
bim pdf attach.list FILEList embedded attachments. The same names already appear in info under attachments; for PDF-BIM packages this returns the same set as bim.list.
bim pdf attach.extract FILE [--out FILE] [--out-dir FILE]Extract attachments to a directory; use --out <dir> (--out-dir accepted as a legacy alias)
bim pdf attach.add FILE ATTACHMENT [--out FILE]Embed a file as an attachment; uses --out because exactly one PDF is written
bim pdf attach.remove FILE --name NAME [--out FILE]Remove an attachment by name

bookmark

VerbWhat it does
bim pdf bookmark.list FILEList all bookmarks
bim pdf bookmark.export FILE [--out FILE]Export bookmarks to JSON
bim pdf bookmark.import FILE BOOKMARKS-JSON [--out FILE]Import bookmarks from JSON

form

VerbWhat it does
bim pdf form.fields FILEList all form fields
bim pdf form.fill FILE DATA-JSON [--out FILE]Fill form fields from a JSON data file
bim pdf form.flatten FILE [--out FILE]Flatten form fields (make non-editable)

page

VerbWhat it does
bim pdf page.merge --files FILE --out FILE [--no-bookmarks]Merge multiple PDFs into one; inserts a top-level bookmark for each source file by default; falls back to Ghostscript if pdfcpu cannot parse an input
bim pdf page.split FILE [--out-dir FILE] [--span N] [--by-bookmark]Split a PDF into individual pages or spans; --by-bookmark splits at top-level bookmark boundaries (ignores --span)
bim pdf page.collect FILE --pages PAGES [--out FILE]Extract specific pages into a new PDF
bim pdf page.rotate FILE --degrees N [--pages PAGES] [--out FILE]Rotate pages by 90, 180, or 270 degrees
bim pdf page.crop FILE --box BOX [--out FILE]Crop pages to a bounding box; negative coords supported (Revit PDFs use a centered origin — use -- to separate flags from negative values, e.g. page crop -- --box -785,-123,-348,195 in.pdf)
bim pdf page.canonical FILE --page N [--out FILE]Write the CANONICAL single-page PDF of one page (document control's content-addressed page unit): exactly one page, only the resources its content names, every flate/LZW/ASCII stream decoded and re-flated at a fixed level, non-stream objects inlined, objects numbered in a fixed traversal order, sorted dict keys, no /Info, no annotations, /ID derived from the bytes. Two packages that draw the same page yield the same bytes even when other pages, metadata or timestamps differ. Output: {path, page, page_hash (hex sha256 of the bytes), bytes, geometry, canon}.

package

VerbWhat it does
bim pdf package.explode FILE --out-dir FILEExplode a PDF (bim-pdf package or plain) into content-addressed parts under --out-dir: pages/<h>.pdf (canonical single page, as page.canonical), sidecars/<h>.json (that page's slice of the page-scoped sidecars: marks, scales entry, titleblock row, pages.json entry, annotations, model3d placements; canonical JSON, no meta / positional page / exported_at), docdata/<h>.json (elements, levels, schedules ONCE per source document), and explode.json {canon, source, docdata: {source_document_id: h}, pages: [{page, page_id, page_hash, sidecar_hash, geometry, ...}], unsliced}. page_hash IS the page identity: any canonical byte change is a change (no visual/perceptual fallback).

security

VerbWhat it does
bim pdf security.encrypt FILE [--user-password USER-PASSWORD] [--owner-password OWNER-PASSWORD] [--out FILE]Encrypt with user and owner passwords
bim pdf security.decrypt FILE [--password PASSWORD] [--out FILE]Remove encryption

stamp

VerbWhat it does
bim pdf stamp.add FILE [--text TEXT] [--image FILE] [--font FONT] [--points N] [--scale SCALE] [--color COLOR] [--opacity OPACITY] [--rotation ROTATION] [--page N] [--position POSITION] [--x X] [--y Y] [--width WIDTH] [--height HEIGHT] [--out FILE]Add a text or image watermark; supply either --text or --image, not both
bim pdf stamp.remove FILE [--page N] [--out FILE]Remove watermarks/stamps from every page, or from one page with --page

scene

VerbWhat it does
bim pdf scene.save FILE [--out FILE]Save a minimal empty scene file for a PDF (use session.save in viewer for live sessions)

bim

VerbWhat it does
bim pdf bim.validate FILEValidate PDF–BIM package structure. bim-pdf 1.13 view links: an error when a ref-targeted view (any scales.json refs[].view_id) is placed on more than one page; a warning per page whose scales.json pages[n].unresolved > 0 (markers on that sheet whose target view the producer could not resolve)
bim pdf bim.pack --file FILE --sidecar FILE [--out FILE]Embed sidecar JSON files into a PDF as PDF/A-3 attachments — produces a PDF-BIM package
bim pdf bim.splice FILE --patch FILE --out FILESplice a PARTIAL re-export (the patch: only the sheets a change touched) into the previous full PDF-BIM package (the base): every base page in base order, each page the patch carries (matched by pages.json page_id) replaced by the patch's page; sidecars merged by scope (page rows by page, elements by id, the patch's export provenance). A patch page the base lacks is refused (a splice never adds or reorders pages). A ref-targeted view (any scales.json refs[].view_id) placed on a patch page AND a kept page has moved: refused with kind stale-target ("<view> moved to <patch sheet>; re-export <kept sheet> too"; exit 1 through bim, like every refusal; nothing written). Views no ref targets (legends) may repeat. Untouched pages keep their bytes, so markups on them keep their anchors at the next markups.ingest
bim pdf bim.list FILEList all sidecar files embedded in a PDF-BIM package. For PDF-BIM packages this is the same set returned by attach.list and by info.attachments.
bim pdf bim.extract FILE --name NAME [--out FILE]Extract a named sidecar from a PDF-BIM package and return its JSON content
bim pdf bim.extract-titleblock FILEExtract title block fields (sheet number, name, project, date, scale, revision) from a PDF-BIM package
bim pdf bim.extract-room-schedule FILEExtract room elements (class=room) from a PDF-BIM package as a structured room schedule

set

VerbWhat it does
bim pdf set.extract FILEExtract a drawing-set intermediate representation (set-IR) from an issued PDF drawing set: sheets, sheet index, callouts, and detail targets. Output is JSON-only.
bim pdf set.check FILERun referential-integrity checks over a drawing-set PDF: index completeness, callout resolution, and titleblock consistency. Outputs typed JSON findings with severity (error|warning) and a summary count.

markups

VerbWhat it does
bim pdf markups.session-new FILE [--role ROLE] [--name NAME] [--base BASE] [--qr-out FILE]Start a markup review session: upload a bim-pdf package, mint a 6-digit single-use pairing code (5 min) and print {joinUrl, code, projectId, documentId, name, ..., qrAscii} with joinUrl as the FIRST key. The join URL is also printed alone on the console's first line, followed by an ASCII QR of it (the same art as qrAscii). The package is the positional <file> (or --file <path>); --package belongs to markups.ground, not here. Needs Application Default Credentials (GOOGLE_APPLICATION_CREDENTIALS or `gcloud auth application-default login`), else the gcloud CLI login (`gcloud auth login`; BIM_GCLOUD names the binary) (or FIRESTORE_EMULATOR_HOST + FIREBASE_STORAGE_EMULATOR_HOST) and BIM_FIREBASE_PROJECT (default pdfmarkuptool); or BIM_LOCAL_REVIEW_URL (the local review server's /local root, e.g. http://127.0.0.1:4101/local) for a Firebase-free session.
bim pdf markups.list PROJECTID [--status STATUS]List the session's markups (all fields, status defaulted to open, rev to 1). Expired proposals are moved to needs-human first. Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf markups.ground PROJECTID MARKUPID [--package FILE] [--anchor ANCHOR]Ground an open markup to BIM elements from marks.json by the pdf-bim coords README rule: cloud/rectangle/ellipse/polygon take elements wholly inside; freehand/polyline/line/arrow strokes ground as a region when closed and large (bbox centre inside the loop), else to the element most stroke points land on, else to an element within 12 pt of either endpoint (the anchor for creation intents); callout tip / text centre / count take the smallest mark under the point, callout and text then within 12 pt. Prints rule (region|polygon|points|endpoint|point, or declared with --anchor), hits, and for endpoint the endpoint and distance. Sets linkedElementIds and status grounded, or needs-human when nothing matches. With --anchor the drafter declares the anchor instead (see --anchor). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf markups.element FILE ELEMENTID [--documentId DOCUMENTID]Print one element record from a package's elements.json plus its marks (package: positional <file> or --file)
bim pdf markups.propose PROJECTID MARKUPID [--addresses ADDRESSES] [--request FILE] [--review FILE] [--delta FILE] --summary SUMMARY [--ttl TTL]Propose a model edit for a grounded markup and, with --addresses, the other markups of the same redline (strokes, a callout): in ONE transaction every addressed markup moves to proposed (rev+1; an open one walks open>grounded>proposed), a reply callout (in_reply_to = the primary, addresses = the set) is written, and the set plus the reply become one group markup so the sheet and the markup list show one reviewable unit. Review contract v7: the primary and the reply carry `request` (the executable: {driver, verb, args, preimage?, script?} + base_package_hash) and `review` (the record: summary, base_rev, base_revs, alternatives?, revision?, revision_of?, previous_summary?, trace_id, hash = sha256 of the canonical {request, review minus hash/selected/outcome}); every member, the reply and the group carry trace_id and contractVersion 7; no legacy delta is written. The trace_id is the input's, else minted tr-<reply id>. Input: --request + optional --review, or the LEGACY --delta (mapped the same way; exactly one of --delta / --request). Output: {markupId, proposalMarkupId, groupId, addresses, status, rev, request, review, traceId, delta (legacy read-only view), expiresAt}. From proposed until the set leaves the band (resolved / rejected > open / needs-human) every member is LOCKED (contract statuses[s].locked): it takes only a status transition -- no geometry/text/style edit, no grouping change, no delete, by the app (firestore.rules) or by any drafter verb (refused with kind `locked`). A proposal not accepted within --ttl goes to needs-human on a later verb call. Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf markups.wait PROJECTID [--status STATUS] [--timeout N]Block until any markup in the session has --status, print it and exit; {ok:false, error:{kind:timeout, retriable:true}} after --timeout seconds (a Firestore failure is kind connection, also retriable) Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf markups.apply PROJECTID MARKUPID [--op-id OP-ID] [--expect-revision EXPECT-REVISION] [--scope SCOPE]Apply an accepted proposal. Refuses unless (invariants, not rev arithmetic -- a benign write between propose and Accept cannot block it): status is accepted; on the primary AND every member the ONLY status_history entry after the drafter's last `proposed` is a human `accepted` (kind status-walk otherwise), by an active project admin; delta.hash matches the stored delta (delta-changed); the package is unchanged (stale-package). A legacy delta without delta.hash (pdf-cli <= 0.6.2) keeps the old rev check (members at delta.base_revs+1; without base_revs the primary at base_rev+2). Writes applying, runs `bim <driver> <verb> --<arg> ...` of the reading the reviewer picked in the Accept (contract v5: delta.alternatives[delta.selected], default 0 = the proposed delta; an out-of-range pick is refused bad-selection and handed back) and judges the LAST line of stdout that parses as a JSON object, never the exit code: `error`, ok:false or result.ok:false fail; {ok:true, result}, ok:true, or a bare resource object without error/ok (0.6.6) succeed. Success stays applying until markups.ingest; failure -> needs-human. An ACCEPTED set a guard refuses (status walk, delta changed, member not accepted, package changed) is handed back: every member walks accepted > applying > needs-human (history event apply-refused, needs_human_reason = the refusal; reply and group kept) and the refusal is returned -- a human Reopens it, then re-ground and re-propose. Safe to re-run: a finished attempt is replayed, never re-executed. --op-id (contract v8 ops.record item 1): pass the op_id of the operation record tracking this apply and it becomes the apply journal's attempt id, so a retried call with the SAME op_id is the SAME attempt (replayed: true once it finished; apply-interrupted, never a second bim call, if it crashed mid-invoke); omitted, a fresh attempt id is minted as before. Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf markups.fail PROJECTID MARKUPID --reason REASONHand a markup back with a reason. NEVER touches a live proposal (pdf-cli >= 0.7.1): a markup in the locked band (proposed / accepted / applying), or the reply, the group or any member of a set whose primary is in the locked band, is refused with kind `locked` and nothing is written -- a live proposal is settled by the reviewer (Accept / Reject). A needs-human markup the DRAFTER parked (its last history entry is the drafter's needs-human) gets its needs_human_reason REPLACED in place (contract v5: field write, rev+1, no status change, no history entry; result reason_updated); a needs-human a human set is refused (kind human-parked). Any other markup (open, grounded) moves to needs-human. Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf markups.resolve PROJECTID MARKUPID --executed --result RESULTResolve a set whose apply ALREADY RAN its delta but that the drafter handed back to needs-human (review contract v6 executedResolve: e.g. apply's judge misread a successful bug filing). Requires --executed (the drafter asserts the delta ran) and --result '<json object>' (what it did, e.g. {"issue":"DIM-1834"}; at most 500 bytes). Refuses unless the primary is needs-human with the DRAFTER's needs-human as its last history entry (a human's needs-human: kind human-parked) and its apply journal has invoked_at (never ran: kind not-executed). Every member walks needs-human -> resolved in one transaction (history event executed-resolved, reason = the result JSON); the delta is NOT run again, so no duplicate issue or element. Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf markups.events PROJECTID [--type TYPE] [--trace-id TRACE-ID] [--ack ACK]List the session's REVIEWER EVENTS (review contract v7 reviewerEvents; replaces markups.signals): projects/{p}/reviewer_events/{id} = {type, by, at, trace_id?, text?, index?, scope?, markup_ids?, page?, op_id?}, appended by the reviewer in the app -- note (the text of a Reject, trace_id of the rejected proposal), dismiss (drop the intent trace_id), select_alternative (the reading index picked in an Accept), dispatch (the drafter's ONLY intake: the reviewer names exactly what the drafter should read now -- scope selection|page|document; selection carries markup_ids [1..200], page carries page (int >= 0), document carries neither), restore / reapply (contract v8: op_id, reapply's optional index), reading.correct (2026-10-01, additive: the reviewer's 'Fix reading' -- trace_id of the intent + text, what the ink actually says (1..280 chars); cancel that intent's draft and re-draft from the text). Prints {events: [{id, type, by, at, trace_id?, text?, index?, scope?, markup_ids?, page?, op_id?}]} oldest first (at = ISO 8601 UTC with milliseconds), optionally filtered by --type / --trace-id. With --ack <id,id> it deletes those events (the drafter consumed them, dispatch acked the same way as any other type) and prints {acked: [ids], missing: [ids]} instead. Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf markups.upgrade PROJECTIDUpgrade a whole review session to review contract v7 (drafter only; IDEMPOTENT -- a second run upgrades nothing). For each proposal set (from each reply: in_reply_to, addresses, groupId) the primary, the reply, the members and the group are rewritten in ONE transaction: a legacy delta becomes request + review (review.hash recomputed; review.outcome derived from an executed-resolved history entry or a finished apply journal), and every document of the set gets the set's trace_id (the record's, else tr-up-<primary id>) and contractVersion 7, rev + 1 and updatedAt on each changed document; then any other document still carrying delta is upgraded on its own. A representational rewrite (no status, geometry or pointer change), so it passes the lock on proposed/accepted/applying markups. A delta whose legacy hash no longer matches (delta-changed), an unhashed delta in the locked band (legacy-unhashed) or a locked markup with no trace (no-trace) is left as is. Prints {upgraded: [ids], skipped: [{id, reason}], unchanged: n}. Every drafter write already upgrades the one markup it touches; this verb gives the members, reply and group the set trace. Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf markups.ingest PROJECTID FILE [--op-id OP-ID] [--hold HOLD] [--documentId DOCUMENTID] [--no-status] [--markupIds MARKUPIDS] [--elements ELEMENTS]DOCUMENT CONTROL (phase 1): against a review service with document.upsert, the package becomes a PATCH upsert of the session document (only changed / new canonical pages, page slices and docdata are uploaded; an identical render is a no-op; result mode upsert + `upsert` {uploaded, skipped, diff, noop}); the review.ingest path below is kept for services without it. LEGACY (review.ingest): append a re-exported package (positional <file> or --file) as a new immutable REVISION of the session document (the revision model, pdf-bim #403): upload it to a NEW storage object `<dir>/<documentId>-rev-<n>-<sha8>.pdf` (never the current one), then call the review service's review.ingest with expectedRevision = the document's current `revision` (null for its first) and pages from pages.json (bim-pdf 1.12; without one, positional page ids `positional:<n>` + the sheet map); the service, in ONE transaction, creates documents/{d}/revisions/r{n}, moves the document (revision, version = n, packageHash, storagePath, pageCount, name, downloadURL) and the project Set index / packageHash, and re-anchors markups carrying page_id (moved -> pageNumber; page removed or its geometry changed -> needs-human). Another ingest from the same base is refused kind stale-revision (re-read and re-render; never retried). --op-id replays an ingest whose outcome is unknown. Result: mode (revision | legacy), revision {revision, n, parent, replayed, moved, needsHuman, skipped}, storagePath, op_id. If the deployed service lacks review.ingest (an older deploy), a document that never had a revision falls back to the legacy ingest (overwrite the one object; set name, pageCount, fileSizeBytes, packageHash, version+1; project packageHash/sheetMap; logged on stderr, mode legacy + legacy_reason); a revisioned document is refused kind revisions-unsupported. Then resolve each successfully-applied markup whose linked elements still have marks (missing element -> needs-human); a set whose anchor was declared as a sheet resolves while the new package still has that sheet (else needs-human). bim-pdf 1.12: when the previous AND the new package both carry pages.json, a linked id counts only when marked on a page of the source the markup was drawn on (result `scoping`: per-source, or why it fell back to the 1.11 marked-anywhere rule). --no-status (review REQUEST store, R5 D10: the request and op records carry the state): ingest and re-anchor exactly as above but write NO markup status, needs_human_reason or resolved_package_hash and close no proposal reply; the decision is returned in `check` instead ({markups: {id: {ok, reason?, missing?, members?}}}, plus --markupIds checked whatever their status and --elements checked against the new marks). The service's own page re-anchor (revision.needsHuman) is the service's write, not pdf-cli's Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf markups.status PROJECTID --markupIds MARKUPIDS --stage STAGE --text TEXT [--traceId TRACEID] [--dry-run]Write the drafter's LIVE PROGRESS line (`agent_status` = {stage, text, at, trace_id?}) onto markups, for the app to show next to them. Stages (loop order): reading, read, drafting, verifying, proposing, applying, applied (2026-10-01: the apply returned, export/ingest pending -- the app shows 'Applied -- updating sheet...' until done / failed), exporting, ingesting, done, failed. DISPLAY-ONLY: one batched write of agent_status on every listed markup (one Firestore commit, field-mask: no other field is rewritten); no rev bump, no status change, no status_history entry, no updatedAt -- it never moves the review contract, so it is allowed on markups locked by a live proposal (proposed / accepted / applying). at = now (ISO 8601 UTC, ms). A later call replaces the whole field. Markups that do not exist are skipped, never created, and reported in `missing`. Refuses (bad-args, nothing written) an unknown --stage, --text over 200 chars or not printable one-line ASCII, a malformed --traceId, or no ids. Output: {projectId, agent_status, updated: [ids], missing: [ids], applied}. Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf markups.watch PROJECTID [--only ONLY] [--timeout N] [--max N]PUSH INTAKE: a long-running read that prints ONE JSON LINE (NDJSON) per change to the session's reviewer events and markups, until --timeout, --max or an interrupt. The Kubernetes list+watch pattern: on start, and again after any dropped connection, it LISTS both collections and prints only what differs from what it already printed (phase list), then one {kind: synced, counts, resync, reason} line, then streams each change (phase watch) -- Firestore backend: real-time listeners (Query.Snapshots); local backend (BIM_LOCAL_REVIEW_URL): the local review server's SSE GET /local/stream. A change line is {kind: event|markup, op: added|modified|removed, id, version, phase, cursor, at, event|markup}: event as markups.events prints one, markup as markups.list prints one (removed lines carry neither). cursor = <run>.<seq>, strictly increasing within a run; version = the document's content fingerprint (its resourceVersion), so a caller that restarts the verb (it re-lists everything) drops any (kind, id, version) it already handled. Ends with {kind: end, reason: timeout|max|canceled}; a store unreachable for 5 consecutive lists ends with the error envelope (kind connection, retriable) as the last line. Read-only: never acks events or moves expired proposals (markups.events --ack / markups.list do). Through bim the driver call is capped by BIM_DRIVER_TIMEOUT_SECS (default 300 s): raise it for a long watch, or pass --timeout below it and re-run (the re-list makes a re-run lossless). Needs Application Default Credentials (GOOGLE_APPLICATION_CREDENTIALS or `gcloud auth application-default login`), else the gcloud CLI login (`gcloud auth login`; BIM_GCLOUD names the binary) (or FIRESTORE_EMULATOR_HOST + FIREBASE_STORAGE_EMULATOR_HOST) and BIM_FIREBASE_PROJECT (default pdfmarkuptool); or BIM_LOCAL_REVIEW_URL (the local review server's /local root, e.g. http://127.0.0.1:4101/local) for a Firebase-free session. Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.

ops

VerbWhat it does
bim pdf ops.record PROJECTID [--op-id OP-ID] --kind KIND --driver DRIVER --documentId DOCUMENTID [--trace-id TRACE-ID] [--request FILE] [--restore-plan FILE] [--preimage-ref PREIMAGE-REF] [--revision-before REVISION-BEFORE] [--accepted-by ACCEPTED-BY] [--requested-by REQUESTED-BY] [--applied-by APPLIED-BY] [--status STATUS]Create an operation record (review contract v8 `operations`, item 1: projects/{p}/operations/{op_id}): one record per apply/restore/reapply/render the review loop makes to a source document. Writer: drafter (bim-cli service principal); readers: every project member. Idempotent on --op-id: a second call with the same op_id and the same --kind returns the existing record unchanged (a retried enqueue); a conflicting --kind is refused (op-exists). --op-id is minted (op-<20 [a-z0-9]>) when omitted. Output: the stored record plus id, status planned (or --status), status_history [{status, at}]. Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf ops.set-status PROJECTID OP-ID --status STATUS [--result RESULT] [--revision-after REVISION-AFTER] [--revision-before REVISION-BEFORE] [--of-op OF-OP] [--reason REASON]Walk an operation's status along the op status machine (Go-native, not part of review-contract.json): planned -> queued -> applying -> applied -> rendered; applying -> failed; applied|rendered -> restoring -> restored; restored -> reapplying -> applied; restoring|reapplying -> needs-human. Any other move is refused (illegal-transition). Appends a status_history entry {status, at[, reason]}; --result, --revision-after and --revision-before (when passed) are recorded on the record (a later value overwrites; omitting one leaves the stored value). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf ops.list PROJECTID [--kind KIND] [--status STATUS] [--trace-id TRACE-ID]List the session's operations, oldest first, optionally filtered by --kind / --status / --trace.
bim pdf ops.show PROJECTID OP-IDPrint one operation record by id.
bim pdf ops.restore PROJECTID OP-ID [--by BY]Q0 restore entry point (spec--review-operation-queue): writes the SAME reviewer_events `restore` entry {type: restore, op_id, by, at} the iPad/SPA writes (contract v8 reviewerEvents.types.restore) -- it does NOT run the Revit/document restore itself. The drafter loop's queue reads this event through markups.events, runs the inverse, and calls ops.set-status. Refuses (not-found) an unknown --op-id. Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf ops.reapply PROJECTID OP-ID [--by BY] [--index N]Q0 reapply entry point: writes the SAME reviewer_events `reapply` entry {type: reapply, op_id, by, at[, index]} the iPad/SPA writes (contract v8 reviewerEvents.types.reapply) -- it does NOT run the reapply itself. --index optionally names a different alternative to run than the operation originally ran. The drafter loop consumes it through markups.events. Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.

review

VerbWhat it does
bim pdf review.request.dispatch PROJECTID --ink INK --page N --documentId DOCUMENTID [--scope SCOPE] [--op-id OP-ID]Review REQUEST service verb request.dispatch (one transaction in the `review` service; the request contract decides the move): (new request) -> queued by admin|editor. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.request.claim PROJECTID --trace TRACE --worker WORKER [--case CASE] [--op-id OP-ID]Review REQUEST service verb request.claim (one transaction in the `review` service; the request contract decides the move): request queued -> working by drafter; reclaim: request working -> working by drafter. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.request.beat PROJECTID --trace TRACE --token TOKEN [--stage STAGE] [--reading READING] [--progress-text PROGRESS-TEXT] [--op-id OP-ID]Review REQUEST service verb request.beat (one transaction in the `review` service; the request contract decides the move): request working -> (same) by drafter. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.request.propose PROJECTID --trace TRACE --token TOKEN --proposal PROPOSAL [--op-id OP-ID]Review REQUEST service verb request.propose (one transaction in the `review` service; the request contract decides the move): request working -> proposed by drafter. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.request.needs_you PROJECTID --trace TRACE --token TOKEN --error ERROR [--op-id OP-ID]Review REQUEST service verb request.needs_you (one transaction in the `review` service; the request contract decides the move): request working -> needs-you by drafter. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.request.accept PROJECTID --trace TRACE --n N --alt N [--text TEXT] [--op-id OP-ID]Review REQUEST service verb request.accept (one transaction in the `review` service; the request contract decides the move): request proposed -> accepted by admin. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.request.reject PROJECTID --trace TRACE --n N [--text TEXT] [--op-id OP-ID]Review REQUEST service verb request.reject (one transaction in the `review` service; the request contract decides the move): request proposed -> queued by admin|editor. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.request.reopen PROJECTID --trace TRACE [--case CASE] [--text TEXT] [--op-id OP-ID]Review REQUEST service verb request.reopen (one transaction in the `review` service; the request contract decides the move): request needs-you -> editing by admin|editor; after-op: request accepted -> editing by admin|editor. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.request.send PROJECTID --trace TRACE [--ink INK] [--op-id OP-ID]Review REQUEST service verb request.send (one transaction in the `review` service; the request contract decides the move): request editing -> queued by admin|editor. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.request.release PROJECTID --trace TRACE [--case CASE] [--text TEXT] [--op-id OP-ID]Review REQUEST service verb request.release (one transaction in the `review` service; the request contract decides the move): request queued|working|proposed|needs-you|editing -> cancelled by admin|editor; accepted: request accepted -> (same) by admin|editor; failed-apply: request accepted -> cancelled by admin|editor. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.request.post PROJECTID --trace TRACE --type TYPE --text TEXT [--reading-n N] [--op-id OP-ID]Review REQUEST service verb request.post (one transaction in the `review` service; the request contract decides the move): note: request queued|working|proposed|needs-you|editing|accepted -> (same) by admin|editor|drafter; correction: request proposed|needs-you -> queued by admin|editor; correction: request accepted -> queued by admin|editor; correction: request queued|working|editing -> (same) by admin|editor. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.op.restore PROJECTID --trace TRACE [--op-id OP-ID]Review REQUEST service verb op.restore (one transaction in the `review` service; the request contract decides the move): request accepted -> (same) by admin. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.op.reapply PROJECTID --trace TRACE [--op-id OP-ID]Review REQUEST service verb op.reapply (one transaction in the `review` service; the request contract decides the move): request accepted -> (same) by admin. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.op.begin PROJECTID --op OP --worker WORKER [--trace TRACE] [--fingerprint-before FINGERPRINT-BEFORE] [--op-id OP-ID]Review REQUEST service verb op.begin (one transaction in the `review` service; the request contract decides the move): op queued -> applying by drafter. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.op.reclaim PROJECTID --op OP --worker WORKER [--trace TRACE] [--op-id OP-ID]Review REQUEST service verb op.reclaim (one transaction in the `review` service; the request contract decides the move): op applying -> (same) by drafter. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.op.beat PROJECTID --op OP --token TOKEN [--trace TRACE] [--op-id OP-ID]Review REQUEST service verb op.beat (one transaction in the `review` service; the request contract decides the move): op applying -> (same) by drafter. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.op.end PROJECTID --op OP --token TOKEN [--trace TRACE] --case CASE [--result RESULT] [--error ERROR] [--op-id OP-ID]Review REQUEST service verb op.end (one transaction in the `review` service; the request contract decides the move): applied: op applying -> applied by drafter; failed: op applying -> failed by drafter; conflict: op applying -> failed by drafter. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.op.retry PROJECTID --op OP --token TOKEN [--trace TRACE] [--op-id OP-ID]Review REQUEST service verb op.retry (one transaction in the `review` service; the request contract decides the move): op applying -> queued by drafter. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.op.render PROJECTID --op OP [--trace TRACE] [--case CASE] [--revision-after REVISION-AFTER] [--error ERROR] [--op-id OP-ID]Review REQUEST service verb op.render (one transaction in the `review` service; the request contract decides the move): op applied -> rendered by drafter; failed: op applied -> (same) by drafter. Prints the service result {trace_id, verb, case?, record, from, to, status, request_status, op?, n?, lease?, replayed, ...}; a refusal is an error whose kind is the engine code (owned, lease-held, lease-lost, not-enabled, ...). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.requests.list PROJECTID [--status STATUS]READ-ONLY request-contract read requests.list through the `review` service (no writes; `calls` stripped). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.request.get PROJECTID --trace TRACEREAD-ONLY request-contract read request.get through the `review` service (no writes; `calls` stripped). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.ops.list PROJECTID [--request-id REQUEST-ID] [--status STATUS]READ-ONLY request-contract read ops.list through the `review` service (no writes; `calls` stripped). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.
bim pdf review.worker.poll PROJECTIDREAD-ONLY request-contract read worker.poll through the `review` service (no writes; `calls` stripped). Service URL: BIM_REVIEW_URL, else http://$FUNCTIONS_EMULATOR_HOST/<project>/us-central1/review, else https://us-central1-<project>.cloudfunctions.net/review (project = BIM_FIREBASE_PROJECT, default pdfmarkuptool). Bearer: BIM_REVIEW_TOKEN, else the emulator owner token, else an ADC access token (GOOGLE_APPLICATION_CREDENTIALS), else the gcloud CLI login (`gcloud auth print-access-token`; BIM_GCLOUD names the binary). Through bim, pass ids as flags (--projectId <id> --markupId <id>); a bare id as the first argument is read as a subverb.

document

VerbWhat it does
bim pdf document.upsert --projectId PROJECTID [--documentId DOCUMENTID] [--create-name CREATE-NAME] [--source-driver SOURCE-DRIVER] [--source-document-id SOURCE-DOCUMENT-ID] --file FILE [--mode MODE] [--expected-revision EXPECTED-REVISION] [--op-id OP-ID] [--dry-run]Upsert a PDF (bim-pdf package or plain) as a document revision made of CONTENT-ADDRESSED pages (document control phase 1): explode it (package.explode: canonical pages, page slices, per-source docdata), ask Storage which hashes the project already holds (projects/{p}/pages|sidecars|docdata/{h}; a metadata HEAD, no download), stage ONLY the missing ones at projects/{p}/staging/drafter/, stage the whole PDF too (the service copies it to the phase-1 compat object projects/{p}/assembled/{documentId}-r{n}-{sha8}.pdf; skipped when nothing changed), then call the review service's document.upsert (verify + move staged hashes, compare-and-set on --expected-revision, write r{n+1} with its per-page diff, update the document and the project Set). An identical page list + docdata is a no-op (noop: true, the current revision, nothing uploaded). --mode patch sends only pages whose page_hash / sidecar_hash differ from the base revision plus new pages, and `remove` for pages the file no longer has (a changed page ORDER falls back to replace, mode_reason says so). A PDF without pages.json gets positional page ids <documentId>:p<n>. Output: {documentId, created, revision, n, noop, mode, diff {added, changed, unchanged, removed}, uploaded {pages, sidecars, docdata, bytes, assembled, assembled_bytes}, skipped {pages, sidecars, docdata, unchanged_pages}, pages_sent, assembled, op_id}. A stale base is refused kind stale-revision; a service without document.upsert kind upsert-unsupported. effect: mutate -- writes Storage (staging + the phase-1 assembled PDF) and, through the review service, a new immutable document revision, the document and the project Set (one transaction; op_id idempotent). --dry-run plans it without writing: reads the document, explodes the file and HEADs the content paths, then reports what a real call would upload and send (dry_run: true; uploaded = would upload; revision / n / noop predicted, diff empty) -- no Storage upload, no service call. Needs Application Default Credentials (GOOGLE_APPLICATION_CREDENTIALS or `gcloud auth application-default login`), else the gcloud CLI login (`gcloud auth login`; BIM_GCLOUD names the binary) (or FIRESTORE_EMULATOR_HOST + FIREBASE_STORAGE_EMULATOR_HOST) and BIM_FIREBASE_PROJECT (default pdfmarkuptool); or BIM_LOCAL_REVIEW_URL (the local review server's /local root, e.g. http://127.0.0.1:4101/local) for a Firebase-free session.
bim pdf document.migrate --projectId PROJECTID --documentId DOCUMENTID [--op-id OP-ID] [--dry-run]Migrate a document created before document control: download its current PDF (documents/{d}.storagePath) and document.upsert it (replace) as the next revision, so it gets content-addressed page refs (and the r1 of a document that never had a revision). Output: document.upsert's plus migrated_from (the object read). effect: mutate -- writes Storage (staging + the phase-1 assembled PDF) and, through the review service, a new immutable document revision, the document and the project Set (one transaction; op_id idempotent). --dry-run plans it without writing: reads the document, explodes the file and HEADs the content paths, then reports what a real call would upload and send (dry_run: true; uploaded = would upload; revision / n / noop predicted, diff empty) -- no Storage upload, no service call. Needs Application Default Credentials (GOOGLE_APPLICATION_CREDENTIALS or `gcloud auth application-default login`), else the gcloud CLI login (`gcloud auth login`; BIM_GCLOUD names the binary) (or FIRESTORE_EMULATOR_HOST + FIREBASE_STORAGE_EMULATOR_HOST) and BIM_FIREBASE_PROJECT (default pdfmarkuptool); or BIM_LOCAL_REVIEW_URL (the local review server's /local root, e.g. http://127.0.0.1:4101/local) for a Firebase-free session.

For agent use: /pdf/llms.txt

Guides