Skip to content

Sensors should be consistent about whether or not they return a value - #70684

Open
SamWheating wants to merge 1 commit into
apache:mainfrom
SamWheating:return-result-from-sensors
Open

SamWheating wants to merge 1 commit into
apache:mainfrom
SamWheating:return-result-from-sensors

Conversation

@SamWheating

@SamWheating SamWheating commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Maybe more of a discussion topic here - but included one example change.

Many sensors will define their own execute() method, which then conditionally calls out to the deferable / non-deferable path.

However, we're pretty inconsistent about whether or not that value should be returned or not. Some sensors will return the value of execute():

def execute(self, context: Context) -> Any:
if not self.deferrable or self.response_check:
return super().execute(context=context)

but most sensors will not, implicitly returning None:

def execute(self, context: Context) -> None:
if not self.deferrable:
super().execute(context=context)

This makes for a really confusing experience when trying to subclass or extend an existing sensor. For example, trying to subclass the ExternalTaskSensor to add a delayed timestamp XCOM output:

class ExternalTaskSensorWithCompletionDelay(ExternalTaskSensor):
    """Return a stable delay target when the upstream task is first observed."""

    def poke(self, context):
        is_done = super().poke(context)
        delay_target = timezone.utcnow() + timedelta(minutes=30).isoformat()
        
        return PokeReturnValue(is_done=bool(is_done), xcom_value=delay_target)

The XCOM value here will not actually be written, because the parent's class execute method ignores the return value and task.execute() returns nothing.

Is this expected behaviour? Or should we always be returning the value of super().execute()? I am open to discussion here but in my mind this sort of thing quietly breaks the extensibility and flexibility of builtin or provided operators.


Important

🛠️ Maintainer triage note for @SamWheating · by @potiuk · 2026-08-13 12:55 UTC

Helpful heads-up from the maintainers — please address before this PR can be reviewed:

  • ❌ Merge conflicts. See docs.

Full list of what we check: Pull Request quality criteria.

The ball is in your court — you've been assigned to this PR. Fix the above, then mark it Ready for review.

Automated triage — may be imperfect; a maintainer takes the next look.

@SamWheating SamWheating changed the title return result when using sensor Sensors should return the result of execute() to not swallow XCOM outputs Jul 29, 2026
@SamWheating SamWheating changed the title Sensors should return the result of execute() to not swallow XCOM outputs Sensors should be consistent about whether or not they return a value Jul 29, 2026
@SamWheating
SamWheating force-pushed the return-result-from-sensors branch from 6854b66 to 93ba2ee Compare July 29, 2026 16:16
@SamWheating
SamWheating marked this pull request as ready for review July 29, 2026 16:45
@SamWheating

Copy link
Copy Markdown
Contributor Author

It looks like the one test failure is unrelated / maybe a flakey test

@potiuk
potiuk marked this pull request as draft August 13, 2026 13:00
@SamWheating
SamWheating force-pushed the return-result-from-sensors branch from 93ba2ee to 38fd7cf Compare August 19, 2026 11:22
@SamWheating
SamWheating marked this pull request as ready for review August 19, 2026 11:23
@SamWheating

Copy link
Copy Markdown
Contributor Author

cc @potiuk - conflicts have been fixed. I am mostly interested in your thoughts on this issue / the inconsistency of the interface around sensors. Do you think that this is something we should be opinionated on?

@potiuk potiuk 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 for raising this. My view: yes, we should be opinionated, but narrowly.

BaseSensorOperator.execute() already has a contract. It returns PokeReturnValue.xcom_value (or None for a plain bool poke()). An execute() override that calls super().execute(context) without returning it breaks that contract for anyone who subclasses the sensor, which is exactly your ExternalTaskSensor example. So the rule should be: if a sensor overrides execute() for the non-deferrable path, it must return super().execute(context).

This is behaviour-neutral for our own sensors. None of the 56 overrides that currently drop the value has a poke() that returns PokeReturnValue, so they still return None and push no XCom. The only visible change is for user subclasses that return PokeReturnValue(xcom_value=...), and for them it fixes a silently dropped value. I'd keep execute_complete() return values for deferrable mode out of scope, since changing those would change XComs people already consume.

Suggested way forward: land this PR as the reference change, then do the remaining sensors as one PR per provider. Optionally add a small prek check that flags a sensor execute() calling super().execute(...) without returning it.

On this PR specifically:

  • CI failures are not flaky. When the branch was rebased over #68997, the deferrable branch got poke_interval=self.poll_interval back (lines 485 and 510). poll_interval is now a deprecated property, so the 5 deferrable ExternalTaskSensor tests fail on the prohibited AirflowProviderDeprecationWarning in every compat job. It needs to be self.poke_interval.
  • Please keep the diff minimal: just return super().execute(context) in the existing if not self.deferrable: branch, without removing the else: and dedenting the deferrable code. The restructuring is what caused the bad merge, and it hides the one-line change.
  • Please change the return annotation from -> None to -> Any.
  • Please add a test that fails without the change: a subclass whose poke() returns PokeReturnValue(is_done=True, xcom_value=...), asserting that the non-deferrable execute() returns that value.

Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants