Project status: working internal technical demo. This repository proves the end-to-end approach on a controlled Atlassian development site. It is not a production service, is not intended for live users, and should only be run with disposable demo data.
This Forge app turns linked Confluence pages into an explorable knowledge graph. It can capture a URL from Slack or Rovo, use Gemini to distill the source into structured knowledge, write cross-linked concept, entity, and source pages, visualize those relationships, and report graph-quality problems.
The important claim is deliberately narrow: the complete vertical slice works on Atlassian Forge and demonstrates enough of the product and engineering approach for a team to take forward.
- A Forge Custom UI global page can render an interactive D3 graph from native Confluence pages and links.
- A signed Slack event can create a deduplicated inbox page and start ingestion immediately.
- Gemini can perform the non-deterministic distillation step while Forge code retains deterministic control over page creation, reuse, labeling, and linking.
- Scheduled Forge functions can retry pending ingestion and maintain a human-readable graph-health report.
- Rovo actions can search the graph, report stored health, and file a URL into the same inbox pipeline.
- The same Confluence content remains useful without the visualization or AI layer because pages, labels, and links are all native.
flowchart LR
Slack[Slack Events API] --> Bridge[slackEvents webtrigger]
Rovo[Rovo file-to-inbox action] --> Inbox[fileToInbox]
Bridge --> Inbox
Inbox --> Dedupe[Forge KVS URL deduplication]
Inbox --> Stub[okf-inbox Confluence page]
Bridge --> Ingest[Gemini ingestion]
Hourly[Hourly scheduled trigger] --> Ingest
Stub --> Ingest
Ingest --> Writer[Deterministic Forge writer]
Writer --> Pages[Source, concept, and entity pages]
Pages --> Links[Native Confluence page links]
Links --> Resolver[Forge resolver]
Resolver --> Graph[React and D3 graph]
Daily[Daily scheduled trigger] --> Health[Health report]
Links --> Health
Search[Rovo search and health actions] --> Pages
Confluence labels classify nodes. Native page links define edges; there is no second relationship database to synchronize.
| Label | Meaning | Graph color |
|---|---|---|
okf-concept |
A durable idea, method, or pattern | Purple |
okf-entity |
A person, organization, product, or named work | Green |
okf-source |
A distilled article, paper, note, or video | Yellow |
okf-inbox |
A captured source waiting for ingestion | Blue |
| No OKF label | A normal Confluence page | Gray |
The graph reports two useful quality signals:
- Orphan: a page with no incoming or outgoing page links. Inbox pages are excluded because they are intentionally temporary.
- Unresolved link: a link whose target page does not exist in the selected space.
- Slack sends a channel message event to the
slack-eventswebtrigger, or Rovo invokesfile-to-inbox. - The app extracts the URL, chooses the requested space or first watched space, and uses a URL hash in Forge KVS to prevent duplicates.
- Forge creates an
okf-inboxpage containing the source URL, sharing context, and any deterministically captured source text. - The Slack path attempts ingestion immediately. The hourly trigger retries one pending inbox page per run.
- Gemini 2.5 Flash returns structured JSON containing a title, summary, key points, concepts, and entities.
- Forge code reuses matching pages, creates missing pages, writes native links, applies labels, and marks the inbox page as ingested.
- The app allows three ingestion attempts; the next sweep retires a repeatedly failing item with the
okf-ingest-failedlabel.
Gemini does not write to Confluence. It only proposes structured content; the deterministic Forge layer owns all persistence and graph construction.
The Custom UI invokes Forge resolvers through @forge/bridge. Its Confluence operations and Rovo search use api.asUser(), so they execute in the current user's product context. Scheduled jobs, the Slack bridge, and the shared inbox writer use api.asApp() because those paths must also operate without an interactive Confluence request.
The graph builder reads pages, labels, and storage-format page links, then produces nodes, edges, orphan flags, unresolved links, and summary counts. The daily maintenance function runs the same graph analysis for watched spaces, stores the latest result in KVS, and creates or updates a Knowledge Graph Health page.
The shortest useful review path is:
manifest.ymlfor every Forge module, function, permission, resource, and runtime setting.src/index.jsfor the handler wiring.src/resolvers.js,src/lib/confluence.js, andsrc/lib/graph.jsfor the interactive graph path.static/graph/src/App.jsandstatic/graph/src/App.cssfor the graph experience.src/slack-bridge.js,src/inbox.js, andsrc/ingest.jsfor the complete capture-to-knowledge path.src/maintenance.js,src/rovo.js, andsrc/seed.jsfor health checks, Rovo actions, and the repeatable demo dataset.
| Path | Responsibility |
|---|---|
manifest.yml |
Confluence global page, scheduled triggers, Rovo agent/actions, webtriggers, scopes, egress, and Node.js runtime |
src/index.js |
Exports the handlers named by the manifest |
src/resolvers.js |
Custom UI boundary for spaces, graph data, watch state, health checks, and demo seeding |
src/lib/confluence.js |
Confluence REST helpers for spaces, pages, labels, search, create, and update |
src/lib/graph.js |
Converts pages and native links into graph data and quality signals |
src/slack-bridge.js |
Slack signature verification, event filtering, metadata capture, and immediate ingest attempt |
src/inbox.js |
URL validation, KVS deduplication, and inbox-page creation |
src/ingest.js |
Gemini request, structured-output validation, page reuse/creation, links, labels, and retries |
src/maintenance.js |
Watched-space state, scheduled graph analysis, KVS health snapshots, and report-page writing |
src/rovo.js |
Rovo search and health action handlers |
src/seed.js |
Idempotent demo content, including intentional graph defects for the health-check story |
static/graph |
React 16 Custom UI and D3 force-directed visualization |
The graph needs direct SVG rendering, D3's force simulation, drag, zoom, selection, and responsive canvas behavior. Custom UI is therefore a deliberate prototype choice for this module, not an accidental mixing of UI Kit components. The Forge resolver remains the product API boundary.
- Node.js and npm
- Atlassian Forge CLI, authenticated with
forge login - An Atlassian cloud development site with Confluence
- Rovo access only if reviewing the Rovo agent
- A Slack app only if reviewing Slack capture
- A Gemini API key only if reviewing automated distillation
Install dependencies and build the Custom UI from the repository root:
npm ci
npm --prefix static/graph ci
npm --prefix static/graph run build
forge lintCompany reviewers using the existing app: keep the app.id already present in manifest.yml. They must be granted access to that Forge app. They should not run forge register, because doing so replaces the manifest ID with a separate app registration.
Reviewers creating an independent copy: run the following once. It creates a new Forge app registration and updates the local manifest ID. The new app has separate environments, variables, installations, storage, and webtrigger URLs.
forge register knowledge-graph-reviewThe Forge app ID in manifest.yml identifies the app; it is not a credential.
Cloning this repository does not provide any Gemini, Slack, or YouTube credential. Secret values are not committed to Git. They live in environment-specific Forge encrypted variables and are not copied when someone registers a separate app.
| Variable | Needed for | Required? |
|---|---|---|
GEMINI_API_KEY |
Automated source distillation | Only for the AI ingest path |
SLACK_SIGNING_SECRET |
Authenticating Slack Events API requests | Only for Slack capture |
SLACK_BOT_TOKEN |
Resolving Slack user and channel names | Optional enrichment |
YOUTUBE_API_KEY |
Richer YouTube metadata during capture | Optional enrichment |
Set only the variables needed for the path under review:
forge variables set --encrypt -e development GEMINI_API_KEY YOUR_GEMINI_KEY
forge variables set --encrypt -e development SLACK_SIGNING_SECRET YOUR_SLACK_SIGNING_SECRET
forge variables set --encrypt -e development SLACK_BOT_TOKEN YOUR_SLACK_BOT_TOKEN
forge variables set --encrypt -e development YOUTUBE_API_KEY YOUR_YOUTUBE_KEYRedeploy after changing Forge variables. Encrypted values cannot be recovered with forge variables list; replace them by setting them again.
Without GEMINI_API_KEY, Slack and Rovo can still create inbox pages, and the graph, seed data, search, and health-check paths still work. Automated ingestion safely reports that it was skipped.
forge deploy --non-interactive -e development
forge install --non-interactive --site YOUR_SITE.atlassian.net --product Confluence --environment developmentIf this app is already installed and modules or scopes changed, upgrade the installation after deploying:
forge install --non-interactive --upgrade --site YOUR_SITE.atlassian.net --product Confluence --environment developmentCode-only changes need a deploy but do not need an installation upgrade. Frontend changes must be built before deployment because manifest.yml serves static/graph/build.
The demo uses a small custom Slack app and the Slack Events API.
Create the signed Slack endpoint after deploying and installing:
forge webtrigger create -f slack-events -s YOUR_SITE.atlassian.net -p Confluence -e developmentCreate a Slack app from this manifest and replace YOUR_SLACK_WEBTRIGGER_URL with the generated URL:
display_information:
name: Knowledge Graph Bridge
description: Files shared links into the Confluence knowledge graph inbox
features:
bot_user:
display_name: kg-bridge
always_online: true
oauth_config:
scopes:
bot:
- channels:history
- users:read
- channels:read
settings:
event_subscriptions:
request_url: YOUR_SLACK_WEBTRIGGER_URL
bot_events:
- message.channelsThen:
- Install the Slack app to the controlled demo workspace.
- Store its signing secret and optional bot token as encrypted Forge variables.
- Redeploy the Forge app.
- Invite
@kg-bridgeto the controlled public channel. - Post a new URL with a short sentence explaining why it matters.
Slack event callbacks are verified with HMAC-SHA256 and rejected if the signature is invalid or the timestamp is more than five minutes old. Slack's initial url_verification challenge is returned before that check so the endpoint can be configured. Bot messages, message subtypes, and events without a user are ignored.
The webtrigger URL is a capability URL and should be treated as sensitive configuration even though the Slack handler also verifies Slack's signature.
The hourly job is the normal retry path. For a live demo, ingest-now can trigger the same sweep without waiting for the next scheduled invocation:
forge webtrigger create -f ingest-now -s YOUR_SITE.atlassian.net -p Confluence -e developmentThis endpoint is intentionally demo-only and has no application-level authentication. Keep its generated URL private, use it only on the disposable development site, and do not carry this pattern into a production design.
Use a new or disposable Confluence space.
- Open Apps -> Knowledge Graph.
- Select the demo space.
- Choose Seed demo data. The seed operation reuses pages that already exist, so it is safe to repeat.
- Explore the graph with search, type filters, orphan filtering, drag, zoom, node selection, and double-click navigation.
- Choose Watch this space so scheduled ingestion and maintenance know which space to process.
- Choose Run health check. The seeded data intentionally contains an orphan and an unresolved link so the report has something meaningful to find.
- If Slack and Gemini are configured, share a previously unused URL in the demo channel. The bridge creates the inbox page and immediately attempts distillation.
- Refresh the graph to show the new source, concept, and entity pages and their native Confluence links.
- In Rovo, ask the Knowledge Graph Agent to find a seeded topic or report graph health.
Use a fresh URL when repeating the capture story. URL deduplication is intentional and persists in Forge KVS even if the related page is manually deleted.
Run the same lightweight checks used before handing off this demo:
forge lint
node --check src/index.js
node --check src/resolvers.js
node --check src/inbox.js
node --check src/ingest.js
node --check src/slack-bridge.js
npm --prefix static/graph run build| Symptom | What to check |
|---|---|
GEMINI_API_KEY not set |
Set the encrypted variable in the same Forge environment and redeploy |
Slack returns 401 |
Confirm the signing secret, environment, request URL, and Slack app configuration |
| A shared URL is skipped | Use a new URL; the KVS deduplication record is doing its job |
| Inbox page exists but is not distilled | Confirm the Gemini key, inspect logs, then use the hourly job or private ingest-now URL |
| Frontend changes do not appear | Rebuild static/graph, then redeploy |
| Manifest/module changes do not appear | Deploy, then run forge install --upgrade for the installed site |
| Newly created content is briefly absent from search | Allow for Confluence indexing; direct page reads and the hourly retry path avoid relying only on immediate CQL visibility |
Recent development logs:
forge logs -n 100 -e development --since 15mThis repository is a competent proof of capability, not a claim of production readiness. Its accepted demo constraints are:
- one controlled Atlassian development site, one controlled Slack workspace/channel, and disposable content;
- no live users, sensitive sources, production credentials, or production service-level expectations;
- broad backend egress so the bridge can read arbitrary shared URLs and call Slack, Google, YouTube, and Gemini;
- a capability URL for on-demand ingestion;
- synchronous Slack capture and immediate ingestion, optimized for a clear vertical-slice demo;
- bounded page loading and one scheduled inbox item per ingest invocation, which are sufficient for the demo dataset.
Those choices keep the proof simple and observable. A production engineering handoff would revisit authenticated job triggering, authorization and tenancy boundaries, an asynchronous capture queue, scoped egress and data-governance requirements, pagination and load testing, dependency modernization, accessibility, automated tests/CI, telemetry, retries, and operational runbooks.
docs/why-this-matters.mdexplains the product rationale.docs/project-brief.mdrecords the original project framing; where early assumptions differ from the implementation, this README,manifest.yml, and the current source are authoritative.docs/lessons-learned.mdcaptures implementation notes and tradeoffs from the demo build.
Official Forge references: