What about operators? Do we want to support only tasks using Taskflow API? If not, a new parameter would work for both use cases (e.g. `result=True`).
``` @task(result=True) def my_task(num): return num*2 ``` ``` task = EmptyOperator(task_id="xxx", result=True) ``` Or a new parameter in the Dag constructor: ``` @dag(return_task="my_task_id") ``` The inconvenience of the latter is you limit one task to be a Dag task result (and it seems we want to enable having multiple task results per Dag). On 2026/06/16 10:20:44 Ash Berlin-Taylor wrote: > TaskFlow automatically suffixes pretty close to this out of the box — I think > without the override we’d end up with my_task, my_task__1, my_task__2, > my_task__3 etc. > https://github.com/apache/airflow/blob/376cecdb9f258fdb6f81f264c48f281c1cd2aeb5/task-sdk/src/airflow/sdk/bases/decorator.py#L111-L150 > > -a > > > On 16 Jun 2026, at 10:59, Tzu-ping Chung via dev <[email protected]> > > wrote: > > > > The loop would not work as-is (since it’d create multiple tasks with the > > same id). But as currently designed, you CAN set multiple result tasks on a > > dag. The result is always a dict keyed by tsk_id. So this slightly modified > > example > > > > @dag > > def my_dag(): > > @task > > def t(x): > > return x > > @result > > @task > > def my_task(num): > > return num*2 > > for i in range(4): > > my_task.override(task_id=f"my_task_{i}")(t(i)) > > > > Would have the dag result > > > > { > > "my_task_0": 0, > > "my_task_1": 2, > > "my_task_2": 4, > > "my_task_3": 6, > > } > > > > > >> On 16 Jun 2026, at 17:34, Ephraim Anierobi <[email protected]> > >> wrote: > >> > >> Hi TP, > >> > >> Thanks for bringing up this discussion. > >> > >> I feel like `@result @task` is clean, however, it won't be clear what the > >> Dag's result is if the task is invoked multiple times in a dag. > >> Take for example: > >> > >> @dag > >> def my_dag(): > >> @task > >> def t(x): > >> return x > >> @result > >> @task > >> def my_task(num): > >> return num*2 > >> for i in range(4): > >> my_task(t(i)) > >> > >> Unless I'm not understanding the @result well, but I feel like this means, > >> every invocation of `my_task` is a result of the dag. > >> > >> If result is intended to be singular, I will prefer value inference from > >> the dag: > >> > >> @dag > >> def my_dag(): > >> @task > >> def my_task(): > >> return 1 > >> return my_task() > >> > >> AND > >> > >> with DAG(...) as dag: > >> output = f() > >> dag.add_result(output) > >> > >> Thanks > >> - Ephraim > >> > >> On Tue, 16 Jun 2026 at 08:37, Tzu-ping Chung via dev > >> <[email protected]> wrote: > >> Hi all, > >> > >> I’m currently working on the [Synchronous Dag Execution] feature and > >> trying to gather opinions on how the Taskflow API should work when we want > >> to mark a task as the dag’s “result task” (i.e. “the return value is a > >> final output of the dag, not an intermediate value”). > >> > >> [Synchronous Dag Execution]: https://github.com/apache/airflow/issues/51711 > >> > >> ## Prior art (kind of) > >> > >> We currently have the setup/teardown Taskflow API like this: > >> > >> @setup > >> def f1(): ... > >> > >> @task > >> def f2(): ... > >> > >> setup1 = f1() # This is a setup task. > >> > >> t2 = f2() # This is a normal task. > >> setup2 = t2.as_setup() # This is a setup task. > >> > >> A teardown variant also exists for both cases. > >> > >> ## The decorator syntax > >> > >> The most straightforward syntax would be to have a @result decorator on a > >> plain Python function. However, I don’t like this since a result task > >> still has all the same arguments as a non-result task. Setup and teardown > >> tasks don’t accept most task arguments. If @result needs to work on a > >> plain function, it would need to duplicate and forward all the arguments > >> on @task. I feel we can avoid this redundancy by requiring @result to be > >> used ON TOP OF @task instead: > >> > >> @result > >> @task(put your arguments here...) > >> def f(): ... > >> > >> We COULD also make using @result without @task a shorthand to > >> argument-less calls (which is probably common?) > >> > >> # This... > >> @result > >> def f(): ... > >> > >> # Is equivalent to... > >> @result > >> @task > >> def f(): ... > >> > >> Alternatively, we could use a fluent interface: > >> > >> @task(arguments here...).result > >> def f(): ... > >> > >> Pro: avoids needing a top-level name. Con: Not a common pattern in Airflow. > >> > >> ## The method syntax > >> > >> I don’t think adding a method similar to as_setup/teardown makes sense > >> here. It makes sense for setup/teardown because it allows the same body of > >> code to be BOTH a setup/teardown task AND a normal task at the same time, > >> as shown above. This does not make sense for a result task—a task either > >> returns the result, or it doesn’t. If we want a method-based syntax, it > >> makes more sense to have a method on the dag: > >> > >> with DAG(...) as dag: > >> @task > >> def f(): > >> > >> t = f() > >> dag.add_result(t) > >> > >> ## For @dag decorator > >> > >> One more syntax that only makes sense here is we can automatically detect > >> the return value of an @dag-decorated function: > >> > >> @dag > >> def my_dag(): > >> @task > >> def f1(): ... > >> > >> @task > >> def f2(v): ... > >> > >> result = f2(f1()) > >> > >> return result # Marks f2 as the result task! > >> > >> --------------- > >> > >> Looking forward to hearing thoughts on the above, and more ideas on > >> possible syntaxes. > >> > >> TP > >> > >> > >> > >> > >> --------------------------------------------------------------------- > >> To unsubscribe, e-mail: [email protected] > >> For additional commands, e-mail: [email protected] > >> > > > > > > --------------------------------------------------------------------- > > To unsubscribe, e-mail: [email protected] > > For additional commands, e-mail: [email protected] > > > > --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
