Pagination, Filtering, and Includes
How Page[T] works, how iter() walks all pages, AND filter semantics, include= for related data, and ordering with order_by and cursor.
Every collection in the Management API follows the same pagination, filtering, and includes contract. Learning it once applies to teams, memberships, workflow nodes, node labels, and audit events.
Page[T] — the paginated result
All list(...) methods return a Page[T]:
@dataclass(frozen=True)
class Page(Generic[T]):
items: list[T] # the items in this page
next_cursor: str | None # cursor to fetch the next page; None if no more pages
total: int | None # total count, if available; may be NoneExample:
page = project.teams.list(limit=50)
for team in page.items:
print(team.id)
if page.next_cursor:
next_page = project.teams.list(limit=50, cursor=page.next_cursor)iter() — walk all pages automatically
Use iter(...) to transparently walk all pages without managing cursors:
for team in project.teams.iter(category="engineering"):
print(team.id, team.name)iter() accepts the same filter kwargs as list() but not limit or cursor. It fetches pages lazily as you consume the iterator.
Filters — AND semantics
Filters are conjunctive: all supplied filter kwargs must match. An item is returned only if it satisfies every condition.
# Matches teams where name contains "Photonics" AND category == "engineering":
page = project.teams.list(name="Photonics", category="engineering")
# Matches workflow nodes where label == "SimulationRun" AND solver property == "fdtd":
page = project.workflow_nodes.list(
label="SimulationRun",
has_property={"solver": "fdtd"},
)Omitting a filter means "no constraint on that field".
Filter kwargs per collection
Teams:
| Filter | Type | Meaning |
|---|---|---|
name | str | Team name match. |
category | str | Team category match. |
Team memberships:
| Filter | Type | Meaning |
|---|---|---|
user_id | str | Filter by user. |
role | str | Filter by role. |
Workflow nodes:
| Filter | Type | Meaning |
|---|---|---|
label | str | Node label name. |
created_by | str | User ID of creator. |
has_property | dict[str, str] | One or more property key=value pairs (AND). |
Audit events:
| Filter | Type | Meaning |
|---|---|---|
action | str | Event action string. |
category | str | Event category. |
level | str | Severity level: debug, info, warn, error. |
actor_id | str | User ID of the actor. |
target_type | str | Target entity type: organization, project, team, user, workflow_node, file, invitation, permission_group. |
request_id | str | Request correlation ID. |
since | datetime | Events at or after this timestamp. |
until | datetime | Events at or before this timestamp. |
include=() — load related data
By default, related objects are not fetched — their fields are None. Pass include= to request them:
# Fetch workflow node with all related data:
node = project.workflow_nodes.get(
"node-id",
include=["label", "properties", "attachments", "parents", "children"],
)
# Fetch team membership with user and team populated:
membership = team.memberships.get(
user_id="user-id",
include=["user", "team"],
)
# Fetch team with member count:
team = project.teams.get("team-id", include=["counts"])Include options by collection:
| Collection | Available includes |
|---|---|
| Teams | "counts" |
| Team memberships | "user", "team" |
| Workflow nodes | "label", "properties", "attachments", "parents", "children" |
Ordering
All list() and iter() calls accept order_by and order:
page = project.workflow_nodes.list(
order_by="created_at",
order="desc", # "asc" | "desc"
)order_by options per collection
| Collection | order_by options | Default |
|---|---|---|
| Teams | name, created_at, updated_at, category | — |
| Team memberships | joined_at, role, user_name | — |
| Workflow nodes | created_at, updated_at, display_name, label | created_at desc |
| Node labels | name, id | — |
| Audit events | timestamp, action, level | timestamp desc |
Cursor-based pagination
Cursors are opaque strings. Pass next_cursor from one page as cursor= on the next:
cursor = None
while True:
page = project.workflow_nodes.list(label="SimulationRun", cursor=cursor, limit=100)
for node in page.items:
process(node)
if page.next_cursor is None:
break
cursor = page.next_cursorPrefer iter() over manual cursor management for simple iteration.
Credentials vs Config
The difference between ~/.optixlog/credentials.toml (secret, per-user) and optixlog.toml (committed, per-project) — what goes where and resolution precedence.
Lookup Patterns
When to use get vs get_or_none vs one vs first vs count vs iter — exact raise semantics and when each is appropriate.