-
-
The Downstream impact panel on a Jira work item: two blocking changes, what breaks, dashboards and owners to notify.
-
The issue guard comments on the work item when it is created or edited, so nobody has to remember to check.
-
Claude Code calls get-ticket-impact over MCP: both changes are blocking because orders has an enforced contract.
-
The agent switches to a versioned orders_v2 model, and the new plan passes check-change before any SQL is written.
-
The change receipt on the work item, with the owner nobody notified still flagged.
-
Ask the Downstream Rovo agent what a work item breaks and who to tell.
-
Rovo Search finds the dbt Revenue dashboard, synced through the Teamwork Graph connector, next to the work item.
-
The catalog page: models, sources and dashboards synced from GitHub with owners, contracts and usage.
-
How it fits together: one Forge app serving Jira, Rovo, MCP clients and the Teamwork Graph.
Inspiration
Most data breakages are not bugs in the code that changed. They are changes that were correct in isolation and broke something downstream that nobody on the ticket knew about: a renamed column that a finance dashboard read, a dropped field an ML feature pipeline used. dbt already knows the lineage, owners and contracts, but that knowledge lives in the dbt repository, not in the Jira ticket where the change is planned, and not in the context of the coding agents that now write a growing share of the SQL. Downstream brings it to both.
What it does
Downstream turns dbt lineage into context for everyone who plans or makes a data change.
- Impact in the ticket. For any Jira work item, Downstream finds the models, sources and columns it proposes to change and shows what each change breaks, down to the column ("fct_revenue reads discount_amount"), the dashboards and ML models it feeds, and the owners to notify. Changes to models with enforced dbt contracts are flagged as blocking.
- Plans checked before SQL is edited. Coding agents call
check-changeover MCP. A plan to rename a contracted column comes back blocked, with the reason and a fix: add a new model version and deprecate the old one with its owners. - Receipts after.
submit-change-receiptrecords what changed, who was notified, tests and the PR, and lists affected owners nobody told. - Everywhere. A Rovo agent answers "what breaks if we do DATA-42?" and "who owns orders?". An issue guard comments when a new ticket would break something. A catalog page lists every model with owner, contract and usage. A Teamwork Graph connector imports the dbt project from GitHub so Rovo Search and Chat see models and dashboards and how they relate.
How we built it
A single Forge app on the Node.js 24 runtime with UI Kit frontends.

graph:connectorwith an admin form (repository, project folder, branch, sensitive token). It reads the repository tree in one call, fetches the dbt SQL and YAML, and ingestsatlassian:documentobjects for models and sources, associated with the models they read from, andatlassian:dashboardobjects for exposures. AscheduledTriggerre-syncs daily.- A dbt parser: lineage from
ref()andsource(), owners from meta and groups, contracts fromconfig.contract.enforced, consumers from exposures, and column usage from the SQL. - Change detection that pairs each column with the nearest operation (drop, rename, cast, logic change), ignores negations, and treats plain-word model names as references only in context, followed by severity rules and plain-language advice.
rovo:agentwith a prompt resource and sixactionmodules, exposed throughrovo:mcp.jira:issuePanelandjira:globalPagein UI Kit, atriggeron issue events withignoreSelf, and Forge KVS (the catalog is stored in pages so large projects stay fast).
Reads run asUser, so agents only see the work items their user can see. 38 unit tests cover the parser, lineage, detection, severity, connector and use cases.
Challenges we ran into
- Column-level lineage without running dbt: we read column usage from each model's SQL and treat a final
select *as pass-through, which covers the common staging-to-mart pattern. - Natural language is ambiguous. "Drop discount_amount and rename amount" must produce two changes, not four; "do not drop order_id" must produce none; "orders are slow" is not about the orders model. Clause-aware pairing and context rules solved each case, with tests.
- Teamwork Graph associations are not accepted for every object type, so ingestion retries rejected objects without associations instead of failing the sync.
- Only production surfaced some platform details: dashboards must be ingested at schema 2.0, one invalid object fails the whole batch (we now retry object by object), Forge passes a context object to the connector's validation handler, and unauthenticated GitHub API calls share a small per-IP quota (public files are now read from raw.githubusercontent.com).
Accomplishments that we're proud of
- A coding agent is stopped before it breaks a contracted model, and told how to do it right.
- One implementation serves a Rovo agent, Rovo Studio agents and any external MCP client.
- dbt lineage appears in Jira and Rovo without anyone writing documentation twice.
What we learned
Agents follow constraints they receive at planning time. In our recorded run, Claude Code read the blocking result and its advice and switched to a versioned model on its own, without being told to. An earlier run exposed a gap in our own check: it blocked the very fix it recommends (a new orders_v2), and the agent pressed on anyway. We fixed the check, and learned that a guardrail has to accept its own advice or agents learn to ignore it.
What's next for Downstream
- Read
manifest.jsonfrom dbt Cloud or CI for exact column-level lineage. - Bitbucket and GitHub pull request checks that compare the diff with the receipt.
- Notify owners directly (Jira watchers, Slack) from the receipt.
- Teamwork Graph queries for "which work items touched this model this quarter?".