Outline Python client
An unofficial typed Python client and command-line interface for the Outline knowledge base API - no affiliation with Outline is implied or intended.
Outline's API is RPC-style: every method is a POST to https://your-outline/api/:method. This package wraps all 154 of them in a fully type-hinted client built on Pydantic models generated from the published OpenAPI specification, plus an outline CLI for reaching them from the terminal.
Features
- Complete - every method in the specification, on both the sync and async clients and in the CLI.
- Typed models - schemas are generated from the OpenAPI specification with datamodel-code-generator, so
make schemasis the whole upgrade path when the spec moves. - Sync and async -
OutlineClientandAsyncOutlineClienttake the same arguments and return the same models. - Python client and CLI - use it as a library, or straight from the shell via
outline. - Typed errors - a failed call raises
NotFoundError,RateLimitError,AuthorizationErrorand friends rather than a bare exception. - Forward compatible - fields Outline adds before the specification catches up are preserved rather than dropped.
Installation
The package is published on PyPI as outline-client:
pip install outline-client
# or, with uv:
uv add outline-client
Requires Python 3.12+.
Configuration
An API token is the only credential this client uses. Create one under Settings => API & Apps; Outline's tokens begin with ol_api_. Treat it like a password - it carries the full access of the user who created it, and it is shown only once.
Settings are read from environment variables, or can be passed directly to the client:
| Variable | Description |
|---|---|
OUTLINE_API_URL |
The API endpoint. A workspace URL is accepted and has /api appended. Defaults to https://app.getoutline.com/api. |
OUTLINE_API_TOKEN |
The API token. |
# Cloud-hosted; OUTLINE_API_URL can be omitted.
export OUTLINE_API_TOKEN="ol_api_..."
# Self-hosted; either spelling of the URL works.
export OUTLINE_API_URL="https://outline.example.com"
OAuth 2.0 access tokens work anywhere an API token does - both are sent as a bearer credential - but this package does not implement the authorization-code exchange that obtains one.
Timeouts and retries
Every request carries a timeout (default (5, 60) seconds for connect and read) so a stalled connection cannot hang the caller forever. Pass timeout= to override it (a single float, a (connect, read) tuple, or None to disable), and retries= to retry connection-establishment failures:
# Wait longer, and retry a dropped or stale connection up to 3 times.
client = OutlineClient(timeout=120, retries=3)
retries retries only the connection stage, before any bytes reach the server, which is safe for the non-idempotent writes this client performs: a create whose response is merely lost is never resubmitted. The CLI reads OUTLINE_API_TIMEOUT (seconds) and OUTLINE_API_RETRIES (count) for the same behaviour.
Quick start
Python
from outline_client import OutlineClient
# Reads OUTLINE_API_URL / OUTLINE_API_TOKEN from the environment, or pass
# url= and token= explicitly.
with OutlineClient() as outline:
collection = outline.create_collection("Handbook", permission="read_write")
document = outline.create_document(
title="Onboarding",
text="# Welcome\n\nStart here.",
collection_id=str(collection.id),
publish=True,
)
for hit in outline.search_documents("onboarding"):
print(hit.ranking, hit.document.title, hit.context)
print(outline.export_document(str(document.id)))
Python (async)
AsyncOutlineClient mirrors OutlineClient method for method, so independent
reads can be gathered rather than awaited one at a time:
import asyncio
from outline_client import AsyncOutlineClient
async def main() -> None:
async with AsyncOutlineClient() as outline:
documents, collections, users = await asyncio.gather(
outline.list_documents(limit=100),
outline.list_collections(),
outline.list_users(),
)
print(len(documents), len(collections), len(users))
asyncio.run(main())
CLI
# Every command prints indented JSON, so it pipes into jq unchanged.
outline auth info
outline collections list | jq -r '.[].name'
outline documents list --collection-id "$COLLECTION_ID" --limit 10
# Create a document from a file, then export it back out.
outline documents create --title "Onboarding" --text-file ./onboarding.md \
--collection-id "$COLLECTION_ID" --publish
outline documents export "$DOCUMENT_ID" > onboarding.md
outline documents export "$DOCUMENT_ID" --accept text/html -o onboarding.html
outline --help # every resource
outline documents --help # every method on one resource
OUTLINE_CLI_DISABLE takes a comma-separated list of dotted command paths
(documents.empty-trash,users.delete) and hides them from --help and from
dispatch, so an embedded runtime can suppress the destructive ones.
Usage
Pagination
Outline's list methods take offset and limit, and report back the window
they served rather than a total. paginate walks the pages for any of them:
for user in outline.paginate(outline.list_users, limit=100):
print(user.email)
# On the async client it is an async iterator.
async for document in outline.paginate(outline.list_documents, limit=100):
print(document.title)
Filters
The newer list and search methods take a structured filter expression,
evaluated as an AND of the top-level entries. Conditions and nested
AND/OR groups are both models:
from outline_client import DocumentFilterCondition, DocumentFilterGroup
recent_drafts = outline.list_documents(
filters=[
DocumentFilterCondition(
field="collectionId", operator="eq", value=collection_id
),
DocumentFilterGroup(
operator="OR",
filters=[
DocumentFilterCondition(
field="title", operator="contains", value="draft"
),
DocumentFilterCondition(field="updatedAt", operator="gte", value="P7D"),
],
),
]
)
Errors
A failed call raises a subclass of OutlineAPIError chosen by the HTTP status,
carrying Outline's own message and its machine-readable error identifier:
from outline_client import NotFoundError, RateLimitError
try:
document = outline.get_document(document_id)
except NotFoundError:
document = None
except RateLimitError as exc:
time.sleep(exc.retry_after or 60)
| Exception | Status | Raised when |
|---|---|---|
ValidationError |
400 | The request failed one of Outline's validations. |
AuthenticationError |
401 | The token is missing, malformed, or revoked. |
PaymentRequiredError |
402 | The feature is not available on this installation. |
AuthorizationError |
403 | The token is valid but not permitted this action. |
NotFoundError |
404 | The record does not exist, or is not visible. |
RateLimitError |
429 | Too many requests in the rate-limit window. |
ServerError |
5xx | The request failed inside Outline. |
OutlineConfigurationError is raised before any request is made, when the URL
or token is missing.
Exports and other background jobs
Exporting a collection queues a job rather than returning a file. Poll it, then download what it produced:
queued = outline.export_collection(collection_id, format="outline-markdown")
operation_id = str(queued.file_operation.id)
while outline.get_file_operation(operation_id).state in {"creating", "uploading"}:
time.sleep(1)
Path("handbook.zip").write_bytes(outline.download_file_operation(operation_id))
The escape hatch
Client methods return the data a response carries. The policies and
pagination beside it, an explicit JSON null, and any method a release does
not yet cover are all reachable through request:
body = outline.request("documents.info", {"id": document_id})
print(body["policies"])
Design
Generated schemas, hand-written methods
src/outline_client/schemas/models.py is generated from Outline's published
OpenAPI specification and should not be edited; make schemas regenerates it.
Everything else - the operations, the client methods, the CLI - is written by
hand, so argument names, defaults, and docstrings say what the method does
rather than what a generator guessed.
The generator's output is corrected in three documented ways, each asserted so
that a specification change fails the regeneration rather than passing
silently. See scripts/generate_schemas.py:
- Widened types.
format: uriandformat: emailbecomeAnyUrlandEmailStr, which reject data Outline actually sends - an attachment'surlis the relative path/api/attachments.redirect?id=.... Both widen tostr. - Renamed classes. Anonymous sub-schemas are named after the property they
were found under and disambiguated with a counter, which yields
Operator1andField3. Each is renamed to what it is, e.g.DocumentFilterOperator. - Specification corrections. Places where the specification and the server
disagree, verified against a running Outline 1.10:
Permissionis missingadmin, and a group membership'spermissionis a role within the group (member/admin), not an access level.
A few methods are corrected in the operations layer for the same reason -
notifications.list and pins.list are documented as returning an array but
answer with an object wrapping one, and revisions.list is documented as
taking an optional documentId that the server requires.
Unknown fields are kept
Outline ships response fields before the specification catches up, so models allow extras rather than dropping them. Anything the models do not name is still reachable:
document = outline.get_document(document_id)
print(document.__pydantic_extra__)
Development
make install # sync the locked environment
make schemas # regenerate the models from the OpenAPI specification
make check # ruff and mypy
make test # unit tests, against a mocked transport
make coverage # the same, with a coverage floor
Integration tests
make outline-up starts a throwaway Outline 1.10.1 on
http://localhost:8099 with its own Postgres and Redis,
seeds a workspace, an admin, a second non-admin member, a baseline collection,
and an API token, then prints the two variables the suite reads:
make outline-up
export OUTLINE_API_URL=http://localhost:8099
export OUTLINE_API_TOKEN=ol_api_outlineClientIntegrationTests000000001
make test-integration
make outline-down # stop it and delete its data
The suite exercises the real request and response shapes, which is the only way
to catch the places where the published specification is wrong. Three methods
cannot be reached from a community-edition container - dataAttributes.* and
documents.answerQuestion are not in the open-source server at all, and
apiKeys.create refuses an API token as the caller - so the suite asserts that
each fails for that reason rather than skipping it.
Three things about the live server are worth knowing if you write more of these
tests. Outline caches each user's accessible-collection ids in Redis for ten
seconds and filters the search and list methods by that cached set, so a
document in a brand-new collection is briefly invisible to them; the suite's
eventually helper polls through that window. documents.delete refuses
permanent=True on a live document, so discard trashes it first. And
collections.delete refuses to remove a workspace's only collection, which is
why the seed creates a baseline one for the suite's own collection to sit
beside.
Reference
License
MIT
Release files for outline-client 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| outline_client-0.1.0.tar.gz | 88.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| outline_client-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 208.0 kB
Release files / outline_client-0.1.0.tar.gz
| Download URL | outline_client-0.1.0.tar.gz |
|---|---|
| Size | 88.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
da614e9921436e28a259bbef79de8d2c0f75f196a06ca242ca8f16c45585d43e
|
|
BLAKE2b-256 checksum How to use checksums |
01b1da1d9657a13e893c9d0b9426350e1454dc0c0691903f741f6e0460be2e65
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.
Transparency logRelease files / outline_client-0.1.0-py3-none-any.whl
| Download URL | outline_client-0.1.0-py3-none-any.whl |
|---|---|
| Size | 119.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ae54b889bfc0f450dca2d0b9451d138978ca1e7d1513ee38c02c681bec5dfe8c
|
|
BLAKE2b-256 checksum How to use checksums |
74c4331b8d97c65fdc6e61e758c63d15fd9899eec069507935507361ce7fd9cc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.
Transparency log