Sessions
curl --request GET \
--url https://api.example.com/v0/workspaces/{id}/sessionsimport requests
url = "https://api.example.com/v0/workspaces/{id}/sessions"
response = requests.get(url)
print(response.text)const options = {method: 'GET'};
fetch('https://api.example.com/v0/workspaces/{id}/sessions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/v0/workspaces/{id}/sessions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/v0/workspaces/{id}/sessions"
req, _ := http.NewRequest("GET", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.example.com/v0/workspaces/{id}/sessions")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/v0/workspaces/{id}/sessions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
response = http.request(request)
puts response.read_bodyWorkspaces
Sessions
GET /v0/workspaces/:id/sessions — Recent sessions as a log: who worked, from which harness, each stated reason and the changes made for it
GET
/
v0
/
workspaces
/
{id}
/
sessions
Sessions
curl --request GET \
--url https://api.example.com/v0/workspaces/{id}/sessionsimport requests
url = "https://api.example.com/v0/workspaces/{id}/sessions"
response = requests.get(url)
print(response.text)const options = {method: 'GET'};
fetch('https://api.example.com/v0/workspaces/{id}/sessions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/v0/workspaces/{id}/sessions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/v0/workspaces/{id}/sessions"
req, _ := http.NewRequest("GET", url, nil)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.example.com/v0/workspaces/{id}/sessions")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/v0/workspaces/{id}/sessions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
response = http.request(request)
puts response.read_bodyReturns the workspace’s recent sessions, newest first. A session is computed from the change feed when you read it: a run of changes by one account, through one agent key and one harness conversation, each within 45 minutes of the previous one. Nothing is started, ended, or stored, so there is nothing to open or close. Each agent write states a
why (one sentence on what the person is trying to accomplish). A session reads like a git log: its steps are those whys in order, each followed by the changes made for it: what was done to which item, the versions it created, and the rows it wrote. Any member can read sessions (read). MCP: workspace_sessions. CLI: rip workspace sessions <workspace-id> [--cursor <cursor>].
Load returns the latest five sessions in the same shape.
Auth: Authorization: Bearer tr_... (an account API key). {id} is the workspace UUID.
Query parameters
| Parameter | Type | Description |
|---|---|---|
cursor | string | Continue from the previous page’s nextCursor |
Response
{
"ok": true,
"data": {
"items": [
{
"id": "9c1f2a7b3d4e5f60",
"account": { "id": "rip1…", "alias": "simon" },
"agent": { "name": "Claude Code (laptop)" },
"surface": "claude-code",
"inBrowser": false,
"mine": true,
"unread": true,
"startedAt": "2026-10-08T09:02:00.000Z",
"lastAt": "2026-10-08T10:41:00.000Z",
"steps": [
{
"why": "Set launch prices the team can defend to finance",
"at": "2026-10-08T09:02:00.000Z",
"changes": [
{
"subject": { "type": "artifact", "id": "a1b2…", "title": "Pricing" },
"actions": ["artifact.created", "artifact.edited"],
"versions": [{ "id": "7d8e…", "number": 1 }, { "id": "9f0a…", "number": 2 }],
"versionsTruncated": false,
"events": 2
},
{
"subject": { "type": "artifact", "id": "e5f6…", "title": "Price list" },
"actions": ["artifact.rows_changed"],
"versions": [],
"versionsTruncated": false,
"rows": { "added": 12, "updated": 3, "deleted": 0 },
"events": 2
}
],
"changesTruncated": false
},
{
"why": "Hand pricing over for review",
"at": "2026-10-08T10:39:00.000Z",
"changes": [
{
"subject": { "type": "task", "id": "4c5d…", "title": "Review pricing copy" },
"actions": ["task.created"],
"versions": [],
"versionsTruncated": false,
"events": 1
},
{
"subject": { "type": "artifact", "id": "c3d4…", "title": "Where pricing stands" },
"actions": ["artifact.created"],
"versions": [{ "id": "1b2c…", "number": 1 }],
"versionsTruncated": false,
"handoff": true,
"events": 1
}
],
"changesTruncated": false
}
],
"stepsTruncated": false,
"eventCount": 6,
"earlierActivityNotShown": false
}
],
"hasMore": true,
"nextCursor": "eyJ2Ijox…"
}
}
| Field | Description |
|---|---|
id | Opaque, and the same as the session grows. It is not a stored record and no route takes it |
account | The account that made the changes |
agent | The agent key’s name, shown only on sessions of your own account; otherwise null |
surface | The harness: claude-code, codex, cli, dashboard, … |
inBrowser | The changes were made by a person signed in to the dashboard. Browser edits state no reason |
mine | The session is your own account’s work (yours or another of your agents) |
unread | The session holds a change after your key’s last acknowledged position. Reading sessions never moves that position |
startedAt / lastAt | First and last change |
steps | Each stated reason followed by the changes made for it, oldest first (the latest 10). A new step starts whenever the why changes. why is null for browser edits, for side effects no agent stated a reason for, and for changes made before whys were recorded; at is the step’s first change |
steps[].changes | One entry per item the step touched, in order of first touch (the latest 20) |
subject | The item: type, id (an artifact’s public id, the id every artifact route takes; for an item deleted for good or out of your reach, the id the change recorded), title, and gone: true when the item is in the trash, deleted for good, or out of your reach. A deleted or unreachable item has title: null |
actions | What was done to the item, in order: artifact.created, artifact.edited (a new version), artifact.rows_changed, artifact.details_changed (title, description, alias or metadata), artifact.moved, artifact.audience_changed, artifact.version_deleted, artifact.versions_pruned (old versions deleted), artifact.deleted, artifact.restored, the task.* transitions, and others. Changes recorded before October 2026 may read artifact.updated |
versions | The versions this step created, oldest first (at most five: the first and the latest four, so the step’s range is always first to last): { id, number }. number is null for a version deleted since, and for every version of an item out of your reach. Read everything the step changed with Version diff ?versionId=<last>&fromVersionId=<first>, or rip artifact diff <id> --from-version-id <first> --version-id <last> |
rows | Table rows the step wrote: { added, updated, deleted }. Absent when it wrote none |
handoff | true when the item is a handoff note the step published and you can still open. See Create Artifact handoff |
events | Changes merged into this line |
stepsTruncated / changesTruncated / versionsTruncated | More existed than the list shows |
eventCount | Changes in the session |
earlierActivityNotShown | The session may reach back past the changes this read scanned |
How sessions are grouped
- One read groups the most recent 1,000 changes you are allowed to see. A session too large for one reply is shortened (each change’s versions after its first, down to its first and last; then the oldest changes, then steps; each flagged), never refused. A session that crosses that boundary appears as two: the newer part, flagged
earlierActivityNotShown, on this page, and the older part on a later page. - An external member sees sessions built only from
sharedchanges. A session with no change they can see does not appear, and no internal reason, item, or count leaks. - Lease expiry and other system jobs form no session; they stay in the change feed.
- A connector key gives no conversation id, so two parallel chats on one connector form one session; each change keeps its own
whyin the change feed. - Sessions disappear when activity retention prunes their changes. Handoff notes are ordinary artifacts and stay.
Errors
| Status | Code | Cause |
|---|---|---|
400 | INVALID_CURSOR | The cursor is malformed or belongs to another workspace |
404 | WORKSPACE_NOT_FOUND | No workspace with that id |
403 | WORKSPACE_FORBIDDEN | You are not a member |
410 | WORKSPACE_DELETED | The workspace is in the trash |