REST API Reference

August 5, 2026 ยท View on GitHub

Forgetful exposes a REST API for web UI integration and external access.

Base URL: http://localhost:8020/api/v1

Authentication

The REST API uses the same authentication provider as MCP routes. This means:

  • If you've configured OAuth2/OIDC for MCP, the same tokens work for REST
  • If auth is disabled (FASTMCP_SERVER_AUTH not set), REST endpoints use a default user
  • All FastMCP auth providers are supported (JWT, OAuth2, GitHub, Google, Azure, etc.)

When Auth is Enabled

Include a Bearer token in the Authorization header:

Authorization: Bearer <your-oauth-token>

The token is validated using mcp.auth.verify_token() - the same mechanism used for MCP tool authentication. User identity is extracted from the token's sub claim.

When Auth is Disabled (Default)

If FASTMCP_SERVER_AUTH is not set, all requests use a default user. No Authorization header is required.

Configuration

Authentication is configured via FastMCP environment variables:

# Example: GitHub OAuth
FASTMCP_SERVER_AUTH=github
GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_client_secret

# Example: Bearer token (simple API key)
FASTMCP_SERVER_AUTH=bearer
FASTMCP_SERVER_AUTH_SECRET=your-secret-key

See FastMCP Auth Documentation for all supported providers and configuration options.

Common Response Codes

CodeDescription
200Success
201Created
400Bad Request (validation error, CyclicDependencyError)
401Unauthorized (missing/invalid token)
404Not Found
409Conflict (ConflictError -- version mismatch in optimistic concurrency)
422Unprocessable Entity (InvalidStateTransitionError, DependencyNotMetError)
500Internal Server Error

Health

GET /health

Health check endpoint (no authentication required).

Response:

{
  "status": "healthy",
  "timestamp": "2024-12-05T10:00:00Z",
  "service": "forgetful",
  "version": "0.3.0"
}

Memories

GET /api/v1/memories

List memories with pagination, sorting, and filtering.

Query Parameters:

ParameterTypeDefaultDescription
limitint20Results per page (1-100)
offsetint0Skip N results
sort_bystringcreated_atSort field: created_at, updated_at, importance
sort_orderstringdescSort direction: asc, desc
project_idint-Filter by project
importance_minint-Minimum importance (1-10)
tagsstring-Comma-separated tags
include_obsoleteboolfalseInclude obsolete memories

Response:

{
  "memories": [
    {
      "id": 1,
      "title": "Memory title",
      "content": "Memory content...",
      "importance": 7,
      "tags": ["tag1", "tag2"],
      "created_at": "2024-12-05T10:00:00Z",
      "updated_at": "2024-12-05T10:00:00Z",
      "linked_memory_ids": [2, 3],
      "project_ids": [1],
      "source_repo": "owner/repo",
      "source_files": ["path/to/file.py"],
      "source_url": "https://example.com/source",
      "confidence": 0.85,
      "encoding_agent": "claude-sonnet-4",
      "encoding_version": "1.0.0"
    }
  ],
  "total": 42,
  "limit": 20,
  "offset": 0
}

GET /api/v1/memories/{id}

Get a single memory by ID.

Response: Memory object (see above)

POST /api/v1/memories

Create a new memory.

Request Body:

{
  "title": "Memory title",
  "content": "Memory content...",
  "context": "Why this is being stored",
  "keywords": ["keyword1", "keyword2"],
  "tags": ["tag1"],
  "importance": 7,
  "project_ids": [1],
  "source_repo": "owner/repo",
  "source_files": ["path/to/file.py"],
  "source_url": "https://example.com/source",
  "confidence": 0.85,
  "encoding_agent": "claude-sonnet-4",
  "encoding_version": "1.0.0"
}

Provenance Fields (all optional):

FieldTypeDescription
source_repostringRepository source (e.g., 'owner/repo', max 200 chars)
source_filesstring[]List of file paths that informed this memory
source_urlstringURL to original source material (max 2048 chars)
confidencefloatEncoding confidence score (0.0-1.0)
encoding_agentstringAgent/process that created this memory (max 100 chars)
encoding_versionstringVersion of encoding process/prompt (max 50 chars)

Response (201):

{
  "id": 1,
  "title": "Memory title",
  "linked_memory_ids": [],
  "project_ids": [1],
  "code_artifact_ids": [],
  "document_ids": [],
  "similar_memories": []
}

PUT /api/v1/memories/{id}

Update an existing memory.

Request Body: (all fields optional)

{
  "title": "Updated title",
  "content": "Updated content",
  "importance": 8,
  "tags": ["updated-tag"],
  "source_repo": "owner/repo",
  "source_files": ["path/to/file.py"],
  "source_url": "https://example.com/source",
  "confidence": 0.9,
  "encoding_agent": "manual-review",
  "encoding_version": "1.0.0"
}

Provenance fields can be added or updated after memory creation. See POST /api/v1/memories for field descriptions.

Response: Updated memory object

DELETE /api/v1/memories/{id}

Mark a memory as obsolete (soft delete).

Request Body: (optional)

{
  "reason": "No longer relevant",
  "superseded_by": 42
}

Response:

{
  "success": true
}

POST /api/v1/memories/search

Semantic search across memories.

Request Body:

{
  "query": "search query",
  "query_context": "why searching",
  "k": 10,
  "include_links": true,
  "importance_threshold": 5,
  "project_ids": [1, 2]
}

Response:

{
  "memories": [...],
  "total": 10
}

POST /api/v1/memories/{id}/links

Link memories together (bidirectional).

Request Body:

{
  "related_ids": [2, 3, 4]
}

Response:

{
  "linked_ids": [2, 3, 4]
}

Get memories linked to this memory.

Query Parameters:

  • limit (int, default 20): Max linked memories to return

Response:

{
  "memory_id": 1,
  "linked_memories": [...]
}

DELETE /api/v1/memories/{id}/links/{target_id}

Remove a link between two memories (bidirectional).

Response:

{
  "success": true
}

Projects

GET /api/v1/projects

List all projects.

Query Parameters:

ParameterTypeDescription
statusstringFilter by status: active, archived, completed
repo_namestringFilter by repository name

Response:

{
  "projects": [
    {
      "id": 1,
      "name": "Project Name",
      "description": "Project description",
      "project_type": "development",
      "status": "active",
      "repo_name": "owner/repo",
      "created_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 5
}

GET /api/v1/projects/{id}

Get a single project by ID.

POST /api/v1/projects

Create a new project.

Request Body:

{
  "name": "Project Name",
  "description": "Project description",
  "project_type": "development",
  "repo_name": "owner/repo"
}

Project Types: personal, work, learning, development, infrastructure, template, product, documentation, open-source

PUT /api/v1/projects/{id}

Update an existing project.

Request Body: (all fields optional)

{
  "name": "Updated Name",
  "description": "Updated description",
  "status": "completed"
}

DELETE /api/v1/projects/{id}

Delete a project (preserves associated memories).


Entities

GET /api/v1/entities

List all entities.

Query Parameters:

ParameterTypeDescription
entity_typestringFilter by type: Individual, Organization, Team, Device, System, Other

Response:

{
  "entities": [
    {
      "id": 1,
      "name": "Entity Name",
      "entity_type": "Individual",
      "custom_type": null,
      "notes": "Some notes",
      "created_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 10
}

GET /api/v1/entities/{id}

Get a single entity by ID.

POST /api/v1/entities

Create a new entity.

Request Body:

{
  "name": "Entity Name",
  "entity_type": "Individual",
  "custom_type": "Custom Type (if entity_type is Other)",
  "notes": "Optional notes"
}

PUT /api/v1/entities/{id}

Update an existing entity.

DELETE /api/v1/entities/{id}

Delete an entity.

POST /api/v1/entities/search

Search entities by name.

Request Body:

{
  "query": "search term",
  "entity_type": "Individual",
  "limit": 10
}

POST /api/v1/entities/{id}/memories

Link an entity to a memory.

Request Body:

{
  "memory_id": 1
}

DELETE /api/v1/entities/{id}/memories/{memory_id}

Remove link between entity and memory.

GET /api/v1/entities/{id}/relationships

Get relationships for an entity.

Response:

{
  "relationships": [
    {
      "id": 1,
      "source_entity_id": 1,
      "target_entity_id": 2,
      "relationship_type": "manages",
      "description": "Team lead"
    }
  ],
  "total": 3
}

POST /api/v1/entities/{id}/relationships

Create a relationship between entities.

Request Body:

{
  "target_entity_id": 2,
  "relationship_type": "manages",
  "description": "Optional description"
}

PUT /api/v1/entities/relationships/{id}

Update an entity relationship.

DELETE /api/v1/entities/relationships/{id}

Delete an entity relationship.


Documents

GET /api/v1/documents

List all documents.

Query Parameters:

ParameterTypeDescription
project_idintFilter by project
document_typestringFilter by type
tagsstringComma-separated tags

Response:

{
  "documents": [
    {
      "id": 1,
      "title": "Document Title",
      "description": "Brief description",
      "content": "Full document content...",
      "document_type": "analysis",
      "tags": ["tag1"],
      "created_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 5
}

Document Types: analysis, guide, specification, report, note, reference

GET /api/v1/documents/{id}

Get a single document by ID.

POST /api/v1/documents

Create a new document.

Request Body:

{
  "title": "Document Title",
  "description": "Brief description",
  "content": "Full document content...",
  "document_type": "analysis",
  "tags": ["tag1"]
}

PUT /api/v1/documents/{id}

Update an existing document.

DELETE /api/v1/documents/{id}

Delete a document.


Code Artifacts

GET /api/v1/code-artifacts

List all code artifacts.

Query Parameters:

ParameterTypeDescription
project_idintFilter by project
languagestringFilter by programming language
tagsstringComma-separated tags

Response:

{
  "code_artifacts": [
    {
      "id": 1,
      "title": "Artifact Title",
      "description": "What this code does",
      "code": "def hello():\n    return 'Hello'",
      "language": "python",
      "tags": ["utility"],
      "created_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 10
}

GET /api/v1/code-artifacts/{id}

Get a single code artifact by ID.

POST /api/v1/code-artifacts

Create a new code artifact.

Request Body:

{
  "title": "Artifact Title",
  "description": "What this code does",
  "code": "def hello():\n    return 'Hello'",
  "language": "python",
  "tags": ["utility"]
}

PUT /api/v1/code-artifacts/{id}

Update an existing code artifact.

DELETE /api/v1/code-artifacts/{id}

Delete a code artifact.


Skills

Requires: SKILLS_ENABLED=true

GET /api/v1/skills

List all skills.

Query Parameters:

ParameterTypeDescription
project_idintFilter by project
tagsstringComma-separated tags (OR logic)
importance_thresholdintMinimum importance level

Response:

{
  "skills": [
    {
      "id": 1,
      "name": "code-review",
      "description": "Systematic code review process",
      "license": "MIT",
      "tags": ["development", "review"],
      "importance": 8,
      "project_id": null,
      "created_at": "2024-12-05T10:00:00Z",
      "updated_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 5
}

GET /api/v1/skills/search

Semantic search for skills by description similarity.

Query Parameters:

ParameterTypeDefaultDescription
querystring(required)Search query
kint5Number of results
project_idint-Filter by project

GET /api/v1/skills/{id}

Get a single skill by ID (includes full content).

POST /api/v1/skills

Create a new skill.

Request Body:

{
  "name": "code-review",
  "description": "Systematic code review process",
  "content": "# Code Review\n\n## Steps\n1. Check for...",
  "license": "MIT",
  "compatibility": "Requires Python 3.12+",
  "allowed_tools": ["Read", "Grep"],
  "metadata": {"author": "team"},
  "tags": ["development", "review"],
  "importance": 8,
  "project_id": 1
}

PUT /api/v1/skills/{id}

Update an existing skill (PATCH semantics).

DELETE /api/v1/skills/{id}

Delete a skill.

POST /api/v1/skills/import

Import a skill from Agent Skills markdown format.

Request Body:

{
  "skill_md": "---\nname: code-review\ndescription: ...\n---\n\n# Content...",
  "project_id": 1,
  "importance": 8
}

GET /api/v1/skills/{id}/export

Export a skill to Agent Skills markdown format (SKILL.md).

Response:

{
  "skill_md": "---\nname: code-review\ndescription: ...\n---\n\n# Content..."
}

Graph

GET /api/v1/graph

Get full knowledge graph for visualization. Returns all nodes and edges for the user.

Query Parameters:

ParameterTypeDefaultDescription
node_typesstringmemory,entity,project,document,code_artifactComma-separated list of node types to include
project_idint-Filter to specific project
include_entitiesbooltrueInclude entity nodes (deprecated, use node_types)
limitint100Max memories to include (max 500)
offsetint0Number of memories to skip for pagination
sort_bystringcreated_atSort field: created_at, updated_at, or importance
sort_orderstringdescSort direction: asc or desc

Node Types:

  • memory - Knowledge memories
  • entity - People, organizations, devices
  • project - Project contexts
  • document - Long-form documents
  • code_artifact - Code snippets

Edge Types:

  • memory_link - Memory-to-memory connections
  • entity_memory - Entity linked to memory
  • entity_relationship - Entity-to-entity relationship
  • memory_project - Memory linked to project
  • document_project - Document belongs to project
  • code_artifact_project - Code artifact belongs to project
  • memory_document - Memory linked to document
  • memory_code_artifact - Memory linked to code artifact

Response:

{
  "nodes": [
    {
      "id": "memory_1",
      "type": "memory",
      "label": "Memory Title",
      "data": {
        "id": 1,
        "title": "Memory Title",
        "importance": 7,
        "tags": ["tag1"],
        "created_at": "2024-12-05T10:00:00Z"
      }
    },
    {
      "id": "entity_1",
      "type": "entity",
      "label": "Entity Name",
      "data": {
        "id": 1,
        "name": "Entity Name",
        "entity_type": "Individual"
      }
    },
    {
      "id": "project_1",
      "type": "project",
      "label": "Project Name",
      "data": {
        "id": 1,
        "name": "Project Name",
        "project_type": "development",
        "status": "active"
      }
    }
  ],
  "edges": [
    {
      "id": "memory_1_memory_2",
      "source": "memory_1",
      "target": "memory_2",
      "type": "memory_link"
    },
    {
      "id": "memory_1_project_1",
      "source": "memory_1",
      "target": "project_1",
      "type": "memory_project"
    }
  ],
  "meta": {
    "memory_count": 50,
    "total_memory_count": 1243,
    "offset": 0,
    "limit": 100,
    "has_more": true,
    "entity_count": 10,
    "project_count": 3,
    "document_count": 5,
    "code_artifact_count": 8,
    "edge_count": 75,
    "memory_link_count": 25,
    "entity_relationship_count": 5,
    "entity_memory_count": 15,
    "memory_project_count": 12,
    "document_project_count": 5,
    "code_artifact_project_count": 8,
    "memory_document_count": 3,
    "memory_code_artifact_count": 2
  }
}

Pagination Examples:

# Get first 50 memories sorted by importance
GET /api/v1/graph?limit=50&sort_by=importance&sort_order=desc

# Get next page of memories
GET /api/v1/graph?limit=50&offset=50&sort_by=importance&sort_order=desc

# Filter to project and sort by creation date (oldest first)
GET /api/v1/graph?project_id=4&sort_by=created_at&sort_order=asc

GET /api/v1/graph/subgraph

Get subgraph centered on a specific node using efficient CTE traversal. This is the recommended endpoint for graph visualization with depth-limited traversal.

Query Parameters:

ParameterTypeDefaultDescription
node_idstringrequiredCenter node (e.g., memory_1, entity_5, project_3, document_2, code_artifact_4)
depthint2Traversal depth (1-3, clamped)
node_typesstringmemory,entity,project,document,code_artifactComma-separated list of types to traverse
max_nodesint200Maximum nodes to return (max 500)

Response:

{
  "nodes": [
    {
      "id": "memory_1",
      "type": "memory",
      "depth": 0,
      "label": "Center Memory",
      "data": {...}
    },
    {
      "id": "memory_2",
      "type": "memory",
      "depth": 1,
      "label": "Linked Memory",
      "data": {...}
    }
  ],
  "edges": [...],
  "meta": {
    "center_node_id": "memory_1",
    "depth": 2,
    "node_types": ["memory", "entity"],
    "max_nodes": 200,
    "memory_count": 5,
    "entity_count": 2,
    "project_count": 0,
    "document_count": 0,
    "code_artifact_count": 0,
    "edge_count": 6,
    "memory_link_count": 3,
    "entity_relationship_count": 1,
    "entity_memory_count": 2,
    "memory_project_count": 0,
    "document_project_count": 0,
    "code_artifact_project_count": 0,
    "memory_document_count": 0,
    "memory_code_artifact_count": 0,
    "truncated": false
  }
}

GET /api/v1/graph/memory/{id}

Deprecated: Use /api/v1/graph/subgraph?node_id=memory_{id} instead.

Get subgraph centered on a specific memory.

Query Parameters:

ParameterTypeDefaultDescription
depthint1Link traversal depth (1-3)

Response:

{
  "nodes": [...],
  "edges": [...],
  "center_memory_id": 1,
  "meta": {
    "memory_count": 5,
    "edge_count": 4,
    "depth": 1
  }
}

Activity

Experimental Feature

Activity logging is an experimental feature. The async event-driven architecture may cause issues with SQLite backends due to connection pooling conflicts. If you are using SQLite and do not intend to use activity tracking, leave it disabled by not configuring the activity-related settings. PostgreSQL users can enable this feature but please be advised it is still experimental

Known limitations:

  • SQLite in-memory mode (testing): Events may conflict with concurrent database operations

The Activity API provides read access to the activity log, which tracks all entity lifecycle events (created, updated, deleted) and optionally read/query operations.

Configuration

Activity tracking is controlled by these settings:

SettingTypeDefaultDescription
ACTIVITY_RETENTION_DAYSintNoneDays to keep events (None = forever). Cleanup happens lazily on API access.
ACTIVITY_TRACK_READSboolfalseTrack read/query operations (opt-in, can be high volume)

GET /api/v1/activity

List activity events with filtering and pagination.

Query Parameters:

ParameterTypeDefaultDescription
entity_typestring-Filter by type: memory, project, document, code_artifact, entity, link
actionstring-Filter by action: created, updated, deleted, read, queried
entity_idint-Filter by specific entity ID
actorstring-Filter by actor: user, system, llm-maintenance
sincedatetime-Only events after this timestamp (ISO 8601)
untildatetime-Only events before this timestamp (ISO 8601)
limitint50Results per page (1-100)
offsetint0Skip N results

Response:

{
  "events": [
    {
      "id": 123,
      "entity_type": "memory",
      "entity_id": 1,
      "action": "updated",
      "changes": {
        "title": {"old": "Old Title", "new": "New Title"},
        "importance": {"old": 5, "new": 8}
      },
      "snapshot": {
        "id": 1,
        "title": "New Title",
        "content": "...",
        "importance": 8
      },
      "actor": "user",
      "actor_id": null,
      "metadata": null,
      "created_at": "2026-01-06T12:00:00Z"
    }
  ],
  "total": 42,
  "limit": 50,
  "offset": 0
}

GET /api/v1/activity/{entity_type}/{entity_id}

Get activity history for a specific entity.

Path Parameters:

ParameterTypeDescription
entity_typestringEntity type: memory, project, document, code_artifact, entity
entity_idintEntity ID

Query Parameters:

ParameterTypeDefaultDescription
limitint50Results per page (1-100)
offsetint0Skip N results

Response: Same format as GET /api/v1/activity

Event Types

Entity Types:

  • memory - Knowledge memories
  • project - Project containers
  • document - Long-form documents
  • code_artifact - Code snippets
  • entity - People, organizations, devices
  • link - Memory-to-memory connections
  • entity_memory_link - Entity-to-memory connections
  • entity_project_link - Entity-to-project connections
  • entity_relationship - Entity-to-entity relationships

Action Types:

  • created - Entity was created
  • updated - Entity was modified (includes changes diff)
  • deleted - Entity was soft-deleted (obsolete)
  • read - Entity was read (if ACTIVITY_TRACK_READS=true)
  • queried - Search was performed (if ACTIVITY_TRACK_READS=true)

Actor Types:

  • user - Human user action
  • system - Automated system action
  • llm-maintenance - LLM-based maintenance task (future)

Changes Format

For updated events, the changes field contains a diff:

{
  "changes": {
    "field_name": {
      "old": "previous value",
      "new": "current value"
    }
  }
}

Only modified fields are included. The snapshot field contains the full entity state after the change.

Link events (entity_type: "link") use entity_id: 0 and store source/target in metadata:

{
  "entity_type": "link",
  "entity_id": 0,
  "action": "created",
  "snapshot": {"source_id": 1, "target_id": 2},
  "metadata": {"source_id": 1, "target_id": 2}
}

GET /api/v1/activity/stream

Stream activity events in real-time via Server-Sent Events (SSE).

Events are filtered to only those belonging to the authenticated user. Each event includes a sequence number for gap detection and client recovery.

Query Parameters:

ParameterTypeDescription
entity_typestringFilter by type (optional)
actionstringFilter by action (optional)

Response: SSE stream with text/event-stream content type.

Event Format:

event: activity
data: {"seq": 1, "entity_type": "memory", "action": "created", "entity_id": 42, ...}

event: activity
data: {"seq": 2, "entity_type": "memory", "action": "updated", "entity_id": 42, ...}

Sequence Numbers:

Each event includes a monotonically increasing seq field per user. Clients should track this to detect gaps (e.g., receiving seq 45 when last seen was 42 indicates missed events).

Gap Recovery:

On gap detection, fetch missed events via the REST API:

GET /api/v1/activity?since=<last_seen_timestamp>&limit=100

Configuration:

SettingTypeDefaultDescription
SSE_MAX_QUEUE_SIZEint1000Max events per subscriber queue (backpressure)

When the queue is full, new events are dropped with a warning log. Clients can detect this via sequence gaps and resync via REST.

Example Usage (JavaScript):

const events = new EventSource('/api/v1/activity/stream');
let lastSeq = 0;

events.addEventListener('activity', (e) => {
  const event = JSON.parse(e.data);

  // Gap detection
  if (lastSeq > 0 && event.seq > lastSeq + 1) {
    console.warn(`Gap detected: ${lastSeq} -> ${event.seq}`);
    // Fetch missed events via REST API
  }

  lastSeq = event.seq;
  handleEvent(event);
});

Error Responses:

CodeDescription
400Invalid filter parameter
401Unauthorized
503Activity streaming not enabled (ACTIVITY_ENABLED=false)

Plans

GET /api/v1/plans

List plans with optional filtering.

Query Parameters:

ParameterTypeDescription
project_idintFilter by project
statusstringFilter by status: draft, active, completed, archived

Response:

{
  "plans": [
    {
      "id": 1,
      "title": "Plan Title",
      "project_id": 1,
      "status": "draft",
      "task_count": 3,
      "created_at": "2024-12-05T10:00:00Z",
      "updated_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 5
}

GET /api/v1/plans/{plan_id}

Get a single plan by ID.

Response:

{
  "id": 1,
  "title": "Plan Title",
  "project_id": 1,
  "goal": "Implement feature X",
  "context": "Background context for the plan",
  "status": "draft",
  "user_id": "user123",
  "task_count": 3,
  "created_at": "2024-12-05T10:00:00Z",
  "updated_at": "2024-12-05T10:00:00Z"
}

POST /api/v1/plans

Create a new plan.

Request Body:

{
  "title": "Plan Title",
  "project_id": 1,
  "goal": "Optional goal description",
  "context": "Optional background context",
  "status": "draft"
}
FieldTypeRequiredDescription
titlestringyesPlan title
project_idintyesParent project ID
goalstringnoGoal or objective
contextstringnoBackground context
statusstringnoInitial status (default: draft). Values: draft, active, completed, archived

Response (201): Full plan object (see GET response above)

PUT /api/v1/plans/{plan_id}

Update an existing plan.

Request Body: (all fields optional)

{
  "title": "Updated Title",
  "goal": "Updated goal",
  "context": "Updated context",
  "status": "active"
}
FieldTypeDescription
titlestringUpdated title
goalstringUpdated goal
contextstringUpdated context
statusstringNew status (must be a valid transition)

Plan Status Transitions:

FromAllowed To
draftactive, archived
activecompleted, archived
completedactive (reopen)
archivedactive (unarchive)

Response: Updated plan object

Error Responses:

CodeCondition
404Plan not found
422Invalid status transition

DELETE /api/v1/plans/{plan_id}

Delete a plan. Cascades to all tasks, criteria, and dependencies under the plan.

Response:

{
  "success": true
}

Tasks

GET /api/v1/tasks

Query tasks for a plan. The plan_id query parameter is required.

Query Parameters:

ParameterTypeRequiredDescription
plan_idintyesFilter by plan
statestringnoFilter by state: todo, doing, waiting, done, cancelled
prioritystringnoFilter by priority: P0, P1, P2, P3
assigned_agentstringnoFilter by assigned agent

Response:

{
  "tasks": [
    {
      "id": 1,
      "title": "Task Title",
      "plan_id": 1,
      "state": "todo",
      "priority": "P2",
      "assigned_agent": null,
      "version": 1,
      "criteria_met": 0,
      "criteria_total": 2,
      "blocked": false,
      "created_at": "2024-12-05T10:00:00Z",
      "updated_at": "2024-12-05T10:00:00Z"
    }
  ],
  "total": 10
}

GET /api/v1/tasks/{task_id}

Get a single task with its criteria and dependency IDs.

Response:

{
  "id": 1,
  "plan_id": 1,
  "title": "Task Title",
  "description": "Detailed description",
  "state": "todo",
  "priority": "P2",
  "assigned_agent": null,
  "version": 1,
  "criteria": [
    {
      "id": 1,
      "task_id": 1,
      "description": "Acceptance criterion",
      "met": false,
      "met_at": null,
      "created_at": "2024-12-05T10:00:00Z",
      "updated_at": "2024-12-05T10:00:00Z"
    }
  ],
  "dependency_ids": [2, 3],
  "created_at": "2024-12-05T10:00:00Z",
  "updated_at": "2024-12-05T10:00:00Z"
}

POST /api/v1/tasks

Create a new task with optional inline criteria and dependencies.

Request Body:

{
  "title": "Task Title",
  "plan_id": 1,
  "description": "Optional description",
  "priority": "P2",
  "assigned_agent": "agent-1",
  "criteria": [
    {"description": "Criterion 1"},
    {"description": "Criterion 2"}
  ],
  "dependency_ids": [2, 3]
}
FieldTypeRequiredDescription
titlestringyesTask title
plan_idintyesParent plan ID
descriptionstringnoDetailed description
prioritystringnoPriority level (default: P2). Values: P0 (critical), P1 (high), P2 (medium), P3 (low)
assigned_agentstringnoAgent assigned to the task
criteriaarraynoInline acceptance criteria to create
dependency_idsarraynoIDs of tasks this task depends on

Response (201): Full task object (see GET response above)

Error Responses:

CodeCondition
400Cyclic dependency detected
404Plan or dependency task not found
422Invalid state transition

PUT /api/v1/tasks/{task_id}

Update task metadata. State changes must go through the transition endpoint.

Request Body: (all fields optional)

{
  "title": "Updated Title",
  "description": "Updated description",
  "priority": "P1"
}
FieldTypeDescription
titlestringUpdated title
descriptionstringUpdated description
prioritystringUpdated priority: P0, P1, P2, P3

Response: Updated task object

DELETE /api/v1/tasks/{task_id}

Delete a task.

Response:

{
  "success": true
}

POST /api/v1/tasks/{task_id}/transition

Transition a task to a new state. Uses optimistic concurrency via the version field -- the request is rejected if the provided version does not match the current version.

Request Body:

{
  "state": "doing",
  "version": 1
}
FieldTypeRequiredDescription
statestringyesTarget state: todo, doing, waiting, done, cancelled
versionintyesCurrent version for optimistic concurrency

Task State Transitions:

FromAllowed To
tododoing, waiting, cancelled
doingdone, waiting, todo, cancelled
waitingtodo, doing, cancelled
donetodo (reopen)
cancelledtodo (reinstate)

Response: Updated task object (version is incremented on success)

Error Responses:

CodeCondition
404Task not found
409Version mismatch (optimistic concurrency conflict)
422Invalid state transition, or unmet dependencies block the transition

POST /api/v1/tasks/{task_id}/claim

Claim a task by assigning an agent and transitioning it to doing. Uses optimistic concurrency via the version field.

Request Body:

{
  "agent_id": "agent-1",
  "version": 1
}
FieldTypeRequiredDescription
agent_idstringyesIdentifier of the claiming agent
versionintyesCurrent version for optimistic concurrency

Response: Updated task object (state set to doing, assigned_agent set, version incremented)

Error Responses:

CodeCondition
404Task not found
409Version mismatch (optimistic concurrency conflict)
422Invalid state transition, or unmet dependencies block the claim

Criteria

POST /api/v1/tasks/{task_id}/criteria

Add an acceptance criterion to a task.

Request Body:

{
  "description": "Acceptance criterion description"
}
FieldTypeRequiredDescription
descriptionstringyesCriterion description

Response (201):

{
  "id": 1,
  "task_id": 1,
  "description": "Acceptance criterion description",
  "met": false,
  "met_at": null,
  "created_at": "2024-12-05T10:00:00Z",
  "updated_at": "2024-12-05T10:00:00Z"
}

Error Responses:

CodeCondition
404Task not found
422Task is in a state that does not allow adding criteria

PUT /api/v1/criteria/{criterion_id}

Update a criterion (e.g., mark it as met).

Request Body: (all fields optional)

{
  "description": "Updated description",
  "met": true
}
FieldTypeDescription
descriptionstringUpdated description
metboolWhether the criterion has been met

Response: Updated criterion object

DELETE /api/v1/criteria/{criterion_id}

Delete an acceptance criterion.

Response:

{
  "success": true
}

Dependencies

POST /api/v1/tasks/{task_id}/dependencies

Add a dependency, declaring that task_id depends on another task.

Request Body:

{
  "depends_on_task_id": 2
}
FieldTypeRequiredDescription
depends_on_task_idintyesID of the task that must complete first

Response (201):

{
  "id": 1,
  "task_id": 1,
  "depends_on_task_id": 2,
  "created_at": "2024-12-05T10:00:00Z"
}

Error Responses:

CodeCondition
400Cyclic dependency detected, or task depends on itself
404Task not found

DELETE /api/v1/tasks/{task_id}/dependencies/{depends_on_task_id}

Remove a dependency between two tasks.

Response:

{
  "success": true
}

Error Responses

All error responses follow this format:

{
  "error": "Error message describing what went wrong"
}

For validation errors (400), the error may contain detailed field-level errors:

{
  "error": [
    {
      "loc": ["body", "title"],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}