Is a tool necessary to be asynchronous if it generates files?

Hello, I have some tool functions which will generate files, do I need to wrap them to make them async ? For example:

from langchain_core.tools import tool

from ase.build import molecule
from ase.collections import g2
from ase.io import write 
from ase import Atoms 
from tools.core import ToolOutput
import os 

import asyncio

@tool 
async def build_molecule_by_formula(formula: str, save_path:str):
    '''
    Create a molecule by its formula. Only support simple molecules present in ASE `g2` collection. The molecule will be saved in `{formula}.xyz`.
    Args:
        formula: The chemical formula of the molecule.
        save_path: The absolute path to save the molecule.
    '''
    if not formula in g2.names:
        raise KeyError(f"Error: Molecule {formula} is not included in ASE g2 collection. Build it by SMILES string (`build_molecule_by_smiles`).")

    mol = molecule(formula)
    filename = os.path.join(save_path, f"{formula}.xyz")
    # recommended by AI, I don't know if this is OK.
    await asyncio.to_thread(write, filename=filename, images=mol)
    return ToolOutput(content=f"Molecule {formula} is written to {filename}",
                      outfile_list=[filename]).model_dump(mode='python')

Here, ase.io.write and ase.io.read are synchronous functions, they will take some time if read or write large files.

hi @MSJavaScript

no, the tool doesn’t need to be async just because it handles files.

LangChain already protects the event loop for you: when a plain sync (def) tool is called from an async context (.ainvoke/.astream, LangGraph platform), it automatically runs it in a worker thread.

The docs state it explicitly: “LangChain runs sync tools in a separate thread to avoid blocking, but native async avoids the threading overhead entirely” - Going to production - Docs by LangChain

Is await asyncio.to_thread(write, …) OK?

Yes - with your current signature it’s actually necessary. Since you declared the tool async def yourself, LangChain awaits your coroutine directly on the event loop, so a bare write(…) inside the async def body would block the entire loop.

The LangSmith docs recommend exactly this pattern for unavoidable sync libraries (Cloud Agent Server environment variables - Docs by LangChain).
The catch: you’re hand-rolling what LangChain would do for you automatically if the tool were a plain def.

Recommendation: plain def with a bare write()

ASE has no native async API (and Python file I/O always goes through threads anyway - even aiofiles does), so a thread is the ceiling either way.

Simpler:

  @tool
  def build_molecule_by_formula(formula: str, save_path: str):
      '''...'''
      if formula not in g2.names:
          raise KeyError(...)
      mol = molecule(formula)
      filename = os.path.join(save_path, f"{formula}.xyz")
      write(filename=filename, images=mol)  # blocking is fine - runs in a worker thread under async
      return ToolOutput(...).model_dump(mode="python")
Variant Blocks the event loop? invoke ainvoke Verdict
def + bare write() No (auto thread fallback) :white_check_mark: :white_check_mark: Recommended
async def + asyncio.to_thread (your code) No :cross_mark: :white_check_mark: Correct, but redundant
async def + bare write() Yes :cross_mark: :warning: blocks the loop Avoid

An extra argument for def: an async-only tool cannot be called from a fully synchronous pipeline - StructuredTool._run raises NotImplementedError(“StructuredTool does not support sync invocation.”) (structured.py:98). The def version works in both worlds.

When a real async def actually pays off: when a natively async API exists (HTTP via httpx/aiohttp, async DB drivers) - then you skip the worker-thread overhead entirely. For file I/O it makes no difference. And you get concurrency either way: ToolNode executes multiple tool calls concurrently (thread pool in sync mode, asyncio.gather in async mode), so several molecule files can be written in parallel. If you ever want both implementations at once, there’s also StructuredTool.from_function(func=…, coroutine=…).