Custom Functions in Workflows
Run custom Python functions as workflow steps by passing them as the Step executor.
Custom functions give you full control over a step's logic. Use them to preprocess inputs, orchestrate agents and teams, and postprocess outputs.
Key Capabilities
- Custom Logic: Implement complex business rules and data transformations
- Agent Integration: Call agents and teams within your custom processing logic
- Data Flow Control: Transform outputs between steps
Implementation Pattern
Define a Step with a custom function as the executor. A standard function accepts StepInput and returns StepOutput. Generator functions can yield executor events before yielding their final StepOutput.
Example
from agno.workflow import Step, StepInput, StepOutput, Workflow
def collect_research(step_input: StepInput) -> StepOutput:
return StepOutput(content=f"Research notes for {step_input.input}")
def create_content_plan(step_input: StepInput) -> StepOutput:
research = step_input.previous_step_content or "No research available"
return StepOutput(
content=f"Content plan based on:\n{research}",
)
content_planning_step = Step(
name="Content Planning Step",
executor=create_content_plan,
)
workflow = Workflow(
name="Content Planning Workflow",
steps=[collect_research, content_planning_step],
)
workflow.print_response("Agent architecture", markdown=True)Standard Pattern
A synchronous function step follows this structure:
def custom_content_planning_function(step_input: StepInput) -> StepOutput:
content = step_input.previous_step_content or str(step_input.input)
return StepOutput(content=f"Processed:\n{content}")Class-based executor
You can also use a class-based executor by defining a class that implements the __call__ method.
class CustomExecutor:
def __call__(self, step_input: StepInput) -> StepOutput:
content = step_input.previous_step_content or str(step_input.input)
return StepOutput(content=f"Processed:\n{content}")
content_planning_step = Step(
name="Content Planning Step",
executor=CustomExecutor(),
)When is this useful?
- Configuration at initialization: Pass in settings, API keys, or behavior flags when creating the executor
- Stateful execution: Maintain state while the same executor instance is reused
- Reusable components: Create configured executor instances that can be shared across multiple workflows
Protect mutable executor state when workflow runs can overlap.
class CustomExecutor:
def __init__(self, prefix: str = "Processed"):
self.prefix = prefix
self.call_count = 0
def __call__(self, step_input: StepInput) -> StepOutput:
self.call_count += 1
content = step_input.previous_step_content or str(step_input.input)
return StepOutput(
content=f"{self.prefix} call {self.call_count}:\n{content}"
)
content_planning_step = Step(
name="Content Planning Step",
executor=CustomExecutor(prefix="Content plan"),
)Class-based executors also support async execution. Define the __call__ method as an async function.
class CustomExecutor:
async def __call__(self, step_input: StepInput) -> StepOutput:
content = step_input.previous_step_content or str(step_input.input)
return StepOutput(content=f"Processed:\n{content}")
content_planning_step = Step(
name="Content Planning Step",
executor=CustomExecutor(),
)For a detailed example see Class-based Executor.
Streaming from a Custom Function
An async generator function can yield agent or team events followed by its final StepOutput. AgentOS can then expose those events through the workflow stream.
Use .arun() for an agent or team called from an async function executor.
from typing import AsyncIterator, Union
from agno.agent import Agent
from agno.db.in_memory import InMemoryDb
from agno.models.openai import OpenAIResponses
from agno.run.agent import RunOutputEvent
from agno.workflow import Step, StepInput, StepOutput
content_planner = Agent(
name="Content Planner",
model=OpenAIResponses(id="gpt-5.2"),
instructions="Create a concise content plan from the supplied research.",
db=InMemoryDb(),
)
async def custom_content_planning_function(
step_input: StepInput,
) -> AsyncIterator[Union[RunOutputEvent, StepOutput]]:
message = step_input.input
previous_step_content = step_input.previous_step_content
planning_prompt = f"""
Topic: {message}
Research: {previous_step_content or "No research results"}
Create a four-week content plan with three posts per week.
"""
try:
events = content_planner.arun(
planning_prompt, stream=True, stream_events=True
)
async for event in events:
yield event
response = content_planner.get_last_run_output()
yield StepOutput(content=response.content if response else "")
except Exception as exc:
yield StepOutput(
content=f"Custom content planning failed: {exc}",
success=False,
)
content_planning_step = Step(
name="Content Planning Step",
executor=custom_content_planning_function,
)Streaming works the same way with a class-based executor. Define the __call__ method to yield the events.
Install the Streaming Dependencies
uv pip install -U agno openaiSet OpenAI Key
Set your OPENAI_API_KEY as an environment variable. You can get one from OpenAI.
export OPENAI_API_KEY=sk-***