airbyte.agents.models

Response models for the Airbyte Agents API.

⚠️ Experimental Interface

The Airbyte Agents Python interfaces are experimental. Class names, method signatures, and result models may change or be removed without notice between minor versions of PyAirbyte. Pin an exact PyAirbyte version if you depend on them.

All models allow extra fields, because the Agents API returns rich connector-specific payloads that PyAirbyte deliberately does not attempt to model exhaustively.

  1# Copyright (c) 2026 Airbyte, Inc., all rights reserved.
  2"""Response models for the Airbyte Agents API.
  3
  4> ## ⚠️ Experimental Interface
  5>
  6> **The Airbyte Agents Python interfaces are experimental.** Class names, method signatures,
  7> and result models may change or be removed without notice between minor versions of
  8> PyAirbyte. Pin an exact PyAirbyte version if you depend on them.
  9
 10All models allow extra fields, because the Agents API returns rich connector-specific
 11payloads that PyAirbyte deliberately does not attempt to model exhaustively.
 12"""
 13
 14from __future__ import annotations
 15
 16from typing import Any
 17
 18from pydantic import BaseModel, ConfigDict, Field, field_validator
 19
 20from airbyte.exceptions import PyAirbyteInputError
 21
 22
 23class AgentWorkspaceInfo(BaseModel):
 24    """Summary information about a workspace, as returned by the Agents API."""
 25
 26    model_config = ConfigDict(extra="allow")
 27
 28    id: str
 29    """The workspace ID."""
 30
 31    name: str | None = None
 32    """The workspace name."""
 33
 34    organization_id: str | None = None
 35    """The ID of the organization the workspace belongs to."""
 36
 37    status: str | None = None
 38    """The workspace status, for example `active`."""
 39
 40
 41class AgentConnectorInfo(BaseModel):
 42    """Summary information about a connector, as returned by the Agents API."""
 43
 44    model_config = ConfigDict(extra="allow")
 45
 46    id: str
 47    """The connector ID."""
 48
 49    name: str | None = None
 50    """The connector name, for example `GitHub - <workspace_id>`."""
 51
 52
 53class AgentContextStoreEntity(BaseModel):
 54    """An entity that a connector supports caching in the Airbyte Context Store."""
 55
 56    model_config = ConfigDict(extra="allow")
 57
 58    entity: str
 59    """The entity name, for example `issues`."""
 60
 61    suggested: bool | None = None
 62    """Whether Airbyte suggests caching this entity."""
 63
 64
 65class AgentContextStoreReadiness(BaseModel):
 66    """Context Store readiness information for a connector."""
 67
 68    model_config = ConfigDict(extra="allow")
 69
 70    supported_context_store_entities: list[AgentContextStoreEntity] = Field(default_factory=list)
 71    """The entities this connector can cache in the Context Store."""
 72
 73    configured_cache_entities: list[dict[str, Any]] = Field(default_factory=list)
 74    """The entities currently configured for caching, with their sync status."""
 75
 76
 77class AgentSkillInfo(BaseModel):
 78    """Summary information about a skill, as returned by the Agents API."""
 79
 80    model_config = ConfigDict(extra="allow")
 81
 82    id: str
 83    """The skill ID. Pass it to `read_skill_docs` to read this skill's docs."""
 84
 85    kind: str | None = None
 86    """The skill category, for example `static` or `connector_source`."""
 87
 88    title: str | None = None
 89    """The human-readable skill title."""
 90
 91    summary: str | None = None
 92    """A short summary of what the skill documents."""
 93
 94    tags: list[str] = Field(default_factory=list)
 95    """Search and categorization tags for the skill."""
 96
 97    warnings: list[Any] = Field(default_factory=list)
 98    """Non-fatal issues reported while building or reading the skill's docs."""
 99
100
101class AgentSkillList(BaseModel):
102    """A page of skills, as returned by the Agents API."""
103
104    model_config = ConfigDict(extra="allow")
105
106    data: list[AgentSkillInfo]
107    """The skills on this page."""
108
109    next_cursor: str | None = None
110    """The cursor to pass as `cursor` to fetch the next page, when one is available."""
111
112
113class AgentSkillSection(BaseModel):
114    """A section of a skill's docs, as listed in the docs outline."""
115
116    model_config = ConfigDict(extra="allow")
117
118    id: str
119    """The section ID. Pass it as `section` to read this section."""
120
121    title: str | None = None
122    """The human-readable section title."""
123
124    summary: str | None = None
125    """A short summary of the section content."""
126
127    available: bool = True
128    """Whether this section can currently be read."""
129
130
131class AgentSkillDocs(BaseModel):
132    """Documentation for a single skill, as returned by the Agents API."""
133
134    model_config = ConfigDict(extra="allow")
135
136    metadata: AgentSkillInfo
137    """Metadata for the requested skill."""
138
139    outline: list[AgentSkillSection] = Field(default_factory=list)
140    """The sections available for this skill."""
141
142    section_id: str | None = None
143    """The requested section ID, or `None` for the default docs response."""
144
145    content: list[dict[str, Any]] = Field(default_factory=list)
146    """Rendered docs content blocks, such as headings, paragraphs, and code blocks."""
147
148
149class AgentConnectorDetails(BaseModel):
150    """Connector metadata returned by the Agents API `inspect` endpoint."""
151
152    model_config = ConfigDict(extra="allow", populate_by_name=True)
153
154    connector_id: str
155    """The connector ID."""
156
157    name: str | None = None
158    """The connector name."""
159
160    workspace_id: str | None = None
161    """The ID of the workspace the connector belongs to."""
162
163    organization_id: str | None = None
164    """The ID of the organization the connector belongs to."""
165
166    source_definition_id: str | None = None
167    """The ID of the underlying Airbyte source definition."""
168
169    integration_name: str | None = Field(default=None, alias="source_definition_name")
170    """Name of the underlying integration, for example `GitHub` or `Snowflake`."""
171
172    docs_skill_id: str | None = None
173    """Skill ID to pass to `AgentWorkspace.get_skill(...).read_docs()` (MCP:
174    `read_agent_skill_docs`) for this connector's usage docs."""
175
176    context_store_readiness: AgentContextStoreReadiness | None = None
177    """Context Store readiness information, when reported."""
178
179    warnings: list[Any] = Field(default_factory=list)
180    """Warnings reported by the Agents API, for example degraded capabilities."""
181
182    @property
183    def context_store_entities(self) -> list[str]:
184        """The entity names this connector can cache in the Context Store.
185
186        Note that this lists Context Store-supported entities specifically. The Agents API
187        does not publish an exhaustive list of executable entity and action pairs, so an
188        entity may be executable via `AgentConnector.execute()` without appearing here.
189        """
190        if self.context_store_readiness is None:
191            return []
192        return [
193            entity.entity
194            for entity in self.context_store_readiness.supported_context_store_entities
195        ]
196
197
198class AgentExecutionMetadata(BaseModel):
199    """Metadata describing how an Agents connector action was executed."""
200
201    model_config = ConfigDict(extra="allow")
202
203    connector_instance_id: str | None = None
204    """The connector instance that served the request."""
205
206    execution_time_ms: int | None = None
207    """The server-side execution time, in milliseconds."""
208
209
210class AgentConnectorMetadata(BaseModel):
211    """Connector-reported metadata about a single action's result, including pagination."""
212
213    model_config = ConfigDict(extra="allow")
214
215    has_next_page: bool | None = None
216    """Whether more entities are available after this page, when the connector reports it."""
217
218    end_cursor: str | None = None
219    """The cursor for the next page, when one is available. Pass it as `cursor` for Context Store
220    `search`, or as the connector's own cursor argument in `api_args` for direct connector
221    actions."""
222
223
224class AgentExecuteResult(BaseModel):
225    """The result of executing a single action against an Airbyte Agents connector."""
226
227    model_config = ConfigDict(extra="allow")
228
229    status: str
230    """The execution status reported by the Agents API, for example `success`."""
231
232    result: Any = None
233    """The action's payload. Entity-returning actions put a list of entities here."""
234
235    connector_metadata: AgentConnectorMetadata = Field(default_factory=AgentConnectorMetadata)
236    """Connector-reported metadata about the result, including pagination cursors."""
237
238    execution_metadata: AgentExecutionMetadata = Field(default_factory=AgentExecutionMetadata)
239    """Metadata describing how the action was executed."""
240
241    warning: dict[str, Any] | None = None
242    """A warning reported alongside an otherwise successful result."""
243
244    @field_validator("connector_metadata", "execution_metadata", mode="before")
245    @classmethod
246    def _none_to_empty(cls, value: object) -> object:
247        return {} if value is None else value
248
249    @property
250    def entities(self) -> list[dict[str, Any]]:
251        """The result as a list of entities.
252
253        Raises `PyAirbyteInputError` if the action did not return a list of entities. Use
254        `result` for actions whose payload is not a list of entities.
255        """
256        if not isinstance(self.result, list):
257            raise PyAirbyteInputError(
258                message="This action did not return a list of entities.",
259                guidance="Use the `result` attribute to read non-entity result payloads.",
260                context={"result_type": type(self.result).__name__},
261            )
262
263        invalid_types = sorted(
264            {type(entity).__name__ for entity in self.result if not isinstance(entity, dict)}
265        )
266        if invalid_types:
267            raise PyAirbyteInputError(
268                message="This action returned a list that is not a list of entities.",
269                guidance="Use the `result` attribute to read non-entity result payloads.",
270                context={"unexpected_item_types": invalid_types},
271            )
272        return self.result
273
274    @property
275    def has_next_page(self) -> bool:
276        """Whether the connector reported more entities after this page."""
277        return bool(self.connector_metadata.has_next_page)
278
279    @property
280    def end_cursor(self) -> str | None:
281        """The cursor for the next page, or `None` when there is no next page."""
282        return self.connector_metadata.end_cursor
class AgentWorkspaceInfo(pydantic.main.BaseModel):
24class AgentWorkspaceInfo(BaseModel):
25    """Summary information about a workspace, as returned by the Agents API."""
26
27    model_config = ConfigDict(extra="allow")
28
29    id: str
30    """The workspace ID."""
31
32    name: str | None = None
33    """The workspace name."""
34
35    organization_id: str | None = None
36    """The ID of the organization the workspace belongs to."""
37
38    status: str | None = None
39    """The workspace status, for example `active`."""

Summary information about a workspace, as returned by the Agents API.

id: str = PydanticUndefined

The workspace ID.

name: str | None = None

The workspace name.

organization_id: str | None = None

The ID of the organization the workspace belongs to.

status: str | None = None

The workspace status, for example active.

class AgentConnectorInfo(pydantic.main.BaseModel):
42class AgentConnectorInfo(BaseModel):
43    """Summary information about a connector, as returned by the Agents API."""
44
45    model_config = ConfigDict(extra="allow")
46
47    id: str
48    """The connector ID."""
49
50    name: str | None = None
51    """The connector name, for example `GitHub - <workspace_id>`."""

Summary information about a connector, as returned by the Agents API.

id: str = PydanticUndefined

The connector ID.

name: str | None = None

The connector name, for example GitHub - <workspace_id>.

class AgentContextStoreEntity(pydantic.main.BaseModel):
54class AgentContextStoreEntity(BaseModel):
55    """An entity that a connector supports caching in the Airbyte Context Store."""
56
57    model_config = ConfigDict(extra="allow")
58
59    entity: str
60    """The entity name, for example `issues`."""
61
62    suggested: bool | None = None
63    """Whether Airbyte suggests caching this entity."""

An entity that a connector supports caching in the Airbyte Context Store.

entity: str = PydanticUndefined

The entity name, for example issues.

suggested: bool | None = None

Whether Airbyte suggests caching this entity.

class AgentContextStoreReadiness(pydantic.main.BaseModel):
66class AgentContextStoreReadiness(BaseModel):
67    """Context Store readiness information for a connector."""
68
69    model_config = ConfigDict(extra="allow")
70
71    supported_context_store_entities: list[AgentContextStoreEntity] = Field(default_factory=list)
72    """The entities this connector can cache in the Context Store."""
73
74    configured_cache_entities: list[dict[str, Any]] = Field(default_factory=list)
75    """The entities currently configured for caching, with their sync status."""

Context Store readiness information for a connector.

supported_context_store_entities: list[AgentContextStoreEntity] = PydanticUndefined

The entities this connector can cache in the Context Store.

configured_cache_entities: list[dict[str, typing.Any]] = PydanticUndefined

The entities currently configured for caching, with their sync status.

class AgentSkillInfo(pydantic.main.BaseModel):
78class AgentSkillInfo(BaseModel):
79    """Summary information about a skill, as returned by the Agents API."""
80
81    model_config = ConfigDict(extra="allow")
82
83    id: str
84    """The skill ID. Pass it to `read_skill_docs` to read this skill's docs."""
85
86    kind: str | None = None
87    """The skill category, for example `static` or `connector_source`."""
88
89    title: str | None = None
90    """The human-readable skill title."""
91
92    summary: str | None = None
93    """A short summary of what the skill documents."""
94
95    tags: list[str] = Field(default_factory=list)
96    """Search and categorization tags for the skill."""
97
98    warnings: list[Any] = Field(default_factory=list)
99    """Non-fatal issues reported while building or reading the skill's docs."""

Summary information about a skill, as returned by the Agents API.

id: str = PydanticUndefined

The skill ID. Pass it to read_skill_docs to read this skill's docs.

kind: str | None = None

The skill category, for example static or connector_source.

title: str | None = None

The human-readable skill title.

summary: str | None = None

A short summary of what the skill documents.

tags: list[str] = PydanticUndefined

Search and categorization tags for the skill.

warnings: list[typing.Any] = PydanticUndefined

Non-fatal issues reported while building or reading the skill's docs.

class AgentSkillList(pydantic.main.BaseModel):
102class AgentSkillList(BaseModel):
103    """A page of skills, as returned by the Agents API."""
104
105    model_config = ConfigDict(extra="allow")
106
107    data: list[AgentSkillInfo]
108    """The skills on this page."""
109
110    next_cursor: str | None = None
111    """The cursor to pass as `cursor` to fetch the next page, when one is available."""

A page of skills, as returned by the Agents API.

data: list[AgentSkillInfo] = PydanticUndefined

The skills on this page.

next_cursor: str | None = None

The cursor to pass as cursor to fetch the next page, when one is available.

class AgentSkillSection(pydantic.main.BaseModel):
114class AgentSkillSection(BaseModel):
115    """A section of a skill's docs, as listed in the docs outline."""
116
117    model_config = ConfigDict(extra="allow")
118
119    id: str
120    """The section ID. Pass it as `section` to read this section."""
121
122    title: str | None = None
123    """The human-readable section title."""
124
125    summary: str | None = None
126    """A short summary of the section content."""
127
128    available: bool = True
129    """Whether this section can currently be read."""

A section of a skill's docs, as listed in the docs outline.

id: str = PydanticUndefined

The section ID. Pass it as section to read this section.

title: str | None = None

The human-readable section title.

summary: str | None = None

A short summary of the section content.

available: bool = True

Whether this section can currently be read.

class AgentSkillDocs(pydantic.main.BaseModel):
132class AgentSkillDocs(BaseModel):
133    """Documentation for a single skill, as returned by the Agents API."""
134
135    model_config = ConfigDict(extra="allow")
136
137    metadata: AgentSkillInfo
138    """Metadata for the requested skill."""
139
140    outline: list[AgentSkillSection] = Field(default_factory=list)
141    """The sections available for this skill."""
142
143    section_id: str | None = None
144    """The requested section ID, or `None` for the default docs response."""
145
146    content: list[dict[str, Any]] = Field(default_factory=list)
147    """Rendered docs content blocks, such as headings, paragraphs, and code blocks."""

Documentation for a single skill, as returned by the Agents API.

metadata: AgentSkillInfo = PydanticUndefined

Metadata for the requested skill.

outline: list[AgentSkillSection] = PydanticUndefined

The sections available for this skill.

section_id: str | None = None

The requested section ID, or None for the default docs response.

content: list[dict[str, typing.Any]] = PydanticUndefined

Rendered docs content blocks, such as headings, paragraphs, and code blocks.

class AgentConnectorDetails(pydantic.main.BaseModel):
150class AgentConnectorDetails(BaseModel):
151    """Connector metadata returned by the Agents API `inspect` endpoint."""
152
153    model_config = ConfigDict(extra="allow", populate_by_name=True)
154
155    connector_id: str
156    """The connector ID."""
157
158    name: str | None = None
159    """The connector name."""
160
161    workspace_id: str | None = None
162    """The ID of the workspace the connector belongs to."""
163
164    organization_id: str | None = None
165    """The ID of the organization the connector belongs to."""
166
167    source_definition_id: str | None = None
168    """The ID of the underlying Airbyte source definition."""
169
170    integration_name: str | None = Field(default=None, alias="source_definition_name")
171    """Name of the underlying integration, for example `GitHub` or `Snowflake`."""
172
173    docs_skill_id: str | None = None
174    """Skill ID to pass to `AgentWorkspace.get_skill(...).read_docs()` (MCP:
175    `read_agent_skill_docs`) for this connector's usage docs."""
176
177    context_store_readiness: AgentContextStoreReadiness | None = None
178    """Context Store readiness information, when reported."""
179
180    warnings: list[Any] = Field(default_factory=list)
181    """Warnings reported by the Agents API, for example degraded capabilities."""
182
183    @property
184    def context_store_entities(self) -> list[str]:
185        """The entity names this connector can cache in the Context Store.
186
187        Note that this lists Context Store-supported entities specifically. The Agents API
188        does not publish an exhaustive list of executable entity and action pairs, so an
189        entity may be executable via `AgentConnector.execute()` without appearing here.
190        """
191        if self.context_store_readiness is None:
192            return []
193        return [
194            entity.entity
195            for entity in self.context_store_readiness.supported_context_store_entities
196        ]

Connector metadata returned by the Agents API inspect endpoint.

connector_id: str = PydanticUndefined

The connector ID.

name: str | None = None

The connector name.

workspace_id: str | None = None

The ID of the workspace the connector belongs to.

organization_id: str | None = None

The ID of the organization the connector belongs to.

source_definition_id: str | None = None

The ID of the underlying Airbyte source definition.

integration_name: str | None = None

Name of the underlying integration, for example GitHub or Snowflake.

docs_skill_id: str | None = None

Skill ID to pass to AgentWorkspace.get_skill(...).read_docs() (MCP: read_agent_skill_docs) for this connector's usage docs.

context_store_readiness: AgentContextStoreReadiness | None = None

Context Store readiness information, when reported.

warnings: list[typing.Any] = PydanticUndefined

Warnings reported by the Agents API, for example degraded capabilities.

context_store_entities: list[str]
183    @property
184    def context_store_entities(self) -> list[str]:
185        """The entity names this connector can cache in the Context Store.
186
187        Note that this lists Context Store-supported entities specifically. The Agents API
188        does not publish an exhaustive list of executable entity and action pairs, so an
189        entity may be executable via `AgentConnector.execute()` without appearing here.
190        """
191        if self.context_store_readiness is None:
192            return []
193        return [
194            entity.entity
195            for entity in self.context_store_readiness.supported_context_store_entities
196        ]

The entity names this connector can cache in the Context Store.

Note that this lists Context Store-supported entities specifically. The Agents API does not publish an exhaustive list of executable entity and action pairs, so an entity may be executable via AgentConnector.execute() without appearing here.

class AgentExecutionMetadata(pydantic.main.BaseModel):
199class AgentExecutionMetadata(BaseModel):
200    """Metadata describing how an Agents connector action was executed."""
201
202    model_config = ConfigDict(extra="allow")
203
204    connector_instance_id: str | None = None
205    """The connector instance that served the request."""
206
207    execution_time_ms: int | None = None
208    """The server-side execution time, in milliseconds."""

Metadata describing how an Agents connector action was executed.

connector_instance_id: str | None = None

The connector instance that served the request.

execution_time_ms: int | None = None

The server-side execution time, in milliseconds.

class AgentConnectorMetadata(pydantic.main.BaseModel):
211class AgentConnectorMetadata(BaseModel):
212    """Connector-reported metadata about a single action's result, including pagination."""
213
214    model_config = ConfigDict(extra="allow")
215
216    has_next_page: bool | None = None
217    """Whether more entities are available after this page, when the connector reports it."""
218
219    end_cursor: str | None = None
220    """The cursor for the next page, when one is available. Pass it as `cursor` for Context Store
221    `search`, or as the connector's own cursor argument in `api_args` for direct connector
222    actions."""

Connector-reported metadata about a single action's result, including pagination.

has_next_page: bool | None = None

Whether more entities are available after this page, when the connector reports it.

end_cursor: str | None = None

The cursor for the next page, when one is available. Pass it as cursor for Context Store search, or as the connector's own cursor argument in api_args for direct connector actions.

class AgentExecuteResult(pydantic.main.BaseModel):
225class AgentExecuteResult(BaseModel):
226    """The result of executing a single action against an Airbyte Agents connector."""
227
228    model_config = ConfigDict(extra="allow")
229
230    status: str
231    """The execution status reported by the Agents API, for example `success`."""
232
233    result: Any = None
234    """The action's payload. Entity-returning actions put a list of entities here."""
235
236    connector_metadata: AgentConnectorMetadata = Field(default_factory=AgentConnectorMetadata)
237    """Connector-reported metadata about the result, including pagination cursors."""
238
239    execution_metadata: AgentExecutionMetadata = Field(default_factory=AgentExecutionMetadata)
240    """Metadata describing how the action was executed."""
241
242    warning: dict[str, Any] | None = None
243    """A warning reported alongside an otherwise successful result."""
244
245    @field_validator("connector_metadata", "execution_metadata", mode="before")
246    @classmethod
247    def _none_to_empty(cls, value: object) -> object:
248        return {} if value is None else value
249
250    @property
251    def entities(self) -> list[dict[str, Any]]:
252        """The result as a list of entities.
253
254        Raises `PyAirbyteInputError` if the action did not return a list of entities. Use
255        `result` for actions whose payload is not a list of entities.
256        """
257        if not isinstance(self.result, list):
258            raise PyAirbyteInputError(
259                message="This action did not return a list of entities.",
260                guidance="Use the `result` attribute to read non-entity result payloads.",
261                context={"result_type": type(self.result).__name__},
262            )
263
264        invalid_types = sorted(
265            {type(entity).__name__ for entity in self.result if not isinstance(entity, dict)}
266        )
267        if invalid_types:
268            raise PyAirbyteInputError(
269                message="This action returned a list that is not a list of entities.",
270                guidance="Use the `result` attribute to read non-entity result payloads.",
271                context={"unexpected_item_types": invalid_types},
272            )
273        return self.result
274
275    @property
276    def has_next_page(self) -> bool:
277        """Whether the connector reported more entities after this page."""
278        return bool(self.connector_metadata.has_next_page)
279
280    @property
281    def end_cursor(self) -> str | None:
282        """The cursor for the next page, or `None` when there is no next page."""
283        return self.connector_metadata.end_cursor

The result of executing a single action against an Airbyte Agents connector.

status: str = PydanticUndefined

The execution status reported by the Agents API, for example success.

result: Any = None

The action's payload. Entity-returning actions put a list of entities here.

connector_metadata: AgentConnectorMetadata = PydanticUndefined

Connector-reported metadata about the result, including pagination cursors.

execution_metadata: AgentExecutionMetadata = PydanticUndefined

Metadata describing how the action was executed.

warning: dict[str, typing.Any] | None = None

A warning reported alongside an otherwise successful result.

entities: list[dict[str, typing.Any]]
250    @property
251    def entities(self) -> list[dict[str, Any]]:
252        """The result as a list of entities.
253
254        Raises `PyAirbyteInputError` if the action did not return a list of entities. Use
255        `result` for actions whose payload is not a list of entities.
256        """
257        if not isinstance(self.result, list):
258            raise PyAirbyteInputError(
259                message="This action did not return a list of entities.",
260                guidance="Use the `result` attribute to read non-entity result payloads.",
261                context={"result_type": type(self.result).__name__},
262            )
263
264        invalid_types = sorted(
265            {type(entity).__name__ for entity in self.result if not isinstance(entity, dict)}
266        )
267        if invalid_types:
268            raise PyAirbyteInputError(
269                message="This action returned a list that is not a list of entities.",
270                guidance="Use the `result` attribute to read non-entity result payloads.",
271                context={"unexpected_item_types": invalid_types},
272            )
273        return self.result

The result as a list of entities.

Raises PyAirbyteInputError if the action did not return a list of entities. Use result for actions whose payload is not a list of entities.

has_next_page: bool
275    @property
276    def has_next_page(self) -> bool:
277        """Whether the connector reported more entities after this page."""
278        return bool(self.connector_metadata.has_next_page)

Whether the connector reported more entities after this page.

end_cursor: str | None
280    @property
281    def end_cursor(self) -> str | None:
282        """The cursor for the next page, or `None` when there is no next page."""
283        return self.connector_metadata.end_cursor

The cursor for the next page, or None when there is no next page.