The CLK REST API is a thin FastAPI wrapper around the CLK CLI. It lets you
start, monitor, and cancel research tasks (using the commands init, idea,
plan, run, loop, and status — see /api/capabilities for the
authoritative list); manage isolated workspaces; and stream live output from
running tasks — all over plain HTTP.
# 1. Install API dependencies
pip install "clk-harness[api]"
# 2. Start the server (default port 8001)
clk-api
# or via the module entry point
python -m clk_harness.api
# or via uvicorn directly
uvicorn clk_harness.api:app --host 0.0.0.0 --port 8001
# 3. Create a workspace
curl -s -X POST http://localhost:8001/api/workspaces \
-H 'Content-Type: application/json' \
-d '{"name": "my-project"}'
# → {"ok": true, "workspace_id": "<uuid>", "path": "/workspaces/<uuid>"}
# 4. Capture an idea and stream the output
WS=<workspace_id from step 3>
curl -s -X POST http://localhost:8001/api/research \
-H 'Content-Type: application/json' \
-d "{\"command\": \"idea\", \"args\": [\"A local-first journaling app\"], \"workspace_id\": \"$WS\"}"
# → {"ok": true, "task_id": "<uuid>", "workspace_id": "<uuid>"}
TASK=<task_id from above>
curl -sN http://localhost:8001/api/research/$TASK/stream
# Streams SSE events until the task finishes.| Variable | Default | Purpose |
|---|---|---|
CLK_WORKSPACES_DIR |
/workspaces |
Root directory under which workspaces are created. Mount a volume here so workspace directories persist across container restarts (note: the in-memory workspace registry is not persisted — see workspace notes below). |
CLK_API_PORT |
8001 |
TCP port the server binds to when launched as clk-api or python -m clk_harness.api. |
The API has no authentication. It relies on network-boundary trust. Do not expose it to the public internet without a reverse-proxy or firewall.
Most endpoints return JSON. Successful responses always include "ok": true;
error responses include "ok": false and an error object:
// success
{ "ok": true, ... }
// error
{
"ok": false,
"error": {
"code": "workspace_not_found",
"message": "Workspace 'abc' not found."
}
}Exceptions: GET /api/research/{task_id}/stream returns text/event-stream
(SSE — see the stream endpoint section for the event format) and
GET /api/research/{task_id}/artifacts/{path} returns the raw file content with
the file's natural MIME type.
Liveness check.
Response
{
"ok": true,
"version": "0.1.0",
"uptime_s": 42.7
}Return the list of CLK commands exposed by this API.
Response
{
"ok": true,
"modes": ["init", "idea", "plan", "run", "loop", "status"]
}Return the bundled workflow templates.
Response
{
"ok": true,
"workflows": [
{ "name": "engineering", "path": "engineering.yaml", "description": "..." },
{ "name": "discovery", "path": "discovery.yaml", "description": "..." }
]
}If the template package cannot be loaded the response will be HTTP 500 with
{"ok": false, "error": {"code": "template_load_failed", "message": "Failed to load workflow templates."}}.
Create a named workspace directory. Every call allocates a brand-new UUID
and a new directory on disk, even if the same name has been used before.
Important — workspace registry is in-memory only. The
WORKSPACESmapping lives entirely in the server process and is lost on every restart. Workspace directories on disk survive a restart, but they are not automatically re-registered — there is currently no "recover/import existing directory" mechanism.Practical consequences:
- After a server restart, previously-created workspace directories on disk are not addressable through the API. Any stored workspace UUID or task ID from a previous server session is no longer valid.
- Posting to
/api/workspacesagain (with any name) creates a completely new workspace with a new UUID — it does not reconnect to an existing directory.
Request body
{ "name": "my-project" }Response 201 Created
{
"ok": true,
"workspace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"path": "/workspaces/3fa85f64-5717-4562-b3fc-2c963f66afa6"
}List all workspaces known to this server instance.
Response
{
"ok": true,
"workspaces": [
{
"id": "3fa85f64-...",
"name": "my-project",
"path": "/workspaces/3fa85f64-...",
"created_at": "2024-01-15T12:00:00.000000Z"
}
]
}Note: The workspace registry is in-memory and resets when the server restarts. Workspace directories on disk survive a restart, but they are not re-registered automatically. See
POST /api/workspacesfor details.
Delete a workspace and all its files.
Returns 409 Conflict if any task using this workspace is currently pending
or running. Cancel all active tasks before deleting the workspace.
Response 200 OK
{ "ok": true }Start a CLK command as a background task.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
command |
string | yes | One of init, idea, plan, run, loop, status. |
args |
string[] |
no | Extra CLI arguments forwarded verbatim (e.g. ["A journaling app"]). |
workspace_id |
string | no | Existing workspace UUID. Omit to create an ephemeral workspace. |
workflow |
string | no | Convenience shortcut: injects --workflow <value> when command is run. Ignored if --workflow or --workflow=<value> is already in args. |
Example — capture an idea in an existing workspace
{
"command": "idea",
"args": ["A local-first journaling app that summarizes my week"],
"workspace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}Example — run the engineering workflow
{
"command": "run",
"workflow": "engineering",
"workspace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}Example — ephemeral workspace (auto-created)
{
"command": "init"
}Response 202 Accepted
{
"ok": true,
"task_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"workspace_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}CLK automatically runs
initbefore the requested command whenever the workspace has not yet been initialised (i.e..clk/is absent), regardless of whetherworkspace_idwas provided. The auto-initstep is skipped only when the requested command is alreadyinit.
Poll task status.
Response
{
"ok": true,
"task_id": "7c9e6679-...",
"workspace_id": "3fa85f64-...",
"command": "idea",
"status": "running",
"created_at": "2024-01-15T12:00:04.000000Z",
"started_at": "2024-01-15T12:00:05.123456Z",
"finished_at": null,
"exit_code": null,
"line_count": 42
}status values:
| Value | Meaning |
|---|---|
pending |
Task accepted, not yet started (started_at is null). |
running |
Subprocess is active. |
done |
Subprocess exited with code 0. |
failed |
Subprocess exited with non-zero code. |
cancelled |
Task was cancelled via POST /cancel. |
Timestamp fields:
| Field | Description |
|---|---|
created_at |
ISO-8601 UTC timestamp set when the task is accepted (always present). |
started_at |
ISO-8601 UTC timestamp set when the subprocess begins executing; null while status is pending. |
finished_at |
ISO-8601 UTC timestamp set when the task reaches a terminal state; null while pending or running. |
Server-Sent Events (SSE) stream of task output. Connect with
EventSource in a browser or curl -N from the shell.
Media type: text/event-stream
Events during execution:
data: {"line": "CLK initialized.", "seq": 0}
data: {"line": " project_root: /workspaces/3fa85f64-...", "seq": 1}
...
Terminal event (always sent last):
data: {"status": "done", "exit_code": 0}
The stream closes after the terminal event. If the client disconnects early the server stops generating events.
List all files in the workspace after (or during) a task run.
Response
{
"ok": true,
"task_id": "7c9e6679-...",
"artifacts": [
{
"path": ".clk/state/idea.json",
"size": 234,
"modified": "2024-01-15T12:00:10.000000Z"
}
]
}Download a single artifact by its relative path within the workspace.
Paths that escape the workspace boundary are rejected with 403 Forbidden.
The response body is the raw file content; the Content-Type is inferred
from the file extension.
Send SIGTERM to the running subprocess and mark the task cancelled.
Has no effect on tasks that are already done, failed, or cancelled.
Response
{ "ok": true }BASE=http://localhost:8001
# 1. Create a workspace
WS=$(curl -s -X POST $BASE/api/workspaces \
-H 'Content-Type: application/json' \
-d '{"name": "journal-app"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['workspace_id'])")
echo "Workspace: $WS"
# 2. Capture an idea
TASK=$(curl -s -X POST $BASE/api/research \
-H 'Content-Type: application/json' \
-d "{\"command\":\"idea\",\"args\":[\"A local-first journaling app\"],\"workspace_id\":\"$WS\"}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['task_id'])")
echo "Task: $TASK"
# 3. Stream output
curl -sN $BASE/api/research/$TASK/stream
# 4. Check status
curl -s $BASE/api/research/$TASK | python3 -m json.tool
# 5. List artifacts
curl -s $BASE/api/research/$TASK/artifacts | python3 -m json.tool
# 6. Download a specific artifact
curl -s $BASE/api/research/$TASK/artifacts/.clk/state/idea.json
# 7. Run a development cycle
TASK2=$(curl -s -X POST $BASE/api/research \
-H 'Content-Type: application/json' \
-d "{\"command\":\"run\",\"workflow\":\"engineering\",\"workspace_id\":\"$WS\"}" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['task_id'])")
curl -sN $BASE/api/research/$TASK2/streamSee the main README for Docker-specific instructions.
The clk-telegram-bot console script consumes this API: /api/workspaces,
/api/research, /api/research/{id}, /api/research/{id}/cancel,
/api/research/{id}/stream, and /api/research/{id}/artifacts. See
README → Telegram Bot for setup, the wizard,
and the chat command reference.