Confluence Integration
Crewlet integrates with Confluence bidirectionally: agents read and write Confluence pages via MCP tools, and Confluence pushes content change events to agents via webhooks.
Confluence is one of two knowledge backends, and it is not the default.
The engine ships its own — knowledge.backend: native, which is what a
company that says nothing gets — with every page a record on the engine’s
own pages log, applied on every data node, and searched by keyword over
each node’s own BM25 index (and by meaning as well where
knowledge.vectors is on, the default for a company with an embeddings
provider). Set knowledge.backend: confluence (or declare this block,
which derives it) to run Confluence instead. It buys one thing the native
backend does not have: search runs as the agent’s own Atlassian user,
so Confluence’s page permissions are the boundary rather than the engine.
Exactly one backend per company — a config naming both is refused. See
Knowledge System.
Prerequisites — the Atlassian side is set up by hand. Atlassian offers no API for provisioning users, so the operator creates the Atlassian site (Cloud or Data Center) and each agent’s Atlassian account and API token manually, then wires the tokens into mcp_env as shown below. crewlet confluence provision registers the inbound hooks on either deployment; on Cloud the Crewlet Forge app is the supported alternative (see Webhooks).
Setting it up from the dashboard
Section titled “Setting it up from the dashboard”Connect on the Atlassian tile in Settings › Integrations collects the
site address, the account email and the API token and generates whichever
webhook credential your deployment needs (a signing secret for Data Center, a
shared token for Cloud); the reconcile loop then registers the hooks on its
next tick, running the same pass crewlet confluence provision runs. It
appears under Atlassian, beside Jira.
See Running the provisioning pass.
Configuration
Section titled “Configuration”The integrations.confluence block is non-tool config — the admin/service account for org-level REST calls and the inbound webhook secret. Org-wide knowledge spaces live in the separate knowledge: block. The Confluence MCP tool server is a separate mcp_servers entry, shared with Jira under the name atlassian:
integrations: confluence: url: "${CONFLUENCE_URL}" # Confluence instance URL (Cloud or Data Center) token: "${CONFLUENCE_API_TOKEN}" # API token (admin/service account) email: "${CONFLUENCE_EMAIL}" # Cloud only — admin email for Basic Auth webhook_secret: "${CONFLUENCE_WEBHOOK_SECRET}" # Data Center: required, HMAC-SHA256
knowledge: backend: confluence # this company's knowledge base scope: ["HANDBOOK"] # org-wide containers every agent can search
mcp_servers: - name: atlassian # shared by Jira + Confluence shared: false # per-agent: each role supplies its own token command: uvx args: ["mcp-atlassian"] env: CONFLUENCE_URL: "${CONFLUENCE_URL}" # declare explicitly — the engine does not inject itNote: Instead of url, you can provide cloud_id (Atlassian Cloud ID) — the base URL is constructed automatically. Provide one or the other, not both.
Human-clickable links agents share: with cloud_id, the mcp-atlassian tools return api.atlassian.com/ex/confluence/{cloud_id}/... gateway URLs, which colleagues can’t open. To have agents share a clickable …atlassian.net/wiki/spaces/…/pages/… link, set a skill variable — skill_variables.confluence_base_url: "https://mycompany.atlassian.net/wiki" — for your mention/link Tool Skill to reference. (The bundled examples/tool-skills/platform-mentions.md already references this variable.) Note this is enforced-reading guidance (the required-skill guard puts the rule + base in context before the agent can post), not a rewrite of tool results — mcp-atlassian builds result links from CONFLUENCE_URL and does not read a site-URL env. (This is independent of site_url, which the notification transport and knowledge search use for their own links.)
On Cloud, crewlet confluence provision registers token-bearing hooks on /webhooks/confluence/{event} through an endpoint Atlassian has never documented; the Forge app is the supported alternative and delivers to POST /webhooks/forge. webhook_secret signs Data Center deliveries; webhook_token authenticates Cloud ones. See Webhooks.
Since Jira and Confluence share the Atlassian platform, they use the same mcp-atlassian server — declare it once in mcp_servers as atlassian and set both JIRA_URL and CONFLUENCE_URL in its env; the engine does not derive them from the jira: / confluence: sections.
MCP Server (Agents Control Confluence)
Section titled “MCP Server (Agents Control Confluence)”The atlassian MCP server gives agents full wiki capabilities — searching spaces, reading pages, creating and updating content, managing attachments, and adding comments. Set CONFLUENCE_URL in the server’s env.
Key MCP Tools
Section titled “Key MCP Tools”Agents see these tools in their tool list (discovered dynamically via MCP):
| Tool | Description |
|---|---|
confluence_search | Search pages and blog posts using CQL |
confluence_get_page | Read a page’s content (returns Confluence storage format) |
confluence_create_page | Create a new page in a space |
confluence_update_page | Update an existing page’s content |
confluence_get_page_children | List child pages (for navigating hierarchies) |
confluence_get_comments | Read page comments |
confluence_add_comment | Add a comment to a page |
Per-Unit Confluence Space (identity)
Section titled “Per-Unit Confluence Space (identity)”A unit declares the Confluence space it owns under space. This is integration identity — it routes inbound page webhooks to the unit lead and is the team’s write / skill-promotion home. It is not a tool credential (those stay per-role in mcp_env.atlassian) and it does not scope knowledge reads (read scope is the org-wide knowledge.scope — see Scoped spaces below).
units: - name: Engineering type: department lead: CTO space: "ENG" # unit identity: routing + write home (NOT read scope) roles: - name: CTO mcp_env: atlassian: { CONFLUENCE_API_TOKEN: "${CTO_CONFLUENCE_TOKEN}" } - name: Architect mcp_env: atlassian: { CONFLUENCE_API_TOKEN: "${ARCHITECT_CONFLUENCE_TOKEN}" }
- name: Product type: department lead: VP Product space: "PROD" roles: - name: VP Product mcp_env: atlassian: { CONFLUENCE_API_TOKEN: "${VP_PRODUCT_CONFLUENCE_TOKEN}" }Each role keeps its own CONFLUENCE_API_TOKEN (and CONFLUENCE_USERNAME) in mcp_env.atlassian — the credential the mcp-atlassian server and the query-time searcher authenticate with. The unit’s space is read by the engine (routing + skill-promotion), not passed to the MCP server.
Webhooks (Confluence Pushes to Agents)
Section titled “Webhooks (Confluence Pushes to Agents)”Confluence Cloud and Data Center use different webhook models. Data Center signs one hook with webhook_secret; Cloud carries a token in one hook per event, or uses the Crewlet Forge app.
Confluence Cloud — a token-bearing hook per event (the default)
Section titled “Confluence Cloud — a token-bearing hook per event (the default)”crewlet confluence provision -public-url https://your-engine.example.com registers one hook per event on your Cloud site and mints a shared token into integrations.confluence.webhook_token. Every hook’s URL is https://your-engine.example.com/webhooks/confluence/<event>?token=…, and the route compares the token constant-time.
Read this before relying on it. Confluence Cloud has no webhook page in its administration UI and no documented API for registering one; the request for it, CONFCLOUD-36613, has been open since 2015. The endpoint the engine uses, /wiki/rest/webhooks/1.0/webhook, answers on Cloud with an ordinary API token and does deliver, but Atlassian has never stated its support status and can change or remove it without notice. Every fact below was measured against a live site rather than read from a document, because no document exists.
What was measured, and what it decides:
- A Cloud delivery carries no signature. The endpoint accepts a
secreton registration and silently ignores it. Nothing varying with the body arrives, so there is nothing to verify an HMAC against. - Userinfo in the URL is dropped, and no registration field becomes a header.
- The query string is delivered verbatim. It is the only channel through which anything secret reaches the engine, which is why the token rides there.
- The payload names no event. Which one fired is known only from which hook was registered for it, so the engine registers one hook per event with the event in the path.
- The endpoint validates no event names. A registration for an event Confluence will never emit answers 201 and never fires.
That makes the Cloud token exactly what Datadog’s is: a shared token doing a signing key’s job with none of the guarantees. A replayed delivery is indistinguishable from a fresh one, and anyone holding the token can forge a page event. Treat webhook_token as a signing key, rotate it the same way (-recreate-webhooks re-registers every hook with a fresh one), and keep it a ${VAR}. The engine never logs the query string on this route.
A value this deployment already sealed is reused, not reminted. Both halves take the same rule: a ${VAR} that already resolves is used as it is, and one that resolves to nothing is minted into — unless the fleet already holds a value under that name, which is read back instead. That read-back matters to the reconcile loop rather than to the command: a ${VAR} resolves from a snapshot taken at apply time, so in the window between a pass sealing a value and something rebuilding that snapshot the resolver answers empty for one the fleet already holds, and without it the loop minted again on every tick — re-registering every hook with a token the running engine does not hold, which on Cloud is the entire authentication. On a node with no keyring at all (secrets.keys unset) nothing is minted and no hook is registered: the surface reports ingress_blocked against integrations.confluence.webhook_token on Cloud or integrations.confluence.webhook_secret on Data Center, naming secrets.keys. A Cloud hook registered with no token would be an open endpoint anybody could drive a turn through, so registering one anyway is not an option.
Because the token is the entire check, its length is the entire strength, so Crewlet refuses one shorter than 26 characters — the length crewlet confluence provision and the dashboard’s Generate button both mint (130 bits of base32). crewlet validate and PATCH /config reject a short literal, and the route answers 503 for a short resolved value, so a ${VAR} pointing at a weak token is refused exactly where the weak token itself would be.
If you would rather not carry the risk of an undocumented endpoint, the Forge app below remains the supported route, and an Automation rule is the documented way to reach this same route without it.
The account whose token is in integrations.confluence.token needs Confluence administrator rights to register hooks.
Confluence Cloud, an Automation rule as the sender (documented alternative)
Section titled “Confluence Cloud, an Automation rule as the sender (documented alternative)”Confluence Automation’s Send web request action is Atlassian’s documented way for a Cloud site to call an outside URL, and it reaches the same per-event route as the hooks above. It can do the one thing the registered hook cannot: carry the token in a header rather than the query string. POST /webhooks/confluence/<event> reads X-Crewlet-Token first and falls back to ?token= only when the header is absent, so a rule and a registered hook share one route and one credential.
One rule per event, built in Space settings (or Global automation) with the trigger that matches the path:
| Trigger | Path | Body |
|---|---|---|
| a page is published | /webhooks/confluence/page_created | the page body below |
| a page is edited | /webhooks/confluence/page_updated | the page body below |
| a comment is added | /webhooks/confluence/comment_created | the comment body below |
The action is Send web request with:
- URL:
https://your-engine.example.com/webhooks/confluence/<event>(the path above) - Method:
POST - Headers:
X-Crewlet-Tokenset to the value ofintegrations.confluence.webhook_token, with Hidden ticked, andContent-Type: application/json - Body: custom data, on one line. The page body:
{"page":{"id":"{{page.id}}","title":{{page.title.asJsonString}},"version":{"number":"{{page.version.number}}"}},"space":{"key":"{{space.key}}"},"userAccountId":"{{initiator.accountId}}"}and the comment body:
{"comment":{"id":"{{comment.id}}","parent":{"id":"{{page.id}}","title":{{page.title.asJsonString}},"contentType":"page"}},"space":{"key":"{{space.key}}"},"userAccountId":"{{initiator.accountId}}"}The version number and the comment id are not decoration: they are what stops two events collapsing into one. This route has no per-delivery identifier to claim, so it claims a hash of the body (see Delivery deduplication below), and a body carrying only a page id and a title is byte-identical for two saves of the same page five minutes apart. The second would be answered 200 {"status":"duplicate"} and wake nobody. {{page.version.number}} changes on every save and {{comment.id}} is unique per comment, so each event keys as itself.
The engine answers 200 and logs webhook_received source=confluence; a wrong or missing token answers 401. Use the rule’s own validate step and one real edit to confirm the smart values render on your site before relying on the rule.
What it costs, and what to watch:
- It is metered. Every run is an Automation step, pooled per organisation. A site that reaches its allowance stops running rules, silently from the engine’s side, so a quiet feed can mean a spent allowance rather than a quiet wiki.
- A failed request is not retried. Automation is fire-and-forget on a non-2xx answer. If the engine is down for a minute, the events of that minute are gone, where a registered hook and the Forge app both retry.
- A hidden header does not survive a copy. Duplicating, exporting or importing a rule drops the hidden value, so re-enter the token on the copy.
- It is the same shared token. Everything said above about
webhook_tokenapplies: a header is not a signature, so treat it as a signing key and rotate it the same way.
Confluence Cloud — the Forge app (supported alternative)
Section titled “Confluence Cloud — the Forge app (supported alternative)”Install the Crewlet Forge app from the Atlassian Marketplace (or via a private installation link). The Forge app forwards these Confluence events to the Crewlet backend:
avi:confluence:created:page— new page createdavi:confluence:updated:page— page content or title changedavi:confluence:trashed:page— page moved to trashavi:confluence:deleted:page— page permanently deletedavi:confluence:created:comment— new comment on a pageavi:confluence:updated:comment— comment editedavi:confluence:created:blogpost— new blog postavi:confluence:updated:blogpost— blog post updated
Events are delivered via Forge Remote to POST /webhooks/forge. The Forge platform handles authentication automatically. Label events are not currently forwarded by this integration.
Confluence Data Center — Direct Webhook Registration
Section titled “Confluence Data Center — Direct Webhook Registration”Data Center supports webhook registration via the admin UI or REST API:
- Go to Administration > Further Configuration > Webhooks
- Set URL to
https://your-server.com/webhooks/confluence - Select events:
page_created,page_updated,comment_created - Set a Secret for HMAC-SHA256 signature verification
Or register dynamically via the REST API:
POST /rest/webhooks/1.0/webhookContent-Type: application/json
{ "name": "crewlet", "url": "https://your-server.com/webhooks/confluence", "events": ["page_created", "page_updated", "comment_created"], "active": true, "secret": "your-shared-secret"}Inbound requests are verified using HMAC-SHA256 against the X-Hub-Signature header, at the route, before the delivery is recorded or published — the same point at which the GitHub and GitLab webhooks verify theirs. POST /webhooks/confluence is exempt from the API’s bearer token precisely because it authenticates by provider HMAC, so the check belongs there.
webhook_secret is therefore required for Data Center webhooks: without one the endpoint answers 503 with a Retry-After, exactly as its peers do, rather than accepting deliveries it cannot verify. That is deliberately not a 4xx — the sender’s request is fine, what is missing is on this side, and a 4xx would tell it to discard a delivery nobody else has a copy of. The delivery waits at Confluence and flows once the secret is set. Cloud is unaffected: its deliveries arrive on the per-event token route above or through the Forge app on /webhooks/forge, and neither carries this signature.
Delivery deduplication
Section titled “Delivery deduplication”Data Center deliveries are claimed fleet-wide on the X-Atlassian-Webhook-Identifier the instance sends, which is stable across its own retries — so a redelivery is answered 200 {"status":"duplicate"} and wakes nobody. The claim lasts five minutes. A route whose provider sends no such header — the Cloud token route and the Forge relay always, and a Data Center build that does not set one — is claimed on a hash of the raw body instead.
On the Cloud token route the body is the whole key, so what the sender puts in it decides what counts as one event. Confluence’s own registered hooks carry the content id, its version and a timestamp, and are therefore distinct per event without help. An Automation rule carries only what its body template names, which is why the recipe above includes the page version and the comment id: a template without them makes two saves of one page within the claim window indistinguishable, and the second wakes nobody. The payload is what stays identical across a provider’s own retry, and byte identity is deliberately preferred to derived coordinates: every field left out of a coordinate set is a way for two different events to collapse into one, and a collapsed event is a message nobody ever answers. A hash cannot do that — any difference at all yields a different key.
Query-Time Confluence Search
Section titled “Query-Time Confluence Search”Crewlet keeps no local copy of Confluence content. Shared knowledge is searched live at query time: the auxiliary model turns a turn’s trigger into a short text query — once per turn — and the searcher runs it as CQL against /rest/api/content/search. There is no startup walk, no vector index, and no webhook-driven re-index of page bodies; Confluence is the live source of truth and is read on demand.
The query text is capped and escaped before it reaches the CQL literal. A pathologically long fragment is both a slow query and a sign the auxiliary model misbehaved, and an unescaped quote would end the literal early — turning a search into a query nobody wrote.
The search backs the ## Relevant knowledge prefetch and the search_knowledge builtin (see Knowledge System). Agents that want to search or read Confluence directly use the confluence_search and confluence_get_page MCP tools.
Authentication
Section titled “Authentication”The query-time search authenticates as the agent’s own Atlassian user, reusing the per-agent token already configured for direct Confluence MCP calls in role.mcp_env["atlassian"]:
- Cloud —
CONFLUENCE_USERNAME+CONFLUENCE_API_TOKEN. - Data Center —
CONFLUENCE_PERSONAL_TOKEN.
The credential is read from mcp_env.confluence if a seat has one, otherwise from the shared mcp_env.atlassian — Atlassian’s own MCP server covers both products, so the documented entry is named atlassian and a product-specific block exists only where somebody deliberately made one.
One account, one answer, and it used to be three. Jira, Confluence and the Atlassian provisioner each derived “does this seat have an Atlassian credential” from its own list of server names and key spellings, and those lists had drifted. A seat holding mcp_env.atlassian.JIRA_API_TOKEN — the spelling the provisioner’s own advice tells you to write — read as ready to Jira and as no Confluence credential yet to Confluence, on the same account, in the same block, permanently. There is one reader now, and in the shared block each product will also accept the other’s *_API_TOKEN: on Cloud one API token belongs to the account and authenticates both. A *_PERSONAL_TOKEN never crosses, because a Data Center PAT is issued by one product and refused by the other — the spelling already carries the distinction. Authorization: Bearer … is accepted here too, which Confluence never read before.
Roles without a per-agent Confluence token fall back to the org token (integrations.confluence.token), which sees whatever that account sees. That fallback is exactly why an unscoped search is then refused rather than run: searching the whole instance on a shared credential is how one seat reads a page its own account never could.
Page-level restrictions — enforced natively by Confluence
Section titled “Page-level restrictions — enforced natively by Confluence”Because the search runs as the agent’s own Atlassian user, Confluence enforces its page permissions natively: a page the agent’s user cannot see simply does not appear in the result set. There is no engine-side restricted-page handling — no has_restrictions flag, no empty-content audit rows, no separate ACL store.
To control what an agent can read, set Confluence page/space permissions on that agent’s Atlassian user — exactly as you would for a human team member. To make a restricted page agent-readable, grant the agent’s user access (or move the content to a space the agent can reach).
Scoped spaces
Section titled “Scoped spaces”The CQL query is narrowed by a space IN (...) clause built from one source: the org-wide knowledge.scope list. It is the same for every agent — a unit’s own space is identity (routing + writes, above) and does not narrow reads.
Scoping is optional, and empty is the useful default. If knowledge.scope is empty, the search falls back on the auth model: a role with its own Confluence credentials searches unscoped (the space IN (...) clause is dropped and Confluence ACLs bound the results — it sees every space its account can read), while a credential-less role — which would otherwise search the org admin’s entire view — searches nothing. So with per-agent tokens everywhere you can omit knowledge.scope entirely and let Confluence ACLs scope reads; set it only to narrow the search to a curated floor.
knowledge: backend: confluence scope: ["HANDBOOK", "GENERAL"] # optional read-scope floor — omit to rely on per-agent ACLs
units: - name: Engineering space: "ENG" # ENG is Engineering's WRITE / routing home — it does NOT scope readsRead scope is computed at query time from the normalised knowledge.scope, so a live config edit to the scope takes effect with no restart and no refresh hook. With the config above, every agent’s search is scoped to {HANDBOOK, GENERAL} regardless of unit; an Engineering agent is not restricted to ENG (and with knowledge.scope omitted it searches across everything its Atlassian account can read).
Unreviewed auto-drafted skills never reach an agent. A hit whose ancestor chain includes Auto-Drafted Skills is dropped, and so is one whose title still carries the [Auto-draft] prefix — two tests, because the first can silently stop matching (an instance that answered without the ancestor expand) and an exclusion that quietly matches nothing looks exactly like a knowledge base with no drafts in it. A lead publishes a draft by moving it out of that parent, which is the review gesture; the prefix is cleared with it.
The tool-skills space is not knowledge. Pages in knowledge.skills_container (default TS) are machinery — a seat told to read one would follow an instruction written for a different phase of a different turn — so they are excluded from search and from routing alike, while still being indexed into the skill registry.
When Confluence search is unavailable
Section titled “When Confluence search is unavailable”The ## Relevant knowledge prefetch stays empty — logged, never an error — whenever the search cannot run: no confluence block, an org token that did not resolve, an unreachable instance, a refused query. A turn must not die because a wiki was slow, so every failure path is an empty result.
There is also a cheap no-I/O pre-gate: when a search is a guaranteed no-op (no scope AND no per-seat credential), the turn-start prefetch skips the auxiliary model call that would have generated the query. That call is the expensive half, so a gate that had to reach the network to answer would cost more than it saves.
The search needs a Confluence connection and a model for query generation. It needs no database and no embeddings provider.
How It Works
Section titled “How It Works”Confluence serves two roles in a Crewlet company: knowledge source (agents search and read docs) and knowledge sink (agents publish results back).
Inbound: Confluence Content Changes Wake Agents
Section titled “Inbound: Confluence Content Changes Wake Agents”Routing Strategy
Section titled “Routing Strategy”A wiki page event names only who edited it — there are no assignees — so routing has three signals, and the last is a fallback in the strict sense: it says “this concerns your team”, never “this is yours”.
- Subscribers — seats that have touched this page before. A seat is subscribed when it edits the page or when somebody @mentions it there.
- @mentions — a comment or page body containing
<ri:user ri:account-id="…"/>markup routes to the seats named. The Confluence UI does not allow mentioning service accounts, but the API can insert mention markup, so agents commenting through their MCP tools can direct a page to a colleague. - Space leads — if neither produced a recipient, every unit lead mapped to the space key gets it, minus the actor.
Steps 1 and 2 are one tier, not a precedence. Ordering them against each other is the wrong question: a mention is a directed ask and a subscription is a declared interest, and suppressing either in favour of the other loses a recipient who genuinely wanted the event. Both fire, and a seat that is both mentioned and subscribed gets exactly one notification (under the mention, the stronger reason) — two copies would be two turns for one page change. Only the lead fallback is exclusive: it exists for the case where nobody was found at all.
“Resolved to a known agent” means the account maps to an agent seat in the org, not to an agent running on the node that received the webhook — a recipient owned by another node is routed to normally, since the notification is addressed by handle and consumed by whichever node owns that seat. Humans resolve to nothing here, deliberately: Confluence already notified them natively, and counting one as a delivered recipient would suppress the space-lead fallback in favour of a notification the engine then skips.
The subscription list is the engine’s, not Confluence’s
Section titled “The subscription list is the engine’s, not Confluence’s”Confluence does keep watchers, and reading them is the obvious design. It is the wrong one here, for three reasons that compound:
- the watcher list is mostly people, who Confluence has already notified and who resolve to nothing the engine can wake;
- reading it costs a call per event, on a path that has to stay cheap;
- a per-role token frequently cannot read another user’s watch state, so the answer would be “nobody watching” on exactly the deployments this is documented for.
So the engine keeps its own list, of the only parties it can route to anyway, on the coordination store — a seat subscribed by a mention one node handled has to be found by whichever node handles the next event. Membership is asked as a single question per event (“which of my seats is subscribed to this page?”), so the cost does not grow with a page’s history.
The list is bounded by the coordination bucket’s retention rather than a per-page expiry: a page nobody has touched inside that window drops its subscribers, which is the right forgetting — a seat that edited a page a year ago is not waiting on it.
Every node reads the same list, a single embedded node included: the bucket rides the node’s own broker, so there is no shape that routes by mentions and space leads alone.
An edit subscribes you; a comment does not
Section titled “An edit subscribes you; a comment does not”This asymmetry is the whole delegation loop, and it is also the rule Confluence applies to people. Editing a page is a claim on it. Commenting on one is often the opposite — handing it over — so a lead answering a page with “@teammate, this is yours” must not thereby subscribe itself, or every later event comes straight back and the delegation achieved nothing. The mention subscribes the teammate; the comment does not subscribe the lead.
Pages in the Tool Skills space subscribe nobody: their events are machinery and are excluded from routing entirely, so a subscription there could only ever produce notifications the parser drops.
Self-ignore: an agent is never notified about its own action
Section titled “Self-ignore: an agent is never notified about its own action”Every routing step excludes the user who triggered the webhook — the agent already knows about the action it just performed. This matters most for space-lead routing: a lead acting in the space it leads (e.g. a CEO commenting on a page in the leadership space) is the default space-lead recipient for the resulting comment_created / page_updated webhook. Without the exclusion, that webhook routes straight back to the lead, which wakes it to “acknowledge” the change — posting another comment that triggers another webhook, an endless self-notification loop.
When the only candidate recipient is the trigger user (the sole subscriber, or the sole space lead), the event is dropped rather than falling through to a later routing step. The notification service adds a transport-agnostic backstop: any inbound notification whose actor_external_id resolves to the recipient itself is skipped (recorded as a NotificationSkipped event). actor_external_id is the one actor key every integration stamps (a per-integration key protects the integrations somebody remembered and silently protects none of the others), and the actor is resolved through the handle registry rather than string-matched, so a seat’s bot identity and its member identity compare equal. Both layers depend on each agent authenticating as a distinct Atlassian user (per-role CONFLUENCE_API_TOKEN) so the engine can tell whose action it was. See the per-role-token note below.
Lead-fallback prompt hint
Section titled “Lead-fallback prompt hint”When a lead receives an event via routed_via = "space_lead" (steps 1-2 produced no recipient), the Confluence notification prompt adds a ## Why You Received This section that names the space, warns the lead that no one else is watching the page, and lays out three explicit decisions:
- Delegate —
@mentionthe right teammate in a comment on the page. The mention subscribes them, so the next event on the page reaches them instead. Commenting does not subscribe the lead; editing the page would. - Act yourself — if the page concerns the lead’s own work or needs a lead-level response, reply directly.
- Escalate — if the page is outside the team’s scope or the lead can’t identify the right reviewer,
@mentiontheir own manager (named in the identity prompt) in a comment, which subscribes the manager and lets them decide where the page belongs. Space-lead fallback fires only when nobody else is involved, so silently walking away would leave the page with no subscriber at all.
The hint is suppressed for watcher / mention routings — those carry their own signal of personal involvement.
Example 1: Agent SWE edits a page, which subscribes it. A human then comments:
- Agent SWE gets the notification (
routed_via: watcher) - Unit leads do NOT get it — a subscriber was found
Example 2: Agent PM comments on a page mentioning @Agent CTO via the API:
- Agent CTO gets the notification (
routed_via: mention) and is subscribed to the page - The next event on the page reaches Agent CTO (
routed_via: watcher) - Agent PM is not subscribed by having commented — see An edit subscribes you; a comment does not
Important: per-role tokens are what make subscriptions work. A seat is subscribed by editing or being mentioned, and both are attributed to whichever Atlassian user acted — so this only distinguishes agents when each authenticates as a distinct one (per-role CONFLUENCE_API_TOKEN in mcp_env). If every agent shares one service account, Confluence records the same user for every edit, one seat’s subscription is every seat’s, and the self-ignore rule silences the page for all of them. See Jira Integration for the mcp_env pattern.
Commenting deliberately does not subscribe you. Only an edit subscribes its author. That asymmetry is what makes delegation work — a lead handing a page over by comment must not stay subscribed to it — and it matches what Confluence does for people. A seat that wants a page it only commented on has to edit it, or be mentioned on it by somebody.
@mentions via API only. The Confluence UI does not allow @mentioning service accounts. However, the API can insert mention markup (<ri:user ri:account-id="..."/>) when agents create comments via MCP tools. The transport extracts these mentions and routes accordingly. For human users wanting to direct a comment to an agent, they must use the agent’s display name in the comment text (not @mention) — the notification still routes to the page’s subscribers, and to the space lead if it has none.
Outbound: Agents Write to Confluence
Section titled “Outbound: Agents Write to Confluence”Agents use MCP tools directly to create or update pages. Common patterns:
- Decision records — after a DACI decision resolves, the driver agent publishes an ADR page
- Sprint reports — a PM agent summarizes completed Jira tickets into a Confluence page
- Incident postmortems — an agent compiles findings and publishes a structured postmortem
- Meeting notes — agents document outcomes from multi-agent discussions
Publishing Local Pages from Your Machine (CLI)
Section titled “Publishing Local Pages from Your Machine (CLI)”Operators publish locally authored markdown — Tool Skills and knowledge docs — to Confluence with crewlet confluence import. Each .md file is routed by its frontmatter: a trigger: makes it a Tool Skill page (in the Tool Skills space); everything else becomes a knowledge doc whose space is its parent-directory name and title is its first # H1.
crewlet confluence import <company.yaml> ./docs-to-publish(Neither bundled Nimbus example ships a confluence: block — both run the engine’s own knowledge base — so neither works as the positional argument as-is: add the block at the top of this page to one of them first, and switch its knowledge.backend to confluence in the same edit. examples/nimbus-docs/ is the set of pages either publishes.)
- The first positional argument is the Tier B company YAML and the second is the directory — the Confluence credentials come from its
confluence:block, resolved through the node’s secret store and then the environment (pass-configto name a different Tier A document). - Every target space is checked before a single page is written. A typo in a directory name would otherwise be discovered half way through, leaving an operator to work out which pages landed. The importer never creates a space: that names a container the whole company then works in, and guessing it is not this command’s job.
- A page that exists is updated in place, matched by title within its space. Confluence has no external-id field, so a page somebody renamed in the UI is orphaned and a re-import creates a second one — a real limitation of the backend, reported rather than worked around with a marker pressed into service as a second identity.
- Frontmatter may declare
parent:andlabels:. Frontmatter may also declare aparent:— the title of a page in the same space to nest this one under, which is the one thing a flat directory of files cannot say about a wiki that has trees in it — andlabels:, the author’s own page labels. The plan is ordered parents-first so aparent:naming a page published by the same run resolves; a cycle stops the walk naming the files, and a parent nobody publishes is a note and a page at the space root, because a doc nobody can read is worse than a doc in the wrong place. An existing page is never re-parented — where a page sits is something people move deliberately, and a run that dragged it back every time would be fighting them with no way to say so. Labels are lower-cased and de-duplicated at parse time, because that is what Confluence stores and answers with; a label that will not attach is a note, not a page failure. - Every skill page this command writes gets the
crewlet-skilllabel. That is provenance, not identity: it says only that the importer wrote the page, which is a fact no field on the page carries.-pruneis the one thing that needs it. - Page failures are isolated. A restricted page or one 403 does not cost the other forty; the run reports what failed and exits non-zero.
-space KEYpublishes tool skills into a space other thanknowledge.skills_container; empty reads$CREWLET_TOOL_SKILLS_SPACE, then the config field. A company that has turned tool skills off withskills_container: ""has nowhere for a skill file to go, so a tree containing one stops the walk naming both the setting and this flag.-prunedeletes the skill pages this tool published that no local file publishes any more — labelled, parsing as a skill, and with a key this run’s tree does not carry. All three conditions are required: the label protects a lead’s hand-authored page, the parse protects an ordinary page filed in the same space, and the key comparison makes a renamed skill a delete-and-create rather than a silent duplicate. A prune that cannot enumerate the space deletes nothing and fails the run, because the orphan set is derived by subtraction and a partial read deletes live pages. Deleted pages go to the space’s trash, so an operator who pruned something they wanted restores it in the UI.- Add
-dry-runto print the plan and write or delete nothing.
Publish first, then start the engine — two commands, in that order. The importer reads its credentials from the Tier B company YAML, so it works before a node is configured at all, and running it first means the engine’s boot-time sync finds the pages already there.
crewlet confluence resync <company.yaml> is the read-only diagnostic beside it: it runs the same walk and the same admission every node’s skill sync runs, against a throwaway registry, and prints the keys that loaded plus any page that declares a trigger: and does not parse. It answers “why is this skill not being applied”, not “make it apply”: it does not reach into a running engine, which applies a page change on every node through the page webhook and catches up anything that path missed on its periodic walk, every 10 minutes give or take a fifth (see Keeping every node current). -space targets a space other than the configured one, for checking a container before pointing the company at it. It exits non-zero on a page that meant to be a skill and failed to decode, because the only other symptom is guidance that never appears. See the CLI reference.
Teaching Agents to Use Confluence
Section titled “Teaching Agents to Use Confluence”MCP tools give agents the capability to use Confluence, but agents also need to know when and why to use it. Three layers carry this guidance:
1. Behavioral guidelines (per role, in YAML)
Section titled “1. Behavioral guidelines (per role, in YAML)”These render directly into the role’s executor system prompt — no DB seed step.
roles: - name: Architect behavioral_guidelines: - "Search Confluence (ENG space) for existing architecture docs before proposing changes" - "Publish all architecture decisions to Confluence under 'Architecture Decisions'" - name: PM behavioral_guidelines: - "After each sprint, summarize completed work in a Confluence page under 'Sprint Reports'" - "When creating a new project, check Confluence for existing requirements docs first"2. The Onboarding page convention (per unit)
Section titled “2. The Onboarding page convention (per unit)”Each unit’s Confluence space can host an Onboarding page that fresh agents are nudged to read on their first turn. A dedicated first-turn onboarding pass runs before the executor, shown a ## First-turn onboarding block listing every Onboarding page on the agent’s unit chain (org root + each ancestor unit + own unit); the agent reads them, captures conventions via reflect_and_persist, and calls mark_onboarded to suppress the hint until the org chain changes. See Agent Learning § Prompt scaffolding.
3. Query-time Confluence search (engine side)
Section titled “3. Query-time Confluence search (engine side)”The ## Relevant knowledge block runs a live Confluence search for the seat: the Confluence searcher has the auxiliary LLM generate a CQL query from the trigger and runs it against the Confluence REST API, optionally narrowed to the org-wide knowledge.scope (empty ⇒ unscoped, with the agent’s own Confluence ACLs bounding the results). The search_knowledge builtin runs the same search on a query the executor writes itself. See Knowledge System § Relevant-knowledge prefetch. Agents that want to search or read pages themselves use the confluence_search and confluence_get_page MCP tools.
This approach keeps tool guidance configurable per-org and per-role rather than hardcoded in the engine. The same Confluence MCP tools are available to all agents, but each agent’s prompt scaffolding and accessible spaces determine how they use them.
Mentioning Other Agents in Confluence Comments
Section titled “Mentioning Other Agents in Confluence Comments”The Confluence UI does not allow @mentioning service accounts, but agents can mention each other via the API by including Atlassian user markup in comment bodies. To mention another agent, use the confluence_add_comment tool with the following HTML format:
<p>Hey <ac:link><ri:user ri:account-id="ACCOUNT_ID"/></ac:link>, please review this page.</p>Replace ACCOUNT_ID with the target agent’s Atlassian account ID. The parser extracts the mention, routes the notification to the agent named, and subscribes them to the page so later events reach them too.
This syntax is carried by a Tool Skill — a Confluence-sourced prompt fragment triggered for any role with atlassian in its mcp_env. The skill’s summary appears in the per-phase catalogue and the full mention-syntax body loads on demand via load_tool_skill, so agents with Confluence tools know how to mention others without paying the token cost on every turn.
Integration with Jira
Section titled “Integration with Jira”When both Jira and Confluence are configured, agents can cross-reference between them:
- Jira ticket → Confluence page: An agent working on a Jira issue can search Confluence for related documentation, architecture decisions, or runbooks.
- Confluence page → Jira ticket: An agent reading a requirements page can create Jira tickets for each action item.
- Linked artifacts: Agents can add Confluence page links to Jira tickets and vice versa, maintaining traceability.
This works naturally because both integrations share the mcp-atlassian MCP server — all Jira and Confluence tools are available in the same tool list.
The Confluence MCP tool server is declared separately in mcp_servers (the atlassian entry shown under Configuration), and it is a different surface from the one this page has been describing: the MCP server is what an agent calls, while the integrations.confluence block is what the engine reads — the inbound webhook, the knowledge search and the tool-skill walk. For Cloud, install the Crewlet Forge app, which delivers webhooks over Forge Remote.
Part of Crewlet. Generated from crewlet/crewlet main at f665f5a. This is not the current version — see the latest docs.