Change feed
Read every change to a workspace in order with a cursor, so a script or agent can react to new and edited files.
Every write, move, delete, conflict, restore and comment in a workspace is recorded in order with a sequence number. The sync CLI uses this feed to keep folders up to date, the activity feed is built on it, and you can read it too.
Treehouse doesn't push webhooks. You poll the feed from your own process, as often as you need.
Request
GET /api/workspaces/{workspaceId}/changes?sinceSeq=0&limit=200
x-api-key: th_example_key
| Parameter | Default | Description |
|---|---|---|
sinceSeq | - | Return changes with a sequence number greater than this. Use 0 to start from the beginning. |
limit | 200 | Changes to scan, from 0 to 1000. |
includeComments | off | 1 or true to include comment_* events. |
Response
{
"changes": [
{
"id": "0b6f…",
"workspaceId": "c2a1…",
"path": "Inbox/call-notes.md",
"op": "write",
"version": 1,
"principal": "human",
"actorId": "u_91…",
"actorKeyId": null,
"actorLabel": null,
"contentHash": "5e88…",
"seq": 1042,
"createdAt": "2026-09-29T09:12:44.103Z"
}
],
"head": 1042,
"nextSeq": 1042
}
| Field | Meaning |
|---|---|
changes | Changes after your cursor that you're allowed to see, oldest first |
head | The workspace's latest sequence number right now |
nextSeq | The cursor for your next request. Store it. |
Using the cursor
- Start at
sinceSeq=0to replay the whole history, or readheadfirst to start from now: a request withlimit=0returnsheadwithout any changes. - Always continue from
nextSeq, not from the last change you received. Changes you aren't allowed to see (another member's private files, or comments you didn't ask for) are filtered out, andnextSeqstill moves past them. A page can come back empty with a highernextSeq. - You're caught up when
nextSeqequalshead. Until then, request again straight away. - Sequence numbers always increase but can have gaps. Don't expect every number to appear.
Change fields
| Field | Description |
|---|---|
seq | Position in the feed |
path | The file (or, for access events, the file or folder) the change applies to |
op | What happened. See below. |
version | The file's version after the change. For a delete, the version that was deleted. null for access events. |
principal | agent or human, from the acting key's actor type. Browser sessions are always human. |
actorId | The user id behind the change. GET /api/workspaces/{id}/members maps it to a name. |
actorKeyId | The API key that made the change, or null for a browser session |
actorLabel | The key's name at the time of the change, such as claude-code |
contentHash | SHA-256 of the new content for write, move and restore, otherwise null |
createdAt | Server timestamp |
actorKeyId is set for any key, including human CLI keys, so use principal to tell agents from people.
Operations
op | Meaning |
|---|---|
write | A file was created or updated, including one recreated after a delete |
move | A file arrived at path by a move or rename. Paired with a delete at its old path, and version is 1. |
delete | A file was deleted, or moved away from path |
conflict | A conflicted copy was created at path |
restore | An earlier version was restored as a new version |
access_revoked | An item was made private and you can no longer see it. Drop your copy of path and everything under it. Only sent to members who lost access. |
access_granted | A private item was shared again and you can see it now. Not sent to the person who shared it. |
comment_created | A comment thread was started on the file at path |
comment_replied | Someone replied to a thread |
comment_resolved | A thread was resolved |
comment_reopened | A resolved thread was reopened |
The comment_* operations only appear with includeComments=1.
Time-based mode
The web app's activity panel reads the feed by time instead: ?since=<ISO timestamp>, optionally with order=desc for newest first. This mode returns changes and head but no nextSeq, and orders by timestamp. Use sinceSeq for anything that must not miss a change.
Example: poll for new files
This loop polls every two seconds, the same interval the sync CLI uses, and reacts to new files in Inbox/. It saves its cursor so a restart picks up where it left off.
import { readFile, writeFile } from "node:fs/promises";
const API = "https://api.trytree.house";
const KEY = process.env.TREEHOUSE_API_KEY!;
const WS = process.env.TREEHOUSE_WORKSPACE_ID!;
const CURSOR_FILE = ".treehouse-cursor";
type Change = { seq: number; path: string; op: string; version: number | null; principal: string };
type Page = { changes: Change[]; head: number; nextSeq: number };
async function loadCursor(): Promise<number> {
try {
return Number(await readFile(CURSOR_FILE, "utf8"));
} catch {
// First run: start from now rather than replaying the whole history.
const page = await fetchPage(0, 0);
return page.head;
}
}
async function fetchPage(sinceSeq: number, limit = 500): Promise<Page> {
const res = await fetch(
`${API}/api/workspaces/${WS}/changes?sinceSeq=${sinceSeq}&limit=${limit}`,
{ headers: { "x-api-key": KEY } },
);
if (!res.ok) throw new Error(`change feed: ${res.status}`);
return (await res.json()) as Page;
}
async function onChange(c: Change): Promise<void> {
const arrived = (c.op === "write" && c.version === 1) || c.op === "move";
if (arrived && c.path.startsWith("Inbox/")) {
console.log(`New in Inbox: ${c.path} (by ${c.principal})`);
// Hand it to your agent here.
}
}
let cursor = await loadCursor();
for (;;) {
const page = await fetchPage(cursor);
for (const change of page.changes) await onChange(change);
cursor = page.nextSeq;
await writeFile(CURSOR_FILE, String(cursor));
if (cursor >= page.head) await new Promise((r) => setTimeout(r, 2000));
}
Save the cursor after you've handled a page, not before, so a crash replays the page instead of skipping it. That means your handler may see a change twice; make it safe to repeat.
Ideas
- An inbox agent. Watch
Inbox/as above and start an agent on each new file, so notes you drop in are filed or summarised. The routines playbook covers running agents on a schedule. - A daily digest. Once a day, read from yesterday's saved cursor to
head, group changes bypathandprincipal, and write a summary file back to the workspace. - Review triggers. With
includeComments=1, watch forcomment_createdon files your agent owns and have it respond. See the review loop playbook.
Remember that Treehouse only stores the files and the feed: the process doing the polling runs on your own machine or server.
Related
- See what changed in a workspace
The activity feed shows every change and comment in a workspace, who made it, and whether it came from a person or an agent.
- Set up routines
Recurring agent work, like a morning inbox run or a weekly review, and where the schedule lives when Treehouse doesn't run agents itself.
- Versions and conflicts
The concurrency contract every Treehouse client shares - version numbers, compare-and-swap writes, conflicted copies, deletes, moves and restores.
Last updated