Projects
A project is a unit of work inside an organization. It is not an organization of its own: it carries a timeline, an assigned roster, and a workspace of stories and epics.
The part worth reading twice: story data is not stored in Axowl’s database. A project is
attached to an app group, and the work tables (axowl_pm_*) are created in that app group’s
connected database — your external Postgres, or a Strix database we provisioned for you. Axowl
keeps only the roster and the gates. If you disconnect from us, your stories stay where they are.
Requirements
Section titled “Requirements”Creating a project needs three things. The server enforces all three; the wizard mirrors them.
| Requirement | Error when missing |
|---|---|
| A paid plan (Pro or higher) on the organization | UPGRADE_REQUIRED |
| An app group to attach to | APP_GROUP_REQUIRED |
| An active database connection on that app group | PM_DATASOURCE_REQUIRED |
The database is not optional, because there is nowhere else for the work data to go.
Projects
Section titled “Projects”GET /api/org/{slug}/projectsPOST /api/org/{slug}/projectsGET /api/org/{slug}/projects/{id}PUT /api/org/{slug}/projects/{id}POST /api/org/{slug}/projects/{id}/statusPUT /api/org/{slug}/projects/{id}/app-group { appGroupId }Create takes name, optional description, startDate/endDate, and a required appGroupId:
{ "name": "Payment rewrite", "description": "Move checkout off the legacy gateway", "startDate": "2026-09-01", "endDate": "2026-11-30", "appGroupId": "9f2c…"}Reads are gated on organization membership. Writes to the project itself (rename, reschedule,
status) need the org.project.update scope.
A new project starts Active — creating it is the act of starting it. From there the statuses
are OnHold, Completed, Cancelled, and Planning if you want to put it back on the drawing
board. Archived is not reachable through /status; it has its own route, below.
app-group attaches an app group to a project that has none — projects created before the
attachment requirement landed have an empty appGroupId and cannot store stories until this is
called. It refuses (APP_GROUP_ALREADY_ATTACHED) rather than re-pointing a project that already
has one, because the existing work data lives in that group’s database.
Archiving
Section titled “Archiving”POST /api/org/{slug}/projects/{id}/archivePOST /api/org/{slug}/projects/{id}/restoreGET /api/org/{slug}/projects/archivedGET /api/org/{slug}/projects/{id}/archive/downloadArchiving takes the project out of the active list without deleting anything. Before the status
changes, the server reads the whole workspace out of your database — stories, epics, sub-tasks,
owners, activity, attachments and links, including rows already archived individually — and writes
it as a single JSON snapshot to Axowl storage. GET /projects stops returning the project;
GET /projects/archived returns it with the snapshot metadata.
{ "archived": true, "previousStatus": "Active", "snapshotRows": 412, "snapshotStored": true, "snapshotError": null}If the snapshot cannot be written — your database is unreachable, or the project never had one —
the project is still archived and snapshotStored is false with a reason in snapshotError.
The original rows are untouched either way; the snapshot is a spare copy, not the only one.
restore puts the project back in the status it held before archiving (previousStatus), not
blindly into Active. It also replays the snapshot into your database, inserting only rows that
are missing (ON CONFLICT DO NOTHING), so a project whose tables were dropped in the meantime
comes back with its work intact and one whose data never moved gets reimportedRows: 0.
archive/download streams the raw snapshot JSON. Storage is private, so the download goes through
the API with your session — there is no public URL. Both routes need org.project.update.
Roster
Section titled “Roster”POST /api/org/{slug}/projects/{id}/members { connectedId, role }DELETE /api/org/{slug}/projects/{id}/members/{connectedId}POST /api/org/{slug}/projects/{id}/invite { email, role }GET /api/org/{slug}/projects/{id}/invitesRoles are Manager, Contributor, Guest, Observer. Anyone with a badge in the organization
can be assigned — including teams (a group ConnectedId assigns everyone in it) and
AI agents from the app group’s roster.
invite brings in someone from outside the organization, with role either Freelancer or
Guest. It rides the normal invitation machinery (same email, expiry, seat accounting), so an
invited person accepts at /invite/{code} exactly as an ordinary member would.
A project guest is confined to that project. The invitation carries a scope variable, which
compiles into org.project.read:project=<id> on the guest’s badge. Reads of any other project —
through the UI or the API — are refused, and a guest cannot write at all.
Stories and epics
Section titled “Stories and epics”All routes below are under /api/org/{slug}/projects/{projectId}.
GET /work # stories + epics in one call (board)GET /activity?storyId=&epicId= # comments and history
POST /stories # createPATCH /stories/{storyId} # partial updatePOST /stories/{storyId}/state # move between board columnsPUT /stories/{storyId}/owners # replace the assignee setPOST /stories/{storyId}/archivePOST /stories/reorder # backlog priority
POST /stories/{storyId}/tasks # sub-taskPOST /stories/{storyId}/tasks/{taskId}/toggleDELETE /stories/{storyId}/tasks/{taskId}
GET /stories/{storyId}/attachments?withData=truePOST /stories/{storyId}/attachmentsDELETE /stories/{storyId}/attachments/{attachmentId}
GET /stories/{storyId}/links # pull requests, commits, branchesPOST /stories/{storyId}/linksDELETE /stories/{storyId}/links/{linkId}
POST /comments { storyId | epicId, body }
POST /epicsPATCH /epics/{epicId}POST /epics/{epicId}/archiveGET /epics/{epicId}/burndown # raw points; draw the curve yourself
GET /assignees # active agents eligible for assignmentA story has a type (Feature, Bug, Chore), a state (Backlog, ToDo, InProgress,
InReview, Done), multiple owners, an optional epic, an estimate, a due date, sub-tasks,
attachments and links. Archiving hides an item everywhere; it does not delete the row.
Access
Section titled “Access”Reading needs organization membership and, for scoped guests, the matching project.
Writing needs an active ProjectMember record on that project — Observer and Guest are
refused. This is enforced server-side, so calling the API directly does not widen anything.
When the database is not reachable
Section titled “When the database is not reachable”These routes talk to your database, so they can fail in ways ours cannot:
| Code | HTTP | Meaning |
|---|---|---|
PM_DATASOURCE_REQUIRED | 409 | The project has no app group, or the group has no active database |
PM_SCHEMA_FAILED | 409 | The axowl_pm_* tables could not be created — usually the connected account lacks DDL rights |
PM_DATASOURCE_UNREACHABLE | 503 | The database refused the connection or timed out |
PM_NOT_ASSIGNED | 403 | You are not an assigned member of this project (or you are an observer/guest) |
PM_DATASOURCE_REQUIRED is the one people misread as a permission error. It is not: it says the
project has nowhere to put the work. Attach an app group with a live database (PUT /app-group)
and the same call succeeds.
The work tables are created on first use and the DDL is idempotent, so nothing breaks if you reconnect the same database later.
Events
Section titled “Events”Project lifecycle is audited: creation, updates, status changes, roster assignment and
removal, outside invitations, and archiving (org.project.created … org.project.invite_sent,
org.project.archived, org.project.restored).
Archiving and restoring are separate events rather than status changes, because they carry a fact a status change does not: where the snapshot went and how many rows it holds — which is what answers “can this still be brought back?” later.
Day-to-day story movement is deliberately not an event stream — dragging a card or ticking a
sub-task would flood the audit log. That history lives in the project’s own axowl_pm_activity
table and is served by GET /activity.