factflow-confluence
Confluence Cloud source workflow. Three auto-discovered adapters crawl the Confluence hierarchy — space(s) → page(s) → attachment(s) — and feed page bodies plus attachment binaries into the shared markdown / embedding pipeline. Discovery, listing, and metadata use the Confluence Cloud v2 REST API; the attachment binary download uses the v1 endpoint (v2 has no working OAuth binary-download path).
Tier and role
Section titled “Tier and role”- Tier: workflow
- Import name:
factflow_confluence - Version: 0.1.0
- Source:
backend/packages/workflows/factflow-confluence/
Source pipeline producing stored page HTML and attachment binaries from a Confluence Cloud site. Page bodies flow to the markdown chain; attachments flow through the shared document_converter. Both converge on /queue/markdown.ready, where segmentation / embedding / knowledge extraction run downstream.
Context
Section titled “Context”One subpackage, confluence/:
adapter.py— the three pipeline adapters and a shared_ConfluenceClientMixin(the mixin deliberately does not inheritPipelineAdapter, so it is never registered):ConfluenceSpaceCrawlerAdapter(type: confluence_space_crawler) — enumerates a discovery unit (spaces/subtree/all_visible) and fans out one message per page. Pure fan-out terminal (continue_pipeline=False); each emitted message carries an explicitdestinationso the engine routes by destination, not outbound when-conditions. The only adapter that takesdb_pool.ConfluencePageFetcherAdapter(type: confluence_page_fetcher) — fetches a page's storage-format body, stores it at<storage_prefix>/<space_key>/<page_id>.html, then fans out one HTML body message (to the markdown chain) plus one descriptor per attachment (to the attachment fetcher). Pure fan-out.ConfluenceAttachmentFetcherAdapter(type: confluence_attachment_fetcher) — downloads one attachment binary, stores it, emits one document-convert message. The only one of the three that continues the pipeline (1:1 transform,continue_pipeline=True).
client.py—ConfluenceClient, an asynchttpxwrapper (HTTP/2, follow-redirects) with_RetryPolicy, Link-header / cursor pagination helpers, and the streamingdownloadthat raisesAttachmentTooLargeErroronce the body exceeds the cap.auth.py— theConfluenceAuthProviderProtocol and its two concrete providers (ApiTokenBasicAuthProvider,OAuthAuthProvider), plus URL / timeout constants. The provider owns both the auth header and the API base URL, so switching flows is a contained provider swap.cursor.py—ConfluenceCursor(frozen dataclass) andConfluenceCursorStore, which persists per-discovery-unit cursors in the Postgresconfluence_cursorstable for incremental crawl.models.py— the Pydantic adapter configs:ConfluenceCrawlerConfig,ConfluencePageFetcherConfig,ConfluenceAttachmentFetcherConfig.settings.py—ConfluenceSettings, env-drivenBaseSettings(prefixFACTFLOW_CONFLUENCE_).
Rationale
Section titled “Rationale”- v2 for discovery, v1 for binaries. The v2 surface drives space discovery, page listing, body fetch, and attachment metadata. The v2
_links.downloadservlet returns 401 for OAuth tokens, so the attachment binary is fetched via the v1 endpoint/rest/api/content/{pageId}/child/attachment/{attId}/download— the only working path (requires the classicread:confluence-content.allscope; do not strip it). - Pluggable auth behind one Protocol.
ConfluenceAuthProviderabstracts both the header and the base URL.api_token(default) does tenant-direct HTTP Basic auth;oauthdoes 2LO client-credentials bearer against the Atlassian gateway. Moving from a classic API token to a scoped token to OAuth is a provider swap, not a rewrite. - Incremental by cursor, not full re-crawl. Each discovery unit keeps a cursor (
space:<key>orsubtree:<key>:<root>) anchored onversion.createdAt. Newest-firstspaceslisting breaks at the first stale page; unorderedsubtree/descendantsfilters instead. The single UPSERT stays correct under concurrency, so theconcurrency 1on the crawl route is a convention for predictable cursor advance, not a correctness requirement. - Stable
document_idfor replay-safe vectors. Fetchers mintconfluence-<space>-<page>(bodies) andconfluence-<space>-<page>-<attachment>(attachments), persist it in the storage sidecar, and propagate it on the message so the markdown converter honors it instead of minting a random id. Replays map to the same vectors. - Converter stage is shared and separate. Attachments are stored as binaries with the original (sanitized) extension, then converted by the shared
document_converter, so conversion can replay independently and only MarkItDown-supported formats are converted.
Public API
Section titled “Public API”The top-level factflow_confluence/__init__.py re-exports the auth layer and its factory. __all__:
from factflow_confluence import ( ApiTokenBasicAuthProvider, ConfluenceAuthProvider, # Protocol ConfluenceSettings, OAuthAuthProvider, create_auth_provider,)create_auth_provider(settings: ConfluenceSettings) -> ConfluenceAuthProvider— module-level factory defined in__init__.py. Selects bysettings.auth_kind(api_token→ApiTokenBasicAuthProvider;oauth→OAuthAuthProvider; anything else →ValueError). Mirrors the canonical factflow factory idiom (create_storage_provider,create_completion_client).
The three adapter classes are not re-exported at the package root — they live in factflow_confluence.confluence.adapter, are auto-discovered by factflow_engine.discovery, and are referenced from pipeline YAML by their type names (confluence_space_crawler, confluence_page_fetcher, confluence_attachment_fetcher), not imported. Likewise ConfluenceClient, the config models (ConfluenceCrawlerConfig, ConfluencePageFetcherConfig, ConfluenceAttachmentFetcherConfig), ConfluenceCursorStore, and ConfluenceCursor are internal and not re-exported.
Credentials come from env (gitignored .env), prefix FACTFLOW_CONFLUENCE_:
FACTFLOW_CONFLUENCE_AUTH_KIND—api_token(default) |oauthapi_token(all required):FACTFLOW_CONFLUENCE_BASE_URL(e.g.https://<tenant>.atlassian.net/wiki),FACTFLOW_CONFLUENCE_EMAIL,FACTFLOW_CONFLUENCE_API_TOKENoauth(CLIENT_ID+CLIENT_SECRETrequired):FACTFLOW_CONFLUENCE_CLIENT_ID,FACTFLOW_CONFLUENCE_CLIENT_SECRET, and optionalFACTFLOW_CONFLUENCE_CLOUD_ID(auto-resolved from/oauth/token/accessible-resourceswhen the grant has exactly one site; set it explicitly to disambiguate multiple sites)
OAuth needs all five scopes — read:space:confluence, read:page:confluence, read:attachment:confluence, read:hierarchical-content:confluence (subtree mode), and the classic read:confluence-content.all (attachment binary download). Dropping the classic scope breaks attachment download.
Dependencies
Section titled “Dependencies”- Workspace:
factflow-protocols(AdapterResult,PipelineAdapter,PipelineContext,StorageProtocol),factflow-foundation,factflow-engine(auto-discovery + execution-scoped queues) - External (third-party):
httpx[http2] >=0.28.1— the only declared third-party runtime dep - External services: Confluence Cloud REST API (v2 for discovery / listing / metadata, v1 for attachment binaries), a storage provider for stored page HTML and attachment binaries, and Postgres for the
confluence_cursorstable (incremental crawl). The crawler is the only adapter injected withdb_pool(AsyncConnectionPool), wrapped inConfluenceCursorStore; the fetchers are injected withstorage.
psycopg_pool / psycopg (cursor store) and pydantic / pydantic-settings (models, settings) are used at runtime as transitive deps, not declared directly in this package.
Testing
Section titled “Testing”Tests at backend/packages/workflows/factflow-confluence/tests/. Integration tests that hit a live Confluence Cloud site require credentials in the package's gitignored .env and are skipped without them.
Related
Section titled “Related”factflow-markdown— downstream of the page-body path;confluence-ingest.yamlhands stored HTML to the markdown chain,confluence-markdown.yamlinlines itfactflow-sharepoint— sibling source workflow that shares the samedocument_converterstage for binary-to-markdown conversion- Build a Confluence pipeline — the two shipped configs (
confluence-ingest.yamlstops at storage;confluence-markdown.yamlconverts page bodies for the knowledge path)