API reference contents
API reference
Documentation
Pagination
Three fields and two parameters. limit and cursor go in;
next_cursor and open_total come back.
The parameters
| Parameter | Where | Value |
|---|---|---|
limit | GET /work, GET /activity, GET /events | 1–200. Default 50. Enforced, not clamped: a value outside the range is brought into it, so limit=0 behaves as 1 and limit=5000 behaves as 200. |
cursor | GET /work | An opaque string from a previous response's next_cursor. Do not construct one, do not decode one, do not store one for later use. |
status | GET /work | open, in_progress or waiting. |
kind | GET /work | A work kind. Get the valid values for a desk from GET /desk. |
arrangement | GET /work | An arrangement id, to narrow the queue to one arrangement. |
The response
{
"data": {
"items": [ /* up to limit work items */ ],
"next_cursor": "aWQ6NTg",
"open_total": 34
}
}
| Field | Meaning |
|---|---|
items | This page. May be empty while later pages still exist, if a filter removes everything in the window. |
next_cursor | A string when there is more, and null when there is not. Null is the only reliable end-of-list signal. |
open_total | The size of the whole open queue before filtering and paging. It does not change as you page, and it does not reflect status, kind or arrangement. |
open_total is not the number of results. Ask for
/work?status=waiting&limit=5 on a desk with six waiting rows and thirty-four
open ones and you get five items with open_total: 34. The field answers "how
much work is on this desk", which is a different question from "how many rows match".
There is no count of matching rows in version 1. If a client needs one, it has to page
to the end and count.
Paging
Pass the cursor from one response into the next request. Stop when
next_cursor is null.
# First page
curl -s "https://outcomedev.com/api/v1/work?limit=50" \
-H "Authorization: Bearer $OD_KEY"
# Next page - the cursor is the exact string that came back
curl -s "https://outcomedev.com/api/v1/work?limit=50&cursor=aWQ6NTg" \
-H "Authorization: Bearer $OD_KEY"
A cursor that cannot be read — a truncated one, a hand-made one — is not an error. The list restarts from the beginning, and the response is a valid first page. That is a decision rather than an oversight: a bad cursor produces a correct, if repetitive, answer rather than a failure a client would have to handle. The practical risk is an infinite loop, so a client should stop on a repeated page rather than trusting the cursor blindly.
What is stable, and what is not
Being precise about this is worth more than a promise that cannot be kept. Work is changing while it is being read: rows close, priorities move, new rows arrive.
| Thing | Stable? |
|---|---|
The meaning of limit, cursor, next_cursor and open_total | Yes. These do what is described here. |
| The internal contents of a cursor | No. Opaque. It is not an id, not an offset, and not a promise about what the next page contains. |
| A cursor surviving a later session | No. Use one within the paging loop that received it. Do not persist one. |
| The order of rows across pages | No. See below. |
| Every row appearing exactly once across a full walk | No. A row can move position between two page requests and be missed or seen twice. |
| An item's fields | Mostly. A field can be added without a version change, so ignore keys you do not know. |
The ordering is the screen's ordering, not a guarantee
GET /work returns rows in the order the product's own screen shows them:
overdue first, then by priority, then by due date. That is a presentation decision, made
so that somebody opening the desk at nine in the morning does not have to decide what to
work on. It is not a stable sort and it is not a contract.
Concretely, a client should not: assume two consecutive pages are globally sorted, page through the queue and treat the concatenation as a snapshot, or use a row's position as an identifier. If a specific order is needed, sort in the client after fetching.
The queue is read to a fixed depth
There is one honest limit worth stating, because it would otherwise be discovered as a
missing row. The queue behind /work is read to a fixed depth before
limit and cursor are applied. On a desk with an exceptionally
large open queue, the tail beyond that depth is not returned and no cursor reaches it.
In practice this is far beyond any real desk's open work. It is mentioned because a
silent truncation is exactly the kind of thing a caller should not have to discover, and
because the mitigation is simple: narrow the queue with status,
kind or arrangement rather than reading it whole. A filtered read
is a smaller queue, and a smaller queue pages completely.
Which endpoints page
| Endpoint | Paging | Order |
|---|---|---|
GET /work | limit and cursor, plus next_cursor and open_total | The screen's working order |
GET /activity | limit only. No cursor, no total | Newest first |
GET /events | limit only. No cursor, no total | Newest first |
GET /arrangements | None. Returns everything | Most in need of attention first |
GET /units | None. Returns everything | The desk's own order |
GET /connections, /domains, /permissions, /watches | None. Small, fixed-size sets | — |
/activity and /events are the two that catch people out: they
take a limit, so they look pageable, and there is no cursor. Asking for the
next page means raising limit, and once the limit is at 200 there is no
further page. For a full record, read /activity regularly and keep what you
need, or ask
support@outcomedev.com about
the volume.
A complete walk, in JavaScript
The loop that is safe to write: follow the cursor, stop on null, and stop if a page repeats.
const BASE = 'https://outcomedev.com/api/v1'
const headers = { Authorization: `Bearer ${process.env.OUTCOMEDEV_API_KEY}` }
async function allOpenWork({ status, kind } = {}) {
const out = []
const seenPages = new Set()
let cursor = null
for (;;) {
const url = new URL(`${BASE}/work`)
url.searchParams.set('limit', '200')
if (status) url.searchParams.set('status', status)
if (kind) url.searchParams.set('kind', kind)
if (cursor) url.searchParams.set('cursor', cursor)
const res = await fetch(url, { headers })
if (!res.ok) throw new Error(`List failed with ${res.status}`)
const { data } = await res.json()
const fingerprint = data.items.map((i) => i.id).join(',')
// A cursor that cannot be read restarts the list. Stop rather than loop.
if (fingerprint && seenPages.has(fingerprint)) break
if (fingerprint) seenPages.add(fingerprint)
out.push(...data.items)
cursor = data.next_cursor
if (!cursor) break
}
return out
}
The fingerprint check is there for one reason. A client that follows
next_cursor without it will loop for ever if a cursor ever fails to decode,
because the service will keep answering with the first page and a fresh cursor. That is a
property of the API worth coding against, not a defect to report.
Related
- Work — the row being paged, and the ordering in context.
- Idempotency — the same care about retries, applied to writes.
- Errors — and why a bad cursor is not one.