Skip to content

Let plugins add their own actions to Dag, run and task instance rows - #75229

Merged
bbovenzi merged 1 commit into
apache:mainfrom
astronomer:pr/plugin-ui-actions
Oct 22, 2026
Merged

bbovenzi merged 1 commit into
apache:mainfrom
astronomer:pr/plugin-ui-actions

Conversation

@bbovenzi

@bbovenzi bbovenzi commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Let plugins add their own custom buttons to a Dag, Dag Run or Task Instance. These buttons will appear on the Header card of any details page but also the list of dags, runs and TIs.

They can link to an existing page or plugin in Airflow or open an external page. They accept all the new applie_to fields to target the actions. Think adding a special "open an AI chat" button to explain failed task instance logs.

Examples:

Task Instance action for tasks with bash operator and dag tag of "example". External link
Screenshot 2026-10-20 at 12 30 07 PM

Dag action for draining dags. Internal link.
Screenshot 2026-10-20 at 12 29 07 PM

Dag run action for a failure. External Link.
Screenshot 2026-10-20 at 12 29 26 PM

Combing a "base" destination plugin with url params to open a custom modal on a button click:
Screenshot 2026-10-20 at 12 52 56 PM


Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)
    Claude Opus 5

  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

@bbovenzi
bbovenzi requested a review from jason810496 as a code owner October 20, 2026 20:26
@dheerajturaga

Copy link
Copy Markdown
Member

@bbovenzi , awesome! will review this today. I hope the 3.4 train hasn't left the station

Comment thread airflow-core/src/airflow/ui/src/pages/ReactPlugin.test.tsx Outdated
Comment thread airflow-core/tests/unit/plugins/test_plugins_manager.py Outdated
Comment thread airflow-core/src/airflow/plugins_manager.py Outdated
Comment thread airflow-core/docs/howto/custom-action-plugin.rst
Comment thread airflow-core/docs/howto/custom-action-plugin.rst
Comment thread airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx Outdated
Comment thread airflow-core/src/airflow/ui/src/utils/links.ts Outdated
Comment thread airflow-core/src/airflow/ui/src/components/PluginActions/PluginReactAction.tsx Outdated
Comment thread providers/common/ai/tests/unit/common/ai/plugins/test_model_panel.py Outdated
@bbovenzi
bbovenzi force-pushed the pr/plugin-ui-actions branch from 3dfee57 to 4139c56 Compare October 21, 2026 13:46

@pierrejeambrun pierrejeambrun left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice extension point, and it holds up well end to end: I ran it behind a base_url sub-path with link, React, applies_to-scoped and invalid actions. Unsafe hrefs get dropped and templated values are encoded, nice.

A few things before merge, mainly the base-path link resolution and keeping the response schema permissive so we don't lock out future fields. Details inline.

Comment thread airflow-core/newsfragments/75229.feature.rst Outdated
Comment thread airflow-core/src/airflow/ui/src/utils/links.ts Outdated
Comment thread airflow-core/src/airflow/api_fastapi/core_api/datamodels/plugins.py Outdated
Comment thread airflow-core/tests/unit/plugins/test_plugins_manager.py Outdated
Comment thread airflow-core/tests/unit/plugins/test_plugins_manager.py Outdated
Comment thread airflow-core/src/airflow/api_fastapi/core_api/datamodels/plugins.py Outdated
Comment thread airflow-core/src/airflow/ui/src/utils/pluginAppliesTo.ts
Comment thread airflow-core/src/airflow/ui/src/pages/DagsList/DagsListCountsContext.tsx Outdated
Comment thread airflow-core/src/airflow/ui/src/utils/pluginTemplate.ts Outdated
@bbovenzi
bbovenzi force-pushed the pr/plugin-ui-actions branch 5 times, most recently from 9e4cdea to e43b85e Compare October 21, 2026 17:33

@dheerajturaga dheerajturaga left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a useful extension point. I ran it locally with a test plugin that covers link and React actions on all three destinations: in-app and external links, applies_to scoping on tags, state, operator and dag_id, custom and dark-mode icons, a popover and a dialog React action, and a set of deliberately invalid entries. Everything behaved as documented, including the load-time rejections and their log messages.

There are two small things inline, neither blocking:

  • the bulk-action bar on the Dags list lost its translation when it was extracted (DagsList.tsx:120);
  • the docs should warn that a React action's name can collide with a browser global (custom-action-plugin.rst:252), plus a related one-line comment fix (PluginActionBoundary.tsx:33).

This review was drafted by an AI-assisted tool and
confirmed by an Apache Airflow maintainer. The maintainer
approving this PR has read the findings and signed off. If
something feels off, please reply on the PR and a maintainer
will follow up.

More on how Apache Airflow handles maintainer review:
Contributing guide.

Comment thread airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx Outdated
Comment thread airflow-core/docs/howto/custom-action-plugin.rst
Comment thread airflow-core/src/airflow/ui/src/components/PluginActions/PluginActionBoundary.tsx Outdated

@pierrejeambrun pierrejeambrun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Brent, everything from the previous round is addressed, and base-path links now navigate in-app end to end. Could you take a look at the open comments from the latest review before merging? Two small things inline.

Comment thread airflow-core/src/airflow/ui/rules/table-renderers.js Outdated
Comment thread airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_plugins.py Outdated
@bbovenzi
bbovenzi force-pushed the pr/plugin-ui-actions branch from e43b85e to ea6bf2e Compare October 22, 2026 13:16
Airflow draws a fixed set of actions beside a Dag, a Dag Run and a Task
Instance -- trigger, clear, mark state -- and a plugin had no way to put
anything next to them. A plugin could add a whole page as a tab, but not a
single button on the row or header card where the user already is, so the
work it offers for one entity had to be reached by navigating away from that
entity.

An action is either a `link`, which opens a URL templated with the entity's
context, or `react`, which mounts the plugin's own component behind a trigger
button. The React kind is handed `open`, `onClose` and `triggerRef` and
portals whatever surface it wants -- a popover, a dialog, a drawer -- rather
than being given a modal to fill, because the right surface depends on what
the plugin is showing.

Actions are scoped by the same `applies_to` field paths as tabs and panels,
through the same matchers, so a button can be offered only where it applies:
only on failed task instances, only on Dags of a given team. That is the
reason the two were designed together.

A React action's `name` is the global its bundle registers under, so it
cannot share one with a React App or another React action -- the second to
load would silently win. Such an action is refused rather than loaded.
@bbovenzi
bbovenzi force-pushed the pr/plugin-ui-actions branch from ea6bf2e to 74a810b Compare October 22, 2026 13:22
@bbovenzi
bbovenzi merged commit c510e4d into apache:main Oct 22, 2026
146 of 147 checks passed
@bbovenzi
bbovenzi deleted the pr/plugin-ui-actions branch October 22, 2026 16:10
@bbovenzi bbovenzi added the backport-to-v3-4-test Backport to v3-4-test label Oct 22, 2026
@bbovenzi bbovenzi added this to the Airflow 3.4.0 milestone Oct 22, 2026
bbovenzi added a commit that referenced this pull request Oct 22, 2026
…75229) (#75427)

Airflow draws a fixed set of actions beside a Dag, a Dag Run and a Task
Instance -- trigger, clear, mark state -- and a plugin had no way to put
anything next to them. A plugin could add a whole page as a tab, but not a
single button on the row or header card where the user already is, so the
work it offers for one entity had to be reached by navigating away from that
entity.

An action is either a `link`, which opens a URL templated with the entity's
context, or `react`, which mounts the plugin's own component behind a trigger
button. The React kind is handed `open`, `onClose` and `triggerRef` and
portals whatever surface it wants -- a popover, a dialog, a drawer -- rather
than being given a modal to fill, because the right surface depends on what
the plugin is showing.

Actions are scoped by the same `applies_to` field paths as tabs and panels,
through the same matchers, so a button can be offered only where it applies:
only on failed task instances, only on Dags of a given team. That is the
reason the two were designed together.

A React action's `name` is the global its bundle registers under, so it
cannot share one with a React App or another React action -- the second to
load would silently win. Such an action is refused rather than loaded.

(cherry picked from commit c510e4d)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants