Forgetful exposes a REST API for web UI integration and external access.
Base URL: http://localhost:8020/api/v1
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_AUTHnot set), REST endpoints use a default user - All FastMCP auth providers are supported (JWT, OAuth2, GitHub, Google, Azure, etc.)
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.
If FASTMCP_SERVER_AUTH is not set, all requests use a default user. No Authorization header is required.
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-keySee FastMCP Auth Documentation for all supported providers and configuration options.
| Code | Description |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Bad Request (validation error, CyclicDependencyError) |
| 401 | Unauthorized (missing/invalid token) |
| 404 | Not Found |
| 409 | Conflict (ConflictError -- version mismatch in optimistic concurrency) |
| 422 | Unprocessable Entity (InvalidStateTransitionError, DependencyNotMetError) |
| 500 | Internal Server Error |
Health check endpoint (no authentication required).
Response:
{
"status": "healthy",
"timestamp": "2024-12-05T10:00:00Z",
"service": "forgetful",
"version": "0.3.0"
}List memories with pagination, sorting, and filtering.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 20 | Results per page (1-100) |
offset |
int | 0 | Skip N results |
sort_by |
string | created_at | Sort field: created_at, updated_at, importance |
sort_order |
string | desc | Sort direction: asc, desc |
project_id |
int | - | Filter by project |
importance_min |
int | - | Minimum importance (1-10) |
tags |
string | - | Comma-separated tags |
include_obsolete |
bool | false | Include 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 a single memory by ID.
Response: Memory object (see above)
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_id": 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):
| Field | Type | Description |
|---|---|---|
source_repo |
string | Repository source (e.g., 'owner/repo', max 200 chars) |
source_files |
string[] | List of file paths that informed this memory |
source_url |
string | URL to original source material (max 2048 chars) |
confidence |
float | Encoding confidence score (0.0-1.0) |
encoding_agent |
string | Agent/process that created this memory (max 100 chars) |
encoding_version |
string | Version 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": []
}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
Mark a memory as obsolete (soft delete).
Request Body: (optional)
{
"reason": "No longer relevant",
"superseded_by": 42
}Response:
{
"success": true
}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
}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": [...]
}Remove a link between two memories (bidirectional).
Response:
{
"success": true
}List all projects.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
status |
string | Filter by status: active, archived, completed |
repo_name |
string | Filter 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 a single project by ID.
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
Update an existing project.
Request Body: (all fields optional)
{
"name": "Updated Name",
"description": "Updated description",
"status": "completed"
}Delete a project (preserves associated memories).
List all entities.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_type |
string | Filter by type: Individual, Organization, Team, Device, 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 a single entity by ID.
Create a new entity.
Request Body:
{
"name": "Entity Name",
"entity_type": "Individual",
"custom_type": "Custom Type (if entity_type is Other)",
"notes": "Optional notes"
}Update an existing entity.
Delete an entity.
Search entities by name.
Request Body:
{
"query": "search term",
"entity_type": "Individual",
"limit": 10
}Link an entity to a memory.
Request Body:
{
"memory_id": 1
}Remove link between entity and memory.
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
}Create a relationship between entities.
Request Body:
{
"target_entity_id": 2,
"relationship_type": "manages",
"description": "Optional description"
}Update an entity relationship.
Delete an entity relationship.
List all documents.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
project_id |
int | Filter by project |
document_type |
string | Filter by type |
tags |
string | Comma-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 a single document by ID.
Create a new document.
Request Body:
{
"title": "Document Title",
"description": "Brief description",
"content": "Full document content...",
"document_type": "analysis",
"tags": ["tag1"]
}Update an existing document.
Delete a document.
List all code artifacts.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
project_id |
int | Filter by project |
language |
string | Filter by programming language |
tags |
string | Comma-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 a single code artifact by ID.
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"]
}Update an existing code artifact.
Delete a code artifact.
Requires: SKILLS_ENABLED=true
List all skills.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
project_id |
int | Filter by project |
tags |
string | Comma-separated tags (OR logic) |
importance_threshold |
int | Minimum 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
}Semantic search for skills by description similarity.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
query |
string | (required) | Search query |
k |
int | 5 | Number of results |
project_id |
int | - | Filter by project |
Get a single skill by ID (includes full content).
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
}Update an existing skill (PATCH semantics).
Delete a skill.
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
}Export a skill to Agent Skills markdown format (SKILL.md).
Response:
{
"skill_md": "---\nname: code-review\ndescription: ...\n---\n\n# Content..."
}Get full knowledge graph for visualization. Returns all nodes and edges for the user.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
node_types |
string | memory,entity,project,document,code_artifact |
Comma-separated list of node types to include |
project_id |
int | - | Filter to specific project |
include_entities |
bool | true | Include entity nodes (deprecated, use node_types) |
limit |
int | 100 | Max memories to include (max 500) |
offset |
int | 0 | Number of memories to skip for pagination |
sort_by |
string | created_at |
Sort field: created_at, updated_at, or importance |
sort_order |
string | desc |
Sort direction: asc or desc |
Node Types:
memory- Knowledge memoriesentity- People, organizations, devicesproject- Project contextsdocument- Long-form documentscode_artifact- Code snippets
Edge Types:
memory_link- Memory-to-memory connectionsentity_memory- Entity linked to memoryentity_relationship- Entity-to-entity relationshipmemory_project- Memory linked to projectdocument_project- Document belongs to projectcode_artifact_project- Code artifact belongs to projectmemory_document- Memory linked to documentmemory_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=ascGet subgraph centered on a specific node using efficient CTE traversal. This is the recommended endpoint for graph visualization with depth-limited traversal.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
node_id |
string | required | Center node (e.g., memory_1, entity_5, project_3, document_2, code_artifact_4) |
depth |
int | 2 | Traversal depth (1-3, clamped) |
node_types |
string | memory,entity,project,document,code_artifact |
Comma-separated list of types to traverse |
max_nodes |
int | 200 | Maximum 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
}
}Deprecated: Use /api/v1/graph/subgraph?node_id=memory_{id} instead.
Get subgraph centered on a specific memory.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
depth |
int | 1 | Link traversal depth (1-3) |
Response:
{
"nodes": [...],
"edges": [...],
"center_memory_id": 1,
"meta": {
"memory_count": 5,
"edge_count": 4,
"depth": 1
}
}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.
Activity tracking is controlled by these settings:
| Setting | Type | Default | Description |
|---|---|---|---|
ACTIVITY_RETENTION_DAYS |
int | None | Days to keep events (None = forever). Cleanup happens lazily on API access. |
ACTIVITY_TRACK_READS |
bool | false | Track read/query operations (opt-in, can be high volume) |
List activity events with filtering and pagination.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
entity_type |
string | - | Filter by type: memory, project, document, code_artifact, entity, link |
action |
string | - | Filter by action: created, updated, deleted, read, queried |
entity_id |
int | - | Filter by specific entity ID |
actor |
string | - | Filter by actor: user, system, llm-maintenance |
since |
datetime | - | Only events after this timestamp (ISO 8601) |
until |
datetime | - | Only events before this timestamp (ISO 8601) |
limit |
int | 50 | Results per page (1-100) |
offset |
int | 0 | Skip 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 activity history for a specific entity.
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
entity_type |
string | Entity type: memory, project, document, code_artifact, entity |
entity_id |
int | Entity ID |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 50 | Results per page (1-100) |
offset |
int | 0 | Skip N results |
Response: Same format as GET /api/v1/activity
Entity Types:
memory- Knowledge memoriesproject- Project containersdocument- Long-form documentscode_artifact- Code snippetsentity- People, organizations, deviceslink- Memory-to-memory connectionsentity_memory_link- Entity-to-memory connectionsentity_project_link- Entity-to-project connectionsentity_relationship- Entity-to-entity relationships
Action Types:
created- Entity was createdupdated- Entity was modified (includeschangesdiff)deleted- Entity was soft-deleted (obsolete)read- Entity was read (ifACTIVITY_TRACK_READS=true)queried- Search was performed (ifACTIVITY_TRACK_READS=true)
Actor Types:
user- Human user actionsystem- Automated system actionllm-maintenance- LLM-based maintenance task (future)
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}
}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:
| Parameter | Type | Description |
|---|---|---|
entity_type |
string | Filter by type (optional) |
action |
string | Filter 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:
| Setting | Type | Default | Description |
|---|---|---|---|
SSE_MAX_QUEUE_SIZE |
int | 1000 | Max 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:
| Code | Description |
|---|---|
| 400 | Invalid filter parameter |
| 401 | Unauthorized |
| 503 | Activity streaming not enabled (ACTIVITY_ENABLED=false) |
List plans with optional filtering.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
project_id |
int | Filter by project |
status |
string | Filter 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 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"
}Create a new plan.
Request Body:
{
"title": "Plan Title",
"project_id": 1,
"goal": "Optional goal description",
"context": "Optional background context",
"status": "draft"
}| Field | Type | Required | Description |
|---|---|---|---|
title |
string | yes | Plan title |
project_id |
int | yes | Parent project ID |
goal |
string | no | Goal or objective |
context |
string | no | Background context |
status |
string | no | Initial status (default: draft). Values: draft, active, completed, archived |
Response (201): Full plan object (see GET response above)
Update an existing plan.
Request Body: (all fields optional)
{
"title": "Updated Title",
"goal": "Updated goal",
"context": "Updated context",
"status": "active"
}| Field | Type | Description |
|---|---|---|
title |
string | Updated title |
goal |
string | Updated goal |
context |
string | Updated context |
status |
string | New status (must be a valid transition) |
Plan Status Transitions:
| From | Allowed To |
|---|---|
draft |
active, archived |
active |
completed, archived |
completed |
active (reopen) |
archived |
active (unarchive) |
Response: Updated plan object
Error Responses:
| Code | Condition |
|---|---|
| 404 | Plan not found |
| 422 | Invalid status transition |
Delete a plan. Cascades to all tasks, criteria, and dependencies under the plan.
Response:
{
"success": true
}Query tasks for a plan. The plan_id query parameter is required.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
plan_id |
int | yes | Filter by plan |
state |
string | no | Filter by state: todo, doing, waiting, done, cancelled |
priority |
string | no | Filter by priority: P0, P1, P2, P3 |
assigned_agent |
string | no | Filter 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 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"
}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]
}| Field | Type | Required | Description |
|---|---|---|---|
title |
string | yes | Task title |
plan_id |
int | yes | Parent plan ID |
description |
string | no | Detailed description |
priority |
string | no | Priority level (default: P2). Values: P0 (critical), P1 (high), P2 (medium), P3 (low) |
assigned_agent |
string | no | Agent assigned to the task |
criteria |
array | no | Inline acceptance criteria to create |
dependency_ids |
array | no | IDs of tasks this task depends on |
Response (201): Full task object (see GET response above)
Error Responses:
| Code | Condition |
|---|---|
| 400 | Cyclic dependency detected |
| 404 | Plan or dependency task not found |
| 422 | Invalid state transition |
Update task metadata. State changes must go through the transition endpoint.
Request Body: (all fields optional)
{
"title": "Updated Title",
"description": "Updated description",
"priority": "P1"
}| Field | Type | Description |
|---|---|---|
title |
string | Updated title |
description |
string | Updated description |
priority |
string | Updated priority: P0, P1, P2, P3 |
Response: Updated task object
Delete a task.
Response:
{
"success": true
}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
}| Field | Type | Required | Description |
|---|---|---|---|
state |
string | yes | Target state: todo, doing, waiting, done, cancelled |
version |
int | yes | Current version for optimistic concurrency |
Task State Transitions:
| From | Allowed To |
|---|---|
todo |
doing, waiting, cancelled |
doing |
done, waiting, todo, cancelled |
waiting |
todo, doing, cancelled |
done |
todo (reopen) |
cancelled |
todo (reinstate) |
Response: Updated task object (version is incremented on success)
Error Responses:
| Code | Condition |
|---|---|
| 404 | Task not found |
| 409 | Version mismatch (optimistic concurrency conflict) |
| 422 | Invalid state transition, or unmet dependencies block the transition |
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
}| Field | Type | Required | Description |
|---|---|---|---|
agent_id |
string | yes | Identifier of the claiming agent |
version |
int | yes | Current version for optimistic concurrency |
Response: Updated task object (state set to doing, assigned_agent set, version incremented)
Error Responses:
| Code | Condition |
|---|---|
| 404 | Task not found |
| 409 | Version mismatch (optimistic concurrency conflict) |
| 422 | Invalid state transition, or unmet dependencies block the claim |
Add an acceptance criterion to a task.
Request Body:
{
"description": "Acceptance criterion description"
}| Field | Type | Required | Description |
|---|---|---|---|
description |
string | yes | Criterion 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:
| Code | Condition |
|---|---|
| 404 | Task not found |
| 422 | Task is in a state that does not allow adding criteria |
Update a criterion (e.g., mark it as met).
Request Body: (all fields optional)
{
"description": "Updated description",
"met": true
}| Field | Type | Description |
|---|---|---|
description |
string | Updated description |
met |
bool | Whether the criterion has been met |
Response: Updated criterion object
Delete an acceptance criterion.
Response:
{
"success": true
}Add a dependency, declaring that task_id depends on another task.
Request Body:
{
"depends_on_task_id": 2
}| Field | Type | Required | Description |
|---|---|---|---|
depends_on_task_id |
int | yes | ID 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:
| Code | Condition |
|---|---|
| 400 | Cyclic dependency detected, or task depends on itself |
| 404 | Task not found |
Remove a dependency between two tasks.
Response:
{
"success": true
}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"
}
]
}