Example — Confluence & Jira

Cache understanding over enterprise knowledge and invalidate it from webhooks — ids derived from each page/issue, never hardcoded.

Enterprise RAG usually blends a knowledge base (Confluence) with tickets (Jira). Both expose webhooks, so freshness is event-driven. The pattern: stamp artifact_id from each page/issue during retrieval, and fire the matching change from the webhook payload.

1. Retriever — derive ids from the source

Your Confluence + Jira content lives in a vector DB. This is a plain vector lookup, so extend BaseVectorRetriever — implement search (your client's query) and to_chunk (map one hit, deriving the id from its metadata):

from coalent import BaseVectorRetriever, Chunk

class KnowledgeRetriever(BaseVectorRetriever):
    def __init__(self, vector, embed):
        self._vector, self._embed = vector, embed

    def search(self, query, namespace):
        return self._vector.search(self._embed(query), top_k=6)

    def to_chunk(self, hit):
        # metadata already carries the source + id + revision:
        #   {"source": "confluence", "id": "98231", "version": "7", "text": "..."}
        return Chunk(
            artifact_id=f"{hit['source']}:{hit['id']}",   # confluence:98231 / jira:OPS-412
            text=hit["text"],
            version=str(hit["version"]),
        )

(Not a plain vector lookup? Implement the Retriever interface directly — just a class with a retrieve method, as in the MCP/tools and vector-cache examples.)

2. Wire webhooks → invalidate

The bundled event layer turns native payloads into change events. Jira has a built-in connector; add a tiny one for Confluence. Both derive the id from the payload:

from coalent import EventDispatcher, JiraConnector, ChangeEvent
from coalent.events import EventConnector

class ConfluenceConnector(EventConnector):
    source = "confluence"

    def parse(self, payload):
        page = payload.get("page") or {}
        if not page.get("id"):
            return []
        return [ChangeEvent(
            artifact_id=f"confluence:{page['id']}",          # dynamic — from the webhook
            version=str(page.get("version", {}).get("number", "")),
            kind="delete" if payload.get("event") == "page_removed" else "update",
        )]

dispatcher = EventDispatcher(
    sink=cache.invalidate,                                   # feed events straight to the cache
    connectors=[JiraConnector(), ConfluenceConnector()],
)

JiraConnector emits jira:<KEY> from an issue payload — the same scheme your retriever stamped, so they line up.

3. Receive the webhook

@app.post("/webhooks/{source}")
def receive(source: str, payload: dict):
    events = dispatcher.dispatch(source, payload)   # parses + invalidates
    return {"invalidated": len(events)}

Point Confluence at /webhooks/confluence and Jira at /webhooks/jira. A page edit or an issue update now dirties exactly the cognition units that used it; a page deletion evicts them.

Use Confluence's version.number and Jira's fields.updated as the version — Coalent then skips no-op events where the revision didn't actually move.

Next