Docs / Admin
Managing documents, templates, and versions
Summary: The Documents workbench (/documents, aliased from /outputs) is where staff create, edit, and track every generated document — concept notes, policy briefs, reports, presentations, minutes, and agreements. Documents move through a review status workflow, keep an immutable version history, and can be generated from reusable templates with {{entity.field}} placeholders.
Purpose: Give staff and admins a single place to draft, revise, approve, and archive outputs, with a full audit trail of every content and status change.
Audience: Staff, reviewer, lead, and admin roles for day-to-day document work. Admin role for creating and managing templates.
Prerequisites:
- None to view or create a document — any
staffrole and above can use/documents. - Creating or editing templates at
/admin/templatesrequires theadminrole.
Overview
Key concepts:
- Document — a titled record of one of six
documentTypevalues, with astatus, optionalcontent(rich text) and/orfilePath(uploaded file), and an optional link to a pipeline opportunity, organization, or consultation. - Version — every content edit or file upload creates a new, immutable
document_versionsrow. The document always points at itscurrentVersionId. - Template — a reusable skeleton with
{{entity.field}}placeholders that auto-fill from a linked opportunity, organization, or consultation when a document is created from it. - Status workflow — documents move through review statuses (
draft,ai_draft,revision_needed,approved,rejected) and can only reach the terminallockedstatus fromapproved. - Locking — signing a document via the e-signature workflow sets
is_locked = trueandstatus = 'locked', making the document permanently immutable. See Sending a document for e-signature.
Screenshot: The Documents workbench with the document list on the left, filters for Type and Status, and the selected document's detail panel on the right showing status action buttons, the Signatures section, and Content.
Document types
| Value | Label |
|---|---|
concept_note | Concept Note |
policy_brief | Policy Brief |
report | Report |
presentation | Presentation |
minutes | Minutes |
agreement | Agreement |
These are the only six types the system supports; the type is chosen when the document is created and cannot be changed afterward through the UI.
Document status workflow
| Status | Label | Meaning |
|---|---|---|
draft | Draft | Default status for a newly created document. |
ai_draft | (not shown in status-action buttons) | Reserved for AI-generated drafts pending human review. |
revision_needed | Revision Needed | Reviewer requested changes ("Request Changes" button). |
approved | Approved | Reviewer signed off; this is the only status from which the document can be locked. |
rejected | Rejected | Reviewer declined the document. |
locked | Locked | Terminal. Set automatically when all e-signature parties sign, or manually from approved. Immutable — content, file, and status can never change again. |
Transition rules (src/lib/modules/outputs/transitions.ts):
- The five review statuses (
draft,ai_draft,revision_needed,approved,rejected) can move freely to any other review status. lockedis only reachable fromapproved.lockedis terminal — no status change is allowed once a document is locked.- Setting a status equal to the current status is a no-op (idempotent).
Attempting an illegal transition throws: Illegal document status change: "<from>" → "<to>". Allowed from "<from>": <list> (or none (terminal status) if locked).
On the document detail page, the status action buttons are: Approved, Request Changes (maps to revision_needed), Rejected, and Locked — the Locked button only appears when the document's current status is approved. All status buttons disappear once the document is locked.
Every status change is recorded in Status History on the document detail page, showing <from> → <to> (or Created as <status> for the initial entry) with the date and any notes.
Creating a document
From /documents, click + New Document in the left panel (NewDocumentForm, src/components/documents/new-document-form.tsx):
- Enter a title.
- Choose a document type from the six types above (defaults to Report).
- Optionally drop or browse a file (PDF, Word, PNG, JPG). The drop zone reminds you: "Upload a PDF if this will need e-signature (Word/Docs → Save as PDF)."
- Click Create.
If no explicit type is set and a file is attached, the type is inferred from the MIME type: PDF → report, Word/.docx → concept_note, image → presentation. This inference only happens through the quick-create path (createDocumentAndUploadAction); the file upload itself is best-effort — if it fails, the document record is still created and a toast shows the error.
A successful create shows the toast "Document created".
Generating a document from a template
Templates live at /admin/templates and let staff skip retyping boilerplate. A template has:
- Sections — an ordered list of
{ title, body, order }blocks. - Fields — declared variables
{ name, entityPath, label }(e.g.entityPath: "organization.name"). - Content skeleton — free text containing
{{entity.field}}placeholders, e.g.{{opportunity.title}}or{{organization.name}}.
When a document is created with a templateId and a linked entity (linkedEntity + linkedEntityId), the service resolves every placeholder against live data (src/lib/modules/outputs/services.ts, resolveEntityData):
linkedEntity | Resolvable placeholders |
|---|---|
opportunity | opportunity.title, opportunity.sector, opportunity.estimated_value_usd, opportunity.stage, opportunity.description, plus the linked organization's organization.name, organization.short_name, organization.org_type, organization.cluster, organization.country, organization.description, organization.website |
organization | Same organization.* fields as above |
consultation | consultation.title, consultation.consultation_type, consultation.scheduled_at, consultation.location, consultation.attendee_count, consultation.attended_count, consultation.insights (all insight-type notes, newline-joined) |
| always available | date.today (ISO date), date.today_long (long-form date) |
Any placeholder that doesn't resolve renders as an uppercase bracketed field name for manual fill, e.g. {{opportunity.custom_field}} → [OPPORTUNITY_CUSTOM_FIELD]. If a template is selected with no linked entity, the raw skeleton (with placeholders unresolved) becomes the initial content.
Building a template manually
At /admin/templates, under Create Template Manually:
- Enter Name, Document Type, and optional Description.
- Write the Content Skeleton using
{{entity.field}}syntax — the placeholder text on the page shows a working example. - Optionally supply Sections and Fields as raw JSON arrays.
- Click Create Template.
Building a template from an existing file
Under Upload Existing Document as Template:
- Enter a Template Name and choose a Document Type.
- Upload a DOCX, PDF, or image file (
.docx,.pdf,.png,.jpg,.jpeg,.tiff,.bmp,.webp). - Click Upload & Extract.
The file's text is extracted via OCR (extractText from @/lib/platform/ocr) and stored as a single "Extracted Content" section and as the raw content skeleton, with no fields pre-tagged. Staff can structure sections and tag fields afterward — the page notes "You can structure sections and tag fields after upload," but there is no dedicated section/field editor beyond re-submitting the manual create form; the extracted skeleton is the working draft you edit directly.
Templates list shows Active/Inactive status (green/gray badge) and a per-row Activate/Deactivate toggle. Only active templates appear when creating a new document from a template.
Editing content and versions
On a document's detail page (unless it is locked):
- Edit Content — expands a textarea pre-filled with the current content, plus an optional "What changed?" change summary. Saving creates a new version and updates the document's current content.
- Upload New Version — uploads a new file to Cloudflare R2, creates a version record pointing at the new file key, and updates the document's
filePathto the latest upload.
Both actions create a new row in document_versions — versions are never edited or deleted, only added. Version numbers are sequential per document, starting at 1 (the initial version created at document creation, if content or a template skeleton was present).
Version History on the detail page lists every version with its number, a file badge if it has an attached file, the change summary, and the date. Each version with a file has a download link (signed URL, VersionDownload). Any version that is not the current version shows a Restore link.
Clicking Restore creates a new version copying that old version's content/file (labeled Restored from v<N>) and makes it current — it does not rewind history, it appends to it.
Uploading a new version or restoring is blocked with Document is locked (thrown from the service) once the document is locked.
Linking documents to entities
A document can be linked to at most one primary entity via linkedEntity + linkedEntityId (set at creation), and to any number of additional entities via document_links (many-to-many, entityType + entityId, unique per document/entity pair). The document detail page shows "Linked to: <linkedEntity>" next to the type/status badges when a primary link exists.
Linked entities also drive template field resolution (see above) and are how the e-signature workflow's "ready to advance to Closed" notification targets the right pipeline opportunity.
Locking (immutability)
is_locked is set on the underlying documents row and mirrored by status = 'locked'. Locking happens in exactly two ways:
- Automatically, when the last signer completes an e-signature request (
src/lib/modules/signatures/repository.ts):UPDATE documents SET is_locked = true, status = 'locked' .... - Manually, by a staff member clicking the Locked status button on the detail page — only available when the document's current status is
approved.
Once locked:
- The status-action buttons, Approval section, "Send for Signature" button, Edit Content, and Upload New Version controls all disappear from the detail page.
- Attempting to send a locked document for signature fails with
Document is locked and cannot be signed(fromsrc/lib/modules/signatures/services.ts). - Attempting to edit content or upload a version on a locked document throws
Document is locked and cannot be edited/Document is lockedrespectively. - No status transition out of
lockedis possible — see the transitions table above.
For the full signing flow — placing fields, inviting signers, tracking Documenso status, and what happens on completion — see Sending a document for e-signature.
Archiving
Archive Document (bottom of the detail page, with a confirm dialog: "'<title>' will be moved to the archive. It won't appear in lists but can be restored later.") sets is_archived = true. Archived documents are excluded from the default list (listDocuments filters is_archived = false unless includeArchived is explicitly requested) but the underlying record and all versions are preserved — this is a soft delete, consistent with the platform-wide no-hard-delete rule. There is no restore-from-archive control in the current UI.
Filtering the document list
The left panel's Type and Status filters (DocumentsFilter, a MultiSelectFilter) are multi-select and OR'd within each facet, driven by the URL query string (?type=concept_note,report&status=draft,approved). The selected document persists across filter changes via ?selected= (documents linked from a consultation page use ?doc= instead — both are accepted).
Roles and permissions
| Action | Minimum role |
|---|---|
| View documents, create documents, edit content, upload versions, change status, archive, link/unlink entities | staff |
| Create or update document templates, upload a template from a file | admin |
All document actions are gated with requireRole(...) from src/lib/modules/outputs/actions.ts.
Best practices
Attach a PDF if the document will ever need a signature. Signing only works on .pdf files. If you start with a Word document, export to PDF and upload it as a new version before requesting signature.
Use the change summary field. It is the only human-readable trail distinguishing versions beyond the timestamp — "Fixed budget figures" is far more useful later than a bare date.
Link the document to its driving entity at creation. Linking an opportunity or organization at creation time (rather than after) lets template placeholders resolve immediately and gives the e-signature completion notification a target.
Warnings
Locking is permanent. There is no unlock path anywhere in the codebase. If a locked document turns out to be wrong, you must create a new document — do not expect to edit or reopen it.
Restoring a version doesn't undo — it adds. Restore creates a new version on top; the version you restored from remains visible in history, and so does everything created after it.
Troubleshooting
"Illegal document status change: ..."
Cause: You (or an automation) tried to move a document to locked from a status other than approved, or tried to move a locked document anywhere.
Fix: Move the document to approved first, then lock it. A locked document can never change status again.
"Document is locked and cannot be edited"
Cause: Someone attempted to save new content on a locked document. Fix: None — create a new document if a correction is needed.
"Document is locked"
Cause: Someone attempted to upload a new file version to a locked document. Fix: Same as above — locked documents accept no further versions.
Template placeholders show as [OPPORTUNITY_TITLE] instead of real values
Cause: The document wasn't created with both a templateId and a linkedEntity/linkedEntityId, or the placeholder path doesn't match a resolvable field (see the resolvable-fields table above).
Fix: Recreate the document from the template with the correct entity link, or manually replace the bracketed placeholder in the content.
FAQ
Can I change a document's type after creating it? No. There is no update-type action in the current UI or service layer.
Can I delete a document?
No hard delete exists. Use Archive Document, which sets is_archived = true and hides it from the default list.
Does the system generate PDFs from document content?
There is a PDF export link on the detail page (/api/export/pdf?id=<documentId>), separate from e-signature. It exports the current content/state as a PDF for download.
What is ai_draft for?
It is one of the six document statuses in the schema, intended for AI-generated content pending human review, consistent with the platform rule that AI output is always a draft requiring human approval. It is not exposed as one of the quick-action status buttons on the detail page in the current UI.
Who can create templates?
Only admin. Staff can use existing active templates when creating documents but cannot create or edit templates themselves.
Related articles
- Sending a document for e-signature — how signing locks a document and notifies the opportunity owner
- The investment pipeline lifecycle — how documents link to and drive pipeline opportunities
- A dedicated guide to the approval-chain workflow is planned.