Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Starf An activity monitor for Apple silicon that never invents a number e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Start here
Concepts
Release notes
What's new
Elyra
Working from your editor

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-views reads 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_since is there so polling stays cheap.