Working from your editor
Setting up an IDE or a coding agent to read issues and log time against Félagi, end to end, with the commands to paste.
Why this exists
A developer already has an editor open. Switching to a browser to find out what an issue says, and switching back at the end of the day to remember how long it took, is the friction that makes time tracking stop happening after a fortnight.
A personal token closes that. It is not gated to admins — anybody who works here can make one, because the point is that the tool on your desk needs nobody's permission to read an issue.
1. Make a token
Settings → API tokens. Give it a name you will recognise in six months, choose Read or Read and write, and optionally an expiry.
You get the secret once. Félagi stores a hash of it, so it cannot be recovered — only replaced.
Underneath it is a block ready to paste:
FELAGI_URL=https://felagi.example.com
FELAGI_TOKEN=fat_…
FELAGI_WORKSPACE=Acme Industries
A token is the password, and a better one
Félagi will not let an integration authenticate with your account password, and that is a decision rather than an omission:
| Token | Account password | |
|---|---|---|
| Scope | One workspace | Everything you can reach |
| Read-only possible | Yes | No |
| Expires | If you say so | No |
| Revoked | On its own, without touching anything else | By changing your password everywhere |
| Can change your password | No | Yes |
The last row is the whole argument. A password in a file on a laptop unlocks the ability to change that password.
A token acts as you, in one workspace, and can never do more than you can.
2. Check it works
curl -s -H "Authorization: Bearer $FELAGI_TOKEN" "$FELAGI_URL/api/v1/me" | jq
{
"data": {
"user": { "type": "user", "id": 3, "name": "Knut W. Horne" },
"workspace": { "id": 1, "name": "Acme Industries" },
"abilities": ["read", "write"]
}
}
If that returns 401, the token is wrong, revoked, expired, or you have left the
workspace. Félagi answers all four the same way on purpose — a different message
for each tells whoever is guessing which part of the guess was right.
A shell function worth keeping
Everything below assumes this:
felagi() {
local method=$1 path=$2; shift 2
curl -sS -X "$method" "$FELAGI_URL/api/v1$path" \
-H "Authorization: Bearer $FELAGI_TOKEN" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' "$@"
}
Reading work
What is on my plate
felagi GET '/issues?assignee_type=user&assignee_id=3&status=in_progress'
Find something by words
felagi GET '/issues?q=redirect%20loop'
q matches the title and the description. To reach one issue you already know:
felagi GET '/issues/231'
The id in a URL is the database key; the id in the response is the
identifier a person types, ACM-231. They are deliberately different things: one
is stable and internal, the other is what somebody says out loud.
Narrow it down
| Parameter | |
|---|---|
status, type, priority |
Enum values — in_progress, bug, high |
project_id |
|
assignee |
An actor key — user:3, agent:1 — or none for what nobody has picked up |
assignee_type + assignee_id |
The older spelling of the same thing. Still works |
label |
Repeatable. Two labels mean the overlap, not either |
cycle |
The cycle number a person says, or none for unplanned work |
updated_since |
For a client polling: everything touched since it last asked |
per_page |
Up to 100 |
A misspelled parameter answers 422 and names what the endpoint accepts. It used to be
ignored, so ?assignnee_id=3 came back with the whole workspace and looked like a working
filter. If you are generating these, that 422 is the fastest thing you will read all day.
The thread and what runs delivered
felagi GET '/issues/231/comments'
felagi GET '/issues/231/artifacts' # pull requests, branches, documents
Creating work
A project
felagi POST /projects -d '{
"name": "Billing overhaul",
"description": "Rework invoicing before the VAT change.",
"priority": "high",
"issue_types": ["epic", "task", "bug"],
"lead": "user:3"
}'
201. Only name is required. lead takes an actor key — user:3 or
agent:1 — and is checked against your workspace rather than trusted.
Changing one later sends only what moved:
felagi PATCH /projects/4 -d '{"status": "in_progress", "priority": "high"}'
A PATCH leaves out what it does not mention. It will not quietly reset a field
because you were changing a status.
A project has no target_date, and sending one is refused with a message rather
than ignored: a request that returned 200 while dropping the field would leave you
believing a deadline was recorded. Dates live on epics.
An issue
felagi POST /issues -d '{
"title": "Rate limit the export endpoint",
"description": "Anyone can pull the whole database.",
"type": "bug",
"priority": "high",
"project_id": 4,
"estimate_minutes": 240
}'
201. Only title is required.
A comment
felagi POST /issues/231/comments -d '{"body": "Reproduced on staging."}'
Mentioning an agent by name in the body hands it the thread, exactly as it would from the interface.
Correcting one is PATCH /issues/231/comments/7 with a body, and taking one back is
DELETE on the same path. Only your own words, and never a comment a run wrote. An edit
hands out no work, whatever names you put in it — post a comment for that.
Delivering a file
curl -X POST "$FELAGI_URL/api/v1/issues/231/attachments" \
-H "Authorization: Bearer $FELAGI_TOKEN" \
-F "file=@translated-attributes.csv"
One file per request, in a part called file. -F rather than -d, and no Content-Type
of your own — curl sets the multipart boundary.
This is the one that matters most for a run: producing a file is an ordinary outcome of agent work, and without this the only options were to describe the export or to paste it into a description as markup, which is the bloat an attachment exists to avoid. Deliver it and reference it.
POST /issues/231/comments/7/attachments hangs it off a comment instead, after the comment
is posted. DELETE /attachments/12 takes one down — yours, or any of them if you
administer the workspace.
25 MB a file and 50 MB for a video, and the type is read from the contents rather than the name: images, video, PDF, text, Markdown, CSV, JSON, XML, HTML, ZIP and Office documents.
Reading one back is GET /attachments/{id}, which is what the url on an attachment
points at:
felagi GET /attachments/78 -o shot.png
This is the one that matters when an agent is working an issue: a screenshot attached to a task used to be listed, named, sized — and unopenable, so whatever it showed had to be retyped into the description by hand. A refusal says which type it read, which is the part worth having — renaming the file never helps.
A Markdown file that quotes a tag is still Markdown. Sniffing classifies a text file
containing <head> as HTML, so a text extension overrules that one sniff and nothing
else. Both are accepted now in any case; what it buys is the file being filed as text,
which is what lets it be read in place rather than downloaded.
Undoing one you should not have made
felagi DELETE /issues/231 # 204
Only an issue you created, unless you administer the workspace. It refuses with a 422 when anything is attached — children, logged hours, runs, whiteboard notes, a running timer, or a line in a meeting's minutes — and the message says which.
The identifier still resolves afterwards, so a WOR-33 in a commit message does not
become a link to nowhere. What it stops being is a thing on a board and a number in a
progress fraction, which is the whole difference between this and status: canceled.
Logging time
The way you would say it
felagi POST /issues/231/time -d '{
"duration": "2h 30m",
"note": "Traced the redirect loop"
}'
1w, 1d, 90m and 2h 30m all work. {"minutes": 150} is accepted for a tool that
has already done the arithmetic, but nothing makes you convert first.
spent_on defaults to today. An hour worked on Monday and written down on Friday
should say Monday:
felagi POST /issues/231/time -d '{"duration": "1h", "spent_on": "2026-08-03"}'
The hours are always yours
There is no actor field. Sending one does nothing. A credential that could log time against a colleague is a credential that could rewrite their timesheet, and hours are somebody's statement about their own week.
Reading it back
felagi GET '/time-entries?mine=1&from=2026-08-01'
| Parameter | |
|---|---|
mine |
Only yours. Without it you see the whole workspace |
from, to |
On spent_on, the day the work happened |
issue_id |
|
source |
manual, timer or agent_run |
That last one is the distinction worth knowing: timer is measured, manual
is remembered, and a total that adds them without saying so is a guess.
Correcting one
felagi PATCH /time-entries/91 -d '{"duration": "3h 15m", "spent_on": "2026-08-25"}'
Any subset of the duration, the day and the note. Not delete-and-log-again: that turns
a timer entry into a manual one, so a measured figure quietly becomes a remembered one
— and it resets when the work was recorded, changes the id, and has a moment in which the
hours do not exist at all. A correction changes the number, never the provenance.
editable on an entry tells you whether you may, so check it rather than guessing: false
for an agent run, and false for hours somebody else logged.
Taking one back
felagi DELETE /time-entries/91 # 204
Only hours you logged yourself, and never an agent run's — the run is in the timeline with its own duration, and editing the entry would let the report disagree with the history it came from.
Days you were not working
A holiday is not the absence of a record. A week off with nothing written down looks exactly like a week you forgot to fill in, so the timesheet chases you for it.
Absence belongs to no issue, so it has its own route — and it takes a range:
felagi POST /absences -d '{
"kind": "vacation",
"duration": "1d",
"from": "2026-06-01",
"to": "2026-06-14"
}'
One entry per working day. date instead of from for a single day:
felagi POST /absences -d '{"kind": "sick_leave", "duration": "1d", "date": "2026-06-03"}'
Kinds: vacation, sick_leave, child_sick_day, time_off_in_lieu, public_holiday.
Ten entries came back from fourteen days, and the response says why. Do not retry — the retry is a double entry:
{
"summary": "Vacation: 3 days · 2 weekend days skipped · Good Friday as public holiday.",
"skipped": {
"weekends": 4,
"holidays": { "2026-04-03": "Good Friday" },
"already_recorded": []
}
}
Weekends are skipped, a public holiday inside the range is recorded as public_holiday
rather than as the leave you asked for, and a day already carrying absence is left alone.
Your script does not have to know the calendar; this is the same code the interface runs.
Reading and removing:
felagi GET '/absences?mine=1&from=2026-06-01&kind=vacation'
felagi DELETE /absences/104 # 204, your own only
GET /time-entries returns absence too — leaving it out of the whole record of your hours
is how a fortnight disappears — where ?absence=work separates the two. GET /absences
saves you knowing that, and will not hand you issues if you forget it.
The stopwatch
This is the part that pays for the setup. An editor that starts a clock when you open a file:
felagi POST /issues/231/timer -d '{"note": "Pairing on the redirect"}'
{ "data": { "issue": "ACM-231", "issue_id": 231,
"started_at": "2026-08-03T09:12:00+00:00", "max_minutes": 480 },
"stopped": null }
started_at, never an elapsed count. A number of seconds is stale the instant
it is serialised; a timestamp stays right however long the response sat in a pipe,
and your editor counts on its own.
Starting a second clock stops the first and logs it. That is not an error — switching tasks is the normal case — but the response says what it banked:
{ "data": { "issue": "ACM-240", "…": "…" },
"stopped": { "id": 91, "issue": "ACM-231", "minutes": 45 } }
felagi GET /timer # what is running, or null
felagi POST /timer/stop # 201, returns the entry it wrote
felagi DELETE /timer # 204, records nothing
GET /timer answers 200 with null when nothing is running. "Nothing is
running" is an answer; a client made to treat it as an error will treat a real
failure the same way.
A clock left running is capped at eight hours and the note says it was capped, so an editor that forgets to stop one cannot put an indefensible number into a report.
Reading the team's documentation
The endpoint worth having if you are pointing a coding agent at Félagi. Skills are what an agent is given; this is what it can look up.
felagi GET '/articles?q=deploy'
felagi GET /articles/01k9abc… # one article, with its body
A list is a table of contents — titles, depth and a 200-character excerpt, no bodies. Fetch the one you want.
Writing works too:
felagi POST /articles -d '{
"title": "Restoring the database",
"body": "<h2>First</h2><p>Stop the queue workers.</p>",
"published": true,
"parent": "01k8xyz…"
}'
felagi PATCH /articles/01k9abc… -d '{"body": "<p>Corrected.</p>"}'
The body is HTML, sanitised on the way to disk through the same allowlist the
editor's output goes through. A PATCH that omits body leaves the document
alone — it will not blank a page to rename it. Every overwrite keeps a revision,
restorable from the interface.
Somebody else's draft never appears. Half a page found through an API is worse than not finding it, because something will act on it without a person reading it first.
Putting a thought on a whiteboard
felagi GET /whiteboards
felagi GET /whiteboards/01k9abc… # the board, with everything on it
felagi POST /whiteboards/01k9abc…/notes -d '{"text": "Rate limiting came up again"}'
Notes and boxes only — an arrow needs two points you cannot see. Anybody with the board open watches it appear.
Reading what a room decided
felagi GET /meetings # upcoming
felagi GET '/meetings?when=past'
felagi GET /meetings/01k9abc… # with agenda, minutes and action items
The one worth having if you are pointing an agent at Félagi alongside the issues: the action items are already issues it can see, and this is the sentence that produced them.
Writing works for scheduling and for notes:
felagi POST /meetings -d '{
"title": "Architecture review",
"project": 4,
"scheduled_for": "2026-08-09T10:00:00Z"
}'
felagi PATCH /meetings/01k9abc… -d '{"notes": "- Astrid: runbook by Friday"}'
Scheduling sends nothing — invitations are a deliberate step in the interface, and so is circulating minutes. Circulated minutes cannot be rewritten and answer 409.
Running an agent
felagi GET /agents
felagi POST /issues/231/runs -d '{"agent_id": 2}'
202, not 201. The run is queued; whether a machine picks it up depends on a daemon that may not be running, and "created" would promise something the response cannot know.
felagi GET '/runs?status=running'
felagi GET /runs/01k9…
When it goes wrong
401 |
The credential is no good. Wrong, revoked, expired, or you left the workspace — all four read the same |
403 |
A read-only token tried to write |
404 |
It does not exist, or it belongs to another workspace. Telling you which would confirm an id and a customer in one response |
422 |
Validation. The body names the fields |
429 |
Too fast. Retry-After says how long |
Limits are 300 requests a minute per token — far above anything an editor does, and there to stop a loop rather than to pace work.
Every response carries X-RateLimit-Remaining.
A worked morning
# What am I meant to be doing
felagi GET '/issues?assignee_type=user&assignee_id=3&status=todo' \
| jq -r '.data[] | "\(.id) \(.title)"'
# Start on one
felagi POST /issues/231/timer
felagi PATCH /issues/231 -d '{"status": "in_progress"}'
# … work …
# Stop, and say what happened
felagi POST /timer/stop | jq -r '.data | "logged \(.minutes)m on \(.issue)"'
felagi POST /issues/231/comments -d '{"body": "Fixed; intended() ran before the session regenerated."}'
felagi PATCH /issues/231 -d '{"status": "in_review"}'
Pointing a coding agent at it
Claude Code, Codex, Cursor — anything with a shell and a set of instructions can use
this. No MCP server is needed. The API is HTTP with a bearer token, and an agent
that can run curl can already do everything on this page.
What it needs from you is not a protocol. It is three sentences and a token.
What to put in your project's instructions
Whatever file your tool reads — CLAUDE.md, AGENTS.md, a rules file — this is enough:
## Félagi
Our issue tracker. The API is documented at <your-felagi>/app/manual/reference/api.
- Base URL and token are in the environment: `$FELAGI_URL`, `$FELAGI_TOKEN`
- Call it with: `curl -sS -H "Authorization: Bearer $FELAGI_TOKEN" \
-H 'Accept: application/json' "$FELAGI_URL/api/v1/..."`
- Before starting work on an issue, read it: `GET /issues/{id}`
- Log the time when you finish: `POST /issues/{id}/time -d '{"duration":"45m"}'`
- Do not invent endpoints. If one is not in the reference, it does not exist.
That last line earns its place. An agent that cannot find an endpoint will produce a
plausible one — PATCH /issues/231/assignee reads exactly like something we would have
built — and then report the 404 as though Félagi were broken.
Give it a read-only token first
A token that can write can close an issue, and an agent that has misread a conversation will do so confidently. Start with Read, work with it for a week, and widen it when you know what it actually does.
The token acts as you. Hours it logs are your hours, comments it leaves are signed with your name, and there is no way for it to write as somebody else — which is a safety property and a limitation at the same time.
Keep the token out of the transcript
Put it in the environment, never in the instructions file:
export FELAGI_URL=https://felagi.example.com
export FELAGI_TOKEN=fat_…
An instruction file is committed, shared, and pasted into a support conversation on the one day something is broken. An environment variable is none of those.
When an MCP server would be worth it
Not for this. The gain would be that the agent could enumerate the tools instead of reading a reference, and that arguments would be schema-checked rather than shell-quoted — real, but small next to a second surface over the same data that can drift from the REST API it wraps.
It becomes worth it when an agent running inside Félagi needs to ask Félagi
questions mid-run — what the minutes of yesterday's meeting said, what is blocking the
issue it is on. That is a different job from the one on this page, and agents already
carry an mcp_config for exactly that kind of connection.
Not there yet
- No agents, runtimes or skills over the API. Read-only, deliberately: naming a runtime and a provider wrongly over an API is much harder to see than getting it wrong in a form.
- No labels or cycles written over the API. Both are readable and filterable; setting them is done in the interface.
- No writing saved views.
GET /saved-viewsreads the ones you may open -- your own and what the workspace has shared, with the filters as saved, so a view can be carried out rather than only named. Composing one is done by clicking the filters in the interface, and a second way to build a filter would be a second thing to keep in agreement with the first. - No moving or drawing on a whiteboard. Read it, and add a note.
- No sending invitations or circulating minutes. Both go to everybody who was invited and cannot be taken back.
- No article history over the API. Revisions are kept; restoring is done in the interface.
- No OpenAPI document. The reference is the specification. Without one, a coding agent reads prose rather than a schema — workable, and the reason the instruction above tells it not to invent endpoints.
- No MCP server. Not needed for this, and deliberately not built: it would be a second surface over the same data, and the one nobody uses daily is the one that goes wrong.
- No cursors for pagination. Page numbers, which drift if rows are inserted while you page.
- Nothing calls you. Webhooks exist but are configured in the
interface, not through this API.
updated_sinceis there so polling stays cheap.