HTTP API Reference
Complete REST and SSE API reference for the loop-task daemon.
The loop-task daemon exposes a local HTTP API on 127.0.0.1:8845 for managing loops, tasks, projects, logs, and streaming events. All endpoints are localhost-only - the daemon never listens on external interfaces.
Port can be changed via the LOOP_CLI_HTTP_PORT environment variable. The daemon prints the API base URL and links to Swagger UI on startup.
Base URL
http://127.0.0.1:8845Response Envelope
Every JSON response follows a consistent envelope:
Success (2xx):
{
"ok": true,
"data": { ... }
}Error (4xx/5xx):
{
"ok": false,
"error": {
"message": "Not found: abc123"
}
}200- OK, data present201- Created (returned by POST endpoints)400- Validation error or bad request (invalid JSON, missing fields, max runs reached, etc.)404- Resource not found409- Conflict (e.g. loop is already running on trigger)500- Internal server error
Some endpoints return data: null or an empty string for no-content responses.
Loops
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/loops | List all loops |
| POST | /api/loops | Create a new loop |
| GET | /api/loops/:id | Get a single loop's status |
| PATCH | /api/loops/:id | Update a loop |
| DELETE | /api/loops/:id | Delete a loop |
| POST | /api/loops/:id/pause | Pause a running loop |
| POST | /api/loops/:id/resume | Resume a paused loop |
| POST | /api/loops/:id/trigger | Trigger an immediate run |
| POST | /api/loops/:id/stop | Stop and disable a loop |
| POST | /api/loops/stop-all | Stop all running loops |
| GET | /api/loops/:id/logs?tail=N | Fetch the last N lines of output |
| GET | /api/loops/:id/logs/stream | Stream loop output live via SSE |
| GET | /api/loops/:id/runs/:num | Get the log output for a specific run |
GET /api/loops
Returns an array of all loop status objects.
Response data: LoopMeta[]
POST /api/loops
Create and start a new loop.
Request body (JSON):
| Field | Type | Default | Description |
|---|---|---|---|
command | string | - | Shell command to execute (required) |
commandArgs | string[] | [] | Arguments passed to the command |
intervalHuman | string | "5m" | Interval string: 30s, 5m, 1h, 1d, or "manual" for trigger-only loops |
cwd | string | process.cwd() | Working directory for the command |
description | string | "" | Human-readable description |
taskId | string | null | null | ID of a saved task to execute instead of command |
now | boolean | - | Run immediately on start |
maxRuns | number | null | null | Maximum number of runs before auto-stopping (null = unlimited) |
verbose | boolean | false | Capture full stdout/stderr in logs |
projectId | string | - | Project ID for grouping |
offset | number | null | null | Initial delay offset in milliseconds before first run |
context | object | - | Initial key-value context for chain interpolation |
Response data: { "id": "<8-char hex>" } (status 201)
GET /api/loops/:id
Returns the full status object for a single loop.
Response data: LoopMeta
PATCH /api/loops/:id
Update a loop's configuration. Accepts the same body fields as POST. If execution parameters change while the loop is running, the loop is automatically paused and restarted with new settings.
Response data: { "id": "<id>" }
DELETE /api/loops/:id
Permanently delete a loop and its persisted state. The loop is stopped first if running.
Response: data: null (200)
POST /api/loops/:id/pause
Pause a running loop. The loop keeps its schedule position - resume continues where it left off.
Response: data: null (200)
POST /api/loops/:id/resume
Resume a paused loop. Has no effect if the loop is already running or idle.
Response: data: null (200)
POST /api/loops/:id/trigger
Force an immediate run, ignoring the scheduled interval.
Errors:
400- Max runs reached409- Loop is already running
Response: data: null (200)
POST /api/loops/:id/stop
Stop a loop and mark it as stopped. A stopped loop will not auto-start on daemon restart.
Response: data: null (200)
POST /api/loops/stop-all
Stop every running loop.
Response data: { "count": <number> }
GET /api/loops/:id/logs?tail=N
Fetch log output for a loop. Defaults to the last 50 lines. Returns an empty string if no log file exists.
Query parameters:
tail- number of lines to return (default:50)
Response data: string (log content as a single string with newlines)
GET /api/loops/:id/logs/stream
Tail loop log output in real-time via Server-Sent Events. See SSE Streams below.
GET /api/loops/:id/runs/:num
Get the log output specifically for run number :num. The log is sliced using byte offsets recorded in the run history, so you get exactly that run's output.
Response data: string (run-specific log content)
LoopMeta Response Shape
When requesting a single loop or listing all loops, each entry has this structure:
{
"id": "abc12345",
"taskId": null,
"command": "echo hello",
"commandArgs": [],
"commandRaw": "echo hello",
"interval": 300000,
"intervalHuman": "5m",
"immediate": false,
"maxRuns": null,
"verbose": false,
"cwd": "/home/user/project",
"description": "",
"status": "running",
"createdAt": "2025-06-15T10:30:00.000Z",
"sessionStartedAt": "2025-06-15T10:30:05.000Z",
"runCount": 3,
"lastRunAt": "2025-06-15T11:00:00.000Z",
"lastExitCode": 0,
"lastDuration": 1234,
"nextRunAt": "2025-06-15T11:05:00.000Z",
"remainingDelayMs": 245000,
"pid": 48291,
"maxRunsReached": false,
"runHistory": [
{
"runNumber": 1,
"startedAt": "2025-06-15T10:30:05.000Z",
"exitCode": 0,
"duration": 1200,
"logSize": 512,
"status": "completed",
"logOffset": 0,
"chainGroupId": null,
"chainName": null
}
],
"skippedCount": 0,
"projectId": "default",
"offset": null
}| Field | Type | Description |
|---|---|---|
status | string | One of: running, paused, idle, stopped, waiting |
pid | number | Process ID of the daemon managing this loop |
runHistory | RunRecord[] | History of completed runs |
maxRunsReached | boolean | Whether the loop hit its max run limit |
Tasks
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/tasks | List all saved tasks |
| POST | /api/tasks | Create a new task |
| GET | /api/tasks/:id | Get a single task |
| PATCH | /api/tasks/:id | Update a task |
| DELETE | /api/tasks/:id | Delete a task |
GET /api/tasks
Returns an array of all task definitions.
Response data: TaskDefinition[]
POST /api/tasks
Save a reusable task definition that loops can reference via taskId.
Request body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
id | string | no | Custom ID (auto-generated if omitted) |
name | string | yes | Human-readable task name |
command | string | yes | Shell command to execute |
commandArgs | string[] | no | Arguments for the command |
steps | TaskStep[] | no | Multi-step command definitions |
onSuccessTaskId | string | null | no | Chain to this task on success |
onFailureTaskId | string | null | no | Chain to this task on failure |
context | object | no | Initial context for chain interpolation |
Response data: TaskDefinition (status 201)
TaskDefinition Shape
{
"id": "abc12345",
"name": "Health Check",
"command": "curl",
"commandArgs": ["-f", "http://localhost:3000/health"],
"commandRaw": "curl -f http://localhost:3000/health",
"steps": null,
"onSuccessTaskId": "next-task-id",
"onFailureTaskId": "alert-task-id",
"context": { "url": "http://localhost:3000" },
"createdAt": "2025-06-15T10:30:00.000Z"
}GET /api/tasks/:id
Returns a single task definition.
Response data: TaskDefinition
PATCH /api/tasks/:id
Update a task. Accepts any of the fields from POST (except id and createdAt which are immutable).
Response data: TaskDefinition
DELETE /api/tasks/:id
Delete a saved task.
Response: data: null (200)
Projects
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/projects | List all projects |
| POST | /api/projects | Create a project |
| PATCH | /api/projects/:id | Update a project |
| DELETE | /api/projects/:id | Delete a project |
GET /api/projects
Returns an array of all projects.
Response data: Project[]
POST /api/projects
Create a new project for organizing loops.
Request body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Project name |
color | string | no | Hex color (default: "#ffffff") |
Response data: Project (status 201)
Project Shape
{
"id": "abc12345",
"name": "Production",
"color": "#ff6600",
"directory": "/home/user/projects/my-app",
"createdAt": "2025-06-15T10:30:00.000Z",
"isSystem": false,
"isDefault": false
}PATCH /api/projects/:id
Update a project's name and/or color.
Request body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | New project name |
color | string | no | New hex color |
Response: data: null (200)
DELETE /api/projects/:id
Delete a project. The system (default) project cannot be deleted and returns a 400 error.
Response: data: null (200)
Misc
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/events | Subscribe to daemon events (SSE stream) |
| GET | /api/docs | Swagger UI HTML page |
| GET | /api/openapi.json | OpenAPI 3.0.3 JSON specification |
| GET | / | Root serves Swagger UI (same as /api/docs) |
GET /api/openapi.json
Returns the full OpenAPI 3.0.3 specification for all endpoints. This is the authoritative machine-readable reference.
GET /api/docs (and GET /)
Serves the Swagger UI HTML page, loaded from cdn.jsdelivr.net. The UI reads /api/openapi.json to render interactive documentation.
SSE Streams
The daemon exposes two Server-Sent Events (SSE) endpoints.
GET /api/events - Daemon Event Stream
Opens a persistent SSE connection that receives daemon-broadcast events. On connection the server sends an initial : connected comment to confirm the stream is alive.
Event format:
: connected
event: <event-name>
data: { ... }Currently no events are dispatched by the daemon in production. The broadcast infrastructure (HttpApiServer.broadcastEvent) exists and is ready for future use (planned event names include loop:started, loop:stopped, loop:completed).
GET /api/loops/:id/logs/stream - Loop Log Stream
Opens a persistent SSE connection that streams a loop's log output in real-time. On connection it first sends any existing log lines (controlled by the ?tail= query parameter), then streams new lines as they are written.
Query parameters:
tail- number of existing lines to replay before streaming (default:0)
Events emitted:
| Event | Data | Description |
|---|---|---|
(none - data: only) | string | A log line (each data: is one line of output) |
end | {} | The loop's log file was deleted - stream is closing |
Format:
data: [2025-06-15T10:30:05] echo hello
data: hello world
event: end
data: {}Clients should reconnect when the stream ends - the loop may have been recreated or its logs rotated.
curl Examples
List all loops
curl http://127.0.0.1:8845/api/loopsCreate a loop (every 5 minutes)
curl -X POST http://127.0.0.1:8845/api/loops \
-H "Content-Type: application/json" \
-d '{
"command": "curl -f http://localhost:3000/health",
"intervalHuman": "5m",
"description": "Health check every 5 minutes",
"projectId": "default",
"verbose": true
}'Get loop status
curl http://127.0.0.1:8845/api/loops/abc12345Pause and resume
curl -X POST http://127.0.0.1:8845/api/loops/abc12345/pause
curl -X POST http://127.0.0.1:8845/api/loops/abc12345/resumeTrigger immediate run
curl -X POST http://127.0.0.1:8845/api/loops/abc12345/triggerStop a loop
curl -X POST http://127.0.0.1:8845/api/loops/abc12345/stopDelete a loop
curl -X DELETE http://127.0.0.1:8845/api/loops/abc12345Create a task
curl -X POST http://127.0.0.1:8845/api/tasks \
-H "Content-Type: application/json" \
-d '{
"id": "healthcheck",
"name": "Health Check",
"command": "curl",
"commandArgs": ["-f", "http://localhost:3000/health"],
"onFailureTaskId": "alert"
}'Create a project
curl -X POST http://127.0.0.1:8845/api/projects \
-H "Content-Type: application/json" \
-d '{
"name": "Production",
"color": "#ff6600"
}'Fetch logs (last 100 lines)
curl "http://127.0.0.1:8845/api/loops/abc12345/logs?tail=100"Get a specific run's log
curl http://127.0.0.1:8845/api/loops/abc12345/runs/3Stream loop logs in real-time
curl -N "http://127.0.0.1:8845/api/loops/abc12345/logs/stream?tail=10"Subscribe to daemon events
curl -N http://127.0.0.1:8845/api/events