Skip to content
Changelog

Workflow functions run once and work under arun()

October 22, 20261 min read

Agno's Workflow dropped the except TypeError fallback around a function passed as steps, which in earlier releases called the function a second time. Python can't tell a bad argument binding from a bug like "Total: " + 42 in the body, so that fallback fired on ordinary bugs. A sync function with default parameters ran again with no input, and an async function ran again with the same input. A function that charges a card would have charged twice.

A counter on both releases shows it:

calls = []
 
def checkout(workflow=None, execution_input=None):
    calls.append(execution_input.input if execution_input else None)
    return "Total: " + 42
 
Workflow(name="checkout", steps=checkout).run("order-1")
# The same body as async def acheckout(workflow, execution_input) runs under arun("order-2")
# Before (real output)
ERROR   Function signature inspection failed. Falling back to original calling convention.
TypeError: can only concatenate str (not "int") to str
checkout ran 2 time(s): ['order-1', None]
async checkout ran 2 time(s): ['order-2', 'order-2']
 
# Now (real output)
TypeError: can only concatenate str (not "int") to str
checkout ran 1 time(s): ['order-1']
async checkout ran 1 time(s): ['order-2']

Sync functions and generators under arun()

Agno now routes sync generators and sync functions through the same signature check on every run path. Earlier releases called them positionally with every internal keyword argument, so a sync generator failed under arun() whatever its signature, a sync function failed under arun(stream=True), and a narrow sync generator failed under run():

def summarize(execution_input):
    return f"done {execution_input.input}"
 
def stream_summary(execution_input):
    yield f"done {execution_input.input}"
# Before (real output)
summarize, arun(stream=True): TypeError: summarize() got an unexpected keyword argument 'websocket'
stream_summary, arun(): TypeError: stream_summary() got an unexpected keyword argument 'websocket'
stream_summary, arun(stream=True): TypeError: stream_summary() got an unexpected keyword argument 'websocket'
stream_summary, run(): TypeError: stream_summary() takes 1 positional argument but 2 were given
 
# Now (real output)
summarize, arun(stream=True): done order-3
stream_summary, arun(): done order-4
stream_summary, arun(stream=True): done order-4
stream_summary, run(): done order-5

One signature behaves differently now. A positional-only workflow parameter with a default, def steps(workflow=None, /), used to run with workflow=None and now raises TypeError.

See the cookbook, and learn more about the fully Python workflow in the documentation.

Frequently asked questions

Earlier releases wrapped the call to a steps function in except TypeError and called it again. A TypeError raised inside the body, such as "Total: " + 42, triggered that retry. This release calls the function once and raises the original error.

Yes. Agno now passes a sync generator only the parameters it declares under arun() and arun(stream=True). Earlier releases failed with got an unexpected keyword argument 'websocket'.

No. Functions used as a Step executor go through a separate call path with no fallback, so they always ran once.

Shipped around the same time