Docs / Tools
Pagination and filtering
How to page through jobs, schedules and runs with cursors, and how to narrow list results with filters and sort orders.
Last updated May 12, 2026
#How pagination works
Every list endpoint in the Tend API (/jobs, /schedules, /runs and /webhooks/deliveries) uses cursor-based pagination. You pass a limit and, for every page after the first, the cursor value returned in the previous response's next_cursor field. When next_cursor is null, you have reached the end of the collection.
The limit parameter defaults to 50 and accepts a maximum of 200. Values above 200 are rejected with a 400 invalid_request error rather than silently clamped, because silent clamping has a long history of producing code that works until the day it doesn't.
{
"data": [
{ "id": "job_01J8Q4M2ZT7K9XW3R5B6N0CDEF", "status": "succeeded", "created_at": "2026-05-11T14:02:19Z" },
{ "id": "job_01J8Q4M1V8H2P6YD4A9S3G7KTB", "status": "failed", "created_at": "2026-05-11T14:01:53Z" }
],
"has_more": true,
"next_cursor": "cur_eyJ0IjoiMjAyNi0wNS0xMVQxNDowMTo1M1oiLCJpIjoiam9iXzAxSjhRNE0xIn0"
}#Fetching a page
The first request omits cursor. Every subsequent request passes the next_cursor from the previous response, unchanged, along with the same filters and limit. Changing filters mid-iteration returns a 400 invalid_request error because the cursor is bound to the query that produced it.
curl "https://api.tendcomputer.com/v2/jobs?limit=100" \
-H "Authorization: Bearer tnd_live_9fK2xQ7mVb3LpR8dWc1Zh"
# Next page
curl "https://api.tendcomputer.com/v2/jobs?limit=100&cursor=cur_eyJ0IjoiMjAyNi0wNS0xMVQxNDowMTo1M1oiLCJpIjoiam9iXzAxSjhRNE0xIn0" \
-H "Authorization: Bearer tnd_live_9fK2xQ7mVb3LpR8dWc1Zh"from tend import Tend
client = Tend(api_key="tnd_live_9fK2xQ7mVb3LpR8dWc1Zh")
page = client.jobs.list(limit=100)
print(len(page.data), page.has_more)
if page.has_more:
page = client.jobs.list(limit=100, cursor=page.next_cursor)import Tend from "@tend/sdk";
const client = new Tend({ apiKey: "tnd_live_9fK2xQ7mVb3LpR8dWc1Zh" });
let page = await client.jobs.list({ limit: 100 });
console.log(page.data.length, page.hasMore);
if (page.hasMore) {
page = await client.jobs.list({ limit: 100, cursor: page.nextCursor });
}client := tend.NewClient("tnd_live_9fK2xQ7mVb3LpR8dWc1Zh")
page, err := client.Jobs.List(ctx, &tend.JobListParams{Limit: 100})
if err != nil {
log.Fatal(err)
}
if page.HasMore {
page, err = client.Jobs.List(ctx, &tend.JobListParams{
Limit: 100,
Cursor: page.NextCursor,
})
}#Iterating over an entire collection
If you want every record, you do not need to write the loop yourself. Each SDK exposes an auto-paginating iterator that fetches pages lazily and stops when next_cursor is null. Pages are requested one at a time, so a slow consumer does not cause the SDK to buffer the whole collection in memory.
for run in client.runs.list(status="failed", limit=200).auto_paging_iter():
print(run.id, run.job_id, run.error_code)for await (const run of client.runs.list({ status: "failed", limit: 200 })) {
console.log(run.id, run.jobId, run.errorCode);
}# Shell loop: follow next_cursor until it is null
cursor=""
while :; do
resp=$(curl -s "https://api.tendcomputer.com/v2/runs?status=failed&limit=200${cursor:+&cursor=$cursor}" \
-H "Authorization: Bearer $TEND_API_KEY")
echo "$resp" | jq -r '.data[].id'
cursor=$(echo "$resp" | jq -r '.next_cursor // empty')
[ -z "$cursor" ] && break
done#Filtering results
Filters are query parameters. Multiple filters combine with AND. Repeating a parameter, as in status=failed&status=retrying, combines the values with OR within that field. Unknown parameters are rejected instead of ignored, which catches typos early.
| Parameter | Applies to | Description |
|---|---|---|
status | jobs, runs | One of scheduled, queued, running, retrying, succeeded, failed, cancelled. Repeatable. |
schedule_id | jobs, runs | Only records created by the given schedule, for example sch_01J7Z3K8PQ2W4M6V9XD1CTRHNB. |
job_id | runs | Only runs belonging to one job. |
created_after, created_before | jobs, runs | RFC 3339 timestamps. The range is half-open: after is exclusive, before is inclusive. |
region | jobs, runs | One of us-east, us-west, eu-central, ap-southeast. |
tag | jobs, schedules | Exact match on a job or schedule tag. Repeatable. |
name | schedules | Exact match. Schedule names are unique per environment, so this returns zero or one result. |
curl -G "https://api.tendcomputer.com/v2/runs" \
-H "Authorization: Bearer tnd_live_9fK2xQ7mVb3LpR8dWc1Zh" \
--data-urlencode "status=failed" \
--data-urlencode "status=retrying" \
--data-urlencode "region=eu-central" \
--data-urlencode "created_after=2026-05-01T00:00:00Z" \
--data-urlencode "limit=200"runs = client.runs.list(
status=["failed", "retrying"],
region="eu-central",
created_after="2026-05-01T00:00:00Z",
limit=200,
)const runs = await client.runs.list({
status: ["failed", "retrying"],
region: "eu-central",
createdAfter: "2026-05-01T00:00:00Z",
limit: 200,
});#Sorting and result stability
All list endpoints return newest records first by default. Pass order=asc to reverse this. The sort key is always created_at with the record ID as a tiebreaker, so two jobs created in the same millisecond still have a deterministic order. Other sort keys are not supported; a filtered query on an indexed field is almost always what you actually wanted.
Cursors give you stable iteration in the face of concurrent writes. A record created while you are paging newest-first will not shift later pages and cause duplicates, because your position is anchored to a specific (created_at, id) pair rather than an offset. The trade-off is that records created after your first request may be missed when sorting descending. If you need a complete snapshot, pin the upper bound with created_before set to the time of your first request.
- Use
order=ascwithcreated_afterto build incremental syncs: remember the newestcreated_atyou processed and start from it next time. - Runs are immutable once they reach a terminal status (
succeeded,failed,cancelled), so incremental syncs of finished runs will not miss updates. - Jobs are mutable. A job you already processed can change status, so re-fetch by ID when you need current state.
#Errors you may hit while paging
| HTTP | Code | Typical cause while paging |
|---|---|---|
| 400 | invalid_request | limit above 200, an unknown filter name, a malformed timestamp, or a cursor reused with different filters. The details field lists the offending parameters. |
| 401 | invalid_api_key | The key was revoked mid-iteration. Rotate and restart from the beginning. |
| 404 | resource_not_found | A schedule_id or job_id filter references a resource in the other environment. Keys from tnd_dev_ and tnd_live_ cannot see each other's resources. |
| 429 | rate_limit_exceeded | The project exceeded its requests-per-minute limit. Sleep for Retry-After seconds, then resume with the same cursor. |
Cursors remain valid for 24 hours after issue. A rate-limited walk can safely pause and resume with the last cursor you received. An expired cursor returns 400 invalid_request with cursor in details; restart the walk with created_before set to the oldest created_at you saw.