Sticky notes#
Sticky notes are quick capture. A single markdown body, an optional color, two scopes (personal or project), and a one-click "convert to ticket" path. They're deliberately less structured than wiki pages: not searched, not versioned, not backlinked.
If you've ever held a thought just long enough to forget it, this is what sticky notes fix.
Scopes#
| Scope | project_id |
Visible to |
|---|---|---|
| Personal | NULL |
Only the creator. Bots have no personal scope. |
| Project | A project UUID | Anyone in the workspace with access to that project (including bots whose allowlist includes it). |
You choose the scope when you create the note. There's no API to move a note between scopes — if you need to, create a new note in the target scope and delete the old one. Or, if the note has grown up enough to want sharing, convert it to a ticket and let it move into the structured world.
Where they show up#
| Surface | Route | What you see |
|---|---|---|
| Dashboard | / |
Your personal notes. |
| Project home | /projects/{KEY} |
The project's shared notes. |
Both surfaces use the same masonry layout — variable-height cards laid out by CSS columns. No drag-to-reorder; if order mattered, the data should be in a ticket.
Creating a note#
From the UI#
| Surface | How |
|---|---|
| Dashboard | + card at the top of the personal-notes board. |
| Project home | + card at the top of the project-notes board. |
| Expanded view | Inside the note dialog, Duplicate (planned, not v1). |
Fill in the body (markdown, required). Pick a color from the palette (optional). Save.
Bodies are capped at 64 KiB (65,536 characters). The Pydantic schema rejects anything longer with 422 validation.invalid_field. Sticky notes are scratchpads — if the content wants to be longer, it wants to be a wiki page or a ticket body.
From the API (or a bot)#
A bot creating a project-scoped note:
curl -s -X POST http://localhost/api/v1/projects/$PROJECT_UUID/sticky-notes \
-H "Content-Type: application/json" \
-H "X-API-Key: $BOT_TOKEN" \
-d '{"body":"Investigate `transition_ticket` 500 from last night."}'
Personal notes for the current actor:
curl -s -X POST http://localhost/api/v1/sticky-notes \
-H "Content-Type: application/json" \
-H "X-Requested-By: web" \
-b "tb_session=$YOUR_SESSION" \
-d '{"body":"Reminder: rotate the home wifi password."}'
Bots calling the personal-note endpoint receive 403 auth.forbidden (reason=bots_have_no_personal_notes). This is a contract, not an accident — bots are not personal entities; if a bot needs a scratchpad, it belongs in a project-scoped note where the operator can see it.
The color palette#
sticky_note.color is a free-form #RRGGBB hex string. The UI's swatch picker exposes 8 WCAG-AA-tested presets plus a "custom" picker:
| Swatch | Hex | Suggested use |
|---|---|---|
| Yellow | #fde68a |
Default / scratch |
| Green | #a7f3d0 |
Done / shipped thoughts |
| Blue | #bfdbfe |
Information / reference |
| Pink | #fbcfe8 |
Ideas |
| Orange | #fed7aa |
In-flight |
| Purple | #ddd6fe |
Spike / experiment |
| Slate | #cbd5e1 |
Archived feel |
| White | #ffffff |
No accent |
The "suggested use" column is a convention, not a contract. The system does not care; pick whatever scheme makes the boards readable to you.
If you set a custom color, validate it against ^#[0-9A-Fa-f]{6}$ — the API enforces this and returns 422 validation.invalid_field for malformed values.
Markdown rendering#
Sticky notes use a stricter version of the wiki's rendering pipeline. The same markdown-it-py engine, the same sanitizer, but a smaller allowlist:
| What's allowed | What's stripped |
|---|---|
| Bold, italic, strikethrough | Tables (no <table>, <thead>, <tbody>, <tr>, <th>, <td>) |
Inline code |
Images (no <img>) |
Code fences (```) with Pygments highlighting |
Headings h1–h2 (only h3–h6 allowed) |
| Lists (ordered, unordered, task lists) | Task-list checkboxes render as plain text — no <input> |
Links (http://, https://, mailto:, relative) |
|
Blockquotes, <hr>, <br> |
The intent is to keep notes feeling like notes, not mini wiki pages. If your note wants tables and images, it wants to be a wiki page.
Wikilinks are not parsed#
[[slug]] and [[ticket:PROJ-123]] inside a sticky-note body render as literal text. Sticky notes are explicitly outside the wiki link graph. If a note needs a referenced item, that's a signal the content should graduate to a ticket or a wiki page.
Viewing and editing#
Click a card → a Radix dialog opens with the rendered note. Actions:
| Action | Effect |
|---|---|
| Edit | Inline markdown editor in the dialog. Color picker at the top. Save closes the dialog and refreshes the list. No auto-save. |
| Delete | Hard-delete. Permanent. Confirms before firing. |
| Convert to Ticket | Opens the conversion modal (see below). |
A project-scoped note is editable by any workspace actor with access to the project — including bots whose allowlist includes it. A personal note is editable only by its owner.
Converting to a ticket#
The convert flow is the single most useful sticky-note feature. It turns a half-formed thought into structured work without a copy-paste step.
Through the UI#
- Click the note card → dialog opens.
- Click Convert to Ticket.
- Fill in the conversion form:
- Project — required if the source note is personal; pre-filled if the note is already project-scoped.
- Type — epic, story, or subtask. Subtasks require a parent story; the parent picker is type-aware.
- Parent — optional, with type-aware search.
- Assignee — optional.
- Priority — default
medium. - Title — pre-filled with the first line of the note body; editable.
- Submit. The new ticket opens in a new view; you land on its detail page.
The sticky note is not deleted by the conversion. It stays on the board, dimmed, with a "Converted to PROJ-N" badge.
Through the API#
curl -s -X POST http://localhost/api/v1/sticky-notes/$NOTE_ID/convert-to-ticket \
-H "Content-Type: application/json" \
-H "X-Requested-By: web" \
-b "tb_session=$YOUR_SESSION" \
-d '{
"project_id": "01H...",
"type": "story",
"priority": "high"
}'
Behavior:
- The new ticket's
bodyis the note'sbody. - The new ticket's
titleis the request'stitlefield if you supply one, else the first line of the note's body (stripped of markdown markers). - The note's
converted_to_ticket_idis set to the new ticket's id. This is set-once; trying to re-convert returns409 resource.conflict(reason=already_converted). - The note remains in the database. To clean up after conversion, delete it.
Bots can convert project-scoped notes if they have write mode and the project is in their allowlist. Personal notes are never accessible to bots, so they cannot convert them.
Why the note stays#
Two reasons:
- Audit trail. The
sticky_note.convertedevent records the note id → ticket id mapping. Keeping the note visible makes the lineage human-readable. - One-way door safety. Conversion is a creation event, not a destruction event. If you discover the resulting ticket should not have existed, you can delete it without losing the original thought.
If clutter bothers you, delete the note after you've confirmed the ticket is right.
Visibility rules in detail#
The service layer enforces visibility on every read:
| Caller | Personal note | Project note |
|---|---|---|
| Human (the owner) | Visible | Visible if they have project access (in v1, every human has every project). |
| Human (not the owner) | Not visible. Returns 404 (resource.not_found) on direct read. |
Visible. |
| Bot | Never visible — bots have no personal scope. GET /api/v1/sticky-notes returns an empty list. |
Visible if the project is in the bot's allowlist. |
The 404 for someone else's personal note is intentional — we do not distinguish "doesn't exist" from "not visible to you". This matches classic IDOR-defense framing.
Searching#
Sticky notes are not indexed in the FTS corpus. They don't appear in /search results.
Rationale: notes are throwaway. Indexing them pollutes search with low-signal hits. The convert-to-ticket flow promotes valuable content into the indexed corpus (tickets are indexed); the wiki is where stable references live (wiki pages are indexed). Notes are the unstable layer between the two.
If you find yourself wishing notes were searchable, that's a signal you're using notes where wiki pages or tickets would serve you better. The fix is to convert, not to extend the index.
Audit semantics#
Sticky-note events land in the audit log with a category that depends on scope:
| Event | Project note category | Personal note category |
|---|---|---|
sticky_note.created |
activity |
audit_only |
sticky_note.updated |
activity |
audit_only |
sticky_note.deleted |
activity |
audit_only |
sticky_note.converted |
activity |
activity (regardless of source) |
Project-note events surface on the project activity feed. Personal-note events stay in the audit-only stream — no one but the owner would see them anyway, and the project feed should not be polluted with another user's personal scratch.
The sticky_note.converted event is always activity because the resulting ticket is a workspace-visible artifact; the conversion belongs in the human-visible feed.
Common questions#
Why can't bots create personal notes? A personal note is owned by an actor as a scratchpad. Bots are stateless workers — they don't have a "private" scope to write into. Anything a bot would write is workspace-relevant by definition; that belongs in a project-scoped note where the operator can see it.
Why aren't notes versioned like wiki pages? Notes are designed to be edited destructively. Versioning every state of a quickly-iterated scratchpad would balloon the database with low-signal history. If you need history, the content belongs in a wiki page.
Can I attach a note to a ticket? No. Convert it to a ticket (linking is one-way via converted_to_ticket_id), or paste the content into a comment. Tickets carry their own context; sticky notes are pre-ticket.
Why is there no "archive" for notes? Hard-delete is the archive. If you want to keep something around forever, convert it to a ticket or a wiki page. Notes have no deleted_at column.
API surface recap#
| Endpoint | Verb | Purpose |
|---|---|---|
/api/v1/sticky-notes |
GET | List the current actor's personal notes (bots: empty). |
/api/v1/sticky-notes |
POST | Create a personal note for the current actor (bots: 403). |
/api/v1/projects/{id}/sticky-notes |
GET | List project-scoped notes. |
/api/v1/projects/{id}/sticky-notes |
POST | Create a project-scoped note. |
/api/v1/sticky-notes/{id} |
PATCH | Update body, color. |
/api/v1/sticky-notes/{id} |
DELETE | Hard-delete. |
/api/v1/sticky-notes/{id}/convert-to-ticket |
POST | Create a ticket from the note's body. |
Full details: spec/02_api_design.md#sticky-notes and spec/05_wiki_and_notes.md#sticky-notes.
Next#
- If a note has hardened into something you'll reference again, convert it to a ticket or write a wiki page.
- If you want a bot to triage your project notes, configure its project allowlist.