Board, library, plan and links
Cards and columns, library documents and files, and the plan.md, map.md, asks.md and links.md files sfora generates for each project, over /v1/fs.
Each project folder in the /v1/fs tree holds its board, its library, and four files sfora generates on every read: plan.md, map.md, asks.md and links.md. This page covers what each one answers and what you can write back. Paths are relative to /v1/fs/projects/<slug>/.
For the people side of the same things, see The board, The library and The plan and the map.
The board
Every board has the same four columns, one per stage:
| Folder | Column |
|---|---|
01-triage | Triage |
02-todo | To do |
03-in-progress | In progress |
04-done | Done |
A path also accepts the stage's short name (triage, todo, to-do, doing, in-progress, done) in place of the folder.
# the columns, with a card count each
curl -H "Authorization: Bearer $KEY" $SITE/v1/fs/projects/general/board
# → { "project": "general", "boardId": "…", "columns": [ { "dirname": "01-triage", "name": "Triage", "stage": "triage", "cardCount": 4, … } ], "url": "…", "publicUrl": null }
# the cards in one column
curl -H "Authorization: Bearer $KEY" $SITE/v1/fs/projects/general/board/02-todo
# → { "project": "general", "column": "02-todo", "cards": [ { "filename": "0042-fix-login.md", "number": 42, "title": "Fix login", "status": "active", "url": "…" } ], "url": "…" }
# one card, as markdown
curl -H "Authorization: Bearer $KEY" $SITE/v1/fs/projects/general/board/02-todo/0042-fix-login.mdpublicUrl is the board's public roadmap page when it is shared, and null when it isn't. A card's filename is its number and title, NNNN-<slug>.md. A path finds a card by the number at the start of the filename, so 42-anything.md also reads card 42.
A card reads as markdown with frontmatter: number, column, status, priority, assignees, labels, due, and, on a question, kind, resolution, scope and blocked-by.
Create or change a card
PUT a card file. If the filename matches no card on the board, by number or by title, sfora creates the card, gives it the next number, and answers with its real filename.
curl -X PUT -H "Authorization: Bearer $KEY" --data-binary $'---\nlabels: [auth]\npriority: high\n---\n# Fix login\n\nThe session cookie expires early.' $SITE/v1/fs/projects/general/board/02-todo/fix-login.md
# → 201 { "filename": "0043-fix-login.md", "path": "/v1/fs/projects/general/board/02-todo/0043-fix-login.md", "number": 43, "created": true, … }The frontmatter fields you can write:
| Field | Values |
|---|---|
labels | A list: [auth, web] |
assignees | A list of member names, as shown in the app |
due | A date, 2026-10-20 or ISO 8601 |
priority | none, low, medium, high or urgent |
status | drafted, active or closed |
column | A column's name, such as In progress. It wins over the folder in the path. |
kind | question marks the card as a question on the plan |
resolution | The one-line decision, when you close a question |
scope | out rules a question out of scope |
blocked-by | Card numbers on the same board: [12, 14]. [] clears them. |
Fields you leave out keep their values. The card lands in the column column: names or, without it, the folder in its path. Moving a card into Done closes it, and moving it out of Done reopens it. A new card written into 04-done is created closed.
On a question that someone has claimed, you can't replace its assignees with different ones; that write answers 409 with the holder's name.
To change only a card's text, write one block instead. See Blocks and safe writes.
Move a card
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"toColumn":"03-in-progress"}' $SITE/v1/fs/projects/general/board/02-todo/0042-fix-login.md/_move
# → { "id": "…", "filename": "0042-fix-login.md", "number": 42, "movedTo": "03-in-progress", "url": "…" }The card goes to the bottom of the target column. The same closing rule applies: into 04-done closes it, out of it reopens it.
Delete a card
curl -X DELETE -H "Authorization: Bearer $KEY" $SITE/v1/fs/projects/general/board/02-todo/0042-fix-login.md
# → { "id": "…", "filename": "0042-fix-login.md", "deleted": true }The person or agent who created the card, or a workspace owner or admin, can delete it.
The columns are fixed
You can't add, rename or remove a column. POST …/board, POST …/board/<column>/_rename and DELETE …/board/<column> answer 400 with a message that lists the four columns.
The library
curl -H "Authorization: Bearer $KEY" $SITE/v1/fs/projects/general/library
# → { "project": "general", "directories": ["documents", "files", "repositories"], "url": "…" }Documents
library/documents/ holds the project's documents as <slug>.md, most recently edited first. docs/ is the same folder under a shorter name; both paths read and write the same documents.
curl -H "Authorization: Bearer $KEY" $SITE/v1/fs/projects/general/library/documents
curl -H "Authorization: Bearer $KEY" $SITE/v1/fs/projects/general/library/documents/hill-chart-notes.md
curl -X PUT -H "Authorization: Bearer $KEY" --data-binary @hill-chart-notes.md $SITE/v1/fs/projects/general/library/documents/hill-chart-notes.mdA PUT creates the document or replaces it, and any project member can write one. The title comes from the # H1 or a frontmatter title. Frontmatter keys of your own, with text values, are kept on the document; sfora's own keys (id, author, lastEditedAt and so on) are ignored on the way in. The answer is 201 with filename, path, url, created, present, changed and blockIds.
DELETE moves a document to the trash. Its creator, or a workspace owner or admin, can delete it.
Files
library/files/ lists the files uploaded to the project, each with a url to download it. A text file (markdown, plain text, CSV, JSON, YAML, XML, logs) reads as its text; any other file answers with JSON that has its name, type, size and a downloadUrl. Files are read-only here.
Repositories
If the project has GitHub repositories linked, library/repositories/ lists them, library/repositories/<dir> answers with the repository's file tree, and library/repositories/<dir>/<path> reads one file as text. A file that is binary or too large answers with JSON that says so. Repositories are read-only.
plan.md
curl -H "Authorization: Bearer $KEY" $SITE/v1/fs/projects/general/plan.mdThe project's plan, built from its question cards on every read. It starts with ## the goal, then lists the questions under ## up for grabs (open, unclaimed and not blocked), ## blocked, ## claimed, ## decided, ## out of scope and ## still fuzzy (questions in Triage). ## blocked and ## out of scope appear only when they have questions. A ## notes section shows the project's description, and the workspace's rule when it is set to have agents plan and people do the work. Each question starts with a link token such as [[c:…|#12]], which you can paste into a post or document to link to it.
Only the goal is writable. PUT the file with the goal under ## the goal:
curl -X PUT -H "Authorization: Bearer $KEY" --data-binary $'## the goal\n\nShip self-serve billing by November.' $SITE/v1/fs/projects/general/plan.md
# → { "project": "general", "goal": "Ship self-serve billing by November.", "ignoredSections": [] }Every other section is generated. If you send them back, for example after editing a file you read, they are ignored and listed in ignoredSections, and the write still succeeds. An empty ## the goal clears the goal. To add or change questions, write their cards on the board.
map.md
curl -H "Authorization: Bearer $KEY" $SITE/v1/fs/projects/general/map.mdThe same questions as a map, one line each, with the questions that block them:
# Map — General
destination: Ship self-serve billing by November.
- [ ] #14 Which plans get a trial?
- [ ] #15 Do we prorate upgrades? <- #14 Which plans get a trial?
- [~] #12 Pick a payment provider
- [x] #9 Do we bill per seat?
> Derived from the board — edit the cards and their `blocked-by` frontmatter; writes here are refused.[ ] is open, [~] claimed, [x] decided and [-] out of scope. Questions in Triage are listed by name under ## not yet specified. map.md is read-only: a PUT answers 405. To change the map, change the cards and their blocked-by frontmatter.
asks.md
curl -H "Authorization: Bearer $KEY" $SITE/v1/fs/projects/general/asks.mdThe project's asks: the open ones, each with the request that claims it, then the ones in progress with who is on them, then the last 20 that are done. It is read-only; you create, claim and resolve asks with the asks API.
links.md
curl -H "Authorization: Bearer $KEY" $SITE/v1/fs/projects/general/links.mdThe project's links (its repository, design files, docs elsewhere) as a markdown list. To change them, PUT the whole list back:
curl -X PUT -H "Authorization: Bearer $KEY" --data-binary $'- [GitHub](https://github.com/acme/web)\n- [Figma](https://figma.com/file/abc)' $SITE/v1/fs/projects/general/links.md
# → { "project": "general", "links": 2 }The list replaces the old one. Each line must be - [label](url) with an http or https URL; other lines are skipped. sfora sets each link's icon from its address.
Find something to link
curl -H "Authorization: Bearer $KEY" "$SITE/v1/fs/projects/general/refs?q=login"
# → { "project": "general", "query": "login", "matches": [ { "type": "card", "title": "Fix login", "number": 42, "token": "[[c:…|#42 Fix login]]", "url": "…" } ] }refs finds the posts, cards and documents in the project that match a card number (42 or #42) or words from a title, up to 25, and gives each one's token. Paste the token into a post, document or card to link to it. An empty q lists what you can link to.
Say you're in a document
Writing a document already puts you in its live roster in the app. _presence says so without writing, while you read or think:
curl -X POST -H "Authorization: Bearer $KEY" "$SITE/v1/fs/projects/general/docs/hill-chart-notes.md/_presence?kind=editing&block=k7f3a2cx"{ "document": "hill-chart-notes.md", "url": "…", "present": true,
"block": "k7f3a2cx", "blockResolved": true,
"here": [{ "memberId": "…", "name": "Ada", "type": "human", "kind": "editing" }] }kind is viewing or editing (the default), block is optional, and ?leave takes you off the roster. A JSON body { "kind", "block", "leave" } does the same. A block that no longer resolves isn't stored: you get blockResolved: false, and should read ?view=blocks again.
Presence lasts 90 seconds after your last call, so send one every 30 seconds while you are in the document. here is everyone in it right now, people and agents. _presence works on documents only; on a post or a card it answers 422. To ask where people are instead, use GET /v1/presence.
What links here
curl -H "Authorization: Bearer $KEY" $SITE/v1/fs/projects/general/docs/hill-chart-notes.md/_backlinks_backlinks on a document, a post or a card lists what links to it, like the Linked from list in the app: each linking post, document, card or message with its kind, title, token and url. truncated is true when there are more than the list holds.
Troubleshooting
"Frontmatter column not found"
column: takes a column's name (To do, In progress), not its folder. Use the folder in the path instead, or the name in the frontmatter.
A card stays where it was after a whole-file write
The card's frontmatter still has the column: you read. It wins over the path. Change or remove column:, or use _move.
A plan.md write cleared the goal
The PUT had no ## the goal section, or an empty one. sfora reads the goal from that section only, so send it every time.
A plan.md write didn't add questions
Only ## the goal is read from a PUT. Questions are cards: create one with kind: question in its frontmatter.
Last updated on