The local API
The HarmFlow window is one client of the daemon, the background process that runs your tasks; a script of
yours can be another. Everything the window does goes through this API. The authority is the OpenAPI
description in harmflow-app,
contracts/api/openapi.yaml,
version v1: where this page and it differ, it wins.
Reaching the daemon
- Only from the Mac itself. The daemon serves HTTP on the loopback address,
127.0.0.1, which no other computer can reach. Every path is under/api/v1/. - A port of its own each start. The daemon picks a free port when it starts and writes it to
its runtime file,
run/daemon.jsonin HarmFlow's data folder (~/Library/Application Support/HarmFlow, or the folderHARMFLOW_HOMEnames). The file holdsaddress,port,pid,version,startedAt, andtokenStore; only you can read it, and it goes away when the daemon stops. - Not from a web page. A request whose
Hostis not the loopback address, or whoseOriginis a web page, is refused with403.
Authenticating
- A bearer token on every request, the event stream included:
Authorization: Bearer <token>. A missing or wrong token is refused with401. - One token per installation. The daemon makes it on its first start and keeps it, by default, in
your Mac's Keychain, as the item of service
com.harmflow.local-apiand accounttoken, which the window reads too. A start never replaces it. - Replacing it.
POST /settings/token/rotatemakes a new token; its answer is the last one the old token authorizes.
From a terminal on the Mac, with jq installed:
dir=~/Library/Application\ Support/HarmFlow
port=$(jq .port "$dir/run/daemon.json")
token=$(security find-generic-password \
-s com.harmflow.local-api -a token -w)
curl -H "Authorization: Bearer $token" \
"http://127.0.0.1:$port/api/v1/tasks"
Conventions
- Resources are plural, and an action is a
POSTto a verb under its resource:POST /tasks/{taskId}/run. - Bodies are JSON. Keys are camelCase, and values from a closed set are snake_case.
- A task is written as its YAML text,
yaml, or as its document,document, and both go through the same validation as a task file. An update names thebaseRevisionit started from: a file that changed meanwhile is a409conflict, never a silent overwrite.
The resources
Tasks
GET /tasks— every task, with its next firing and its latest execution.POST /tasks— create a task;POST /tasks/validate— validate one, writing nothing;POST /tasks/import— import a portable task file.GET,PUT, andDELETE /tasks/{taskId}— read, update, and delete a task.POST /tasks/{taskId}/run— start it by hand;/enableand/disable— enable and disable it;/duplicate— copy it, disabled, under a new identifier.GET /tasks/{taskId}/export— its portable YAML, with no secrets, logs, or state.POST /schedules/preview— the next occurrences of a schedule, in its time zone.GET /templatesandPOST /templates/{templateId}/draft— the task templates, and a new task's document built from one.GET /catalog— the agents and skills a task can cite.
Executions
GET /executions— executions, newest first, filtered bytaskId,status,active,createdFrom, andcreatedUntil, and paged withlimitandbefore.GET /executions/{executionId}— one execution: its attempts, work items, warnings, changed files, and commands.POST /executions/{executionId}/cancel— cancel it; the others go on.GET /executions/{executionId}/output— what the harness printed, one capture per attempt, with secrets redacted.GET /executions/{executionId}/events— its stored events, after the event idafter.GET /statsandGET /queue— counters of the executions, and the firings that wait or come next.
The daemon and its harnesses
GET /health— its state and version;GET /diagnostics— its folders, address, and token store;POST /daemon/stop— stop it.GET /harnessesandPOST /harnesses/refresh— Claude Code and Codex as the daemon found them, and a new search for them. The harnesses says how.GET /tools— Git and the GitHub CLI as found on the Mac.GETandPATCH /settings,GET /notifications, andPOST /notifications/read.
An example
GET /api/v1/tasks
200 OK
[
{
"id": "weekly-notes",
"name": "Weekly notes",
"type": "general",
"enabled": true,
"revision": "3f9c2a7d1e4b8a60",
"harness": "claude-code",
"fallbackHarnesses": ["codex"],
"schedule": "0 9 * * 1",
"timezone": "America/New_York",
"nextFireAt": "2026-11-09T14:00:00Z",
"running": false,
"queued": false,
"repositories": [],
"warnings": []
}
]
revision identifies the task file's content: an update sends it back as
baseRevision.
The event stream
GET /events/streamanswers withtext/event-stream: Server-Sent Events, every task's and execution's, as they happen.- Each message is
id: <event id>,event: <type>, anddata: <event JSON>. An event holdsid,schemaVersion,timestamp,type, andseverity(debug,info,warning, orerror), and, when they apply,taskId,executionId,attempt,harness,message, andpayload. Its types readobject.action, such asexecution.completed. - A client that reconnects sends the last id it read, as
Last-Event-IDor as the queryafter, and first receives the events after it that the daemon still holds. Anevent: replay_gapmessage says older ones are gone from the stream: read them per execution, fromGET /executions/{executionId}/events.
Errors
Every error answers with one object, error, which holds a code and a
message in English, and may hold messageKey and params to translate the
message, validation with each problem a task has, and currentRevision on a conflict
over a task file.
404 Not Found
{
"error": {
"code": "not_found",
"message": "no task weekly-report"
}
}
The codes are bad_request, unauthorized, forbidden,
not_found, conflict, validation_failed, task_disabled,
executions_running, and internal. A task that fails validation answers with
422, and nothing is written.