loop-task

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:8845

Response 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 present
  • 201 - Created (returned by POST endpoints)
  • 400 - Validation error or bad request (invalid JSON, missing fields, max runs reached, etc.)
  • 404 - Resource not found
  • 409 - 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

MethodEndpointDescription
GET/api/loopsList all loops
POST/api/loopsCreate a new loop
GET/api/loops/:idGet a single loop's status
PATCH/api/loops/:idUpdate a loop
DELETE/api/loops/:idDelete a loop
POST/api/loops/:id/pausePause a running loop
POST/api/loops/:id/resumeResume a paused loop
POST/api/loops/:id/triggerTrigger an immediate run
POST/api/loops/:id/stopStop and disable a loop
POST/api/loops/stop-allStop all running loops
GET/api/loops/:id/logs?tail=NFetch the last N lines of output
GET/api/loops/:id/logs/streamStream loop output live via SSE
GET/api/loops/:id/runs/:numGet 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):

FieldTypeDefaultDescription
commandstring-Shell command to execute (required)
commandArgsstring[][]Arguments passed to the command
intervalHumanstring"5m"Interval string: 30s, 5m, 1h, 1d, or "manual" for trigger-only loops
cwdstringprocess.cwd()Working directory for the command
descriptionstring""Human-readable description
taskIdstring | nullnullID of a saved task to execute instead of command
nowboolean-Run immediately on start
maxRunsnumber | nullnullMaximum number of runs before auto-stopping (null = unlimited)
verbosebooleanfalseCapture full stdout/stderr in logs
projectIdstring-Project ID for grouping
offsetnumber | nullnullInitial delay offset in milliseconds before first run
contextobject-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 reached
  • 409 - 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
}
FieldTypeDescription
statusstringOne of: running, paused, idle, stopped, waiting
pidnumberProcess ID of the daemon managing this loop
runHistoryRunRecord[]History of completed runs
maxRunsReachedbooleanWhether the loop hit its max run limit

Tasks

MethodEndpointDescription
GET/api/tasksList all saved tasks
POST/api/tasksCreate a new task
GET/api/tasks/:idGet a single task
PATCH/api/tasks/:idUpdate a task
DELETE/api/tasks/:idDelete 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):

FieldTypeRequiredDescription
idstringnoCustom ID (auto-generated if omitted)
namestringyesHuman-readable task name
commandstringyesShell command to execute
commandArgsstring[]noArguments for the command
stepsTaskStep[]noMulti-step command definitions
onSuccessTaskIdstring | nullnoChain to this task on success
onFailureTaskIdstring | nullnoChain to this task on failure
contextobjectnoInitial 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

MethodEndpointDescription
GET/api/projectsList all projects
POST/api/projectsCreate a project
PATCH/api/projects/:idUpdate a project
DELETE/api/projects/:idDelete 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):

FieldTypeRequiredDescription
namestringyesProject name
colorstringnoHex 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):

FieldTypeRequiredDescription
namestringyesNew project name
colorstringnoNew 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

MethodEndpointDescription
GET/api/eventsSubscribe to daemon events (SSE stream)
GET/api/docsSwagger UI HTML page
GET/api/openapi.jsonOpenAPI 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:

EventDataDescription
(none - data: only)stringA 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/loops

Create 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/abc12345

Pause 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/resume

Trigger immediate run

curl -X POST http://127.0.0.1:8845/api/loops/abc12345/trigger

Stop a loop

curl -X POST http://127.0.0.1:8845/api/loops/abc12345/stop

Delete a loop

curl -X DELETE http://127.0.0.1:8845/api/loops/abc12345

Create 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/3

Stream 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