List Records

Query, filter, sort, and paginate records across your organization with the todos query.


Use the recordQueries.todos query to read records from Blue with filtering, sorting, and pagination. Records are Record objects in the API; in the app they appear as the cards inside a workspace’s lists. A single call can span one organization, several workspaces, or a precise slice defined by assignees, tags, dates, and custom-field conditions.

This is the only operation that filters records server-side. (In-app search filters client-side after records load, so it can’t reach records beyond what the client has fetched.)

Request

The query lives under the recordQueries namespace. The only required input is companyIds — one or more organization IDs or slugs.

query ListRecords {
  recordQueries {
    todos(filter: { companyIds: ["company_123"] }) {
      items {
        id
        title
        done
        duedAt
      }
      pageInfo {
        totalItems
        hasNextPage
      }
    }
  }
}

The companyIds, projectIds, and todoListIds arrays each accept IDs or slugs.

Parameters

Arguments

ArgumentTypeRequiredDescription
filterTodosFilter!YesFilter criteria (below). companyIds is the only required field.
sort[TodosSort!]NoOrdering. Defaults to [] (the engine’s natural order). Multiple keys apply in sequence.
limitIntNoItems per page. Omit to get 20. Any value over 500 — and any value <= 0 — is coerced to 500 (the max).
skipIntNoItems to skip for offset pagination. Defaults to 0. Cannot be combined with after.
afterStringNoOpaque keyset cursor for constant-cost pagination on large single-workspace scans. Opt-in and narrowly scoped — see Cursor pagination.
limit: 0 is not the default

Omitting limit yields 20, but passing 0 (or a negative number) yields 500, not 20. Pass the explicit page size you want.

TodosFilter

The flat fields below combine with AND by default. For mixed AND/OR logic and the full set of conditions (record-name match, completion-date range, last-updated-by actor), use groups instead — see Group filtering.

FieldTypeDescription
companyIds[String!]!Required. Organization IDs or slugs to query.
projectIds[String!]Restrict to these workspace IDs or slugs.
todoIds[String!]Return only these specific records.
assigneeIds[String!]Records assigned to any of these users.
unassignedBooleanWhen true, only records with no assignees.
tagIds[String!]Records carrying any of these tags.
tagColors[String!]Filter by tag color.
tagTitles[String!]Filter by tag title.
todoListIds[String!]Records in these lists.
todoListTitles[String!]Records in lists with these titles.
doneBooleanFilter by completion state. Prefer showCompleted for the common “hide completed” case.
showCompletedBooleanWhen false, completed records are excluded.
startedAtDateTimeRecords with this exact start date.
duedAtDateTimeRecords with this exact due date.
dueStartDateTimeDue date on or after this (inclusive).
dueEndDateTimeDue date on or before this (inclusive).
duedAtStartDateTimeAlias of dueStart.
duedAtEndDateTimeAlias of dueEnd.
createdStartDateTimeCreated on or after this.
createdEndDateTimeCreated on or before this.
updatedAt_gtDateTimeUpdated strictly after this.
updatedAt_gteDateTimeUpdated on or after this.
searchStringMatch title and text content (case-insensitive).
qStringAlias of search.
excludeArchivedProjectsBooleanExclude records in archived workspaces.
archivedBooleanArchive scope. Omitted/false → active records (archived excluded); true → only archived records (needs the view-archived permission). See Archive scope.
recordNameStringFull-text record-title search (indexed; CONTAINS by default, override with recordNameOp).
coordinatesJSONPolygon coordinates for map-view geo filtering.
notAssigneeIds[String!]Exclude records assigned to any of these users.
notTagIds[String!]Exclude records carrying any of these tags.
notColors[String!]Exclude records with these tag colors.
notTodoListIds[String!]Exclude records in these lists.
hasTagBooleanRecords that have (or, when false, lack) any tag.
hasColorBooleanRecords that have (or lack) a tag color.
hasDueDateBooleanRecords that have (or lack) a due date.
hasDescriptionBooleanRecords that have (or lack) description text.
hasChecklistBooleanRecords that have (or lack) a checklist.
hasDependencyBooleanRecords that have (or lack) a dependency.
hasReferenceBooleanRecords that have (or lack) a reference.
fieldsJSONCustom-field conditions. See Custom-field filtering.
opFilterLogicalOperatorLogical operator applied across fields entries. Defaults to AND.
groups[TodoFilterGroupInput!]Nested filter groups. When provided, every flat field above is ignored in favor of the groups. See Group filtering.
groupLinks[FilterLogicalOperator!]Operators linking adjacent groups; length should be groups.length - 1. A shorter list repeats its last operator.

TodosSort

Each value is a field_DIRECTION enum member. Pass an array to sort by multiple keys in order.

ValueSorts by
position_ASC / position_DESCPosition within the list (the default list order).
title_ASC / title_DESCRecord title, alphabetically.
createdAt_ASC / createdAt_DESCCreation date.
updatedAt_ASC / updatedAt_DESCLast-updated date.
completedAt_ASC / completedAt_DESCCompletion date.
startedAt_ASC / startedAt_DESCStart date.
duedAt_ASC / duedAt_DESCDue date.
createdBy_ASC / createdBy_DESCCreator.
assignees_ASC / assignees_DESCAssignees.
todoTags_ASC / todoTags_DESCTags.
todoListTitle_ASC / todoListTitle_DESCList title.
todoListPosition_ASC / todoListPosition_DESCList position.
projectName_ASC / projectName_DESCWorkspace name.
checklistTitle_ASC / checklistTitle_DESCChecklist title.
checklistItemTitle_ASC / checklistItemTitle_DESCChecklist-item title.
todoCustomFieldDate_ASC / todoCustomFieldDate_DESCA date custom field.
todoCustomFieldSelectSingle_ASC / todoCustomFieldSelectSingle_DESCA single-select custom field.
todoCustomFieldSelectMulti_ASC / todoCustomFieldSelectMulti_DESCA multi-select custom field.
todoCustomFieldAssignee_ASC / todoCustomFieldAssignee_DESCAn assignee custom field.
todoCustomFieldRollup_ASC / todoCustomFieldRollup_DESCA rollup custom field.

FilterLogicalOperator

ValueMeaning
ANDAll conditions must match.
ORAny condition may match.

Response

{
  "data": {
    "recordQueries": {
      "todos": {
        "items": [
          {
            "id": "clm4n8qwx000008l0g4oxdqn7",
            "title": "Draft Q3 launch plan",
            "done": false,
            "duedAt": "2026-07-15T17:00:00.000Z"
          },
          {
            "id": "clm4n8qwx000108l0haz1f2pe",
            "title": "Review pricing page copy",
            "done": true,
            "duedAt": null
          }
        ],
        "pageInfo": {
          "totalItems": 2,
          "hasNextPage": false
        }
      }
    }
  }
}

TodosResult

FieldTypeDescription
items[Record!]!The records on this page.
pageInfoPageInfo!Pagination metadata.

PageInfo

FieldTypeDescription
totalItemsIntTotal records matching the filter, across all pages. null on cursor (after) pages — the exact-count query is skipped for speed.
totalPagesIntTotal pages at the current limit. null on cursor (after) pages.
pageIntCurrent page, derived from skip and limit.
perPageIntPage size in effect (the resolved limit).
hasNextPageBoolean!Whether a next page exists.
hasPreviousPageBoolean!Whether a previous page exists.
nextCursorStringOpaque cursor to pass as after for the next page. Returned on cursor-eligible queries (see Cursor pagination); null on the final page and on any query not eligible for cursor pagination.
startCursorStringDeprecated. Use nextCursor with the after argument instead.
endCursorStringDeprecated. See nextCursor.

Record

The most useful fields on each returned record. The Record type exposes more — these are the ones you’ll select most often.

FieldTypeDescription
idID!Unique identifier.
uidString!Short, human-facing identifier.
positionFloat!Position within its list.
titleString!Record title.
textString!Description as plain text.
htmlString!Description as HTML.
startedAtDateTimeStart date.
duedAtDateTimeDue date.
timezoneStringTimezone for the record’s dates.
colorStringColor label.
coverStringCover image URL.
coverLockedBooleanWhether the cover is locked from auto-updates.
doneBoolean!Completion state. There is no status enum — completion is this boolean.
completedAtDateTimeWhen the record was last marked complete.
archivedBoolean!Whether the record is archived.
createdAtDateTime!Creation timestamp.
updatedAtDateTime!Last-update timestamp.
createdByUserThe user who created the record. Select fullName/email, not name.
lastUpdatedActorTodoActorPolymorphic actor (user, automation, API key, system, or form) that last updated the record. Nullable for older rows.
commentCountInt!Number of comments.
checklistCountInt!Total checklist items.
checklistCompletedCountInt!Completed checklist items.
isRepeatingBoolean!Whether the record recurs.
repeatingJSONRecurrence configuration.
reminderJSONReminder configuration.
isReadBooleanRead state for the current user.
isSeenBooleanSeen state for the current user.
todoListRecordList!The list this record belongs to.
todoLists[RecordList!]All lists this record appears in (for cross-list records).
repeatingTodoListRecordListThe list a recurring record was generated into.
users[User!]!Assigned users. Select fullName/email, not name.
tags[Tag!]!Tags. Select title/color (a Tag has no name).
checklists[Checklist!]!Checklists on the record.
customFields[CustomField!]!Custom-field values. Select value fields on each element (name, type, value, text, number, checked, selectedOption { title }, selectedOptions { title }).
dependOn[Record!]Records this one depends on (blocking).
dependBy[Record!]Records that depend on this one.
timeTrackingTimeTrackingTime-tracking summary.

Full example

Filter to open, high-priority records in two workspaces, due in 2026, sorted by due date then list position, with a rich selection set.

query ListRecordsAdvanced {
  recordQueries {
    todos(
      filter: {
        companyIds: ["company_123"]
        projectIds: ["project_123", "project_456"]
        assigneeIds: ["user_123"]
        tagIds: ["tag_123"]
        showCompleted: false
        dueStart: "2026-01-01T00:00:00Z"
        dueEnd: "2026-12-31T23:59:59Z"
        search: "launch"
        excludeArchivedProjects: true
        fields: [
          {
            type: "CUSTOM_FIELD"
            customFieldId: "field_123"
            customFieldType: "SELECT_SINGLE"
            op: "IN"
            values: ["option_123", "option_456"]
          }
        ]
        op: AND
      }
      sort: [duedAt_ASC, todoListPosition_ASC]
      limit: 50
      skip: 0
    ) {
      items {
        id
        uid
        title
        text
        duedAt
        done
        completedAt
        todoList {
          id
          title
        }
        users {
          id
          fullName
          email
        }
        tags {
          id
          title
          color
        }
        customFields {
          id
          name
          type
          value
          selectedOption {
            title
          }
        }
        createdBy {
          id
          fullName
        }
      }
      pageInfo {
        totalPages
        totalItems
        page
        perPage
        hasNextPage
        hasPreviousPage
      }
    }
  }
}

Archive scope

Archiving takes a record out of its list without deleting it (see Archive a record). The archived filter selects which side of that partition you read:

archived valueReturns
omittedActive records only — archived records are excluded. Default.
falseActive records only — identical to omitting it.
trueArchived records only. Active records are excluded.

There is no value that returns active and archived records together in a single call — the filter is a partition, not an inclusion toggle, so each query reads one side or the other. To assemble a complete picture, run the query twice (once with archived: false or omitted, once with archived: true) and merge the two result sets client-side.

query ArchivedRecords {
  recordQueries {
    todos(filter: { companyIds: ["company_123"], projectIds: ["project_123"], archived: true }) {
      items {
        id
        title
        archived
        archivedAt
      }
      pageInfo {
        totalItems
      }
    }
  }
}
Archived results need the view-archived permission

archived: true is honored only for roles permitted to view archived records. A role without that permission is silently downgraded to the active scope — it gets active records back rather than an error — so archived: true can never leak archived rows to a caller who shouldn’t see them.

List all records in a workspace

To read every record in one workspace, filter by that workspace and page through the results. companyIds is always required; add the workspace to projectIds and omit todoListIds so the query spans every list:

query AllWorkspaceRecords {
  recordQueries {
    todos(
      filter: { companyIds: ["company_123"], projectIds: ["project_123"] }
      limit: 500
      skip: 0
    ) {
      items {
        id
        title
        todoList {
          id
          title
        }
      }
      pageInfo {
        totalItems
        hasNextPage
      }
    }
  }
}

A few things to keep in mind:

  • Paginate to completion. A page is capped at 500 (the limit ceiling). Increment skip by your page size until pageInfo.hasNextPage is false; pageInfo.totalItems gives you the full count up front so you can size the loop.
  • This returns active records only. Archived records are excluded by default — see Archive scope. To include them, run the same query a second time with archived: true and combine the pages.
  • One query covers the whole workspace. There’s no per-list call to make — a single query with no todoListIds returns records from every list in the workspace.
  • Span workspaces or the whole org. projectIds accepts several IDs to cover multiple workspaces at once; drop projectIds entirely to read across every workspace in the organizations named in companyIds.
  • Visibility still applies. Records the caller can’t see — restricted lists, assigned-only roles, tag-based rules — are filtered out silently rather than erroring.

Cursor pagination

Offset pagination (skip) is simple, but it gets more expensive the deeper you page — reaching row 100,000 still means the database walks past the 99,999 rows before it. For large single-workspace scans, the optional after argument provides a keyset cursor that seeks directly to the page boundary through an index, so page 100 costs the same as page 1.

after is opt-in and deliberately narrow. It is honored only when all of the following hold; violating any one returns a BAD_USER_INPUT error rather than silently falling back to offset mode:

  • The query is scoped to a single workspace that does not use multi-list records (records that appear in more than one list). Cross-workspace queries are not supported yet — use skip.
  • sort is exactly [position_DESC]. Passing any other sort — including omitting sort — is rejected; use skip for other orderings.
  • after is not combined with skip.

To start a chain, run a request that meets those conditions (with or without skip) and read pageInfo.nextCursor — an eligible offset request hands back a usable cursor from page 1, so there is no dedicated “give me a cursor” round trip. Pass that value as after on the next request, and keep following nextCursor until it returns null, which marks the final page.

Cursor pages skip the exact-count query for speed, so pageInfo.totalItems and totalPages come back null on any request that passes after. Drive the loop with hasNextPage/nextCursor instead of a total.

query CursorPage {
  recordQueries {
    todos(
      filter: { companyIds: ["company_123"], projectIds: ["project_123"] }
      sort: [position_DESC]
      limit: 100
      after: "eyJwb3NpdGlvbiI6..."
    ) {
      items {
        id
        title
      }
      pageInfo {
        hasNextPage
        nextCursor
      }
    }
  }
}

Custom-field filtering

TodosFilter.fields is typed JSON. Each entry must include a type discriminator — entries without one are silently dropped. The query layer routes on type:

typeFilters by
CUSTOM_FIELDA custom field’s value.
FIELDA built-in record field.
TIME_IN_LISTHow long a record has sat in its current list.
RELATIVE_DUEDATEA due date relative to now (e.g. “overdue”, “due this week”).

A CUSTOM_FIELD entry has this shape:

{
  "type": "CUSTOM_FIELD",
  "customFieldId": "field_123",
  "customFieldType": "SELECT_SINGLE",
  "op": "IN",
  "values": ["option_123", "option_456"]
}
KeyTypeDescription
typeStringMust be "CUSTOM_FIELD".
customFieldIdStringThe custom field’s ID.
customFieldTypeStringOne of the CustomFieldType enum values (below). Determines how the value is matched.
opStringA FilterComparisonOperator: IS, NOT, EQ, NE, IN, NIN, GT, GTE, LT, LTE, CONTAINS, NOT_CONTAINS. Which operators apply depends on the field type.
valuesdepends on typeOption IDs for selects, a number for numeric types, a boolean for CHECKBOX, a string for text types.

Filterable custom-field types

customFieldType must be a CustomFieldType enum value. These types can be filtered:

TEXT_SINGLE, TEXT_MULTI, URL, EMAIL, PHONE, UNIQUE_ID (text matching), NUMBER, CURRENCY, PERCENT, RATING, FORMULA (numeric comparison), CHECKBOX (IS true / IS false), SELECT_SINGLE, SELECT_MULTI (option matching), COUNTRY, DATE, ASSIGNEE, REFERENCE, LOOKUP, and ROLLUP.

The remaining CustomFieldType values are not filterable: LOCATION, FILE, TIME_DURATION, BUTTON, CURRENCY_CONVERSION, and REFERENCED_BY.

Group filtering

For mixed AND/OR logic, supply groups. When groups is present, the flat TodosFilter fields are ignored. Each group is a TodoFilterGroupInput with its own internal operator (op), and adjacent groups are linked by groupLinks.

query ListRecordsByGroups {
  recordQueries {
    todos(
      filter: {
        companyIds: ["company_123"]
        groups: [
          { op: AND, tagIds: ["tag_123"], showCompleted: false }
          { op: OR, recordName: "launch", recordNameOp: CONTAINS }
        ]
        groupLinks: [AND]
      }
      limit: 50
    ) {
      items {
        id
        title
      }
      pageInfo {
        totalItems
      }
    }
  }
}

TodoFilterGroupInput accepts the same assignee/tag/list/date/existence fields as the flat filter, plus group-only conditions:

FieldTypeDescription
opFilterLogicalOperatorHow conditions combine within this group.
completedStartDateTimeCompleted on or after this (latest mark-complete timestamp on currently-done records).
completedEndDateTimeCompleted on or before this.
recordNameStringMatch against the record title (full-text).
recordNameOpFilterComparisonOperatorOperator for recordName (e.g. CONTAINS, EQ).
lastUpdatedByUserIds[String!]Records last updated by any of these users.
lastUpdatedByAutomationIds[String!]Records last updated by any of these automations.
lastUpdatedByActorTypes[ActorType!]Records whose last-update actor is one of these types: USER, AUTOMATION, API_KEY, SYSTEM, FORM.

The three lastUpdatedBy* axes compose with OR — a record matches if it satisfies any supplied axis. fields works exactly as in Custom-field filtering.

Errors

This query degrades gracefully: an inaccessible workspace or a non-existent record ID inside an organization you can access simply contributes no rows, rather than raising an error. Errors are reserved for authentication and authorization.

CodeWhen
UNAUTHENTICATEDThe request carries no valid credentials.
FORBIDDENThe token is valid but is not a member of the requested organization, or the organization is inactive.

Permissions

The caller must be authenticated and a member of every organization in companyIds; the organization must be active. Beyond that, visibility is scoped per user:

  • Records in workspaces the user can’t access are excluded.
  • Users restricted to assigned-only records see only their own.
  • Hidden lists (per role configuration) are excluded automatically.
  • Tag-based access rules further narrow results.

This filtering is applied silently — restricted records are absent from items, not flagged.