Projects and issue types
A project is what a company has committed to. Issues belong to it; agents work inside it.
Projects
Projects → New project. Name, description, status, priority, and a lead — who may be a person or an agent, since leadership here means accountability for the work, not for people.
There is no target date. A project here is a system, and it does not finish.
| Status | Means |
|---|---|
planned |
Agreed, not started |
in_progress |
Active |
paused |
Deliberately stopped, not abandoned |
completed |
Done |
canceled |
Will not happen |
The list hides closed projects behind an Open / All toggle, and orders by what is happening now rather than alphabetically.
What is open, not how far through
A project shows how many issues are open on it — measured, computed on read, with no field to update and therefore no number that can drift from reality. Standing work is left out: support on a system is capacity it consumes rather than a deliverable it is waiting for.
It used to show a completion percentage and a target date, and both went for the same reason. A project here is a system that will still be running in three years, so:
- A percentage described the backlog, not progress. It drifts towards a hundred as old work is closed, whatever is happening now.
- A target date was either ignored or quietly wrong. A target permanently in the past teaches everybody to ignore the colour red.
Time-bounded work belongs to an epic, which has a start date, a due date and milestones — and milestones were built on the epic rather than on the project from the start, for this exact reason.
Two things went with the date. The Gantt no longer draws a yellow diamond on a project's group row, and the health rule "Later than its project" is gone. There is deliberately no substitute comparing an issue to its epic's date: that would be a new rule rather than the same one.
The issue prefix
Issue prefix, beside the name on the create form and when you edit a project. Leave it empty
and issues read as the workspace prefix — ACME-14. Set it to PRE and the issues under this
project read PRE-14.
Only the prefix belongs to the project. The number is always the workspace's. Move an issue
to another project and the label in front of it changes while the number behind it does not, so
PRE-14 and ACME-14 are the same issue and both still find it. Typing 14 on its own finds it
too.
That is the decision the feature rests on, and the alternative is worse in a way that is easy to
miss: a per-project counter would mean either renumbering an issue every time it moved, or two
different issues called PRE-1 — and an identifier somebody wrote into a commit message last
month resolving to nothing, or to the wrong thing.
Changing a prefix therefore invalidates nothing anybody wrote down. The old identifier still resolves, and the change is recorded in the project's history with both values so the log explains itself. A prefix already used by another project, or by the workspace, is refused.
It is not part of the URL — an issue still lives at its numeric address.
What a finished issue costs
Under the project's cost line, once anything has finished:
$204 per finished issue, over 3 · $300 when a person finished it (2) · $12 when a machine did (1) — not adjusted for the size of the work
Per issue and not per hour, because an hour is an input and an issue is what somebody actually wanted. Finished work only: the open half is money spent on outcomes that have not happened yet, and dividing it by the issues that have would make every project look cheaper the more unfinished work it carried.
The two per-population figures side by side are the counterfactual in its plainest form — what an issue costs when a person does it against what one costs when a machine does. They are not adjusted for the size of the work, and the line says so: a machine that is handed the small tasks looks cheap per issue for a reason that has nothing to do with the machine. Where some hours are unpriced the figure says it is too low, as the cost line above it already does.
Adding issues
New issue, beside the Issues heading on the project's own page. It lands in this project, in your first column, and the types offered are the ones this project uses rather than all nine — the same list Project → Issue types governs, enforced here and not merely displayed.
The board's form asks which project an issue belongs to, because from there it is a real question. Here it is not, so it is not asked. The other direction still works: move an existing issue into a project from the board, or from the issue's own properties panel.
A due date and an estimate, on the form. Both used to live only on the issue's own page, so
filing an issue with a deadline was two steps and the second was the one that got skipped — and
an issue without an estimate is counted apart on every report, while one without a date joins the
back of every queue. Neither is required. The estimate is written the way it is said (2h 30m,
1d, 1w, 90m) and refused with a reason if it cannot be read, rather than saved as nothing.
Not for an epic: it has no estimate of its own, and its dates are set on its page beside the
start date it also has — choosing that type takes both fields off the form.
Deleting
Deleting a project keeps its issues; they lose the grouping. A project is a container, not an owner.
Issue types per project
Nine types exist in the system:
epic · bug · feature · task · cosmetics · exception · usability_problem ·
performance_problem
A new project starts with four — epic, bug, feature, task — because a picker with nine entries makes people choose badly. Project → Issue types turns on the rest.
The dialog shows how many issues currently hold each type, and enforces two rules:
- A project cannot be left with no types at all
- A type already in use cannot be switched off; the error names it and counts it
And one that protects data rather than configuration: an issue already holding a type the project has since dropped keeps it in its own picker. Changing project settings never silently rewrites existing records.
Statuses are yours to name, and yours to define
Administration → Statuses. Rename them, reorder them, mark which ones mean nobody is moving the work — and say what each one means.
That last part is the feature. Every status carries a category: backlog, unstarted,
started, completed or canceled. No report reads the name. They read the category,
which is why the two operations behave so differently:
| You do this | What moves |
|---|---|
| Rename In review to QA | Nothing anybody counts. The word changes everywhere it appears |
Re-classify QA as completed |
Every count at once — progress, burndown, velocity, throughput, the project tabs — with no report edited |
Re-classifying is recorded in the workspace history with both values, because a graph that changes its mind should be able to say why.
completed and canceled both close an issue; only completed becomes velocity. Paused
is its own switch rather than a category, so a stalled issue stays counted as open work —
nobody is moving it, but it is still somebody's problem.
The key column is shown and cannot be edited. An issue stores the key, the API sends it and your scripts read it, so a rename has to leave all three working — the same reason renaming a project does not renumber its issues.
Not yet: you cannot add or remove statuses. The set can be renamed, re-classified and reordered; the database still holds a fixed set, and a form offering what the database refuses would be worse than one that does not.
Which type to use
The set is broader than most trackers' on purpose — it distinguishes kinds of defect that get handled differently:
| Type | Use when |
|---|---|
| Bug | It does not do what it is supposed to |
| Exception | It crashes — there is a stack trace |
| Usability problem | It works, but people get it wrong |
| Performance problem | It works, but too slowly |
| Cosmetics | It looks wrong |
Separating these means "42 open bugs" stops being a number that hides four different conversations.
Standing work
Support. Datacenter maintenance. A rota. Work that never finishes, that a developer carries as a share of their week rather than as a thing to complete — and that used to have no honest home, because the only tool was an issue that stayed open for ever and dragged every measurement with it.
Type Generic, off by default like the other extra types, and switched on per project
where a team has some. It takes a Share of week instead of an estimate: 10%, 1d,
4h.
The two forms mean different things, deliberately. A percentage is resolved against that
person's own weekly hours, so ten per cent follows somebody down to three days a week.
1d is a day whoever else's week it is, because "Thursdays on support" is a rota rather
than a proportion.
It claims the share while it is in progress, and not a moment longer. The status is the switch — move it back to the backlog and the capacity returns to whoever was carrying it. Nothing to remember, nothing to expire.
A generic issue needs a project. Standing work is always somebody's share of one, and without it the hours have nowhere to be reported — which is half the reason for recording them. Finansportalen: 120h delivery, 32h support is the sentence this makes possible.
What it changes, and what it deliberately does not
| Planning | Somebody on 10% support has 36 plannable hours in a 40-hour week. The Gantt's projections, the bottleneck report and a cycle's capacity all use the smaller number |
| The timesheet | Still expects the whole 40. A developer logs their support hours like any others, so netting them off here would nag about hours that were worked and recorded |
| Progress | Left out of it entirely — an epic's bar, a project's, a cycle's carry-over. A thing that is not supposed to end cannot be behind, and counting it would stop every bar short of a hundred for ever |
| Overruns | It has no estimate, so it is never over one. What it can be is over its share, which is a different and more useful thing to notice |
Over-commitment is reported, not refused. If somebody's standing work adds up past their own week, the reports say so — the same answer cycles already give. Refusing it would block a real temporary overload, and negative capacity would make every projection after it meaningless.
One consequence worth knowing: the status is a fact about now. Capacity for a week in the past is worked out from today's statuses, so forward planning is exact where a historical utilisation figure is approximate.
Estimation and spent time
Both are entered the way people speak — 2h 30m, 1d, 1w, 90m — and stored as minutes. A bare
number means hours, 1d means eight hours rather than twenty-four, and 1w means five of
those days rather than seven. Nobody logging time against an issue means the weekend.
Days are the largest unit anything is displayed in, though, so 1w comes back as 5d.
A week rolls up two conversions where a day rolls up one, and these figures are read in
reports by people who did not type them.
Agent runs count as spent time. When a run completes, its duration is added to the issue. If the total passes the estimate, the field is marked over. An issue with no estimate is never an overrun, because an absent estimate is not a promise.
Filtering the board
Type in the menu to reach anything in it. The box at the top matches across every group at once, so "Ecom" finds the project and "Knut" the person without choosing which submenu to look in first. While something is typed the groups give way to the matches.
Opening Filter puts the cursor in the box. Type, then ↓ and ↑ to move through the matches — the highlight moves and the cursor stays where you are typing — then Enter to apply, Escape to close.
Until this release the box was visible and did nothing with your first keystroke. Focus was on the menu, so the letter drove the menu's own first-letter jump instead: type "pro" expecting to find a project and you walked the menu to Person…, where a later Enter could apply something you never chose. A search box that repurposes a keystroke is worse than no box.
It shipped in 0.70.4 unable to take a single letter, which nothing caught until somebody tried it. A Flux menu runs a typeahead of its own -- press a letter anywhere in it and focus jumps to the first row starting with that letter -- and it moves the focus during the keystroke, before the character is inserted, so not one letter reached the box: you typed "Knut" and read nothing, with focus sitting on a row called Knut Horne. Letters no longer leave the box.
Down arrow reaches the first match, Enter picks it, so a filter can be set without touching the mouse. Neither did anything before, for the same reason the typing did not: the menu walks from whichever row it thinks is current, and while the box has focus there is no such row.
Every applied filter is shown as a chip, under the controls, labelled dimension: value and each with an × that removes only that one.
The row exists because a dropdown can only be read by opening it, and because the button in front of it could not say what was applied. With a person and a project both narrowing the board it read Person: Knut W. Horne — which does not merely omit the project, it states one filter as though it were the filter. Before that it read Filtered, which said less and was better for it. A label cannot express a set, so it stopped trying: the button now reads Filter or Filter (2), and the number agrees with the chips.
One chip per value. Two labels are two chips, because labels narrow with AND and removing one while keeping the other is the point. The order is fixed by dimension and then by the order things were ticked, so a new chip lands at the end and nothing already on screen moves under a cursor on its way to an ×.
The value truncates and the dimension never does — Project: Inside NEXT Datacen… still tells you what kind of thing it is, where …enter - Eviny does not. The full value is on hover, and every × is a button, so the whole row works from the keyboard.
The search box has no chip. It sits on the same row with its own clear, and it was never the thing you had to open something to see.
Search looks in the thread as well. Search issues matches the title, the description, the issue number — and the comments. Most of what is known about an issue is said underneath it rather than in the description: the reproduction somebody added on Tuesday, the endpoint that turned out to be at fault, the decision. Searching only the description finds the issues that were written well and misses the ones that were worked on.
Where the thread is the only reason an issue matched, the card says in a comment. A result with the word nowhere on it reads as a broken search rather than a wider one. There is no badge when the title or the description already holds the word.
Withdrawn comments are not searched. Their words are gone; only the row survives, so the replies under it still point at something.
This is not the ⌘K palette, which deliberately leaves comments out — see Search. The palette is a way to get somewhere and would find the same issue eleven times; this box is a way to narrow a board.
The toolbar does not move when you filter. Search, Filter, Clear, Views, the view tabs and New issue hold the same positions whether nothing is applied or six things are, and the header keeps one height.
It used to rearrange itself. The row spaced its children apart, so the chips arriving as a third child stopped the controls being the right-hand group and made them the middle one: one filter moved Search 440px left and New issue 308px, and the second moved them back as the chips wrapped to a line of their own — 56px of header becoming 118px. Every filter change relocated the button you were about to press, and Search moved furthest while holding your typed text and your caret.
Chips that do not fit collapse into a count. A +3 beside them opens a list of the rest,
each removable from there. One row, one height, and no label ever cut mid-word — which is
what clipping would do, and reading "…merce" and guessing is worse than wrapping.
Clear sits to the right of the count it undoes, and appears when there is something to clear — without moving anything beside it. Its box is always there and only the pixels come and go, which is the same trick the count in Filter (2) uses to hold still.
It clears everything in one action, which is the case it exists for: a board can end up with every column at zero, and the way out should not be reopening the menu to unset things one at a time — the moment somebody least knows what to unset is the moment they cannot see what is applied. The chips answer that now, and Clear is for starting over.
A board can be taken off the view it names, and says when it has been. Removing a chip — or ticking anything at all — while a saved view is applied leaves the board showing something the view does not describe. That is allowed: refusing it would make a named view a worse place to start from than an empty board. So the Views button reads My open bugs · changed and a line underneath says what to do about it — save it again, or reapply the view to go back.
It used to happen silently, which is the kind of thing somebody notices a week later and stops trusting the feature over. Applying a view and then ticking one more status left the button naming the view with no sign that the board had moved.
Ticking the same two statuses in the other order is not a change, and neither is applying a view saved before a filter existed.
The board comes back as you left it. Filters live in the address bar, so opening an issue and returning through the sidebar or the breadcrumb lands on the board still narrowed. It is remembered per person rather than per tab, so two boards open at once share one memory and the link goes back to whichever was looked at last.
Copy a link to one comment. The menu on any comment has Copy link, which copies an
absolute URL ending in #comment-123. Opening it scrolls to that comment and rings it
briefly. Available to anybody who can read the comment, not only its author — the menu used
to appear only for the writer, because until now it held nothing anybody else could use.
A withdrawn comment offers no link. The card stays, because the replies under it point at it, and there are no words to send somebody to.
Double-click a card to open it, anywhere on the card. Not a single click: cards are dragged, and a whole-card single click fights the drag. The identifier and the title are still ordinary links, so a middle-click opens a tab. In List view the title is a link across the whole column.
Drag from anywhere on a card, links included. For a while the title was the one place a drag could not begin, and it did not look like a refusal: a card dragged from a link follows the cursor exactly as a real drag does, and then snaps back on release. That is the browser's own drag on the anchor, not ours — the sorting library declines there without cancelling the event, so something takes over that looks identical and does nothing. Every link on a card now refuses to be dragged itself, which leaves the card free to be.
Filter narrows the board and the list together: status, assignee, project, type, labels, a cycle.
Status, project, type and labels take more than one value. Tick In progress and In review and the board shows both; tick two projects and it shows both. Several values in one filter mean "or", and two filters narrow each other: bug or exception, in Ecommerce is one board rather than three visits to the menu. The questions people actually ask are shaped that way -- what is on the go, which is two statuses, not one.
Labels are the exception and always were: two labels ask for issues carrying both. An issue has one type and one status, so "or" is the only reading that returns anything; it can carry ten labels at once, so "and" is the one that narrows.
Nothing ticked means everything, which is why All statuses and Any type are gone. A row that means "no filter" is redundant beside an empty set of ticks, and it made the menu read as though one value had to be chosen.
The menu stays open while you tick, because a filter you build out of three values should not cost three trips through the same menu.
Links written before this still work. ?status=done -- a shared link, a bookmark, a
saved view holding a single value -- opens the same board it always did; the address bar
writes ?status[]=done&status[]=in_progress once there are two.
There is a third way the old shape arrives, and it was the one that bit: a tab left open across the release, which keeps sending the state it was loaded with. A link and a saved view are both repaired on the way in; a tab sends its state earlier than either repair can run, and that path failed outright -- every click in that tab until it was reloaded.
All three are read the same way now: a single value means one value. Which is why it is repaired rather than assumed. The shape that breaks silently is the worse of the two -- an unnarrowed board that still says Filtered looks like a working page -- and this had one of each.
Assignee and cycle still take one value. Both have entries that mean something other than a value -- unassigned, assigned to agents, no cycle -- and ticking those beside a name asks a question with no answer. Type, status and project have no such rows.
The menu opens with every way to filter by assignee, in one group: anyone, assigned to me, Person…, assigned to agents, Agent… and unassigned. The two pickers sit with their siblings rather than four screens below them — the group is separated from everything else by a divider, and a divider says "this is the whole set", so a picker outside it read as a capability that did not exist.
The names stay behind the submenu. That is what the submenu was for: thirteen of them in the parent menu would push everything under them off a short window, and one row is not thirteen names.
Person is a fifth choice, offered to everybody who can open the board. It narrows to one colleague — has anybody got this already, who owns this area, what has she got in review before I touch that file. Questions people ask out loud every day.
It lists everybody in the workspace, not only the people whose role reads developer. Admins and owners carry issues too, and a picker that could not select them would answer "nothing assigned" for work plainly on the board.
It shipped for owners and administrators only, and that was wrong. The reason given was menu length, which the submenu it sits in had already dealt with: one row, the same one row Cycle and Labels cost, and neither of those asks about a role. Meanwhile the gate protected nothing — the same colleague could narrow the board by URL, by opening a shared view holding a person, or by asking the API, which grants exactly what the browser was refusing. It removed the path people could find and left the ones they could not.
Agent is the same thing for the workforce, and it is offered to everybody. Assigned to agents has always been, and an agent is a tool the team shares rather than a colleague whose workload is their own business — so narrowing to one of them shows nobody anything they were not already being shown.
While one is active the menu says who — Person: Alex Berg rather than a tick inside a submenu nobody has opened — and so does the Filter button. A board narrowed to somebody with nothing on their plate should not read as a board that is broken.
Both read user:5 and agent:3 in the address bar, which is exactly what
GET /issues?assignee= accepts — so a board link somebody sends and a query a script
runs say the same thing without translating between them.
The filter itself is not a permission, and now nothing about it pretends to be. Everybody already sees every member's work on the board and can already assign an issue to anybody, so narrowing to one person hides cards rather than revealing any — which is also why a shared view holding a person works for whoever opens it, instead of quietly filtering to nothing in their hands.
Reports → Standup is still for owners and administrators, and on different grounds. It is not the same cards with some hidden: it ranks colleagues by whose work has gone over its estimate, which is a judgement about people rather than a view of the board. Seeing who is behind on what is the same kind of trust as deciding who works here.
Saved views
Narrow the board — status, assignee, project, type, labels, a cycle — then Filter → Save this view. It comes back from the Views button, and the address bar carries it, so a link opens the board somebody else was looking at.
Private until you share it. A view is a working note until its author decides it is a team tool, so sharing is deliberate rather than the fallback, and a shared view stays its author's to delete.
Saving the same name twice replaces it. A view saved before a filter existed opens without that filter rather than keeping whatever the board happened to be showing — otherwise applying a view would depend on what you were looking at first, which is the one thing a saved view exists to prevent.
Acting on many at once
Switch the board to List, tick the issues, and a bar appears saying how many you have chosen. From there: status, priority, assignee, project.
It appears only when something is ticked. A bar that is always there is a row of buttons whose target you have to work out; one that arrives with a count in it has already answered the question.
Every change is written to each issue's history, marked as a bulk edit — twenty issues changing at once is exactly when somebody later asks what happened.
Assigning to an agent queues a run each. Twenty issues is twenty runs, and a run costs real money, so that is the one action that says how many before it does anything — and, where there is enough history, what it is likely to cost. See Hiring an agent.
The statuses offered are your workspace's own, under the names you gave them.
Attachments
Both projects and issues take files. Drop them on the page while you are reading it — there is no Edit and no Save, the upload is the whole action. New issue takes them too, though there they are held until you file the issue, because an attachment needs something to belong to.
New issue opens as a wide modal, with what you write on the left and what you choose on the right. It was a sidebar, which left the description editor 444 pixels across and 112 tall with no way to make it bigger — fine for a sentence, and the only field on the form that holds prose. Filing an issue is writing, and the form was shaped like a settings panel.
Paste a screenshot and it becomes an attachment. ⌘V anywhere in the form — over the title, in the description, on the dropzone — and the image joins the list.
This works on New issue, on an issue's description while you are editing it, and in the comment box. For a while it worked only on New issue, which is the half that matters least: an issue is filed once and commented on for weeks. A pasted image lands where the form it was pasted into puts its files — a comment's on the comment, the description's on the issue.
The clipboard is where a screenshot lives for the ten seconds between taking it and needing it. Pasting one used to do nothing at all: no image, no error, no attachment, the keystroke dropped. Saving it to disk in order to attach it and then deleting it is three steps for something already in hand.
Tip: Anywhere in the form, because the clipboard does not know which box the pointer is over — and pasting a screenshot into the description means the same thing as pasting it onto the dropzone. Pasted text still goes where you aimed it; only images are taken.
Tip: It becomes an attachment rather than a picture inside the description. That is what
this product has for an image belonging to an issue: a size, a checksum, a thumbnail, a place
in the issue's own list, and a route that checks who is asking before serving it. An image
inside the description would mean allowing img in the text sanitiser, which is a question
about every remote address anybody can paste — a separate decision, not made here.
Tip: A clipboard image has no name of its own, so it is given one from the clock:
pasted-2026-08-28-09-21-40.png. Four files called image.png is a list nobody can read.
Images, video, PDF, text, Markdown, CSV, JSON, XML, HTML, ZIP, Word and Excel, up to 25 MB and 50 MB for a video — which the dropzone now says, because the size limit was the only fact it stated and it is the one nobody hits. Anything the application will not serve back safely is refused where you choose it, and the refusal names the type it read rather than leaving you to guess.
HTML is taken and always handed back as a download, never displayed. PNG, JPEG, GIF, WebP and video are shown inline; an HTML page rendered from this origin could run script as though it were part of Félagi. That is the reason the question was ever open, and the reason it can be answered yes.
Video
MP4, WebM and MOV, up to 50 MB. A screen recording is often the fastest way to show a bug, and without this it went through another channel — which meant it was not attached to the issue, and six months later nobody could find it.
A video gets its own size limit, and the reason is that the general one would have made the feature ornamental: 25 MB is about twenty seconds of a screen recording, so accepting the format at that cap would accept video and refuse the thing people record. Fifty leaves headroom under the request body the application server allows.
Tip: It shows as a row with Preview, not as a thumbnail. Preview plays it in the page, with a scrub bar and no autoplay — a recording that starts talking the moment somebody opens an issue is a recording they close.
Tip: MOV is video/quicktime, which is what a Mac records by default. Refusing it
would have refused the commonest case on the machines this is used from.
Tip: Video is served inline, unlike HTML and SVG, and the difference is a real one
rather than a judgement about how common the type is: a container of frames has no script
to run, and the nosniff header stops a browser reinterpreting it as something that does.
Getting a file back out. Every attachment row has Download, and so does the top right of the preview that opens when you click a thumbnail or press Preview. Both hand you the original — for an image, the file that was uploaded and not the resized copy the preview is showing you.
That button did nothing for a while, and the reason is worth a sentence because it looked finished: the component it was built on decides whether to be a link or a button when the page is rendered, and the address was being filled in afterwards. Fixed, and guarded by a test that sweeps every template for the same shape.
Attachments on an issue are separate from attachments on a comment: a comment is a moment in a conversation, an attachment on the issue is part of what the issue is.
Writing in Markdown
Markdown beside a description or a comment box swaps the visual editor for the Markdown behind it. Visual swaps back. It is per field, so a description can be in Markdown while the comment box below it is not.
Two reasons it exists. Editing by hand is faster in Markdown once you know it. And an agent reads and writes Markdown reliably where it does not reliably produce clean HTML.
What is stored is HTML, always. Markdown is a view of the same text, converted on the way in and out. The sanitiser is what makes it safe to show somebody else's words and it works on tags, so nothing downstream — the API, the search index, the digest mail — learns that Markdown exists. Which box you typed in leaves no trace.
The switch is offered only where it is lossless. Markdown cannot express everything the sanitiser keeps: a pasted table with merged cells is the usual case. Where the words would change, the button is replaced by a short reason rather than being silently absent. Underline, subscript, superscript and highlight have no Markdown syntax, so they appear as the HTML tags they are — Markdown has always passed inline HTML through, and dropping them would be worse.
Tip: In Markdown mode the @ picker is not offered on a comment, because @ is ordinary
text there. A name written as @someone still becomes a mention on save: mentions are read
from the words, not from the picker.
Tip: Two things the sanitiser used to delete are now kept, and both were reachable from the
editor's own shortcuts. Typing --- makes a horizontal rule, and saving removed it. And
strikethrough written in Markdown produces a different tag from the one the visual editor
writes, so the same formatting survived one way and not the other.