How to Use Asynchronous APIs#
Why async matters#
Asynchronous (async) programming in Python lets you start operations that wait on I/O (network, disk, etc.) without blocking the main thread, enabling high concurrency with a single event loop. Async is ideal for I/O-bound workloads (LLM calls, HTTP requests, databases) and less useful for CPU-bound tasks, which should run in worker threads or processes to avoid blocking the event loop.
WayFlow provides asynchronous APIs across models (e.g., generate_async
), conversations (execute_async
),
agents, and flows so you can compose concurrent, high-throughput pipelines using libraries such as anyio
.
Use async in the following cases:
Many parallel LLM requests
Agents calling several tools that perform remote I/O
Flows coordinating multiple steps concurrently
Basic implementation#
This section shows how to:
Use an LLM asynchronously
Execute an agent and a flow asynchronously
Define tools properly for CPU-bound vs I/O-bound tasks
Run many agents concurrently with
anyio
Understand when to still use synchronous APIs
For this tutorial, we will use a LLM. WayFlow supports several LLM API providers, select a LLM from the options below:
from wayflowcore.models import OCIGenAIModel
if __name__ == "__main__":
llm = OCIGenAIModel(
model_id="provider.model-id",
service_endpoint="https://url-to-service-endpoint.com",
compartment_id="compartment-id",
auth_type="API_KEY",
)
from wayflowcore.models import VllmModel
llm = VllmModel(
model_id="model-id",
host_port="VLLM_HOST_PORT",
)
from wayflowcore.models import OllamaModel
llm = OllamaModel(
model_id="model-id",
)
Using asynchronous APIs#
From synchronous code, wrap the coroutine with anyio.run(...)
to execute it in an event loop without blocking.
Inside an async def
function, you would instead write await ...
.
This pattern lets you fire off many I/O-bound calls concurrently and get much higher throughput than sync code.
For example, with an LlmModel
:
import anyio
prompt = "Who is the CEO of Oracle?"
# Run one async generation (non-blocking to the event loop)
anyio.run(llm.generate_async, prompt)
Execute an Agent asynchronously#
In async pipelines, you can now use execute_async
to avoid head-of-line blocking.
from wayflowcore.agent import Agent
agent = Agent(
llm=llm,
custom_instruction="You are a helpful assistant.",
)
conv = agent.start_conversation()
conv.append_user_message("Who is the CEO of Oracle?")
anyio.run(conv.execute_async)
Execute a Flow asynchronously#
Similarly, you can now run Flows
asynchronously:
from wayflowcore.flow import Flow
from wayflowcore.steps import PromptExecutionStep
step = PromptExecutionStep(
llm=llm,
prompt_template="Who is the CEO of Oracle?",
)
flow = Flow.from_steps(steps=[step])
flow_conv = flow.start_conversation()
anyio.run(flow_conv.execute_async)
Async tools vs sync tools#
ServerTool ‘s callable can be synchronous or asynchronous. The tool
decorator
can therefore be applied to both synchronous and asynchronous functionx.
Use async
tools for I/O-bound operations (HTTP calls, databases, storage) so they compose naturally
with the event loop. Keep CPU-bound work in synchronous functions, so that WayFlow automatically
runs them in worker threads in order to not block the event loop.
Tip
Avoid putting heavy CPU work inside an async def
tool. If you must compute in an async context,
offload to a thread or keep it as a synchronous tool so WayFlow can schedule it efficiently.
from wayflowcore.tools.toolhelpers import DescriptionMode, tool
# For CPU-bound tasks, async does not help. WayFlow runs synchronous tools
# in worker threads to avoid blocking the event loop.
@tool(description_mode=DescriptionMode.ONLY_DOCSTRING)
def heavy_work() -> str:
"""Performs heavy CPU-bound work."""
# WORK
return ""
# For I/O-bound tasks, async is optimal. Asynchronous tools are directly used
# inside WayFlow's asynchronous stack to maximize efficiency.
@tool(description_mode=DescriptionMode.ONLY_DOCSTRING)
async def remote_call() -> str:
"""Performs remote API calls (I/O-bound)."""
# CALLS
return ""
Use tools in an async Agent#
Combine tools with an agent and run it asynchronously.
from wayflowcore.agent import Agent
agent_with_tools = Agent(
llm=llm,
custom_instruction="You are a helpful assistant, answering the user request about {{query}}",
tools=[heavy_work, remote_call],
)
async def run_agent_async(query: str, result_list: list[str]) -> str:
conversation = agent_with_tools.start_conversation(
inputs={'query': query}
)
status = await conversation.execute_async()
result_list.append(conversation.get_last_message().content)
Run many Agents concurrently#
To scale throughput, use anyio.create_task_group()
to start many agent runs concurrently.
Each task awaits its own execute_async
call; the event loop interleaves I/O so all runs make progress.
You can bound concurrency by using semaphores if your backend has rate limits.
async def run_agents_concurrently(query: str, n: int) -> list[str]:
solutions: list[str] = []
async with anyio.create_task_group() as tg:
for _ in range(n):
tg.start_soon(run_agent_async, query, solutions)
return solutions
# Spawn 10 agents concurrently
anyio.run(run_agents_concurrently, "who is the CEO of Oracle?", 10)
Synchronous APIs in synchronous contexts#
Synchronous APIs remain useful in simple scripts and batch jobs. Prefer them only when you are not inside
an event loop. If you call execute()
or other sync APIs from async code, you risk blocking the loop;
WayFlow emits a warning and tells you which async method to use instead (for example, execute_async
).
# You can still use the synchronous API in a synchronous context.
sync_conv = agent.start_conversation()
sync_status = sync_conv.execute()
# Using synchronous APIs in an asynchronous context can block the event loop
# and lead to poor performance. WayFlow will emit a warning and indicate the
# appropriate asynchronous API to call instead (e.g., use execute_async).
Agent Spec Exporting/Loading#
You can export the agent configuration to its Agent Spec configuration using the AgentSpecExporter
.
from wayflowcore.agentspec import AgentSpecExporter
config = AgentSpecExporter().to_json(agent_with_tools)
Here is what the Agent Spec representation will look like ↓
Click here to see the assistant configuration.
{
"component_type": "Agent",
"id": "7017dd89-e176-476f-a655-06fce4310399",
"name": "agent_0f9ffda8__auto",
"description": "",
"metadata": {
"__metadata_info__": {}
},
"inputs": [
{
"description": "\"query\" input variable for the template",
"title": "query",
"type": "string"
}
],
"outputs": [],
"llm_config": {
"component_type": "VllmConfig",
"id": "2ceded67-85f7-4e47-be6c-2278f33f54f3",
"name": "LLAMA_MODEL_ID",
"description": null,
"metadata": {
"__metadata_info__": {}
},
"default_generation_parameters": null,
"url": "LLAMA_API_URL",
"model_id": "LLAMA_MODEL_ID"
},
"system_prompt": "You are a helpful assistant, answering the user request about {{query}}",
"tools": [
{
"component_type": "ServerTool",
"id": "bd53ed97-2f4b-4936-99fa-0c5b43d00a90",
"name": "heavy_work",
"description": "Performs heavy CPU-bound work.",
"metadata": {
"__metadata_info__": {}
},
"inputs": [],
"outputs": [
{
"title": "tool_output",
"type": "string"
}
]
},
{
"component_type": "ServerTool",
"id": "e87b2e06-70db-4439-b653-02978390fa36",
"name": "remote_call",
"description": "Performs remote API calls (I/O-bound).",
"metadata": {
"__metadata_info__": {}
},
"inputs": [],
"outputs": [
{
"title": "tool_output",
"type": "string"
}
]
}
],
"agentspec_version": "25.4.1"
}
component_type: Agent
id: 7017dd89-e176-476f-a655-06fce4310399
name: agent_0f9ffda8__auto
description: ''
metadata:
__metadata_info__: {}
inputs:
- description: '"query" input variable for the template'
title: query
type: string
outputs: []
llm_config:
component_type: VllmConfig
id: 2ceded67-85f7-4e47-be6c-2278f33f54f3
name: LLAMA_MODEL_ID
description: null
metadata:
__metadata_info__: {}
default_generation_parameters: null
url: LLAMA_API_URL
model_id: LLAMA_MODEL_ID
system_prompt: You are a helpful assistant, answering the user request about {{query}}
tools:
- component_type: ServerTool
id: bd53ed97-2f4b-4936-99fa-0c5b43d00a90
name: heavy_work
description: Performs heavy CPU-bound work.
metadata:
__metadata_info__: {}
inputs: []
outputs:
- title: tool_output
type: string
- component_type: ServerTool
id: e87b2e06-70db-4439-b653-02978390fa36
name: remote_call
description: Performs remote API calls (I/O-bound).
metadata:
__metadata_info__: {}
inputs: []
outputs:
- title: tool_output
type: string
agentspec_version: 25.4.1
You can then load the configuration back to an assistant using the AgentSpecLoader
.
from wayflowcore.agentspec import AgentSpecLoader
tool_registry = {
multiply.name: multiply,
divide.name: divide,
sum.name: sum,
subtract.name: subtract,
}
new_agent = AgentSpecLoader(tool_registry=tool_registry).load_json(config)
Next steps#
Having learned how to use the asynchronous APIs, you may now proceed to:
Full code#
Click on the card at the top of this page to download the full code for this guide or copy the code below.
1# Copyright © 2025 Oracle and/or its affiliates.
2#
3# This software is under the Universal Permissive License
4# %%[markdown]
5# WayFlow Code Example - How to Use Asynchronous APIs
6# ---------------------------------------------------
7
8# How to use:
9# Create a new Python virtual environment and install the latest WayFlow version.
10# ```bash
11# python -m venv venv-wayflowcore
12# source venv-wayflowcore/bin/activate
13# pip install --upgrade pip
14# pip install "wayflowcore==26.1"
15# ```
16
17# You can now run the script
18# 1. As a Python file:
19# ```bash
20# python howto_async.py
21# ```
22# 2. As a Notebook (in VSCode):
23# When viewing the file,
24# - press the keys Ctrl + Enter to run the selected cell
25# - or Shift + Enter to run the selected cell and move to the cell below# (UPL) 1.0 (LICENSE-UPL or https://oss.oracle.com/licenses/upl) or Apache License
26# 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0), at your option.
27
28
29
30# %%[markdown]
31## Define the llm
32
33# %%
34from wayflowcore.models import VllmModel
35
36llm = VllmModel(
37 model_id="LLAMA_MODEL_ID",
38 host_port="LLAMA_API_URL",
39)
40
41
42# %%[markdown]
43## Single async generation
44
45# %%
46import anyio
47
48prompt = "Who is the CEO of Oracle?"
49# Run one async generation (non-blocking to the event loop)
50anyio.run(llm.generate_async, prompt)
51
52
53# %%[markdown]
54## Async Agent execution
55
56# %%
57from wayflowcore.agent import Agent
58
59agent = Agent(
60 llm=llm,
61 custom_instruction="You are a helpful assistant.",
62)
63conv = agent.start_conversation()
64conv.append_user_message("Who is the CEO of Oracle?")
65anyio.run(conv.execute_async)
66
67
68# %%[markdown]
69## Async Flow execution
70
71# %%
72from wayflowcore.flow import Flow
73from wayflowcore.steps import PromptExecutionStep
74
75step = PromptExecutionStep(
76 llm=llm,
77 prompt_template="Who is the CEO of Oracle?",
78)
79flow = Flow.from_steps(steps=[step])
80flow_conv = flow.start_conversation()
81anyio.run(flow_conv.execute_async)
82
83
84# %%[markdown]
85## Define tools async vs sync
86
87# %%
88from wayflowcore.tools.toolhelpers import DescriptionMode, tool
89
90# For CPU-bound tasks, async does not help. WayFlow runs synchronous tools
91# in worker threads to avoid blocking the event loop.
92@tool(description_mode=DescriptionMode.ONLY_DOCSTRING)
93def heavy_work() -> str:
94 """Performs heavy CPU-bound work."""
95 # WORK
96 return ""
97
98# For I/O-bound tasks, async is optimal. Asynchronous tools are directly used
99# inside WayFlow's asynchronous stack to maximize efficiency.
100@tool(description_mode=DescriptionMode.ONLY_DOCSTRING)
101async def remote_call() -> str:
102 """Performs remote API calls (I/O-bound)."""
103 # CALLS
104 return ""
105
106
107# %%[markdown]
108## Agent with async tools
109
110# %%
111from wayflowcore.agent import Agent
112
113agent_with_tools = Agent(
114 llm=llm,
115 custom_instruction="You are a helpful assistant, answering the user request about {{query}}",
116 tools=[heavy_work, remote_call],
117)
118
119async def run_agent_async(query: str, result_list: list[str]) -> str:
120 conversation = agent_with_tools.start_conversation(
121 inputs={'query': query}
122 )
123 status = await conversation.execute_async()
124 result_list.append(conversation.get_last_message().content)
125
126
127# %%[markdown]
128## Run agents concurrently
129
130# %%
131async def run_agents_concurrently(query: str, n: int) -> list[str]:
132 solutions: list[str] = []
133 async with anyio.create_task_group() as tg:
134 for _ in range(n):
135 tg.start_soon(run_agent_async, query, solutions)
136 return solutions
137
138# Spawn 10 agents concurrently
139anyio.run(run_agents_concurrently, "who is the CEO of Oracle?", 10)
140
141
142# %%[markdown]
143## Synchronous usage
144
145# %%
146# You can still use the synchronous API in a synchronous context.
147sync_conv = agent.start_conversation()
148sync_status = sync_conv.execute()
149
150# Using synchronous APIs in an asynchronous context can block the event loop
151# and lead to poor performance. WayFlow will emit a warning and indicate the
152# appropriate asynchronous API to call instead (e.g., use execute_async).
153
154
155# %%[markdown]
156## Export Config to Agent Spec
157
158# %%
159from wayflowcore.agentspec import AgentSpecExporter
160
161config = AgentSpecExporter().to_json(agent_with_tools)
162
163# %%[markdown]
164## Load Agent Spec Config
165
166# %%
167from wayflowcore.agentspec import AgentSpecLoader
168
169tool_registry = {
170 heavy_work.name: heavy_work,
171 remote_call.name: remote_call,
172}
173new_agent = AgentSpecLoader(tool_registry=tool_registry).load_json(config)