REST API endpoints for the orc orchestrator. Base URL: http://localhost:8080
| Category | Endpoints | Purpose |
|---|---|---|
| Multi-Project | (all services) | Project routing via project_id |
| Tasks | /api/tasks/* |
Task CRUD and execution |
| Projects | /api/projects/* |
Multi-project registry and task operations |
| Branches | Connect RPC | Git branch management |
| Initiatives | /api/initiatives/* |
Task grouping and decisions |
| Decisions | /api/decisions/* |
Gate approval/rejection |
| Configuration | /api/prompts/*, /api/hooks/*, etc. |
Project configuration |
| Integration | /api/github/*, /api/mcp/*, /api/plugins/* |
External integrations |
| Plugins | /api/plugins/*, /api/marketplace/* |
Plugin management & marketplace |
| Session | /api/session |
Current session metrics |
| Dashboard | /api/dashboard/*, /api/stats/* |
Statistics, activity, and file analytics |
| Notifications | Connect RPC | User notification management |
| Attention Dashboard | Connect RPC | Project and cross-project attention hub |
| Recommendations | Connect RPC | Project-scoped recommendation inbox |
| Events | /api/events |
Timeline event queries |
| Workflows | /api/workflows/*, /api/phase-templates/* |
Workflow and phase template configuration |
| Workflow Runs | /api/workflow-runs/* |
Workflow execution instances |
| Real-time | /api/ws |
WebSocket events |
| Feedback | Connect RPC | Real-time user feedback to agents |
All Connect RPC services accept a project_id field in their request messages. This field routes the request to the correct project-specific database.
| Behavior | Condition |
|---|---|
| Legacy single-project mode | project_id is empty or omitted for services that do not define cross-project behavior |
| Cross-project attention dashboard | GetAttentionDashboardData is called with empty project_id while project cache is configured |
| Project-scoped operation | project_id is set to a valid project ID |
How it works:
- The server maintains an LRU cache of project databases
- When
project_idis provided, the request is routed to that project's SQLite database - When
project_idis empty, most services use the CWD-based legacy backend (single project) AttentionDashboardService/GetAttentionDashboardDatais the exception: emptyproject_idreturns a cross-project attention view when the server has project cache wiring- All project-scoped services follow this pattern: TaskService, InitiativeService, WorkflowService, TranscriptService, EventService, ConfigService, HostingService, DashboardService, DecisionService, NotificationService, AttentionDashboardService, RecommendationService, BranchService, FeedbackService
Services with project_id support:
| Service | Proto File | Request Messages Updated |
|---|---|---|
| TaskService | task.proto |
All request messages |
| InitiativeService | initiative.proto |
All request messages |
| HostingService | hosting.proto |
CreatePR, GetPR, MergePR, RefreshPR, SyncComments, AutofixComment, GetChecks, ListPRs, GetPRComments |
| DashboardService | dashboard.proto |
GetStats, GetActivityHeatmap, GetCostSummary, GetMetrics, GetDailyMetrics, GetMetricsByModel, GetOutcomes, GetTopInitiatives, GetTopFiles, GetComparison, GetTaskMetrics, GetCostReport |
| DecisionService | decision.proto |
ListDecisions, ResolveDecision, GetDecision, ListDecisionHistory |
| NotificationService | notification.proto |
ListNotifications, DismissNotification, DismissAllNotifications |
| AttentionDashboardService | attention_dashboard.proto |
GetAttentionDashboardData, PerformAttentionAction, UpdateQueueOrganization |
| BranchService | project.proto |
ListBranches, GetBranch, UpdateBranchStatus, DeleteBranch, CleanupStaleBranches |
| ConfigService | config.proto |
All request messages (GetConfig, UpdateConfig, GetSettings, UpdateSettings, GetSettingsHierarchy, ListHooks, CreateHook, UpdateHook, DeleteHook, ExportHooks, ImportHooks, ListSkills, CreateSkill, UpdateSkill, DeleteSkill, ExportSkills, ImportSkills, ScanClaudeDir, GetClaudeMd, UpdateClaudeMd, GetConstitution, UpdateConstitution, DeleteConstitution, ListPrompts, GetPrompt, GetDefaultPrompt, UpdatePrompt, DeletePrompt, ListPromptVariables, ListAgents, GetAgent, CreateAgent, UpdateAgent, DeleteAgent, ListScripts, DiscoverScripts, GetScript, CreateScript, UpdateScript, DeleteScript, ListTools, GetToolPermissions, UpdateToolPermissions, GetConfigStats) |
| WorkflowService | workflow.proto |
All request messages including run requests (ListWorkflowRuns, GetWorkflowRun, StartWorkflowRun, CancelWorkflowRun, SaveWorkflowLayout) |
| TranscriptService | transcript.proto |
All request messages |
| EventService | events.proto |
All request messages |
| ProjectService | project.proto |
N/A (manages projects themselves, not project-scoped) |
| RecommendationService | recommendation.proto |
CreateRecommendation, GetRecommendation, ListRecommendations, ListRecommendationHistory, AcceptRecommendation, RejectRecommendation, DiscussRecommendation |
| FeedbackService | feedback.proto |
AddFeedback, ListFeedback, SendFeedback, DeleteFeedback |
REST API mapping: For REST endpoints, project_id is passed as a query parameter (?project_id=abc123) or derived from the URL path (/api/projects/:id/tasks). File serving endpoints (/files/tasks/{id}/attachments/*, /files/tasks/{id}/test-results/*) and export/import endpoints (/api/export, /api/import) also accept ?project_id=... for project routing.
CWD-based task operations. These endpoints use the server's working directory as the project root.
Note: When the server is started from a non-orc directory, /api/tasks returns an empty list rather than an error. For explicit project-scoped operations, use /api/projects/:id/tasks instead.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/tasks |
List tasks (?page=N&limit=N&initiative=INIT-001&dependency_status=blocked) - returns empty list if not in orc project |
| POST | /api/tasks |
Create task |
| GET | /api/tasks/:id |
Get task |
| PATCH | /api/tasks/:id |
Update task (title, description, weight, metadata) |
| DELETE | /api/tasks/:id |
Delete task |
| GET | /api/tasks/:id/state |
Get execution state |
| GET | /api/tasks/:id/plan |
Get task plan |
| GET | /api/tasks/:id/transcripts |
Get transcripts |
| POST | /api/tasks/:id/run |
Start task (?force=true to bypass blockers) |
| POST | /api/tasks/:id/pause |
Pause task |
| POST | /api/tasks/:id/resume |
Resume task |
| POST | /api/tasks/:id/skip-block |
Clear blocked_by dependencies |
| POST | /api/tasks/:id/rewind |
Rewind to phase ({"phase": "implement"}) |
| POST | /api/tasks/:id/finalize |
Trigger finalize phase (async) |
| GET | /api/tasks/:id/finalize |
Get finalize status |
Run task response:
{
"status": "started",
"task_id": "TASK-001",
"task": { /* full Task object with status="running" */ }
}The task field contains the updated task with status: "running" set immediately. This allows clients to update their local state without waiting for WebSocket events, avoiding race conditions where the task might briefly appear deleted.
Blocked task response (409 Conflict):
If a task has incomplete dependencies (tasks in blocked_by that aren't completed), the endpoint returns 409 Conflict:
{
"error": "task_blocked",
"message": "Task is blocked by incomplete dependencies",
"blocked_by": [
{"id": "TASK-060", "title": "Add initiative_id field", "status": "planned"},
{"id": "TASK-061", "title": "Add Initiatives section", "status": "running"}
],
"force_available": true
}| Field | Description |
|---|---|
error |
Error code: task_blocked |
message |
Human-readable error message |
blocked_by |
Array of incomplete blocking tasks with id, title, status |
force_available |
Always true - indicates override is possible |
Force override: Add ?force=true query parameter to run the task despite blockers:
POST /api/tasks/TASK-062/run?force=true
Create task body (POST):
Supports both JSON and multipart/form-data. Use multipart when attaching files during creation.
JSON format:
{
"title": "Task title",
"description": "Task description",
"weight": "medium",
"queue": "active",
"priority": "normal",
"category": "feature",
"initiative_id": "INIT-001",
"blocked_by": ["TASK-001", "TASK-002"],
"related_to": ["TASK-003"]
}Multipart form-data fields:
| Field | Type | Description |
|---|---|---|
title |
string | Task title (required) |
description |
string | Task description |
weight |
string | trivial/small/medium/large/greenfield |
queue |
string | active/backlog |
priority |
string | critical/high/normal/low |
category |
string | feature/bug/refactor/chore/docs/test |
initiative_id |
string | Initiative ID to link task to (e.g., INIT-001) |
blocked_by |
string | Comma-separated task IDs that must complete first |
related_to |
string | Comma-separated related task IDs |
attachments |
file[] | Files to attach (repeatable) |
All fields except title are optional. Defaults: queue: "active", priority: "normal", category: "feature", initiative_id: "" (standalone).
Workflow auto-assignment: When weight is specified without an explicit workflow_id, the workflow is auto-assigned based on weight:
| Weight | Workflow ID |
|---|---|
| trivial | implement-trivial |
| small | implement-small |
| medium | implement-medium |
| large | implement-large |
Explicit workflow_id takes precedence over weight-derived assignment.
Query parameters for GET /api/tasks:
| Parameter | Description | Values |
|---|---|---|
page |
Page number for pagination | Integer |
limit |
Items per page (max 100) | Integer |
initiative |
Filter by initiative ID | Initiative ID (e.g., INIT-001) |
dependency_status |
Filter by dependency status | blocked, ready, none |
Dependency status values:
| Value | Description |
|---|---|
blocked |
Tasks with incomplete blockers (waiting on other tasks) |
ready |
Tasks where all dependencies are satisfied |
none |
Tasks with no dependencies defined |
Update task body (PATCH):
{
"title": "New title",
"description": "Updated description",
"weight": "medium",
"queue": "backlog",
"priority": "high",
"category": "bug",
"initiative_id": "INIT-001",
"blocked_by": ["TASK-001"],
"related_to": ["TASK-002", "TASK-003"],
"metadata": {"key": "value"}
}All fields are optional. Only provided fields are updated. Cannot update running tasks.
| Field | Valid Values |
|---|---|
weight |
trivial, small, medium, large, greenfield |
queue |
active, backlog |
priority |
critical, high, normal, low |
category |
feature, bug, refactor, chore, docs, test |
initiative_id |
Initiative ID (e.g., INIT-001) or "" to unlink |
blocked_by |
Array of task IDs that must complete first |
related_to |
Array of related task IDs (informational) |
Weight changes trigger automatic plan regeneration (completed/skipped phases are preserved if they exist in both plans).
Initiative linking: Setting initiative_id links the task to an initiative. The task is auto-added to the initiative's task list (bidirectional sync). Use "" to unlink a task from its initiative.
Dependency validation:
- Referenced task IDs must exist
- Self-references are rejected
- Circular dependencies are detected and rejected
Clear dependency blockers to make a blocked task runnable.
POST /api/tasks/:id/skip-block
No request body required.
Response:
{
"status": "success",
"task_id": "TASK-001",
"message": "Block skipped successfully",
"cleared_blockers": ["TASK-060", "TASK-061"]
}| Field | Description |
|---|---|
status |
Always success on 200 |
task_id |
The task that was unblocked |
message |
Human-readable confirmation |
cleared_blockers |
Array of task IDs that were in blocked_by |
Side effects:
- Clears
blocked_byfield to empty array - Sets
is_blockedtofalse - If task status was
blocked, resets toplanned
Trigger and monitor the finalize phase, which syncs with the target branch, resolves conflicts, and runs tests.
Trigger finalize (POST):
// Request body (optional)
{
"force": false, // Force finalize even if task status normally disallows it
"gate_override": false // Override gate checks
}
// Response
{
"task_id": "TASK-001",
"status": "pending",
"message": "Finalize started"
}The finalize runs asynchronously. Subscribe to WebSocket events for real-time progress updates.
Finalize status (GET):
// While running
{
"task_id": "TASK-001",
"status": "running",
"started_at": "2026-01-10T10:30:00Z",
"updated_at": "2026-01-10T10:31:00Z",
"step": "Syncing with target",
"progress": "Merging changes from main",
"step_percent": 50
}
// On completion
{
"task_id": "TASK-001",
"status": "completed",
"started_at": "2026-01-10T10:30:00Z",
"updated_at": "2026-01-10T10:35:00Z",
"step": "Complete",
"progress": "Finalize completed successfully",
"step_percent": 100,
"result": {
"synced": true,
"conflicts_resolved": 0,
"conflict_files": [],
"tests_passed": true,
"risk_level": "low",
"files_changed": 12,
"lines_changed": 350,
"needs_review": false,
"commit_sha": "abc123def",
"target_branch": "main"
}
}
// On failure
{
"task_id": "TASK-001",
"status": "failed",
"step": "Failed",
"error": "merge conflict in main.go"
}
// Not started
{
"task_id": "TASK-001",
"status": "not_started",
"message": "No finalize operation found"
}| Status | Description |
|---|---|
pending |
Finalize queued, about to start |
running |
Finalize in progress |
completed |
Finalize succeeded |
failed |
Finalize failed with error |
not_started |
No finalize has been triggered |
FinalizeResult fields:
| Field | Type | Description |
|---|---|---|
synced |
boolean | Whether branch was synced with target |
conflicts_resolved |
number | Number of merge conflicts resolved |
conflict_files |
string[] | List of files that had conflicts |
tests_passed |
boolean | Whether tests passed after sync |
risk_level |
string | Risk assessment: low, medium, high |
files_changed |
number | Total files modified in diff |
lines_changed |
number | Total lines added/removed |
needs_review |
boolean | Whether human review is recommended |
commit_sha |
string | Final merged commit SHA |
target_branch |
string | Branch merged into |
WebSocket events: Finalize broadcasts finalize events during execution:
// Progress update
{
"type": "event",
"event_type": "finalize",
"data": {
"task_id": "TASK-001",
"status": "running",
"step": "Running tests",
"progress": "Executing test suite",
"step_percent": 75,
"updated_at": "2026-01-10T10:32:00Z"
}
}
// Completion event (includes result)
{
"type": "event",
"event_type": "finalize",
"data": {
"task_id": "TASK-001",
"status": "completed",
"step": "Complete",
"step_percent": 100,
"updated_at": "2026-01-10T10:35:00Z",
"result": {
"synced": true,
"conflicts_resolved": 0,
"tests_passed": true,
"risk_level": "low",
"files_changed": 12,
"commit_sha": "abc123def",
"target_branch": "main"
}
}
}| Method | Endpoint | Description |
|---|---|---|
| GET | /api/tasks/:id/attachments |
List attachments |
| POST | /api/tasks/:id/attachments |
Upload attachment (multipart/form-data, max 32MB) |
| GET | /api/tasks/:id/attachments/:filename |
Get attachment file |
| DELETE | /api/tasks/:id/attachments/:filename |
Delete attachment |
Attachment response:
{
"filename": "screenshot.png",
"size": 45678,
"content_type": "image/png",
"created_at": "2026-01-10T10:30:00Z",
"is_image": true
}Upload: Use multipart/form-data with file in the file field. Optional filename field overrides original filename.
Download headers:
- Images:
Content-Disposition: inline(renders in browser) - Other files:
Content-Disposition: attachment(triggers download)
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/tasks/:id/dependencies |
Get dependency graph for task |
| GET | /api/tasks/dependency-graph |
Get visualization graph for multiple tasks |
Dependencies response:
{
"task_id": "TASK-001",
"blocked_by": [
{"id": "TASK-060", "title": "Setup auth", "status": "completed", "exists": true}
],
"blocks": [
{"id": "TASK-062", "title": "Add OAuth", "status": "planned", "exists": true}
],
"related_to": [
{"id": "TASK-063", "title": "Update docs", "status": "created", "exists": true}
],
"referenced_by": [
{"id": "TASK-064", "title": "See TASK-001 for context", "status": "planned", "exists": true}
],
"unmet_dependencies": ["TASK-060"],
"can_run": false
}| Field | Description |
|---|---|
blocked_by |
Tasks that must complete before this task (stored) |
blocks |
Tasks waiting on this task (computed inverse) |
related_to |
Related tasks for reference (stored) |
referenced_by |
Tasks whose descriptions mention this task (auto-detected) |
unmet_dependencies |
Blockers that are not yet completed |
can_run |
True if no unmet dependencies |
Returns nodes and edges for visualizing dependencies across an arbitrary set of tasks.
GET /api/tasks/dependency-graph
Query parameters:
ids(required) - Comma-separated list of task IDs to include
Example:
GET /api/tasks/dependency-graph?ids=TASK-060,TASK-061,TASK-062,TASK-063
Response:
{
"nodes": [
{"id": "TASK-060", "title": "Add initiative_id field", "status": "done"},
{"id": "TASK-061", "title": "Add sidebar navigation", "status": "ready"},
{"id": "TASK-062", "title": "Add filter dropdown", "status": "blocked"},
{"id": "TASK-063", "title": "Add initiative badges", "status": "ready"}
],
"edges": [
{"from": "TASK-060", "to": "TASK-061"},
{"from": "TASK-061", "to": "TASK-062"}
]
}| Field | Description |
|---|---|
nodes |
Array of requested tasks with display status |
nodes[].status |
Simplified status: done, running, blocked, ready, pending, paused, failed |
edges |
Dependency relationships within the requested set only |
Status mapping:
| Internal Status | Display Status |
|---|---|
completed, finished |
done |
running, finalizing |
running |
blocked |
blocked |
paused |
paused |
failed |
failed |
created, planned |
ready |
| (other) | pending |
Error responses:
- 400: Missing
idsparameter or no valid task IDs provided
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/tasks/:id/export |
Export artifacts |
| GET | /api/tasks/:id/stream |
SSE transcript stream (legacy) |
Export body:
{"task_definition": true, "final_state": true, "context_summary": true, "transcripts": false}Comments and notes on tasks from humans, agents, or system.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/tasks/:id/comments |
List comments (`?author_type=human |
| POST | /api/tasks/:id/comments |
Create comment |
| GET | /api/tasks/:id/comments/stats |
Get comment statistics |
| GET | /api/tasks/:id/comments/:commentId |
Get single comment |
| PATCH | /api/tasks/:id/comments/:commentId |
Update comment |
| DELETE | /api/tasks/:id/comments/:commentId |
Delete comment |
Create comment body:
{
"author": "claude",
"author_type": "agent",
"content": "This approach uses the existing auth flow",
"phase": "implement"
}Comment response:
{
"id": "TC-a1b2c3d4",
"task_id": "TASK-001",
"author": "claude",
"author_type": "agent",
"content": "This approach uses the existing auth flow",
"phase": "implement",
"created_at": "2026-01-10T10:30:00Z",
"updated_at": "2026-01-10T10:30:00Z"
}Author types:
human- Human user (default)agent- AI agent (Claude during execution)system- System-generated (automated processes)
Multi-project support via global registry. Projects are managed through both REST endpoints and the Connect RPC ProjectService.
REST Endpoints:
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/projects |
List registered projects |
| GET | /api/projects/default |
Get default project ID |
| PUT | /api/projects/default |
Set default project |
| GET | /api/projects/:id |
Get project details |
| DELETE | /api/projects/:id |
Remove project from registry |
| GET | /api/projects/:id/tasks |
List tasks for project |
| POST | /api/projects/:id/tasks |
Create task in project |
Connect RPC: ProjectService (proto/orc/v1/project.proto)
| RPC Method | Description |
|---|---|
| ListProjects | List all registered projects |
| GetProject | Get project by ID |
| GetDefaultProject | Get the default project (ID + project details) |
| SetDefaultProject | Set default project by ID |
| AddProject | Register a new project (name + path) |
| RemoveProject | Remove a project from the registry |
| GetAllProjectsStatus | Unified view of tasks across all projects (active tasks, counts, stale detection) |
Project object:
| Field | Type | Description |
|---|---|---|
id |
string | Unique project identifier |
name |
string | Project display name |
path |
string | Filesystem path to project root |
created_at |
timestamp | When the project was registered |
is_default |
bool | Whether this is the default project |
Fallback project when no selection exists in URL or localStorage. Stored in ~/.orc/projects.yaml.
Get default project:
// Response
{"default_project": "abc123"}Set default project:
// Request
{"project_id": "abc123"}
// Response
{"default_project": "abc123"}Returns 404 if the specified project doesn't exist.
Cross-project aggregation endpoint for dashboard use. Returns active tasks, counts, and stale detection for every registered project. Requires projectCache (returns FailedPrecondition if nil).
Response: ProjectStatus[]
| Field | Type | Description |
|---|---|---|
project_id |
string | Project identifier |
project_name |
string | Display name |
project_path |
string | Filesystem path |
active_tasks |
TaskSummary[] | Tasks with status CREATED, PLANNED, RUNNING, or BLOCKED |
total_tasks |
int32 | Count of all tasks (any status) |
completed_today |
int32 | Tasks completed since UTC midnight |
pending_recommendations |
int32 | Pending inbox items that still need a human decision |
TaskSummary:
| Field | Type | Description |
|---|---|---|
id |
string | Task ID |
title |
string | Task title |
status |
TaskStatus | Current status |
claimed_by_name |
string | Who claimed the task |
claimed_at |
timestamp | When claimed |
is_stale |
bool | True if RUNNING but executor PID is dead |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/projects/:id/tasks/:taskId |
Get task |
| DELETE | /api/projects/:id/tasks/:taskId |
Delete task |
| GET | /api/projects/:id/tasks/:taskId/state |
Get execution state |
| GET | /api/projects/:id/tasks/:taskId/plan |
Get task plan |
| GET | /api/projects/:id/tasks/:taskId/transcripts |
Get transcripts |
| POST | /api/projects/:id/tasks/:taskId/run |
Start task |
| POST | /api/projects/:id/tasks/:taskId/pause |
Pause task |
| POST | /api/projects/:id/tasks/:taskId/resume |
Resume task |
| POST | /api/projects/:id/tasks/:taskId/rewind |
Rewind to phase |
Connect RPC service for managing git branches. All requests accept project_id. See Multi-Project Support.
Connect RPC: BranchService (proto/orc/v1/project.proto)
| RPC Method | Description |
|---|---|
| ListBranches | List branches (filter by type, status, include orphaned) |
| GetBranch | Get branch by name |
| UpdateBranchStatus | Update branch status (active, merged, stale, orphaned) |
| DeleteBranch | Delete a branch (with optional force flag) |
| CleanupStaleBranches | Cleanup stale branches by age threshold (supports dry run) |
Branch object:
| Field | Type | Description |
|---|---|---|
name |
string | Branch name |
type |
BranchType | INITIATIVE, STAGING, or TASK |
created_at |
timestamp | When branch was created |
last_activity |
timestamp | Last activity timestamp |
status |
BranchStatus | ACTIVE, MERGED, STALE, or ORPHANED |
owner_id |
string (optional) | Associated task ID or initiative ID |
commits_ahead |
int32 | Commits ahead of target branch |
commits_behind |
int32 | Commits behind target branch |
target_branch |
string (optional) | Target branch name |
Group related tasks with shared decisions. Initiatives can depend on other initiatives.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/initiatives |
List (?status=active, ?shared=true) |
| POST | /api/initiatives |
Create initiative |
| GET | /api/initiatives/:id |
Get initiative (includes computed blocks field) |
| PUT | /api/initiatives/:id |
Update initiative |
| DELETE | /api/initiatives/:id |
Delete initiative |
| GET | /api/initiatives/:id/tasks |
List initiative tasks |
| POST | /api/initiatives/:id/tasks |
Add task to initiative |
| DELETE | /api/initiatives/:id/tasks/:taskId |
Remove task from initiative |
| POST | /api/initiatives/:id/decisions |
Add decision |
| GET | /api/initiatives/:id/ready |
Get tasks ready to run |
| GET | /api/initiatives/:id/dependency-graph |
Get dependency graph visualization |
Returns nodes and edges for visualizing task dependencies within an initiative.
GET /api/initiatives/:id/dependency-graph
Query parameters:
shared- Iftrue, load from shared initiatives directory
Response:
{
"nodes": [
{
"id": "TASK-060",
"title": "Add initiative_id field to task schema",
"status": "done"
},
{
"id": "TASK-061",
"title": "Add initiative sidebar section",
"status": "ready"
},
{
"id": "TASK-062",
"title": "Add initiative filter dropdown",
"status": "blocked"
}
],
"edges": [
{"from": "TASK-060", "to": "TASK-061"},
{"from": "TASK-061", "to": "TASK-062"}
]
}| Field | Description |
|---|---|
nodes |
Array of tasks in the initiative with display status |
nodes[].status |
Simplified status: done, running, blocked, ready, pending, paused, failed |
edges |
Dependency relationships (from = blocker, to = blocked task) |
Only edges where both tasks are in the initiative are included.
Create initiative body (POST):
{
"title": "React Migration",
"vision": "Migrate all components to React 18",
"blocked_by": ["INIT-001"],
"owner": {
"initials": "JD",
"display_name": "John Doe"
},
"shared": false
}| Field | Description | Default |
|---|---|---|
title |
Initiative title (required) | - |
vision |
Vision statement | "" |
blocked_by |
Initiative IDs that must complete first | [] |
owner |
Owner identity | {} |
shared |
Create in shared directory | false |
Update initiative body (PUT):
{
"title": "Updated Title",
"vision": "Updated vision",
"status": "active",
"blocked_by": ["INIT-001", "INIT-002"]
}All fields are optional. Setting blocked_by replaces the entire list.
Initiative response:
{
"id": "INIT-002",
"title": "React Migration",
"status": "active",
"vision": "Migrate all components to React 18",
"blocked_by": ["INIT-001"],
"blocks": ["INIT-003"],
"tasks": [...],
"decisions": [...],
"created_at": "2026-01-10T10:30:00Z",
"updated_at": "2026-01-10T12:45:00Z"
}| Field | Description |
|---|---|
blocked_by |
Initiative IDs that must complete before this initiative (stored) |
blocks |
Initiative IDs waiting on this initiative (computed) |
Validation:
- Referenced initiative IDs must exist
- Self-references are rejected
- Circular dependencies are detected and rejected
Gate approval/rejection for human gates in headless (API/WebSocket) mode. When a task hits a human gate during API-driven execution, a decision_required WebSocket event is emitted and the decision is stored for resolution.
All DecisionService RPC requests accept project_id to target a specific project database. See Multi-Project Support.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/decisions |
List all pending gate decisions |
| POST | /api/decisions/:id |
Approve or reject a pending gate decision |
GET /api/decisions
Returns all pending gate decisions awaiting resolution. Use this endpoint to load pending decisions on page refresh. For real-time updates, subscribe to WebSocket decision_required events.
Response (200):
[
{
"decision_id": "gate_TASK-001_review_1737504000000000000",
"task_id": "TASK-001",
"task_title": "Add user authentication",
"phase": "review",
"gate_type": "human",
"question": "Please verify the following criteria:",
"context": "Code review passes\nTests pass",
"requested_at": "2026-01-22T10:00:00Z"
}
]| Field | Description |
|---|---|
decision_id |
Unique ID for this decision |
task_id |
Associated task ID |
task_title |
Task title for display |
phase |
Phase awaiting approval |
gate_type |
Gate type (human or ai) |
question |
Prompt to show the user |
context |
Additional context (may be empty) |
requested_at |
When the decision was requested (ISO8601) |
Notes:
- Returns empty array
[]if no decisions are pending - Decisions are removed from the list when resolved via POST
POST /api/decisions/:id
Resolves a pending gate decision by approving or rejecting it.
Request body:
{
"approved": true,
"reason": "LGTM, all issues addressed"
}| Field | Type | Required | Description |
|---|---|---|---|
approved |
boolean | Yes | true to approve, false to reject |
reason |
string | No | Optional explanation for the decision |
Success response (200):
{
"decision_id": "gate_TASK-001_review",
"task_id": "TASK-001",
"approved": true,
"new_status": "planned"
}| Field | Description |
|---|---|
decision_id |
The resolved decision ID |
task_id |
Associated task ID |
approved |
Whether the decision was approved |
new_status |
New task status: planned (approved) or failed (rejected) |
Error responses:
| Status | Condition |
|---|---|
| 400 | Invalid request body |
| 400 | Task is not in blocked status |
| 404 | Decision not found (already resolved or never existed) |
| 404 | Associated task not found |
| 500 | Failed to save task or state |
Side effects:
- Task status changes to
planned(approved) orfailed(rejected) - Gate decision is recorded in task state and database
decision_resolvedWebSocket event is emitted- Decision is removed from pending store (subsequent POSTs return 404)
Notes:
- Pending decisions are stored in-memory; server restart clears them
- Multiple concurrent pending decisions can exist for different tasks
- The task does NOT auto-resume after approval; use
POST /api/tasks/:id/resumeororc resumeCLI - CLI approval via
orc approvecontinues to work independently
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/prompts |
List prompts |
| GET | /api/prompts/variables |
Get template variables |
| GET | /api/prompts/:phase |
Get prompt for phase |
| GET | /api/prompts/:phase/default |
Get default prompt |
| PUT | /api/prompts/:phase |
Save prompt override |
| DELETE | /api/prompts/:phase |
Delete prompt override |
Hooks are stored in the hook_scripts table in GlobalDB. Built-in hooks (is_builtin=true) cannot be modified or deleted.
| RPC Method | Description |
|---|---|
ListHooks |
List all hooks, ordered by built-in status then name |
CreateHook |
Create hook (name, content, event_type required; rejects duplicates) |
UpdateHook |
Update hook by ID (rejects built-in modifications) |
DeleteHook |
Delete hook by ID (rejects built-in deletions) |
ExportHooks |
Export hooks from GlobalDB to .claude/hooks/ (PROJECT or GLOBAL scope) |
ImportHooks |
Import discovered hook files into GlobalDB (rejects duplicates, built-in collisions) |
Event types: PreToolUse, PostToolUse, Notification, Stop
Error codes: InvalidArgument (missing fields), AlreadyExists (duplicate name), NotFound, PermissionDenied (built-in)
Skills are stored in the skills table in GlobalDB. Built-in skills (is_builtin=true) cannot be modified or deleted.
| RPC Method | Description |
|---|---|
ListSkills |
List all skills, ordered by built-in status then name |
CreateSkill |
Create skill (name, content required; rejects duplicates) |
UpdateSkill |
Update skill by ID (rejects built-in modifications) |
DeleteSkill |
Delete skill by ID (rejects built-in deletions) |
ExportSkills |
Export skills from GlobalDB to .claude/skills/ (SKILL.md + supporting files) |
ImportSkills |
Import discovered skill directories into GlobalDB (rejects duplicates) |
Error codes: InvalidArgument (missing fields), AlreadyExists (duplicate name), NotFound, PermissionDenied (built-in)
Bidirectional sync between GlobalDB and .claude/ filesystem directories. Supports both PROJECT (.claude/) and GLOBAL (~/.claude/) scopes.
| RPC Method | Description |
|---|---|
ScanClaudeDir |
Scan .claude/hooks/ and .claude/skills/ for items not in GlobalDB. Returns DiscoveredItem list with new/modified status |
ExportHooks |
Write GlobalDB hooks to .claude/hooks/ as executable scripts (0755) |
ExportSkills |
Write GlobalDB skills to .claude/skills/<name>/SKILL.md + supporting files |
ImportHooks |
Create GlobalDB entries from discovered hooks (validates no duplicates or built-in collisions) |
ImportSkills |
Create GlobalDB entries from discovered skills (validates no duplicates) |
Round-trip workflow: Export (GlobalDB → filesystem) → Edit files → Scan (detect changes) → Import (filesystem → GlobalDB)
Security: Path traversal validation on all names/IDs. Binary files (null bytes) skipped during scan. Content previews truncated to 10KB.
Implementation: config_server_export.go
Claude Code settings from settings.json files. Both global (~/.claude/settings.json) and project (.claude/settings.json) settings are editable via the UI.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/settings |
Get merged settings (global + project) |
| GET | /api/settings/global |
Get global settings only |
| GET | /api/settings/project |
Get project settings only |
| PUT | /api/settings |
Update project settings |
| PUT | /api/settings/global |
Update global settings |
Settings body (PUT):
{
"env": {
"KEY": "value"
},
"statusLine": {
"type": "command",
"command": "echo -n '[$USER:${HOSTNAME%%.*}]:${PWD##*/}'"
}
}Editable fields:
env- Environment variables (key-value pairs)statusLine.type- Type of statusline (command)statusLine.command- Shell command for statusline output
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/tools |
List available tools |
| GET | /api/tools?by_category=true |
List tools grouped by category |
| GET | /api/tools/permissions |
Get tool allow/deny lists |
| PUT | /api/tools/permissions |
Update tool permissions |
Sub-agent definitions stored in SQLite with runtime statistics.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/agents |
List agents with stats (?scope=global|project) |
| POST | /api/agents |
Create agent |
| GET | /api/agents/:name |
Get agent details |
| PUT | /api/agents/:name |
Update agent |
| DELETE | /api/agents/:name |
Delete agent (custom only) |
Query parameters for GET /api/agents:
| Parameter | Description | Values |
|---|---|---|
scope |
Filter by scope | global (from ~/.claude/), project (from SQLite) |
When no scope is specified, returns agents from both project (SQLite) and global sources.
Agent response:
{
"name": "code-reviewer",
"description": "Reviews code for quality issues",
"model": "sonnet",
"prompt": "You are a code reviewer...",
"tools": {
"allow": ["Read", "Grep", "Edit"]
},
"scope": "PROJECT",
"status": "idle",
"stats": {
"tokens_today": 45000,
"tasks_done": 12,
"success_rate": 0.92
}
}| Field | Type | Description |
|---|---|---|
name |
string | Agent identifier |
description |
string | When to use this agent |
model |
string | Model override (sonnet, opus, haiku) |
prompt |
string | System prompt for the agent |
tools |
object | Tool permissions with allow list |
scope |
string | PROJECT or GLOBAL |
status |
string | active (running tasks) or idle |
stats.tokens_today |
number | Tokens used today for this model |
stats.tasks_done |
number | Completed tasks (all time) |
stats.success_rate |
number | Success rate 0.0-1.0 |
Notes:
- Stats are computed per model, not per agent (agents sharing a model share stats)
- Status is
activeif any task is running with the agent's model - Returns empty array (not error) when no agents exist
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/scripts |
List registered scripts |
| POST | /api/scripts |
Register script |
| POST | /api/scripts/discover |
Auto-discover scripts |
| GET | /api/scripts/:name |
Get script details |
| PUT | /api/scripts/:name |
Update script |
| DELETE | /api/scripts/:name |
Remove script from registry |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/claudemd |
Get project CLAUDE.md |
| PUT | /api/claudemd |
Update project CLAUDE.md |
| GET | /api/claudemd/hierarchy |
Get full hierarchy (global, user, project) |
Orc orchestrator configuration from .orc/config.yaml. All settings are editable via the UI.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/config |
Get orc configuration (?with_sources=true for source tracking) |
| PUT | /api/config |
Update orc configuration |
| GET | /api/config/export |
Get export configuration |
| PUT | /api/config/export |
Update export configuration |
Config response:
{
"version": "1.0.0",
"profile": "auto",
"automation": {
"profile": "auto",
"gates_default": "ai",
"retry_enabled": true,
"retry_max": 3
},
"execution": {
"model": "sonnet",
"max_iterations": 10,
"timeout": "30m"
},
"git": {
"branch_prefix": "orc/",
"commit_prefix": "[orc]"
},
"worktree": {
"enabled": true,
"dir": ".orc/worktrees",
"cleanup_on_complete": true,
"cleanup_on_fail": false
},
"completion": {
"action": "pr",
"target_branch": "main",
"delete_branch": true
},
"timeouts": {
"phase_max": "1h",
"turn_max": "5m",
"idle_warning": "2m",
"heartbeat_interval": "10s",
"idle_timeout": "10m"
},
"jira": {
"url": "https://acme.atlassian.net",
"email": "user@acme.com",
"token_env_var": "ORC_JIRA_TOKEN",
"epic_to_initiative": true,
"default_weight": "small",
"default_queue": "active",
"custom_fields": {"customfield_10020": "jira_sprint"},
"default_projects": ["PROJ"],
"status_overrides": {},
"category_overrides": {},
"priority_overrides": {}
}
}Config update body (PUT):
{
"profile": "safe",
"automation": {
"gates_default": "human",
"retry_enabled": true,
"retry_max": 5
},
"execution": {
"model": "opus",
"max_iterations": 20,
"timeout": "1h"
},
"git": {
"branch_prefix": "feature/",
"commit_prefix": "[feature]"
},
"worktree": {
"enabled": true,
"cleanup_on_complete": true,
"cleanup_on_fail": false
},
"completion": {
"action": "merge",
"target_branch": "develop",
"delete_branch": true
},
"timeouts": {
"phase_max": "2h",
"turn_max": "10m"
},
"jira": {
"url": "https://acme.atlassian.net",
"email": "user@acme.com",
"default_projects": ["PROJ1", "PROJ2"]
}
}All fields are optional. Only provided fields are updated. Setting profile applies a preset and then other fields override.
All HostingService RPC requests accept project_id to target a specific project database. See Multi-Project Support.
| RPC Method | Service | Description |
|---|---|---|
| CreatePR | HostingService | Create PR for task branch |
| GetPR | HostingService | Get PR details |
| MergePR | HostingService | Merge PR |
| RefreshPR | HostingService | Refresh PR status (reviews, checks, approval state) |
| SyncComments | HostingService | Sync local comments to PR |
| AutofixComment | HostingService | Queue auto-fix for a PR comment |
| GetChecks | HostingService | Get CI check status |
PR Status Polling:
- PRs are automatically polled every 60 seconds for tasks with open PRs
- Status includes: review state (pending_review, changes_requested, approved), CI checks, mergeability
- PR status is stored in database under the task's
prfield - Manual refresh via
POST /api/tasks/:id/github/pr/refresh - 30 second rate limit between polls for the same task
Auto-Trigger Finalize on Approval:
- When PR status changes to
approvedand automation profile isauto:- Finalize phase is automatically triggered asynchronously
- Controlled by
completion.finalize.auto_trigger_on_approvalconfig - Skips trivial tasks (finalize not applicable)
- Only triggers if finalize hasn't already completed
- WebSocket broadcasts progress via
finalizeevents
PR Status Values:
| Status | Description |
|---|---|
draft |
PR is in draft state |
pending_review |
PR awaiting review |
changes_requested |
Reviewers requested changes |
approved |
PR has been approved |
merged |
PR has been merged |
closed |
PR was closed without merging |
Refresh response:
{
"pr": { "number": 123, "state": "OPEN", ... },
"status": {
"url": "https://github.com/owner/repo/pull/123 (or GitLab equivalent)",
"number": 123,
"status": "approved",
"checks_status": "success",
"mergeable": true,
"review_count": 2,
"approval_count": 2,
"last_checked_at": "2024-01-01T10:00:00Z"
},
"task_id": "TASK-001"
}Merge body:
{"method": "squash", "delete_branch": true}Autofix comment:
Triggers an auto-fix for a specific PR comment. Fetches the comment from the hosting provider (GitHub or GitLab), sets up retry context, and spawns an executor to re-run the implement phase.
POST /api/tasks/:id/github/pr/comments/:commentId/autofix
Response:
{
"result": {
"success": true
}
}| Field | Description |
|---|---|
result.success |
true if autofix was started successfully |
Behavior:
- Returns immediately (~10ms) after spawning executor
- Task status changes to
running - Comment content is stored as retry feedback and exposed via
{{RETRY_FEEDBACK}} - Long comments (>10KB) are truncated
Error responses:
| Status | Code | Condition |
|---|---|---|
| 400 | InvalidArgument |
Missing task_id or comment_id |
| 404 | NotFound |
Task or comment not found |
| 409 | FailedPrecondition |
Task already running or completed |
| 401 | Unauthenticated |
Hosting provider not authenticated |
| 429 | ResourceExhausted |
Hosting provider API rate limited |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/mcp |
List MCP servers from .mcp.json |
| POST | /api/mcp |
Create MCP server |
| GET | /api/mcp/:name |
Get MCP server details |
| PUT | /api/mcp/:name |
Update MCP server |
| DELETE | /api/mcp/:name |
Delete MCP server |
Manage Claude Code plugins from .claude/plugins/. Supports both local plugin management and marketplace browsing.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/plugins |
List installed plugins (?scope=global|project) |
| GET | /api/plugins/resources |
Get aggregated resources (MCP servers, hooks, commands) |
| GET | /api/plugins/updates |
Check for available updates |
| GET | /api/plugins/:name |
Get plugin details (?scope=global|project) |
| GET | /api/plugins/:name/commands |
List plugin commands |
| POST | /api/plugins/:name/enable |
Enable plugin (?scope=global|project) |
| POST | /api/plugins/:name/disable |
Disable plugin (?scope=global|project) |
| POST | /api/plugins/:name/update |
Update plugin to latest version |
| DELETE | /api/plugins/:name |
Uninstall plugin (?scope=global|project) |
Plugin info response:
{
"name": "orc",
"description": "Task orchestration for Claude Code",
"scope": "project",
"enabled": true,
"has_commands": true,
"command_count": 5
}Plugin detail response:
{
"name": "orc",
"description": "Task orchestration for Claude Code",
"author": {"name": "Author Name", "url": "https://example.com"},
"homepage": "https://github.com/example/plugin",
"keywords": ["orchestration", "tasks"],
"path": "/home/user/.claude/plugins/orc",
"scope": "project",
"enabled": true,
"version": "1.0.0",
"has_commands": true,
"has_hooks": false,
"has_scripts": true,
"commands": [{"name": "init", "description": "Initialize project"}],
"mcp_servers": [],
"hooks": []
}Plugin resources response:
{
"mcp_servers": [{"name": "server", "command": "...", "plugin_name": "orc", "plugin_scope": "project"}],
"hooks": [{"event": "pre_prompt", "command": "...", "plugin_name": "orc", "plugin_scope": "global"}],
"commands": [{"name": "init", "description": "...", "plugin_name": "orc", "plugin_scope": "project"}]
}Browse and install plugins from the marketplace. Uses a separate /api/marketplace/plugins prefix to avoid route conflicts with local plugin management.
Note: When the official Claude Code plugin marketplace is unavailable, the API returns sample plugins with is_mock: true and a helpful message.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/marketplace/plugins |
Browse plugins (?page=N&limit=N) |
| GET | /api/marketplace/plugins/search |
Search plugins (?q=query) |
| GET | /api/marketplace/plugins/:name |
Get marketplace plugin details |
| POST | /api/marketplace/plugins/:name/install |
Install plugin (?scope=global|project) |
Browse response:
{
"plugins": [
{
"name": "orc",
"description": "Task orchestration plugin",
"author": {"name": "Author", "url": "https://example.com"},
"version": "1.0.0",
"repository": "https://github.com/example/orc-plugin",
"downloads": 1250,
"keywords": ["orchestration", "tasks"]
}
],
"total": 50,
"page": 1,
"limit": 20,
"cached": true,
"cache_age_seconds": 300,
"is_mock": false,
"message": null
}When marketplace is unavailable:
{
"plugins": [...],
"total": 6,
"page": 1,
"limit": 20,
"is_mock": true,
"message": "Showing sample plugins. The official Claude Code plugin marketplace is not yet available. Install plugins manually via 'claude plugin add <github-repo>'."
}Install body (optional):
{"version": "1.0.0"}Install response:
{
"plugin": {...},
"requires_restart": true,
"message": "Plugin installed. Restart Claude Code to load."
}Git diff visualization for task implementation changes. Compares the task branch against a base branch (default: main).
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/tasks/:id/diff |
Get full diff with file list and hunks |
| GET | /api/tasks/:id/diff/stats |
Get diff statistics only |
| GET | /api/tasks/:id/diff/file/{path} |
Get diff for a single file |
Query parameters:
base- Base branch to compare against (default:main)files- Iftrue, return file list without hunks (for/diffendpoint only)
Working tree support: When the task branch has not diverged from the base branch (same commit), but there are uncommitted changes in the working tree, the diff will include those uncommitted changes. The head field in the response will show "working tree" in this case.
Reference resolution: Branch refs are automatically resolved. If a local branch doesn't exist but origin/<branch> does, the remote tracking branch is used.
Full diff response:
{
"base": "main",
"head": "orc/TASK-001",
"stats": {
"files_changed": 3,
"additions": 150,
"deletions": 20
},
"files": [
{
"path": "internal/api/handlers.go",
"status": "modified",
"additions": 50,
"deletions": 10,
"binary": false,
"syntax": "go",
"hunks": [
{
"old_start": 10,
"old_lines": 5,
"new_start": 10,
"new_lines": 8,
"lines": [
{"type": "context", "content": " func init() {", "old_line": 10, "new_line": 10},
{"type": "deletion", "content": "- oldCode()", "old_line": 11},
{"type": "addition", "content": "+ newCode()", "new_line": 11}
]
}
]
}
]
}File status values: modified, added, deleted, renamed, copied
Line type values: context, addition, deletion
Stats-only response:
{
"files_changed": 3,
"additions": 150,
"deletions": 20
}Endpoints for Playwright test results, screenshots, and traces. Test results are stored in .orc/tasks/{id}/test-results/.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/tasks/:id/test-results |
Get test results summary |
| POST | /api/tasks/:id/test-results |
Save test report |
| POST | /api/tasks/:id/test-results/init |
Initialize test results directory |
| GET | /api/tasks/:id/test-results/screenshots |
List all screenshots |
| POST | /api/tasks/:id/test-results/screenshots |
Upload screenshot (multipart/form-data) |
| GET | /api/tasks/:id/test-results/screenshots/:filename |
Get screenshot file |
| GET | /api/tasks/:id/test-results/report |
Get Playwright HTML report |
| GET | /api/tasks/:id/test-results/traces/:filename |
Get trace file |
Test results response:
{
"has_results": true,
"report": {
"version": 1,
"framework": "playwright",
"started_at": "2026-01-10T10:30:00Z",
"completed_at": "2026-01-10T10:35:00Z",
"duration": 300000,
"summary": {
"total": 10,
"passed": 9,
"failed": 1,
"skipped": 0
},
"suites": [...]
},
"screenshots": [
{
"filename": "dashboard-initial.png",
"page_name": "dashboard initial",
"size": 45678,
"created_at": "2026-01-10T10:32:00Z"
}
],
"has_traces": true,
"trace_files": ["trace-1.zip"],
"has_html_report": true
}Save test report body:
{
"version": 1,
"framework": "playwright",
"started_at": "2026-01-10T10:30:00Z",
"completed_at": "2026-01-10T10:35:00Z",
"duration": 300000,
"summary": {
"total": 10,
"passed": 10,
"failed": 0,
"skipped": 0
},
"suites": [
{
"name": "Login Flow",
"tests": [
{
"name": "should login successfully",
"status": "passed",
"duration": 1500,
"screenshots": ["login-success.png"]
}
]
}
]
}Screenshot upload: Use multipart/form-data with file in the file field. Optional filename field overrides original filename.
Current session metrics for the TopBar component. Session data is scoped to the server instance (not persisted across restarts).
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/session |
Get session metrics (duration, tokens, cost, task counts) |
Session metrics response:
{
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"started_at": "2026-01-21T10:30:00Z",
"duration_seconds": 3600,
"total_tokens": 125000,
"input_tokens": 45000,
"output_tokens": 80000,
"estimated_cost_usd": 2.45,
"tasks_completed": 5,
"tasks_running": 2,
"is_paused": false
}| Field | Description |
|---|---|
session_id |
UUID generated at server startup |
started_at |
Server start time (RFC3339) |
duration_seconds |
Seconds elapsed since server start |
total_tokens |
Sum of input + output tokens for today |
input_tokens |
Input tokens consumed today |
output_tokens |
Output tokens generated today |
estimated_cost_usd |
Total cost for today's activity |
tasks_completed |
Count of tasks with completed status |
tasks_running |
Count of tasks with running status |
is_paused |
Always false (executor-level pause not exposed) |
Notes:
- Token counts aggregate from today's phase-level activity (UTC day boundary)
- Returns zeros for all numeric fields when no tasks exist (graceful empty state)
- Response time target: < 100ms for typical projects (< 100 tasks)
All DashboardService RPC requests accept project_id to target a specific project database. See Multi-Project Support.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/dashboard/stats |
Get dashboard statistics |
| GET | /api/stats/activity |
Get task activity data for heatmap |
| GET | /api/stats/per-day |
Get daily task counts for bar chart (?days=7) |
| GET | /api/stats/outcomes |
Get task outcome distribution for donut chart (?period=30d) |
| GET | /api/stats/top-initiatives |
Get most active initiatives (?limit=10&period=all) |
| GET | /api/stats/top-files |
Get most frequently modified files (?limit=N&period=30d) |
| GET | /api/stats/comparison |
Get period comparison stats (?period=7d) |
| RPC | GetCostReport |
Aggregated cost data from GlobalDB with filtering/grouping |
Dashboard stats response:
Query parameters:
period- Time period:24h,7d,30d,all(default:all)- When period is set, returns period comparison data
{
"running": 1,
"orphaned": 0,
"paused": 0,
"blocked": 2,
"completed": 15,
"failed": 1,
"today": 3,
"total": 19,
"tokens": 245000,
"cache_creation_input_tokens": 5000,
"cache_read_input_tokens": 120000,
"cost": 12.50,
"avg_task_time_seconds": 3420.5,
"success_rate": 93.75,
"period": "7d",
"previous_period": {
"completed": 12,
"tokens": 198000,
"cost": 9.80,
"avg_task_time_seconds": 3100.2,
"success_rate": 85.7
},
"changes": {
"completed": 25.0,
"tokens": 23.7,
"cost": 27.6
}
}| Field | Description |
|---|---|
running |
Tasks currently executing |
orphaned |
Running tasks with stale heartbeat (potential orphans) |
paused |
Tasks paused by user |
blocked |
Tasks waiting for gate approval |
completed |
Total completed tasks (or within period if specified) |
failed |
Tasks that failed |
today |
Tasks completed today |
total |
Total tasks across all statuses |
tokens |
Total input + output tokens |
cache_creation_input_tokens |
Tokens written to prompt cache (aggregated) |
cache_read_input_tokens |
Tokens served from prompt cache (aggregated) |
cost |
Estimated cost in USD |
avg_task_time_seconds |
Average task completion time (only present when period specified) |
success_rate |
Percentage of completed vs completed+failed (only present when period specified) |
period |
Applied time filter (only present when period specified) |
previous_period |
Stats from the equivalent previous period (for comparison) |
changes |
Percentage changes from previous period |
Activity data response (GET /api/stats/activity):
Query parameters:
weeks- Number of weeks to return (default: 16, min: 1, max: 52)
{
"start_date": "2025-09-23",
"end_date": "2026-01-16",
"data": [
{"date": "2025-09-23", "count": 5, "level": 2},
{"date": "2025-09-24", "count": 0, "level": 0}
],
"stats": {
"total_tasks": 247,
"current_streak": 12,
"longest_streak": 45,
"busiest_day": {"date": "2025-12-15", "count": 23}
}
}| Field | Description |
|---|---|
start_date |
First date in range (YYYY-MM-DD) |
end_date |
Last date in range (YYYY-MM-DD) |
data |
Array of weeks * 7 daily activity entries |
data[].date |
Date in YYYY-MM-DD format |
data[].count |
Number of tasks completed on this date |
data[].level |
Activity level 0-4 for heatmap coloring |
stats.total_tasks |
Total completed tasks in range |
stats.current_streak |
Consecutive days ending today/yesterday |
stats.longest_streak |
Longest consecutive days in range |
stats.busiest_day |
Day with most completions (null if none) |
Activity level thresholds:
| Level | Task Count | Description |
|---|---|---|
| 0 | 0 | No activity |
| 1 | 1-2 | Light activity |
| 2 | 3-5 | Moderate activity |
| 3 | 6-10 | High activity |
| 4 | 11+ | Very high activity |
Error responses:
- 400:
weeksparameter invalid (not a number, < 1, or > 52)
Returns daily task completion counts for bar chart visualization.
GET /api/stats/per-day
Query parameters:
days- Number of days to return (default: 7, min: 1, max: 30)
Response:
{
"days": 7,
"data": [
{"date": "2026-01-16", "day": "Thu", "count": 5},
{"date": "2026-01-17", "day": "Fri", "count": 3},
{"date": "2026-01-18", "day": "Sat", "count": 0}
],
"summary": {
"total": 15,
"average": 2.1,
"max": {"date": "2026-01-16", "count": 5}
}
}| Field | Description |
|---|---|
days |
Number of days in response |
data |
Array of exactly N daily entries |
data[].date |
Date in YYYY-MM-DD format |
data[].day |
Short day name (Mon, Tue, etc.) |
data[].count |
Number of tasks completed on this date |
summary.total |
Total completions in period |
summary.average |
Average completions per day |
summary.max |
Day with most completions |
Error responses:
- 400:
days must be a number between 1 and 30
Returns task outcome distribution for donut chart visualization.
GET /api/stats/outcomes
Query parameters:
period- Time filter:24h,7d,30d,all(default:all)
Response:
{
"period": "30d",
"outcomes": {
"completed": 45,
"with_retries": 8,
"failed": 3
},
"total": 56,
"success_rate": 94.6
}| Field | Description |
|---|---|
period |
Applied time filter |
outcomes.completed |
Tasks completed on first attempt |
outcomes.with_retries |
Tasks completed after retry attempts |
outcomes.failed |
Tasks that failed |
total |
Total tasks with a final outcome |
success_rate |
Percentage of successful completions |
Note: The period filter applies to task completion time. Only completed or failed tasks are included.
Error responses:
- 400:
period must be one of: 24h, 7d, 30d, all
Returns most active initiatives ranked by task count.
GET /api/stats/top-initiatives
Query parameters:
limit- Maximum initiatives to return (default: 10, min: 1, max: 25)period- Time filter:24h,7d,30d,all(default:all)
Response:
{
"period": "all",
"initiatives": [
{
"id": "INIT-001",
"title": "User Authentication",
"task_count": 12,
"completed_count": 10,
"completion_rate": 83.3,
"total_tokens": 450000,
"total_cost_usd": 18.50
}
]
}| Field | Description |
|---|---|
period |
Applied time filter |
initiatives[].id |
Initiative ID |
initiatives[].title |
Initiative title |
initiatives[].task_count |
Total tasks linked to this initiative |
initiatives[].completed_count |
Completed tasks (subject to period filter) |
initiatives[].completion_rate |
Percentage of tasks completed |
initiatives[].total_tokens |
Total token usage |
initiatives[].total_cost_usd |
Total cost in USD |
Note: When period filter is applied, only tasks completed within that period count toward completed_count and derived metrics. The task_count shows total linked tasks regardless of period.
Error responses:
- 400:
limit must be a number between 1 and 25 - 400:
period must be one of: 24h, 7d, 30d, all
Returns most frequently modified files across completed tasks, ranked by change count. Aggregates file modification statistics from git diffs of completed task branches.
GET /api/stats/top-files (Connect RPC: DashboardService.GetTopFiles)
Query parameters:
limit- Maximum number of files to return (default: 10, min: 1, max: 50)task_id- Optional filter to get files for a specific task only
Response:
{
"files": [
{
"path": "internal/api/dashboard_server.go",
"change_count": 3,
"additions": 450,
"deletions": 120
},
{
"path": "web/src/components/Board.tsx",
"change_count": 2,
"additions": 180,
"deletions": 45
}
]
}| Field | Description |
|---|---|
files |
Array of files sorted by change_count (descending) |
files[].path |
File path relative to project root |
files[].change_count |
Number of completed tasks that modified this file |
files[].additions |
Total lines added across all tasks |
files[].deletions |
Total lines deleted across all tasks |
How it works:
- Loads all completed tasks with branches (or single task if
task_idspecified) - For each task, gets git diff against
mainbranch via DiffService - Aggregates file statistics: counts modifications, sums additions/deletions
- Returns top N files sorted by change_count descending
Notes:
- Only completed tasks with branches are included
- Tasks without branches are skipped gracefully
- Git diff errors are logged and skipped (graceful degradation)
- Returns empty
filesarray (not error) when no data matches - Binary files are included with 0 additions/deletions
Returns comparison between current and previous period.
GET /api/stats/comparison
Query parameters:
period- Comparison period:7d,30d(default:7d)
Response:
{
"current": {
"tasks_completed": 15,
"total_tokens": 245000,
"total_cost_usd": 12.50,
"success_rate": 93.8
},
"previous": {
"tasks_completed": 12,
"total_tokens": 198000,
"total_cost_usd": 9.80,
"success_rate": 85.7
},
"changes": {
"tasks": 25.0,
"tokens": 23.7,
"cost": 27.6,
"success_rate": 9.5
}
}| Field | Description |
|---|---|
current |
Stats for the current period (last N days) |
previous |
Stats for the equivalent previous period (N to 2N days ago) |
current/previous.tasks_completed |
Number of completed tasks |
current/previous.total_tokens |
Total token usage |
current/previous.total_cost_usd |
Total cost in USD |
current/previous.success_rate |
Percentage of successful completions |
changes.tasks |
Percentage change in task count |
changes.tokens |
Percentage change in token usage |
changes.cost |
Percentage change in cost |
changes.success_rate |
Percentage change in success rate |
Note: Percentage changes are calculated as (current - previous) / previous * 100. When previous is 0, returns 100% if current > 0.
Error responses:
- 400:
period must be one of: 7d, 30d
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/cost/summary |
Get cost summary (`?period=day |
| GET | /api/cost/by-model |
Get costs grouped by model (?project_id=&since=) |
| GET | /api/cost/timeseries |
Get time-bucketed costs (`?granularity=day |
| GET | /api/cost/budget |
Get project budget status |
| PUT | /api/cost/budget |
Set project budget |
Cost summary response:
{
"period": "week",
"start": "2026-01-10",
"end": "2026-01-17",
"total_cost_usd": 28.50,
"total_input_tokens": 890000,
"total_output_tokens": 310000,
"total_tokens": 1200000,
"entry_count": 45,
"by_project": {"project-abc": 20.00, "project-xyz": 8.50},
"by_phase": {"implement": 18.00, "spec": 6.50, "test": 4.00}
}Costs by model response:
{
"opus": 24.00,
"sonnet": 4.20,
"haiku": 0.30
}Cost timeseries response:
{
"granularity": "day",
"data": [
{
"project_id": "project-abc",
"model": "opus",
"phase": "",
"date": "2026-01-15",
"total_cost_usd": 8.50,
"total_input_tokens": 280000,
"total_output_tokens": 95000,
"total_cache_tokens": 120000,
"turn_count": 15,
"task_count": 3
}
]
}Budget status response:
{
"project_id": "project-abc",
"monthly_limit_usd": 100.00,
"current_month_spent": 82.50,
"current_month": "2026-01",
"percent_used": 82.5,
"alert_threshold": 80,
"over_budget": false,
"at_alert_threshold": true
}Set budget body:
{
"project_id": "project-abc",
"monthly_limit_usd": 100.00,
"alert_threshold_percent": 80
}Note: The database layer for cost tracking with model identification is implemented (TASK-406). API endpoint handlers are pending future work.
Aggregated cost data from GlobalDB with filtering and grouping. Used by both the CLI (orc costs) and the web UI (DashboardCostSummary component).
Connect RPC: DashboardService.GetCostReport (proto/orc/v1/dashboard.proto)
Request fields:
| Field | Type | Description |
|---|---|---|
project_id |
string | Filter to specific project |
user_id |
string (optional) | Filter to specific user ID |
since |
Timestamp (optional) | Filter entries after this time |
group_by |
string (optional) | Group dimension: user, project, model |
Response:
{
"total_cost_usd": 125.50,
"breakdowns": [
{"key": "opus", "cost_usd": 100.00},
{"key": "sonnet", "cost_usd": 25.50}
],
"budget_limit_usd": 200.00,
"budget_percent_used": 62.75
}| Field | Type | Description |
|---|---|---|
total_cost_usd |
double | Total cost matching filters |
breakdowns |
array | Cost entries grouped by group_by dimension |
breakdowns[].key |
string | Group key (model name, project ID, or user ID) |
breakdowns[].cost_usd |
double | Total cost for this group |
budget_limit_usd |
double (optional) | Monthly budget limit (only when project_id is set) |
budget_percent_used |
double (optional) | Budget usage percentage (only when project_id is set) |
Notes:
- Requires
SetGlobalDB()to be called on the dashboard server at startup (wired inserver_connect.go) - Budget info is only included when filtering by a specific
project_id - When
group_byisuser, keys are user IDs (resolve to names viaGetUser())
Connect RPC service for user notifications. All requests accept project_id. See Multi-Project Support.
Connect RPC: NotificationService (proto/orc/v1/notification.proto)
| RPC Method | Description |
|---|---|
| ListNotifications | List all notifications for a project |
| DismissNotification | Dismiss a single notification by ID |
| DismissAllNotifications | Dismiss all notifications for a project |
Notification object:
| Field | Type | Description |
|---|---|---|
id |
string | Notification ID |
type |
string | Notification type |
title |
string | Notification title |
message |
string (optional) | Notification body |
source_type |
string (optional) | Source type (e.g., task, initiative) |
source_id |
string (optional) | Source entity ID |
created_at |
timestamp | When notification was created |
expires_at |
timestamp (optional) | When notification expires |
Attention dashboard API for operator-facing triage. Project-scoped calls return running work, queue state, pending recommendation count, and attention items backed by persisted attention signals. Cross-project calls return the aggregated attention inbox plus cross-project pending recommendation count.
Connect RPC Service: orc.v1.AttentionDashboardService
| Method | Description |
|---|---|
GetAttentionDashboardData |
Load the attention dashboard for one project or, when project_id is omitted, the cross-project attention inbox |
PerformAttentionAction |
Retry, skip, force, approve, reject, view, or resolve an attention item |
UpdateQueueOrganization |
Handle queue swimlane UI state and task reordering requests |
project_id controls the scope:
| Request | Behavior |
|---|---|
{"project_id":"proj-123"} |
Returns project-scoped running summary, attention items, queue summary, and pending recommendation count |
{} or {"project_id":""} with project cache configured |
Returns cross-project attention items and aggregated pending recommendation count. running_summary and queue_summary are empty in this mode. |
Attention items generated from persisted signals include these extra fields:
| Field | Type | Description |
|---|---|---|
project_id |
string | Project that owns the signal |
signal_kind |
string | Signal kind such as blocker, decision_request, discussion_needed, or verification_summary |
reference_type |
string | Referenced entity type such as task, run, recommendation, or initiative |
reference_id |
string | Referenced entity ID inside the owning project |
Cross-project attention items use IDs in <project_id>::<item_id> format so they can be sent back to PerformAttentionAction without separately supplying project_id.
Project-scoped recommendation inbox API. Recommendations remain pending until a human accepts, rejects, or marks them discussed. Acceptance is the promotion path: cleanup, risk, and follow-up recommendations create backlog tasks, while decision requests create initiative decisions.
Connect RPC Service: orc.v1.RecommendationService
| Method | Description |
|---|---|
CreateRecommendation |
Create a recommendation record for a project |
GetRecommendation |
Load a single recommendation by ID |
ListRecommendations |
List recommendations with optional status, kind, and source task filters |
ListRecommendationHistory |
Load the audit trail for one recommendation |
AcceptRecommendation |
Accept a recommendation and promote it into a real backlog artifact |
RejectRecommendation |
Mark a recommendation rejected |
DiscussRecommendation |
Mark a recommendation discussed and return a context pack |
All request messages accept project_id. ListRecommendations also supports status, kind, and source_task_id filters. ListRecommendationHistory requires recommendation_id. CreateRecommendation returns AlreadyExists when dedupe_key matches either an existing recommendation or an indexed accepted or promoted artifact in the same project.
| Field | Type | Description |
|---|---|---|
id |
string | Recommendation ID |
kind |
enum | CLEANUP, RISK, FOLLOW_UP, or DECISION_REQUEST |
status |
enum | PENDING, ACCEPTED, REJECTED, or DISCUSSED |
title |
string | Short operator-facing title |
summary |
string | Why this recommendation exists |
proposed_action |
string | Suggested next action |
evidence |
string | Supporting evidence from execution or review |
source_task_id |
string | Task that produced the recommendation |
source_run_id |
string | Workflow run that produced the recommendation |
source_thread_id |
string | Thread tied to the recommendation |
dedupe_key |
string | Project-scoped dedupe key, enforced across live recommendations and indexed accepted or promoted artifacts |
decided_by |
string (optional) | Actor who made the decision |
decided_at |
timestamp | When the decision was recorded |
decision_reason |
string (optional) | Human rationale for the decision |
created_at |
timestamp | When the recommendation was created |
updated_at |
timestamp | Last update timestamp |
promoted_to_type |
string | Target entity type if promoted later |
promoted_to_id |
string | Target entity ID if promoted later |
promoted_by |
string | Actor who promoted it |
promoted_at |
timestamp | When promotion happened |
| Field | Type | Description |
|---|---|---|
id |
int64 | Monotonic history row ID |
recommendation_id |
string | Recommendation the entry belongs to |
from_status |
enum | Previous status, or UNSPECIFIED for the initial record |
to_status |
enum | New status recorded by this history entry |
decided_by |
string (optional) | Actor who made the transition |
decision_reason |
string (optional) | Rationale captured for the transition |
created_at |
timestamp | When the transition was recorded |
Accepting a recommendation records the decision and creates the promoted artifact in the same operation.
CLEANUP,RISK, andFOLLOW_UPrecommendations promote to new backlog tasks.DECISION_REQUESTrecommendations promote to initiative decisions tied to the source initiative or thread.- Repeating
AcceptRecommendationfor an already accepted recommendation is idempotent and returns the existing promoted artifact metadata.
Returns newest-first RecommendationHistoryEntry records for a single recommendation. The initial pending record is included so clients can render a full audit trail without reconstructing the first state locally.
Returns the updated recommendation plus a context_pack string for handoff or follow-up discussion.
Real-time user feedback to agents during task execution. Supports timing options for when feedback is delivered.
Connect RPC Service: orc.v1.FeedbackService
| Method | Description |
|---|---|
AddFeedback |
Add feedback for a task |
ListFeedback |
List feedback for a task |
SendFeedback |
Mark all pending feedback as received |
DeleteFeedback |
Delete specific feedback |
| Type | Description |
|---|---|
FEEDBACK_TYPE_GENERAL |
General comment or feedback |
FEEDBACK_TYPE_INLINE |
Code comment (requires file + line) |
FEEDBACK_TYPE_APPROVAL |
Approval or acknowledgement |
FEEDBACK_TYPE_DIRECTION |
Direction change request |
| Timing | Behavior |
|---|---|
FEEDBACK_TIMING_NOW |
Send immediately, pauses running task |
FEEDBACK_TIMING_WHEN_DONE |
Queue until current phase completes |
FEEDBACK_TIMING_MANUAL |
Save for later manual send |
rpc AddFeedback(AddFeedbackRequest) returns (AddFeedbackResponse)Request:
{
"project_id": "abc123",
"task_id": "TASK-001",
"type": "FEEDBACK_TYPE_GENERAL",
"text": "Consider using a map instead of a slice",
"timing": "FEEDBACK_TIMING_WHEN_DONE"
}Inline feedback (code comment):
{
"project_id": "abc123",
"task_id": "TASK-001",
"type": "FEEDBACK_TYPE_INLINE",
"text": "This function should handle the nil case",
"timing": "FEEDBACK_TIMING_WHEN_DONE",
"file": "internal/api/server.go",
"line": 42
}Response:
{
"feedback": {
"id": "FB-a1b2c3d4",
"task_id": "TASK-001",
"type": "FEEDBACK_TYPE_GENERAL",
"text": "Consider using a map instead of a slice",
"timing": "FEEDBACK_TIMING_WHEN_DONE",
"received": false,
"created_at": "2026-01-15T10:30:00Z"
}
}rpc ListFeedback(ListFeedbackRequest) returns (ListFeedbackResponse)Request:
{
"project_id": "abc123",
"task_id": "TASK-001",
"exclude_received": true
}Response:
{
"feedback": [
{
"id": "FB-a1b2c3d4",
"task_id": "TASK-001",
"type": "FEEDBACK_TYPE_GENERAL",
"text": "Consider using a map instead of a slice",
"timing": "FEEDBACK_TIMING_WHEN_DONE",
"received": false,
"created_at": "2026-01-15T10:30:00Z"
}
]
}Marks all pending feedback for a task as received. Returns count of feedback items sent.
rpc SendFeedback(SendFeedbackRequest) returns (SendFeedbackResponse)Request:
{
"project_id": "abc123",
"task_id": "TASK-001"
}Response:
{
"sent_count": 3
}rpc DeleteFeedback(DeleteFeedbackRequest) returns (DeleteFeedbackResponse)Request:
{
"project_id": "abc123",
"task_id": "TASK-001",
"feedback_id": "FB-a1b2c3d4"
}Query historical events for timeline display. Events are persisted via the event publisher and can be filtered by task, initiative, time range, and event types.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/events |
List events with optional filters |
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
task_id |
string | - | Filter events by task ID |
initiative_id |
string | - | Filter events by initiative (joins with tasks table) |
since |
string | - | Start of time range (ISO8601/RFC3339) |
until |
string | - | End of time range (ISO8601/RFC3339) |
types |
string | - | Comma-separated event types to include |
limit |
number | 100 | Max events to return (1-1000) |
offset |
number | 0 | Offset for pagination |
Response:
{
"events": [
{
"id": 123,
"task_id": "TASK-001",
"task_title": "Implement login flow",
"phase": "implement",
"iteration": 2,
"event_type": "phase_completed",
"data": {"duration_ms": 45000},
"source": "executor",
"created_at": "2026-01-10T10:35:00Z"
}
],
"total": 150,
"limit": 100,
"offset": 0,
"has_more": true
}| Field | Description |
|---|---|
events |
Array of events (empty array if no matches) |
events[].id |
Unique event ID |
events[].task_id |
Associated task ID |
events[].task_title |
Task title (from tasks table join) |
events[].phase |
Phase name (optional) |
events[].iteration |
Phase iteration (optional) |
events[].event_type |
Event type (e.g., phase_completed, task_started) |
events[].data |
Event-specific data (optional) |
events[].source |
Event source (e.g., executor, api, cli) |
events[].created_at |
Event timestamp (ISO8601) |
total |
Total matching events (for pagination) |
limit |
Applied limit |
offset |
Applied offset |
has_more |
True if more events exist beyond current page |
Error responses:
| Status | Condition |
|---|---|
| 400 | Invalid since/until timestamp format |
| 400 | limit < 1 or > 1000 |
| 400 | offset < 0 |
| 500 | Database error |
Notes:
- Invalid
task_idorinitiative_idreturns empty results (not an error) initiative_idfilter works by joining with the tasks table- Events are sorted by
created_atdescending (newest first)
Project knowledge queue (patterns, gotchas, decisions).
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/knowledge |
List entries (?status=pending, ?type=pattern) |
| GET | /api/knowledge/status |
Get queue statistics |
| GET | /api/knowledge/stale |
List stale entries (?days=90) |
| POST | /api/knowledge |
Create knowledge entry |
| GET | /api/knowledge/:id |
Get entry |
| POST | /api/knowledge/:id/approve |
Approve entry |
| POST | /api/knowledge/:id/reject |
Reject entry |
| POST | /api/knowledge/:id/validate |
Validate (reset staleness) |
| DELETE | /api/knowledge/:id |
Delete entry |
| POST | /api/knowledge/approve-all |
Approve all pending |
Configurable workflow definitions with composable phases.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/workflows |
List all workflows |
| POST | /api/workflows |
Create workflow |
| GET | /api/workflows/:id |
Get workflow with phases and variables |
| PUT | /api/workflows/:id |
Update workflow |
| DELETE | /api/workflows/:id |
Delete workflow (custom only) |
| POST | /api/workflows/:id/clone |
Clone workflow |
| POST | /api/workflows/:id/phases |
Add phase to workflow |
| PATCH | /api/workflows/:id/phases/:phaseId |
Update phase (sequence, dependencies, overrides) |
| DELETE | /api/workflows/:id/phases/:phaseId |
Remove phase from workflow |
| PUT | /api/workflows/:id/layout |
Save node positions for visual editor |
| POST | /api/workflows/:id/validate |
Validate workflow structure (check for cycles) |
| POST | /api/workflows/:id/variables |
Add variable to workflow |
| DELETE | /api/workflows/:id/variables/:name |
Remove variable from workflow |
| POST | WorkflowService/AddBeforePhaseTrigger |
Add before-phase trigger to a phase |
| POST | WorkflowService/UpdateBeforePhaseTrigger |
Update before-phase trigger at index |
| POST | WorkflowService/RemoveBeforePhaseTrigger |
Remove before-phase trigger at index |
| POST | WorkflowService/AddLifecycleTrigger |
Add lifecycle trigger to a workflow |
| POST | WorkflowService/UpdateLifecycleTrigger |
Update lifecycle trigger at index |
| POST | WorkflowService/RemoveLifecycleTrigger |
Remove lifecycle trigger at index |
Create workflow body:
{
"id": "my-custom-workflow",
"name": "My Custom Workflow",
"description": "Custom workflow for specific task type",
"default_model": "sonnet",
"default_thinking": false,
"completion_action": "pr"
}| Field | Values | Default |
|---|---|---|
id |
Unique identifier (lowercase, hyphens) | Required |
name |
Display name | Required |
default_model |
sonnet, opus, haiku |
(inherit) |
default_thinking |
true, false |
false |
completion_action |
pr, merge, commit, none, "" |
"" (inherit from config) |
Clone workflow body:
{
"new_id": "my-cloned-workflow",
"new_name": "My Cloned Workflow"
}Update a phase within a workflow. Used by the visual editor for connection management.
PATCH /api/workflows/:id/phases/:phaseId
Request body:
{
"sequence": 3,
"depends_on": ["spec", "tdd_write"],
"max_iterations_override": 50,
"model_override": "opus",
"thinking_override": true,
"gate_type_override": "human",
"condition": "{{HAS_TESTS}}",
"agent_override": "custom-agent",
"sub_agents_override": ["reviewer", "tester"],
"runtime_config_override": "{\"shared\":{\"allowed_tools\":[\"Bash\"]},\"providers\":{\"claude\":{\"hooks\":{\"PreToolUse\":[{\"hooks\":[{\"type\":\"command\",\"command\":\"bash .claude/hooks/pre-commit\"}]}]}}}}"
}All fields are optional. Only provided fields are updated.
| Field | Description |
|---|---|
sequence |
Execution order (lower runs first) |
depends_on |
Phase template IDs that must complete before this phase |
max_iterations_override |
Override max iterations from template |
model_override |
Override model (sonnet, opus, haiku) |
thinking_override |
Override extended thinking setting |
gate_type_override |
Override gate type (auto, human, skip) |
condition |
Conditional execution expression |
agent_override |
Override executor agent for this phase |
sub_agents_override |
Override available sub-agents (array of agent IDs) |
runtime_config_override |
JSON string overriding runtime_config. Shared settings live under shared; provider-native settings live under providers.<provider>. Merged with template config at execution time |
Response: Returns the updated WorkflowPhase object.
Bulk-save node positions for the visual workflow editor. Positions are stored per-phase and restored on reload.
PUT /api/workflows/:id/layout
Request body:
{
"positions": [
{"phase_template_id": "spec", "position_x": 100.0, "position_y": 200.0},
{"phase_template_id": "implement", "position_x": 300.0, "position_y": 200.0}
]
}| Field | Description |
|---|---|
positions |
Array of phase positions |
positions[].phase_template_id |
Phase template ID |
positions[].position_x |
X coordinate in canvas |
positions[].position_y |
Y coordinate in canvas |
Response:
{"success": true}Notes:
- Empty
positionsarray clears all stored positions (triggers auto-layout on reload) - Positions for phases not in the workflow are ignored
- Built-in workflows can also store positions (persisted per-user)
Check workflow structure for cycles, invalid references, and other issues.
POST /api/workflows/:id/validate
No request body required.
Response:
{
"valid": true,
"issues": []
}Response with issues:
{
"valid": false,
"issues": [
{
"severity": "error",
"message": "Dependency cycle detected: implement -> review -> implement",
"phase_ids": ["implement", "review"]
}
]
}| Field | Description |
|---|---|
valid |
true if workflow has no errors |
issues |
Array of validation issues |
issues[].severity |
error or warning |
issues[].message |
Human-readable description |
issues[].phase_ids |
Affected phases (for highlighting in UI) |
Validated conditions:
- No dependency cycles
- All
depends_onreferences point to valid phases in the workflow - No duplicate phase template IDs
Reusable phase definitions with prompts and configuration.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/phase-templates |
List all phase templates |
| POST | /api/phase-templates |
Create phase template |
| GET | /api/phase-templates/:id |
Get phase template |
| PUT | /api/phase-templates/:id |
Update phase template |
| DELETE | /api/phase-templates/:id |
Delete phase template (custom only) |
| GET | /api/phase-templates/:id/prompt |
Get phase template prompt content |
Phase template response:
{
"id": "implement",
"name": "Implementation",
"description": "Write code guided by breakdown",
"prompt_source": "embedded",
"max_iterations": 50,
"gate_type": "auto",
"produces_artifact": false,
"is_builtin": true
}| Field | Description |
|---|---|
prompt_source |
embedded (bundled), db (stored in DB), file (external file) |
gate_type |
auto (AI-approved), human (requires manual approval) |
produces_artifact |
Whether phase generates artifact (spec, tests, etc.) |
artifact_type |
Artifact type: spec, tests, breakdown, docs, etc. |
is_builtin |
Built-in templates cannot be modified |
Create phase template body (POST):
{
"id": "code-analysis",
"name": "Code Analysis",
"description": "Analyze codebase structure",
"prompt_source": "PROMPT_SOURCE_DB",
"prompt_content": "Analyze the codebase at {{PROJECT_ROOT}}...",
"gate_type": "GATE_TYPE_AUTO",
"max_iterations": 20,
"thinking_enabled": true,
"output_var_name": "ANALYSIS_RESULT",
"runtime_config": "{\"providers\":{\"claude\":{\"hooks\":{\"PreToolUse\":[{\"hooks\":[{\"type\":\"command\",\"command\":\"bash .claude/hooks/pre-commit-check\"}]}]},\"skill_refs\":[\"code-review\"]}}}"
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id |
string | Yes | - | Unique template ID (slugified, e.g., code-analysis) |
name |
string | Yes | - | Display name |
description |
string | No | "" |
Template description |
prompt_source |
enum | No | PROMPT_SOURCE_DB |
PROMPT_SOURCE_DB (inline), PROMPT_SOURCE_FILE (external) |
prompt_content |
string | No | - | Inline prompt content (when prompt_source=DB) |
prompt_path |
string | No | - | External prompt file path (when prompt_source=FILE) |
gate_type |
enum | No | GATE_TYPE_AUTO |
GATE_TYPE_AUTO, GATE_TYPE_HUMAN, GATE_TYPE_AI, GATE_TYPE_SKIP |
max_iterations |
int | No | 20 |
Maximum Claude turns |
thinking_enabled |
bool | No | - | Enable extended thinking |
checkpoint |
bool | No | false |
Pause after phase completion |
produces_artifact |
bool | No | false |
Whether phase produces artifact |
artifact_type |
string | No | - | Artifact type if produces_artifact=true |
output_var_name |
string | No | - | Variable name for phase output (e.g., SPEC_CONTENT) |
agent_id |
string | No | - | Executor agent reference |
sub_agent_ids |
string[] | No | [] |
Sub-agent references |
runtime_config |
string | No | - | JSON string with provider-neutral settings under shared and provider-native settings under providers.<provider> |
Error responses:
| Status | Code | Condition |
|---|---|---|
| 400 | InvalidArgument |
Missing id or name |
| 409 | AlreadyExists |
Phase template with ID already exists |
CRUD operations for before-phase triggers on workflow phases. Triggers are stored as a JSON array on workflow_phases.before_triggers. All operations are Connect RPC (not REST).
Implementation: internal/api/workflow_server_before_trigger.go
WorkflowService/AddBeforePhaseTrigger
{
"workflow_id": "my-workflow",
"phase_id": 5,
"agent_id": "dependency-validator",
"mode": "gate",
"input_config": "{\"variables\":[\"TASK_ID\"]}",
"output_config": "{\"on_approved\":\"continue\",\"on_rejected\":\"fail\"}"
}| Field | Required | Default | Description |
|---|---|---|---|
workflow_id |
Yes | - | Target workflow (must not be built-in) |
phase_id |
Yes | - | Database ID of the workflow phase |
agent_id |
Yes | - | Agent to execute (must exist) |
mode |
No | "gate" |
"gate" (blocks phase) or "reaction" (fire-and-forget) |
input_config |
No | - | JSON GateInputConfig string |
output_config |
No | - | JSON GateOutputConfig string |
Response: { "phase": <WorkflowPhase> } — updated phase with appended trigger.
WorkflowService/UpdateBeforePhaseTrigger
Partial update — only provided fields are modified.
{
"workflow_id": "my-workflow",
"phase_id": 5,
"trigger_index": 0,
"agent_id": "new-agent",
"mode": "reaction"
}| Field | Required | Description |
|---|---|---|
workflow_id |
Yes | Target workflow |
phase_id |
Yes | Database ID of the workflow phase |
trigger_index |
Yes | Zero-based index into triggers array |
agent_id |
No | New agent (must exist if provided) |
mode |
No | New mode ("gate" or "reaction") |
input_config |
No | New JSON GateInputConfig |
output_config |
No | New JSON GateOutputConfig |
Response: { "phase": <WorkflowPhase> } — updated phase.
WorkflowService/RemoveBeforePhaseTrigger
{
"workflow_id": "my-workflow",
"phase_id": 5,
"trigger_index": 0
}Response: { "phase": <WorkflowPhase> } — updated phase with trigger removed.
| Code | Condition |
|---|---|
InvalidArgument |
Missing required field, invalid mode, malformed JSON config, index out of range |
NotFound |
Workflow, phase, or agent not found |
PermissionDenied |
Attempting to modify built-in workflow |
Internal |
Database or JSON marshal error |
CRUD operations for workflow-level lifecycle triggers. Triggers are stored as a JSON array in the workflows.triggers column. All operations are Connect RPC (not REST).
Implementation: internal/api/workflow_server_lifecycle_trigger.go
WorkflowService/AddLifecycleTrigger
{
"workflow_id": "my-workflow",
"event": "on_task_completed",
"agent_id": "notify-slack",
"mode": "reaction",
"enabled": true,
"input_config": "{\"include_task\":true}",
"output_config": "{\"on_approved\":\"continue\"}"
}| Field | Required | Default | Description |
|---|---|---|---|
workflow_id |
Yes | - | Target workflow (must not be built-in) |
event |
Yes | - | on_task_created, on_task_completed, on_task_failed, on_initiative_planned |
agent_id |
Yes | - | Agent to execute (must exist) |
mode |
No | "gate" |
"gate" (blocks) or "reaction" (fire-and-forget) |
enabled |
No | false |
Whether trigger is active |
input_config |
No | - | JSON GateInputConfig string |
output_config |
No | - | JSON GateOutputConfig string |
Response: { "workflow": <Workflow> } — updated workflow with appended trigger.
WorkflowService/UpdateLifecycleTrigger
Partial update — only provided fields are modified.
{
"workflow_id": "my-workflow",
"trigger_index": 0,
"enabled": true,
"mode": "gate"
}| Field | Required | Description |
|---|---|---|
workflow_id |
Yes | Target workflow |
trigger_index |
Yes | Zero-based index into triggers array |
event |
No | New event (must be valid if provided) |
agent_id |
No | New agent (must exist if provided) |
mode |
No | New mode ("gate" or "reaction") |
enabled |
No | Enable/disable trigger |
input_config |
No | New JSON GateInputConfig |
output_config |
No | New JSON GateOutputConfig |
Response: { "workflow": <Workflow> } — updated workflow.
WorkflowService/RemoveLifecycleTrigger
{
"workflow_id": "my-workflow",
"trigger_index": 0
}Response: { "workflow": <Workflow> } — updated workflow with trigger removed.
| Code | Condition |
|---|---|
InvalidArgument |
Missing required field, invalid event/mode, malformed JSON config, index out of range |
NotFound |
Workflow or agent not found |
PermissionDenied |
Attempting to modify built-in workflow |
Internal |
Database or JSON marshal error |
Execution instances of workflows.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/workflow-runs |
List workflow runs (?status=running, ?workflow_id=implement) |
| POST | /api/workflow-runs |
Trigger new workflow run |
| GET | /api/workflow-runs/:id |
Get workflow run details |
| POST | /api/workflow-runs/:id/cancel |
Cancel running workflow |
| GET | /api/workflow-runs/:id/transcript |
Get workflow run transcript |
Trigger workflow run body:
{
"workflow_id": "implement",
"prompt": "Add user authentication",
"instructions": "Use JWT tokens",
"context_type": "task",
"task_id": "TASK-001"
}| Field | Description |
|---|---|
workflow_id |
Which workflow to execute |
prompt |
Task description (becomes TASK_DESCRIPTION) |
instructions |
Additional guidance (optional) |
context_type |
task, branch, pr, standalone |
task_id |
Link to existing task (optional) |
Workflow run response:
{
"id": "WRUN-001",
"workflow_id": "implement",
"status": "running",
"current_phase": "spec",
"context_type": "task",
"task_id": "TASK-001",
"total_cost_usd": 0.45,
"total_tokens": 12500,
"started_at": "2026-01-22T10:30:00Z"
}| Status | Description |
|---|---|
pending |
Queued, not yet started |
running |
Currently executing phases |
completed |
All phases finished successfully |
failed |
Execution failed |
cancelled |
Cancelled by user |
Connect to /api/ws for real-time updates.
{"type": "subscribe", "task_id": "TASK-001"}
{"type": "unsubscribe"}
{"type": "command", "task_id": "TASK-001", "action": "pause"}
{"type": "ping"}{"type": "subscribed", "task_id": "TASK-001"}
{"type": "event", "event_type": "state", "data": {...}}
{"type": "event", "event_type": "transcript", "data": {...}}
{"type": "event", "event_type": "phase", "data": {...}}
{"type": "pong"}| Event | Data | Purpose |
|---|---|---|
state |
TaskState |
Full task state update |
phase |
{phase, status} |
Phase started/completed/failed |
transcript |
TranscriptEvent |
Streaming conversation (see below) |
tokens |
TokenUpdate |
Token usage (includes cached tokens) |
complete |
{status, duration} |
Task finished |
error |
{message, fatal} |
Error occurred |
activity |
ActivityUpdate |
Activity state changed (see below) |
heartbeat |
HeartbeatData |
Progress heartbeat during API wait |
warning |
{phase, message} |
Non-fatal warning |
finalize |
FinalizeUpdate |
Finalize phase progress (see below) |
task_created |
{task: Task} |
Task created via CLI/filesystem |
task_updated |
{task: Task} |
Task modified via CLI/filesystem |
task_deleted |
{task_id: string} |
Task deleted via CLI/filesystem |
initiative_created |
{initiative: Initiative} |
Initiative created via CLI/filesystem |
initiative_updated |
{initiative: Initiative} |
Initiative modified via CLI/filesystem |
initiative_deleted |
{initiative_id: string} |
Initiative deleted via CLI/filesystem |
session_update |
SessionUpdate |
Session-level metrics update (see below) |
files_changed |
FilesChangedUpdate |
Files modified during task execution (see below) |
decision_required |
DecisionRequiredData |
Human gate requires approval (see below) |
decision_resolved |
DecisionResolvedData |
Gate decision was resolved (see below) |
attention_signal_created |
AttentionSignalCreatedData |
Persisted attention signal created or updated |
attention_signal_resolved |
AttentionSignalResolvedData |
Persisted attention signal resolved |
Decision events enable real-time gate approval in headless mode. When a task hits a human gate during API execution, a decision_required event is broadcast. When resolved via POST /api/decisions/:id, a decision_resolved event is broadcast.
decision_required:
{
"decision_id": "gate_TASK-001_review",
"task_id": "TASK-001",
"task_title": "Add user authentication",
"phase": "review",
"gate_type": "human",
"question": "Please verify the following criteria:",
"context": "Code review passes\nTests pass",
"requested_at": "2026-01-10T10:30:00Z"
}| Field | Description |
|---|---|
decision_id |
Unique ID for this decision (format: gate_{task_id}_{phase}) |
task_id |
Associated task ID |
task_title |
Task title for display |
phase |
Phase awaiting approval |
gate_type |
Always "human" for these events |
question |
Prompt to show the user |
context |
Gate criteria (newline-separated) |
requested_at |
When the decision was requested (ISO8601) |
decision_resolved:
{
"decision_id": "gate_TASK-001_review",
"task_id": "TASK-001",
"phase": "review",
"approved": true,
"reason": "LGTM",
"resolved_by": "api",
"resolved_at": "2026-01-10T10:35:00Z"
}| Field | Description |
|---|---|
decision_id |
The resolved decision ID |
task_id |
Associated task ID |
phase |
Phase that was approved/rejected |
approved |
Whether the decision was approved |
reason |
Optional reason provided during resolution |
resolved_by |
Resolution source: "api" or "cli" |
resolved_at |
When the decision was resolved (ISO8601) |
Workflow:
- Task hits human gate during API execution
- Task status changes to
blocked decision_requiredevent is broadcast to WebSocket subscribers- UI displays approval prompt
- User clicks approve/reject, frontend calls
POST /api/decisions/:id - Task status changes to
planned(approved) orfailed(rejected) decision_resolvedevent is broadcast- User explicitly resumes task via
POST /api/tasks/:id/resume
Session update events provide aggregate metrics across all tasks. They use task_id: "*" (GlobalTaskID) so all WebSocket subscribers receive them. A session_update is automatically sent when:
- A client subscribes to all tasks (
{"type": "subscribe", "task_id": "*"}) - Session metrics change (task starts/completes, tokens accumulate, etc.)
session_update:
{
"duration_seconds": 3420,
"total_tokens": 245000,
"estimated_cost_usd": 12.50,
"input_tokens": 180000,
"output_tokens": 65000,
"tasks_running": 2,
"is_paused": false
}| Field | Description |
|---|---|
duration_seconds |
Session uptime in seconds |
total_tokens |
Aggregate input + output tokens |
estimated_cost_usd |
Estimated cost in USD |
input_tokens |
Total input tokens |
output_tokens |
Total output tokens |
tasks_running |
Number of currently executing tasks |
is_paused |
Whether the session is globally paused |
{
"task_id": "TASK-001",
"status": "running",
"step": "Syncing with target",
"progress": "Merging changes",
"step_percent": 50,
"updated_at": "2026-01-10T10:31:00Z"
}| Field | Description |
|---|---|
status |
Current status: pending, running, completed, failed |
step |
Current step name (e.g., "Syncing with target", "Running tests") |
progress |
Human-readable progress message |
step_percent |
Progress percentage (0-100) |
error |
Error message (only present on failure) |
result |
Finalize result (only present on completion) |
Activity events provide real-time feedback about executor activity during phase execution.
{
"phase": "spec",
"activity": "spec_analyzing"
}| Field | Description |
|---|---|
phase |
Current phase name |
activity |
Activity state (see below) |
Activity states:
| State | Description | CLI Display |
|---|---|---|
idle |
No activity | - |
waiting_api |
Waiting for Claude API response | "Waiting for Claude API..." |
streaming |
Actively receiving streaming response | - |
running_tool |
Claude is running a tool | "Running tool..." |
processing |
Processing response | - |
spec_analyzing |
Analyzing codebase during spec phase | "Analyzing codebase..." |
spec_writing |
Writing specification document | "Writing specification..." |
Spec phase progress:
The spec_analyzing and spec_writing states are specific to the spec phase and provide granular feedback during long-running specification generation. The spec phase has two distinct stages:
- Analyzing (
spec_analyzing): Claude reads the codebase, researches patterns, identifies affected files - Writing (
spec_writing): Claude writes the specification document
Web UI handling:
TaskCard shows the activity state as subtext under the phase name for running tasks. The ACTIVITY_CONFIG map in web/src/lib/types.ts provides display labels.
Files changed events report real-time file modifications during task execution. Emitted every 10 seconds when files change.
{
"files": [
{"path": "internal/api/task_server.go", "status": "modified", "additions": 45, "deletions": 12},
{"path": "internal/api/task_server_test.go", "status": "added", "additions": 120, "deletions": 0}
],
"total_additions": 165,
"total_deletions": 12,
"timestamp": "2026-01-10T10:31:00Z"
}| Field | Description |
|---|---|
files |
Array of changed files with per-file stats |
files[].path |
Relative file path from worktree root |
files[].status |
Change type: added, modified, deleted, renamed |
files[].additions |
Lines added in this file |
files[].deletions |
Lines removed from this file |
total_additions |
Sum of additions across all files |
total_deletions |
Sum of deletions across all files |
timestamp |
When this snapshot was taken |
Behavior:
- Polls git diff every 10 seconds during task execution
- Only committed/staged changes are reported (not untracked files)
- Compares HEAD against the target branch merge-base
- Deduplicates: only emits when file state actually changes
- Useful for progress indicators showing lines changed during implementation
Heartbeat events indicate continued activity during long-running API calls.
{
"phase": "spec",
"iteration": 1,
"timestamp": "2026-01-10T10:31:00Z"
}Heartbeats are emitted every 30 seconds during API wait states. The CLI displays these as dots to show continued progress.
The transcript event supports real-time streaming of Claude's output:
// Streaming chunk (sent as response generates)
{
"type": "chunk",
"content": "partial response text",
"phase": "implement",
"iteration": 1
}
// Complete response (sent when response finishes)
{
"type": "response",
"content": "full response text",
"phase": "implement",
"iteration": 1
}Client handling:
chunkevents append to streaming buffer; reset buffer when phase/iteration changesresponseevents signal completion; reload transcript files from API- Use
getTranscripts(taskId)orgetProjectTranscripts(projectId, taskId)to fetch saved transcripts
{
"input_tokens": 1500,
"output_tokens": 500,
"cache_creation_input_tokens": 200,
"cache_read_input_tokens": 12000,
"total_tokens": 14200
}| Field | Description |
|---|---|
input_tokens |
Uncached input tokens |
output_tokens |
Generated output tokens |
cache_creation_input_tokens |
Tokens written to prompt cache (optional) |
cache_read_input_tokens |
Tokens served from prompt cache (optional) |
total_tokens |
Sum of all token types |
Tokens are incremental (add to existing totals, don't replace).
Subscribe to "*" to receive database events for all tasks:
{"type": "subscribe", "task_id": "*"}Database events (task_created, task_updated, task_deleted, initiative_created, initiative_updated, initiative_deleted) are only broadcast to global subscribers. These events are triggered when tasks or initiatives are created, modified, or deleted via CLI or API operations.
All errors return JSON:
{
"error": "error message",
"code": "ERROR_CODE",
"details": {}
}Common status codes:
400- Bad request (invalid parameters)404- Resource not found409- Conflict (e.g., task already running)500- Internal server error