# Events and the live stream

> Follow every Taskhold change as it happens: the cursor feed at /api/events, the SSE stream at /api/events/stream, event types and a curl watcher.

Source: https://taskhold.com/docs/events
Last updated: 2026-10-26

Every create, status change, plan, progress report, note, edit and delete writes an event with an increasing id. Read them two ways: poll /api/events with a cursor, or hold open /api/events/stream and receive them as Server-Sent Events.

## Cursor feed

```bash
curl -s "https://taskhold.com/api/events?since=26" -H "Authorization: Bearer API_KEY"
```

```json
{"events": [
  {"id": 27, "type": "task.status", "actor": "agent", "actor_name": "GitHub Actions (deploy.yml)",
   "task_id": "0a017133-...", "root_task_id": "fd11278a-...", "task_title": "Build images", "summary": "Started",
   "data": {"from": "todo", "to": "in_progress", "blocked_reason": null, "completed_summary": null},
   "created_at": "2026-10-26T05:09:47.358Z"},
  {"id": 28, "type": "task.progress", "summary": "60% (3/5) Building api image", ...}
],
 "cursor": 28}
```

- since: return events after this id, oldest first. Without it you get the latest limit events (default 20).
- actor=human or actor=agent: only one side's changes. taskhold_updates uses actor=human.
- task_id: only events on this task and its steps.
- limit: up to 500. Pass the returned cursor as since next time.

## Event types

| type | When |
| --- | --- |
| task.created | A task or step was created |
| subtasks.planned | Steps were added with a plan |
| task.status | Status changed; data.from and data.to, plus blocked_reason or completed_summary |
| task.progress | Progress reported; summary like "60% (3/5) Building api image" |
| task.note | A note was added (replies to blocked tasks arrive this way) |
| task.updated | Title, description, priority or project changed |
| task.deleted | A task was deleted |

Progress events older than 14 days are pruned; the rest stay.

## Server-Sent Events

The stream starts at the current moment (or after Last-Event-ID, or ?since=), sends a ready event with the cursor, then one event: task frame per change. A comment line every 15 seconds keeps proxies from closing it, and the server ends each stream after 10 minutes; reconnect with Last-Event-ID and nothing is lost.

```text
retry: 3000
event: ready
data: {"cursor":26}

id: 27
event: task
data: {"id":27,"type":"task.status","actor":"agent","summary":"Started","task_title":"Build images",...}
```

## Watch from a terminal

One line per change, with curl and jq:

```bash
curl -sN https://taskhold.com/api/events/stream -H "Authorization: Bearer API_KEY" \
  | sed -un 's/^data: //p' \
  | jq -r --unbuffered 'select(.summary) | "\(.created_at[11:19])  \(.actor_name // .actor)  \(.task_title // ""): \(.summary)"'
```

```text
05:09:47  GitHub Actions (deploy.yml)  Build images: Started
05:09:47  GitHub Actions (deploy.yml)  Build images: 60% (3/5) Building api image
```

Browsers can't set an Authorization header on EventSource; in a browser, the signed-in web app uses its session cookie. From scripts, read the stream with fetch as in /docs/slack-alerts.

## Live snapshot

GET /api/live returns what the board shows in one call: active (in progress, with steps), blocked, recently_done (last 2 hours), up_next, counts (in_progress, blocked, todo, done_today), the latest events and the cursor to stream from.

## Related

- [Taskhold REST API: tasks, steps, progress, curl examples](https://taskhold.com/docs/rest-api.md)
- [Slack alerts for blocked tasks from the events stream](https://taskhold.com/docs/slack-alerts.md)
- [Review what each coding agent did this week](https://taskhold.com/docs/agent-review.md)
- [How live progress works: steps, bars, ETA and liveness](https://taskhold.com/docs/live-progress.md)
