← Blog
Tutorials15 min readThe Utter team5 views

How to write project docs

XLinkedIn

You wrote the spec in a comment. Someone rewrote it in a Slack thread. Now nobody knows which one is true. That is the tax you pay for having no home for your docs: the design decision lives in a comment on WEB-142, the API contract got pasted into a channel that scrolled away three days ago, and when a new engineer asks "where's the release runbook," the honest answer is "check four places, then ask me."

A real project documentation knowledge base ends that. Utter gives your specs, notes, and runbooks a home right next to the issues they describe, in the same tool, so the source of truth stops moving. This tutorial walks through building an actual project documentation knowledge base in Utter: where the two doc spaces live, how to structure pages into a tree, how autosave and version history really behave, how docs and issues link both ways, and the honest limits, including exactly what the AI does and does not do with your pages.

Why a knowledge base beats scattered comments and Slack threads

Issue comments are good for one thing: a running conversation about one ticket. They are a terrible place to store a spec, because of how comments behave:

  • They sort by time, not by topic.
  • You cannot nest them.
  • There is no version history to walk back through.
  • A spec inside WEB-142's thread is invisible to anyone working on WEB-158 who needs the same information.

Slack is worse. It is a river, and your architecture decision floated past last Tuesday.

A knowledge base fixes the storage problem. Utter's docs are a Notion-style block editor sitting on a page tree you can nest. You write in blocks (headings, lists, tables, code), pages autosave as you type, each save snapshots a version you can restore, and every page carries its own threaded comment thread. It is a real wiki surface, not a notes field bolted onto the side of the app.

What matters most is where it lives. Your docs sit in the same workspace as your projects, your board, and your issues. A doc can reference an issue and an issue can reference a doc, so the spec and the work that implements it are one click apart instead of one tool apart. That is the whole pitch. Stop context-switching between a wiki and a tracker that have never heard of each other.

Workspace Knowledge vs project Docs: which one to use

Utter has two doc spaces, and the difference is scope, not features. Same editor. Same page tree, same autosave, same versions, same comments. The only thing that changes is who can see the pages and where they hang.

flowchart TD
    A[New page to write] --> B{Useful to someone outside this project?}
    B -->|Yes| C[Workspace Knowledge]
    B -->|No| D[Project Docs]
    C --> E[Every workspace member can read it]
    D --> F[Follows the project's access rules]

Workspace Knowledge lives at /w/[workspace]/knowledge. Its pages have no project attached, so any member of the workspace can reach them. The header reads "Knowledge," with a subtitle about being your team's home for specs, notes, and knowledge. This is where cross-team, cross-project material belongs: your engineering onboarding guide, your code-review conventions, your incident-response playbook, the glossary every team argues about.

Utter workspace-level Knowledge base with the 'Knowledge' title and its specs-and-notes subtitle, showing the same block editor used for project docs. Illustrates the workspace vs project scope distinction.

Per-project Docs live under /w/[workspace]/p/[project]/docs. Same editor, but every page is scoped to that project, and the subtitle tells you which one ("Documentation for WEB"). This is the home for material that only makes sense inside one project. For WEB, that means the API reference, the deploy runbook, the sprint retro notes, the design spec for the "Timeline + Summary tab" feature.

Here is the rule of thumb worth memorizing. If a page would help someone who has never touched this project, it goes in Knowledge. If it only means something to people already working in the project, it goes in that project's Docs. Onboarding and standards live in Knowledge. A project's own runbooks and specs live in its Docs.

Scope has one consequence you should not miss. A doc in a scoped project inherits that project's access rules. If a project is restricted to explicit members, its docs are only visible and mentionable to those members. Workspace Knowledge has no such gate; every workspace member can read it. So if a spec is sensitive and should reach a subset of people, put it in a scoped project's Docs, not in Knowledge.

Create your first page

Open either doc space and click "New page." That button calls createDoc, which appends a fresh page at the end of the current sibling list (ranked with LexoRank so ordering stays stable), titles it "Untitled," and indexes it for search right away. There is no draft-then-publish step. The page exists and is findable the moment you create it.

Now name it. The title field shows "Untitled" as a placeholder. The name you pick is the single most important organizing decision you will make on this page, because it is what shows in the tree and what people scan for. "WEB API reference" tells everyone what they are looking at. "Untitled," or worse, "notes," tells them nothing and quietly rots. Rename before you write the body. Every time.

Give the page an emoji while you are there. The "Page icon" control sets one next to the title, and "Reset to default" clears it back. Sounds cosmetic. It is not. Once your tree holds fifteen pages, a column of distinct icons is how your eye finds the runbook versus the retro versus the spec in half a second. A wall of identical default icons is just a wall.

Land in an empty space and you get a prompt sized to your access:

  • "Start your knowledge base" or "No pages yet," with body copy about creating a page to capture specs and notes right next to your work.
  • A read-only variant if that is all you have: "You have read-only access to these pages."
  • "Pick a page" when the scope has pages but you have not opened one yet.

Write with the block editor and slash commands

The editor is BlockNote, block-based. When a page body is empty, the placeholder reads "Start writing, or press / for commands." That slash is the whole editor in one keystroke.

Type / anywhere and a menu opens with the block types: headings, bullet lists, numbered lists, check lists, tables, code blocks, images, and the rest of BlockNote's default set. You never hunt through a toolbar. You type, and when you need structure, you slash.

Here is what that looks like on a real ticket. Say you are writing the spec for the "Timeline + Summary tab" work on WEB:

  1. Start with a heading for "Acceptance criteria," slash into a check list, and write each criterion as a checkbox ("Summary tab renders burndown," "Timeline supports drag-to-reschedule," "RTL mirrors the Gantt").
  2. Drop a heading for "API shape," slash into a code block, and paste the response JSON so nobody has to reverse-engineer it from the network tab.

Ten minutes of writing, and the next person who touches that feature reads the intent instead of guessing at it.

Images go inline too. Paste or insert one and it uploads through a presigned R2 PUT, then serves back through the docs image proxy. Two honest constraints. Only PNG, JPEG, GIF, and WebP are accepted; SVG and HTML are rejected because they are an XSS risk. And image bytes count against your workspace storage cap and hit the per-plan per-file limit, so on the Free plan a single image cannot exceed 1 MB.

The page body itself is capped at 10 MB of block JSON, which is enormous for text but worth knowing if you are tempted to paste a hundred screenshots into one page. Do not. Split it into sub-pages, which is the next section.

Start from a template instead of a blank page

A blank page is a small tax every time. You retype the same headings, you forget a section, and three people's meeting notes end up structured three different ways. Templates fix that.

On a brand-new empty page, a "Start from a template" gallery appears above the editor with six options:

  • Blank page
  • Meeting notes
  • RFC
  • Status update
  • Retrospective
  • Decision log

Pick one and it seeds a predefined structure of headings, lists, and checklists, localized to your language, so a retro always has the same sections and a decision log always captures the same fields. The value is not the typing you save. It is that everyone's retros look alike, so anyone can read any retro without relearning the layout.

One caveat about how this works. The gallery only shows on a fresh, empty page. Type a single character and it is gone. So the flow is: create the page first, then pick the template before you write anything. If you meant to use the RFC template and started typing, the fastest fix is a new page and pick it there.

Organize pages into a tree: nest, reorder, re-parent

A flat list of forty pages is not a knowledge base. It is a junk drawer. The tree is what turns a pile of pages into something navigable, and Utter gives you real nesting.

Every page can hold sub-pages. To create a child, use "Add sub-page" (or "Add sub-page under WEB runbooks" when you act from a specific page's menu). It calls the same createDoc with a parentId, so the new page hangs under its parent. The left panel renders the whole hierarchy with Expand and Collapse toggles, and your collapse state persists in local storage per scope, so the tree stays how you left it between visits.

You rearrange by dragging. Grab a page in the tree and drop it to reorder it among its siblings, or to re-parent it under a different page. The server recomputes the page's rank from the live siblings at drop time, so ordering stays consistent even if someone else moved things while you were dragging. It also refuses to build an impossible tree:

  • Drop a page onto itself and you get "A page cannot be its own parent."
  • Drop a parent into one of its own descendants and you get "Cannot move a page into its own sub-page."

Those guards mean you cannot accidentally create a loop.

Here is a structure that works for WEB. Make a parent called "WEB runbooks," then nest "Deploy," "Rollback," and "Incident" under it. Make another parent, "WEB specs," with a child per feature. Now the tree tells a story at a glance, and someone hunting the rollback steps expands one node instead of scrolling past thirty root-level pages. The thing to avoid is dumping everything at the root. A shallow, grouped tree is scannable. A flat one is not, no matter how good the titles are.

Autosave, version history, and restoring a page

You never press save. The editor does it for you, debounced, so it batches your keystrokes instead of hammering the server on every character. The toolbar tells you where you stand with "Saving...," "Saved," or "Save failed," so you are never guessing whether your last paragraph made it.

The numbers behind that:

What Behavior
Body save 800ms after you stop typing (block JSON plus a Markdown mirror)
Title save 700ms after you stop typing
Version snapshot On every successful body save
Versions shown in the panel 30 most recent
Versions retained per doc 50, older ones pruned

There is a safety net under that. A save-beacon flushes any pending change when you close the tab, so a fast navigation away does not eat your last edit. And if a save was genuinely interrupted, the next time you open the page you get an "Unsaved changes restored." prompt with "Restore" and "Dismiss," to recover the in-flight edit or throw it away.

Every successful body save also snapshots a version. Open the "Version history" panel and you see the recent versions, each labeled with who made it and when ("by Omar Haris"), each with a "Restore" button that reverts the page's title and body to that snapshot. This is your undo for content. Overwrote a section you needed? Restore the version from before you did.

Be honest with yourself about the shape of this, though. Versions are automatic, created on save; you cannot manually name a checkpoint "before the big rewrite." The panel shows the 30 most recent, and only 50 are retained per doc before older ones get pruned, so this is a rolling recent history, not a permanent archive of every state the page has ever been in.

And critically, this is single-user debounced autosave, not Google-Docs-style live co-editing. No live cursors, no presence, no two people typing in the same paragraph at once. If two people edit the same page in the same window, they will overwrite each other. Coordinate, or split the work into separate sub-pages.

A doc nobody can connect back to the work is just a nicer Slack thread. The point of keeping docs in Utter is that they wire into your issues both directions.

Each page has its own threaded comment panel, the "Comments" section. You reply and nest, edit your own comments ("Edit," "Save," "Cancel"), and delete them. The comment box does real work: the placeholder reads "Write a comment. Use @ to mention, # to reference an issue," and it means it. Type @ and you get a member-mention autocomplete; mentioning someone notifies them, and it respects scoped-project visibility, so you cannot mention someone into a project they cannot see. Type # and you get the issue-reference autocomplete, so a comment on the API spec can point straight at the ticket implementing it. One limit worth stating plainly: viewers cannot comment. Read-only means read-only.

From the other direction, you can reference a doc from any prose in the product, an issue description or an issue comment, using the #d: token. The markdown editor offers a #d: autocomplete trigger; pick a doc and it inserts a reference that renders as a named doc-ref chip, so the ticket links back to its spec. If you keep your project conversations in team chat next to your issues, those references keep the whole trail connected.

An Utter issue detail page, the surface where an issue description or comment can carry a doc reference back to its spec.

The honest caveat: doc references use a UUID token under the hood (#d:<uuid>), not a friendly key like the #KEY-NUM you get for issues. You insert them by autocomplete rather than typing a memorable code, and if the referenced doc is deleted, the reference renders as a greyed-out placeholder instead of a live link. A doc-ref points at a specific page reliably while that page exists, but it is not a human-readable citation you would paste into an email.

Sometimes the doc lives in Utter but the person who needs it is in an email, a ticket in another tool, or a meeting invite. Utter gives you a few ways to get a page out or point someone at it.

Open the "Page actions" menu and you get three moves:

  • "Copy link" produces a deep link of the form ?doc=<id> and confirms with a "Link copied to clipboard." toast. That link is genuinely deep: open it and the docs view auto-selects that exact page and expands its ancestors in the tree, so someone lands on the page you meant, in context, not at the root.
  • "Download Markdown" pulls the page down as a .md file.
  • "Export PDF" opens a client-side print window, which means you need to allow pop-ups; if they are blocked, you see "Allow pop-ups to export a PDF."

Docs also show up in global workspace search. Both the title and the Markdown body are indexed on every save, and a search result links straight to the page. So "where's that thing about the rollback steps" is a search away, not a tree-spelunking expedition.

Utter global workspace search results, the surface where indexed doc titles and bodies come back as direct links to the page.

The limit to know: search is role-gated. A workspace member who is not an owner or admin does not see issues or docs in the global search results. Search is there, but it is scoped by role, so do not assume a page is findable-by-search for everyone just because it is findable for you.

Ground AI agents on your docs (and the limits to know)

This is where being honest matters most, because it is the easiest place to oversell.

Here is the true picture. The in-product AI chat does not automatically read your Knowledge or Docs pages. There is no document retrieval wired into the assistant. Ask the chat a question and it is not silently searching your docs and citing them back to you. Do not build a workflow that assumes it does.

What does happen is that external agents can ground themselves on your docs by reading them through the v1 REST API with an API key. That is a real, supported path:

sequenceDiagram
    participant Agent
    participant API as Utter v1 API
    Agent->>API: GET docs list with API key
    API-->>Agent: page ids and titles
    Agent->>API: GET export for one doc, format md
    API-->>Agent: Markdown body
    Agent->>Agent: Use the pages as context

An agent starts by listing the docs it can see. Workspace-level Knowledge by default, or one project's docs with the project filter:

# List the WEB project's docs (drop ?project= for workspace Knowledge)
curl -H "Authorization: Bearer utp_live_YOUR_KEY" \
  "https://utter.ae/api/v1/workspaces/acme/docs?project=WEB"

From there it can GET an individual doc, its versions, and its comments. To pull the actual content in a form a model can read, it hits the export endpoint and gets Markdown back:

curl -H "Authorization: Bearer utp_live_YOUR_KEY" \
  "https://utter.ae/api/v1/workspaces/acme/docs/DOC_ID/export?format=md"
const res = await fetch(
  "https://utter.ae/api/v1/workspaces/acme/docs/DOC_ID/export?format=md",
  { headers: { Authorization: "Bearer utp_live_YOUR_KEY" } },
);
const markdown = await res.text();
import requests

res = requests.get(
    "https://utter.ae/api/v1/workspaces/acme/docs/DOC_ID/export",
    params={"format": "md"},
    headers={"Authorization": "Bearer utp_live_YOUR_KEY"},
)
markdown = res.text

That is how you point an agent at your knowledge base: it reads the pages over the API and uses them as context. If you are wiring this up, the deeper mechanics are in give your AI agent a knowledge base, and the broader model is in what agentic project management actually means.

Utter's developer overview page, where you create the API key an agent uses to read docs over the v1 REST API.

Two API caveats. The export supports md and html only; ask for format=pdf and you get a 400 with "PDF export is not available yet," so the PDF path exists only in the UI's print-window flow, not the API. And permissions carry through the API exactly as they do in the UI.

On permissions generally: owners, admins, and members can create, edit, move, and delete pages and can snapshot and restore versions. Viewers are read-only. Deletion is the one to respect. Deleting a page soft-deletes it and every sub-page under it, and the confirm dialog says so plainly: "This deletes the page and every sub-page under it. This cannot be undone." Unlike version restore, there is no in-UI undo button for a deleted page. It is a soft delete under the hood and follows the same retention as other content, but from where you sit in the product, treat "Delete page" as final. Move things you are unsure about into an "Archive" parent instead of deleting a subtree you might want back.

Give your project docs one home, name the pages so they scan, nest them into a shallow tree, and link them to the issues they describe. That is the whole discipline, and Utter is built to hold it. Open your project's Docs tab and write the first page you have been meaning to write down.

Frequently asked questions

Should I use workspace Knowledge or a project's Docs?

Use workspace Knowledge for anything cross-project (onboarding, code-review conventions, incident playbooks) since every workspace member can read it. Use a project's Docs for material that only makes sense inside that project, like its API reference or deploy runbook. They are the same editor; scope is the only difference.

Does Utter's AI chat read my docs automatically?

No. The in-product AI chat has no document retrieval, so it does not silently search or cite your Knowledge and Docs pages. External agents can ground on your docs, but only by reading them through the v1 REST API with an API key and the export endpoint.

Can two people edit the same doc at once?

Not safely. Saving is single-user debounced autosave (body at 800ms, title at 700ms), not live co-editing. There are no cursors or presence, and two people in the same page will overwrite each other. Split the work into separate sub-pages instead.

How does version history and restore work?

Every successful body save snapshots a version automatically. The Version history panel shows the 30 most recent, each with who made it and a Restore button that reverts title and body. Only 50 are retained per doc before older ones are pruned, and you cannot manually name a checkpoint.

How do I link a doc to an issue?

In a doc comment, type # to reference an issue by its key. From an issue description or comment, type #d: to open the doc-reference autocomplete, which inserts a named doc-ref chip. That gives you a two-way link between the spec and the work.

What happens when I delete a page?

Deleting a page soft-deletes it and every sub-page under it, and the confirm dialog warns that it cannot be undone. Unlike version restore, there is no in-UI button to recover a deleted page, so move uncertain content into an Archive parent instead of deleting a subtree.

Can I export a doc to PDF or Markdown?

Yes. The Page actions menu offers Download Markdown (a .md file) and Export PDF, which opens a client-side print window and needs pop-ups allowed. Note the v1 API export only supports md and html; asking it for PDF returns a 400.

Why can't a teammate find a doc in search?

Global search is role-gated. A workspace member who is not an owner or admin does not see docs or issues in search results, even though titles and bodies are indexed on every save. Do not assume a page is findable-by-search for everyone just because it is for you.

Related reading

أضف تعليقًا

ابدأ النقاش.