Concepts
Ten words carry the whole product. They are ordinary on purpose: this is a task tracker where some of the workers happen not to be human, and inventing vocabulary would only make that harder to see.
The hierarchy
Workspace the company — the tenant boundary
└── Project a system, with a lead. Long-lived, and it does not finish
└── Epic a container issue, grouping related work
└── Issue the unit of work
└── Task one run of one agent against that issue
Only Workspace → Issue is mandatory. A project is optional, an epic is optional, and an issue
worked on by a human never has a task at all.
Workspace
The tenant boundary, and the coordinate system for everything else. Issues, projects, agents, runtimes, skills and members all belong to exactly one workspace, and nothing crosses between them.
A workspace carries an issue prefix (ACME) and a counter, so issues get human identifiers
like ACME-42. A project may override the prefix; the counter is always the workspace's. It also carries context — a system prompt prepended to every agent's own
instructions — and a repository allowlist, which is the only set of repositories an agent may
check out.
Member
A user's role inside one workspace: owner, admin or member. The same person can be an owner
in one workspace and a member in another.
This is not the same as being a platform administrator. See Permissions.
Department
A name people can be filed under, per workspace. Deliberately nothing more: no lead, no budget, no parent. A department with those is an org chart, and an org chart is a second hierarchy to keep in step with the projects.
The department lives on the membership, not on the person — somebody can be in Engineering in one workspace and Finance in another, and neither statement contradicts the other. A phone number is the mirror case: it lives on the person, because it follows them into every workspace they join.
Dissolving a department does not remove anybody from the workspace. They lose the department, which is what happened.
Project
A portfolio-level container. Status (planned, in_progress, paused, completed, canceled),
priority, and a lead — who may be a person or an agent.
A project does not finish, and carries no date. These are systems: a project here will still be running in three years, so a target date on one was either a date nobody looked at or a date that was quietly wrong — and a target permanently in the past teaches everybody to ignore the colour red. What bounds work in time is the epic, and its checkpoints are milestones, which were built on the epic rather than on the project for exactly this reason.
For the same reason a project reports how much is open, not how far through it is. A completion percentage on something that never completes describes the shape of the backlog, and drifts towards a hundred as old work is closed.
A project also configures which issue types its issues may use.
A project may carry its own issue prefix, so its issues read PRE-14 rather than ACME-14.
Only the prefix: the number is always the workspace's counter. That is what lets an issue move
between projects without changing what it resolves to — PRE-14 and ACME-14 are the same issue,
and both find it. Numbering per project would mean renumbering on every move, or two issues called
PRE-1, and an identifier quoted in a commit message that no longer works.
A prefix cannot be shared with another project or with the workspace. Not because it would be ambiguous — the number decides — but because telling you which project an issue belongs to is the label's only job.
Deleting a project keeps its issues. They simply lose the grouping.
Issue
The unit of work. Title, rich-text description, acceptance criteria, and:
| Field | |
|---|---|
| Project | Optional grouping |
| Priority | none · low · medium · high · urgent |
| Type | epic · bug · feature · task · cosmetics · exception · usability_problem · performance_problem · generic |
| Status | Named by the workspace. Ships as backlog · todo · in_progress · in_review · done · canceled, each carrying a category that reports read instead of the name |
| Assignee | A person or an agent |
| Due date | |
| Estimation | Entered as 2h 30m, 1d or 1w, stored as minutes. A day is eight hours and a week is five days |
| Share of week | generic issues only, instead of an estimate: 10% or 1d. See Standing work |
| Spent time | Human time plus every agent run's duration |
Paused is a flag rather than a status or a type, because it describes where the work stands, not what kind of work it is — and a paused issue is still open work that somebody owns.
An issue can be marked a duplicate of another. The duplicate is closed and kept, never deleted: its identifier is quoted in a standup and a commit message, and a link that leads nowhere is worse than one leading to a closed issue that says where the work went.
Merging moves content — comments, files, hours and labels — and leaves decisions alone: the surviving issue keeps its own estimate, status, assignee and cycle. Agent runs stay where they happened, because a run is a fact about a machine at a moment, not something that can be reattributed. Unmarking a duplicate undoes the pointer, not the move.
Standing work
A generic issue: support, datacenter maintenance, a rota. Work that never finishes and
that somebody carries as a proportion of their week rather than as a thing to complete.
It has a share of the week instead of an estimate — 10% or 1d — and claims it while
the issue is in progress. The status is the switch, so nothing has to be remembered or
expired.
The distinction it exists for: an estimate is a total and a promise, and a share is a rate and an arrangement. So standing work is out of everything measured by completion — an epic's progress, a project's, a cycle's carry-over — because a thing that is not supposed to end cannot be behind. What it counts towards instead is capacity: somebody on 10% support has 36 plannable hours in a 40-hour week, and every projection uses the smaller number.
The timesheet still expects the whole 40, because the support hours are logged like any others. The share is the plan; the entries are what happened. A standing task that consistently takes a quarter of somebody's week says so.
It always belongs to a project, or the hours have nowhere to be reported.
Workflow state
A status a workspace uses, and what that status means.
Each state carries a key, a name and a category. The key is identity — an issue
stores it, the API sends it, a script reads it — and never changes. The name is a label and
may. The category is one of backlog, unstarted, started, completed, canceled, and
it is the contract: nothing systemic reads a status by name, because the next workspace uses
a different one.
So a report asks "is this finished", never "is this done". Rename In review to QA
and every count stays the same; re-classify it as completed and every count changes at
once, without a report being edited.
completed and canceled both mean the work is over. Only completed means it was
delivered, which is why a cancelled issue is closed but is never velocity.
And why the board draws no column for it. Cancelled work is not work: a handful of
abandoned issues used to take a full-width column beside the empty ones and read as the
busiest thing on the page. Whatever a workspace classifies as canceled is left off the
board, however the status is named.
It is one of three things the board leaves off by default. See What the board leaves off.
Paused is a separate flag rather than a category, because whether anybody is moving the work is a different question from how far along it is. Folding the two together is how a stalled issue ends up filed with the finished ones.
Every workspace starts with the seven states Félagi has always had. They can be renamed,
re-classified and reordered at Administration → Statuses. They cannot yet be added to or
removed: issues.status is still a fixed set in the database, so a state invented here
could be shown and never stored.
What the board leaves off
Three things, and each answers a question the board is not being asked:
| Why | Where it is instead | |
|---|---|---|
| Cancelled work | Cancelled work is not work. It used to take a full-width column beside the empty ones | The List view |
| Epics | An epic is an issue, which is why it sat among the issues — and why it read as one more card rather than as the thing twenty of them belong to | Epics and the Timeline |
| Work finished over 14 days ago | The Done column grew without limit, so the one thing on it worth reading — what landed this week — was at the bottom of a hundred cards | The List view, and the reports |
Counted, never silently dropped. A strip at the end of the columns names each reason with its own count, and pressing one lifts that default alone. A number that vanishes without a trace is the same page giving two answers to "how much is here", and one total across three reasons would leave somebody guessing which of them explains the gap.
An explicit request outranks the default. Filtering to a cancelled status, or to the
epic type, is asking to see them — a board that then refused would be a filter that looks
broken. Lifting a default is recorded in show rather than by rewriting a filter, so
pressing it never discards what somebody had already narrowed to.
Cancelled is read from the workspace's own classification, not from the word: rename Canceled to Dropped and it is still left off; classify On hold as cancelled and that one is left off too.
Ageing off needs a date, and updated_at is not it — that moves when somebody edits a
title or logs an hour, so a six-month-old issue tidied yesterday would come back and one
closed last week would stay for ever. issues.closed_at is set when an issue enters a
finished or cancelled status and cleared when it is reopened; absent is not old, so an
issue with no closing date stays on the board. Cancelling something already done keeps the
first date: the tidy-up is a correction to the record, not a second closing.
The List view shows everything except epics. The board is where work is moved along; the list is where it is audited, and it is the only place that can cancel several issues at once.
What a person's board adds
One thing, and it is the other half of the same idea.
Narrow the board or the list to a person — Assigned to me, or a named colleague — and the backlog nobody has taken comes with it. A developer's board is their work plus the pool the team picks from.
Without it, the one view somebody works out of all day was the one view that could not answer "what should I take next": the answer sat on a board they had to un-filter to see, among everybody else's work.
Assigned backlog stays where it is — on the board of whoever has it. That is what assigning it meant, and dropping it onto everybody's board would answer "who is doing this" four times.
Four things about how it is drawn:
The backlog category, not a status called "backlog". Rename it, or classify a second status that way, and the pool follows — as everywhere else here, statuses are data.
Only the backlog. An unassigned issue somebody has started is not work waiting to be picked up; it is a question about who is doing it. Ask it directly with the Unassigned filter, which covers every column.
No pool on an agent's board. An unclaimed issue is work waiting for somebody to pick it up, and a machine does not pick things up — an agent runs what it is handed.
It narrows with everything else. The pool is added inside the assignee condition rather than fetched alongside it, so a board filtered to one project shows that project's pool and not the whole workspace's.
And it says so, because the total on a narrowed board is no longer that person's work: a line above the columns counts what was added, and on the board each pool card carries an unassigned badge. A card with no avatar is otherwise the whole signal, and "no avatar" is not a sentence — somebody reads their own board, counts eleven things and believes eleven are theirs. The List needs neither: its assignee column already prints the word, in line with every other row.
Started and untouched
A started status is counted as work everywhere — capacity, Flow, the standup. An issue that has sat in one for a week with nobody touching it is not work; it is a question about who is doing it. Waiting is somebody else's problem; stalled is the assignee's, and the two used to look identical on every screen.
Touched means any activity of any kind, or a day with hours logged. updated_at
deliberately does not count — it moves when the sweeper renews a lease or a report caches a
column, which is nobody touching anything.
Three calendar days before a started issue is called stalled: long enough that a weekend on its own never trips it, short enough that "started Monday, nothing since" is caught by Thursday's standup. Paused statuses never stall — a paused issue is waiting, and nobody was meant to be on it.
Where it is said: an untouched 6d badge on the board card; a count per person and a badge per row on the Standup; and in Flow, where the stalled share of finished work is taken out of working rather than presented as it.
Epic
An issue whose type is epic. It contains other issues through parent_id, and its progress is
measured in closed children.
Three rules, each enforced and tested:
- Only an epic can be a parent
- An epic cannot be filed inside another epic
- An epic with children cannot stop being an epic
Progress counts children whose status falls in a finished category, not
children whose status is called done — so a workspace that renames or re-classifies its
statuses gets a progress bar that agrees with every other figure on the page.
Epics have a page of their own at Epics, listing them by progress rather than by status, with a count of how many people outside the company are subscribed to each, and a New epic button. They are still ordinary issues: an existing one becomes an epic by setting its Type to Epic, and either way it is filled from the epic's own page or by choosing an epic on any issue.
See Working with epics.
Agent
An AI worker with a name, a description, a provider, a runtime, instructions, environment variables, extra CLI arguments, a concurrency limit and a visibility setting.
An agent is not a model. It is a configured worker identity that happens to be backed by a CLI. It appears in the assignee picker beside people, comments in threads, and shows up in the audit log — through the same columns as any human.
Agents are archived, never deleted: their name appears on issues, comments and runs that must keep making sense.
Runtime
A machine that can execute agent work — specifically, one agent CLI on one machine. A laptop with
both claude and codex installed registers as two runtimes.
A runtime is online when it has sent a heartbeat within the last 45 seconds. That is computed, not stored, so a crashed daemon cannot leave a runtime looking alive.
Task
One run of one agent against one issue. This is the distinction that matters most: an issue is what should be done and lives forever; a task is one attempt at doing it and carries status, session id, working directory, attempts and how long it ran.
One issue, many tasks. The queue lives in the status column rather than in Redis, because it has to be visible in the interface and claimable by an external process.
queued → dispatched → running → completed
↘ failed (retryable → back to queued)
↘ canceled
Session resumption: the next task for the same (agent, issue) pair reuses the previous
session_id and work_dir. The agent continues its own conversation instead of starting cold.
Artifact
What a run handed over. The summary says what happened; an artifact says where it is.
A pull request, a branch, a commit, a document, a link. They appear as chips on the issue, beside the agent's summary comment — where a colleague would have put one.
Félagi knows nothing about GitHub, and that is the design rather than an omission. An issue here can
be about code, a document or an analysis, so building one host's model of "a pull request" into the
core would make every other kind of work second-class. The agent opens the pull request itself, with
gh or glab, because a skill told it to. Félagi stores a link and a state.
Two consequences worth knowing:
A pull request moves the issue to In review. The one place an agent changes an issue's status, and narrow on purpose — never from a status after review, never for a branch or a link.
Nothing polls. A merged pull request stays marked open here until something says otherwise. There is a webhook for that, and it takes a URL and a state so any forge can post it.
See Delivering work.
Timesheet
One person's hours, laid out over the days they were spent on. Not a second
system: the same time_entries rows an issue writes, read a week at a time.
The distinction from a report is what it is for. A report aggregates and asks "where did the hours go"; a timesheet shows the empty days, because "what have I forgotten" is the question somebody actually has on a Friday afternoon.
Expected hours come from the same weekly rate the Gantt and cycles use. An agent's hours appear beside a person's and can be edited by nobody — the run is the record.
See Timesheets.
Timer
A stopwatch running against an issue, and the only thing in Félagi that is stored because it is happening rather than because it happened.
Server-side, not in the browser: a timer that lives in a tab dies with the tab and takes the work it was measuring with it. The page counts elapsed seconds from the start time, so it survives a reload and costs no requests.
One per person, enforced by a unique index rather than a check in code. Stopping
writes an ordinary time_entry with the source timer — measured, as against the
manual entries somebody typed from memory. That distinction is what lets a report
say how much of a figure was watched happening.
Capped at eight hours, because a clock that ran overnight did not measure a night of work.
See Timesheets.
Article
A page in the knowledge base, nested arbitrarily deep. Unlike an epic, which is one level on purpose: a plan with grandchildren stops being readable, and documentation is the opposite because the nesting is the organisation.
The row is metadata; the prose is a file under storage/knowledgebase. A tree
of four hundred articles is drawn on every page load and none of it needs the
text — and the documents stay greppable, rsyncable and readable without a database
client, which is worth something for the page about restoring the database.
See Knowledge base.
Whiteboard
An endless surface, and deliberately the least structured thing in Félagi. Everywhere else a piece of work needs a title, a type and somewhere to live; here it needs a position.
What separates it from a drawing tool in another tab is the wire back to the data model: a note becomes a real issue and stays on the board pointing at it, and an issue already in Félagi can be dropped on as a card showing its live status.
Shared with the workspace by default, or private to whoever made it. Cursors are whispered browser to browser and never reach the server.
See Whiteboard.
Label
A word on an issue, and nothing more. Created on use, deduplicated case-insensitively, coloured by a stable hash of the name.
No settings page to visit first — a team that has to define needs-design before
they can write it down writes it in the title instead. Two labels filter with AND,
because an issue carries several and picking a second one asks for the overlap.
Attachment
A file hanging off a project, an issue, a comment, an article or a whiteboard, through one polymorphic table.
Served by the application rather than from a public folder: a file on a public disk is readable by anybody who guesses the URL, and these are screenshots of somebody's production incident. The stored path is a random ULID — a path derived from a filename is a filename that can be a path.
Images, PDFs and text files can be previewed in the page. A preview is a different
request from a download: it is served inline only under Content-Security-Policy: sandbox,
which is the reason it may be inline at all. SVG is excluded on purpose — it is an image
and also a document that can carry script.
Subscriber
Somebody outside the workspace who is told how the work is going. Deliberately not a user: an external stakeholder wants an email, not an account — and giving them one would put them in the member list and in every "who can see this" question from then on.
A subscriber follows a project, an epic or an issue, and hears exactly twice: when work
enters a started state, and when it enters a completed one. The trigger is the
category, never the status name, so renaming a status does not stop a
client's mail arriving.
Only external_note is ever sent. The description is written for colleagues and will
one day contain something nobody meant a client to read, so it is never included and never
used as a fallback. The email carries the title, that one sentence, why it arrived, and a
one-click unsubscribe.
Every message is branded with config('app.name') — the installation's name, not Félagi's.
Cycle
A fixed window of time, generated on a cadence. Not a sprint: there is nothing to open, nothing to name and nothing to close.
The distinction matters for two reasons that are specific to this product. Agents do not attend planning meetings — they work continuously, which is the point of them — and capacity here has always been a rate rather than a rota, so a window is a view over the existing timeline rather than a separate plan competing with it.
Three consequences worth knowing:
Unfinished work moves into the next window, and the moves are counted. "Carried for four cycles" is the most useful number a planning tool produces and the one most of them hide.
Starting work plans it. An issue in no cycle that moves into a started status joins the current window, wherever the status changed — otherwise the fortnight's numbers describe a different set of issues from the one the team actually worked on. An issue already planned into a window keeps it.
Capacity includes the machines. An agent's weekly hours count exactly as a person's, so "planned 6 days against 40 days of capacity, half of it machine" is a sentence Félagi can say.
See Cycles.
Skill
A markdown document describing how your team does something. Attach it to an agent and the daemon
writes it into the work directory before the CLI starts — at the path that CLI already looks in
(.claude/skills/…, .codex/skills/…, .elyra/skills/…).
A skill is not a prompt template and not code. It is the thing you would otherwise explain to a new colleague, and then explain again.
Removing one
An issue created in error can be taken off the board by whoever created it, or by an administrator. It leaves every list and every board, and — the point of it — stops counting towards the progress of its epic and its project.
Cancelling is not the same thing. Cancelled work was real and then abandoned; an issue created by mistake was never work at all. Progress is closed children over total children, so a junk issue inflates both halves for ever and nothing shows it.
It refuses when anything is attached — children, logged hours, runs, whiteboard notes, a running clock, or a line in a meeting's minutes — and says which. Removing an issue must never quietly destroy somebody's hours, and there is no override: an issue created in error has none of those, so an override would only ever be used on an issue that was real. If something is in the way, cancel it instead.
The identifier still resolves afterwards. WOR-33 quoted in a commit message or a standup
does not become a link to nowhere; the issue simply is not anywhere a person works. Its own
comments and files go with it, and there is no way back.
Comment and activity
Comments are rich text authored by a person or an agent. Mentioning an agent (@Freya) hands it
the thread: a run is queued exactly as if it had been assigned.
Written in either the visual editor or Markdown, switched per field. What is stored is always HTML, through the same sanitiser either way, so nothing downstream — the API, search, the digest mail — learns that Markdown exists. See Writing in Markdown.
A comment can be corrected by whoever wrote it. The thread shows that the wording changed, not what it said before — the history of a remark is not kept the way an article's revisions are, and who edited it and when is on the issue's activity. A comment an agent's run produced cannot be edited at all: it is the record of what the agent reported.
Editing hands out no work. Adding @Freya to a comment by editing it queues nothing, because a
mention that dispatched on every save would be a mention nobody could correct. Post a comment to
hand out the work.
A comment can be taken back, by its author or by anybody who administers the workspace. The words and the files go; the entry stays, marked as withdrawn, because the replies to it still point at it. There is no way back from it.
Taking a comment back also takes back the work it handed out, as far as it can: a run it queued that has not started yet is canceled, since the agent would otherwise be sent to read a comment with nothing left in it. A run already under way is left alone — the agent has read the words, and stopping it is a separate decision.
Activities are the audit log — every status change, assignment and run event, with a polymorphic actor that may be a user, an agent, or nothing at all when the system acted on its own.