How to Use Asynchronous APIs#

python-icon Download Python Script

Python script/notebook for this guide.

Asynchronous APIs how-to script

Prerequisites

This guide assumes familiarity with:

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:

  1. Use an LLM asynchronously

  2. Execute an agent and a flow asynchronously

  3. Define tools properly for CPU-bound vs I/O-bound tasks

  4. Run many agents concurrently with anyio

  5. 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",
    )

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"
}

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)