Every paginated collection in the SDK is exposed three ways. They hit the same endpoint and differ only in how much they buffer.
| Method | Returns | Requests | Use when |
|---|---|---|---|
list | Envless::Page | Exactly one | You are rendering a page, or you only need a count |
list_all | Array | As many as it takes | You need the whole set in memory anyway |
iterate | nil with a block, an Enumerator without one | As many as it takes, lazily | The set is large, or you can stop early |
require "envless/sdk"
page = Envless.products.list(limit: 50)
puts page.items.size
puts "#{page.pagination[:limit]} #{page.pagination[:offset]}"
puts "#{page.total_count} #{page.has_more?}"require "envless/sdk"
everything = Envless.products.list_all
puts everything.sizerequire "envless/sdk"
Envless.products.iterate do |product|
break if product[:slug] == "billing"
puts product[:name]
enditerate is the only one that can stop early. Breaking out of the block stops making requests, so scanning for one item in a large workspace costs one page, not all of them.
Without a block, iterate returns an Enumerator that fetches the next page only when it gets there, so the whole Enumerable toolbox works on a stream: first(10) makes one request, and find stops at the first match.
require "envless/sdk"
first_ten = Envless.products.iterate.first(10)
billing = Envless.products.iterate.find do |product|
product[:slug] == "billing"
end
puts first_ten.size
puts billing[:name] if billingAn Envless::Page is a Data value with items and pagination, plus has_more? and total_count, and its RBS type is generic over the item, so page.items is an Array[product_resource] to Steep.
Options
List methods take these as keyword arguments, after their positional slugs. Each list accepts only the options its route reads, so an option a route ignores raises ArgumentError:
| Option | Type | Notes |
|---|---|---|
limit: | Integer | 1 to 100. Defaults to 25 on the API. iterate and list_all default to 100 to minimise round trips, and clamp anything higher. |
offset: | Integer | Rows to skip. iterate and list_all use it as a starting point and then advance it themselves. |
search: | String or Symbol | Products, product projects, projects, environments, invites, members, roles, keys and webhooks. Each method page says what it matches. |
order: | String or Symbol | name, asc or desc, on products, product projects, projects, environments, keys and webhooks. |
sort_by: | String or Symbol | Roles only: name (the default), updated or created. |
role_id: | String | Members only: keeps the members holding that role. |
status: | String or Symbol | Webhook deliveries only: pending, processing, succeeded or failed. |
product: | String or Symbol | Projects, environments, variables, variable history and versions: disambiguates a project slug that exists in more than one product. |
timeout: | Integer, Float or Rational | Bounds each request of the walk, not the whole walk. |
A limit: or offset: that is not an Integer raises ArgumentError before anything is sent. A limit: outside 1 to 100 is refused by the API with 400 request.invalid_pagination rather than being clamped server-side, and error.field names the offending parameter.
How the walk terminates
list_all and iterate stop on either of two conditions, which matters because it makes them safe against a server that keeps saying hasMore:
hasMore is false
The normal case.
A page comes back empty
Even if hasMore is still true. Without this a bad cursor would loop forever.
The offset advances by the number of items actually received, not by limit, so a short page does not skip rows. A failure on any page fails the whole list_all call, and the rows it had already collected are lost; iterate has already yielded the earlier items, so it raises at the page that failed.
Secondary collections
A namespace can paginate more than one collection. Those get their own trio, named after what they read.
| Namespace | Primary | Secondary |
|---|---|---|
products | list, list_all, iterate | list_projects, list_all_projects, iterate_projects and access.list, access.list_all, access.iterate |
variables | list, list_all, iterate | list_history, list_all_history, iterate_history |
versions | list, list_all, iterate | list_entries, list_all_entries, iterate_entries |
webhooks | list, list_all, iterate | list_deliveries, list_all_deliveries, iterate_deliveries |
Two endpoints look like lists but are not paginated, because the whole set is small and fixed. Both return a plain Hash rather than a page:
| Method | Returns |
|---|---|
roles.list_permissions | permissions_resource with the full catalogue, a byResource index, and the system role sets |
webhooks.list_event_types | event_types_resource with eventTypes and totalCount |
One collection carries an extra field
products.access.list returns an Envless::ProductAccessPage, which is a normal page plus the product's privacy flag, read with private?. That flag tells you whether the grants matter at all: on a public product every member has access and the list is informational.
require "envless/sdk"
access = Envless.products.access.list("internal-tools")
unless access.private?
puts "Public product, every member already has access"
end
puts access.items.map { |entry| entry[:id] }access.list_all and access.iterate only stream items, so they drop the flag. If you need it, use access.list.
Counting without fetching
total_count is the size of the full filtered set, not the page, so a single one-row request is enough to count.
require "envless/sdk"
puts "#{Envless.members.list(limit: 1).total_count} members"